UNPKG

@angular-devkit/core

Version:

Angular DevKit - Core Utility Library

565 lines (564 loc) 22.9 kB
"use strict"; /** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CoreSchemaRegistry = exports.SchemaValidationException = void 0; const ajv_1 = __importDefault(require("ajv")); const ajv_formats_1 = __importDefault(require("ajv-formats")); const http = __importStar(require("node:http")); const https = __importStar(require("node:https")); const Url = __importStar(require("node:url")); const rxjs_1 = require("rxjs"); const exception_1 = require("../../exception"); const utils_1 = require("../../utils"); const utils_2 = require("../utils"); const utility_1 = require("./utility"); const visitor_1 = require("./visitor"); class SchemaValidationException extends exception_1.BaseException { errors; constructor(errors, baseMessage = 'Schema validation failed with the following errors:') { if (!errors || errors.length === 0) { super('Schema validation failed.'); this.errors = []; return; } const messages = SchemaValidationException.createMessages(errors); super(`${baseMessage}\n ${messages.join('\n ')}`); this.errors = errors; } static createMessages(errors) { if (!errors || errors.length === 0) { return []; } const messages = errors.map((err) => { let message = `Data path ${JSON.stringify(err.instancePath)} ${err.message}`; if (err.params) { switch (err.keyword) { case 'additionalProperties': message += `(${err.params.additionalProperty})`; break; case 'enum': message += `. Allowed values are: ${err.params.allowedValues ?.map((v) => `"${v}"`) .join(', ')}`; break; } } return message + '.'; }); return messages; } } exports.SchemaValidationException = SchemaValidationException; class CoreSchemaRegistry { _ajv; _uriCache = new Map(); _uriHandlers = new Set(); _pre = new utils_1.PartiallyOrderedSet(); _post = new utils_1.PartiallyOrderedSet(); _currentCompilationSchemaInfo; _smartDefaultKeyword = false; _promptProvider; _sourceMap = new Map(); constructor(formats = []) { this._ajv = new ajv_1.default({ strict: false, loadSchema: (uri) => this._fetch(uri), passContext: true, }); (0, ajv_formats_1.default)(this._ajv); for (const format of formats) { this.addFormat(format); } } async _fetch(uri) { const maybeSchema = this._uriCache.get(uri); if (maybeSchema) { return maybeSchema; } // Try all handlers, one after the other. for (const handler of this._uriHandlers) { let handlerResult = handler(uri); if (handlerResult === null || handlerResult === undefined) { continue; } if ((0, rxjs_1.isObservable)(handlerResult)) { handlerResult = (0, rxjs_1.lastValueFrom)(handlerResult); } const value = await handlerResult; this._uriCache.set(uri, value); return value; } // If none are found, handle using http client. return new Promise((resolve, reject) => { const url = new Url.URL(uri); const client = url.protocol === 'https:' ? https : http; client.get(url, (res) => { if (!res.statusCode || res.statusCode >= 300) { // Consume the rest of the data to free memory. res.resume(); reject(new Error(`Request failed. Status Code: ${res.statusCode}`)); } else { res.setEncoding('utf8'); let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { const json = JSON.parse(data); this._uriCache.set(uri, json); resolve(json); } catch (err) { reject(err); } }); } }); }); } /** * Add a transformation step before the validation of any Json. * @param {JsonVisitor} visitor The visitor to transform every value. * @param {JsonVisitor[]} deps A list of other visitors to run before. */ addPreTransform(visitor, deps) { this._pre.add(visitor, deps); } /** * Add a transformation step after the validation of any Json. The JSON will not be validated * after the POST, so if transformations are not compatible with the Schema it will not result * in an error. * @param {JsonVisitor} visitor The visitor to transform every value. * @param {JsonVisitor[]} deps A list of other visitors to run before. */ addPostTransform(visitor, deps) { this._post.add(visitor, deps); } _resolver(ref, validate) { if (!validate || !ref) { return {}; } const schema = validate.schemaEnv.root.schema; const id = typeof schema === 'object' ? schema.$id : null; let fullReference = ref; if (typeof id === 'string') { fullReference = Url.resolve(id, ref); if (ref.startsWith('#')) { fullReference = id + fullReference; } } const resolvedSchema = this._ajv.getSchema(fullReference); return { context: resolvedSchema?.schemaEnv.validate, schema: resolvedSchema?.schema, }; } /** * Flatten the Schema, resolving and replacing all the refs. Makes it into a synchronous schema * that is also easier to traverse. Does not cache the result. * * Producing a flatten schema document does not in all cases produce a schema with identical behavior to the original. * See: https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.appendix.B.2 * * @param schema The schema or URI to flatten. * @returns An Observable of the flattened schema object. * @private since 11.2 without replacement. */ async ɵflatten(schema) { this._ajv.removeSchema(schema); this._currentCompilationSchemaInfo = undefined; const validate = await this._ajv.compileAsync(schema); // eslint-disable-next-line @typescript-eslint/no-this-alias const self = this; function visitor(current, pointer, parentSchema, index) { if (current && parentSchema && index && (0, utils_2.isJsonObject)(current) && Object.prototype.hasOwnProperty.call(current, '$ref') && typeof current['$ref'] == 'string') { const resolved = self._resolver(current['$ref'], validate); if (resolved.schema) { parentSchema[index] = resolved.schema; } } } const schemaCopy = (0, utils_1.deepCopy)(validate.schema); (0, visitor_1.visitJsonSchema)(schemaCopy, visitor); return schemaCopy; } /** * Compile and return a validation function for the Schema. * * @param schema The schema to validate. If a string, will fetch the schema before compiling it * (using schema as a URI). */ async compile(schema) { const validate = await this._compile(schema); return (value, options) => validate(value, options); } async _compile(schema) { if (typeof schema === 'boolean') { return async (data) => ({ success: schema, data }); } const schemaInfo = { smartDefaultRecord: new Map(), promptDefinitions: [], }; this._ajv.removeSchema(schema); let validator; try { this._currentCompilationSchemaInfo = schemaInfo; validator = this._ajv.compile(schema); } catch (e) { // This should eventually be refactored so that we we handle race condition where the same schema is validated at the same time. if (!(e instanceof ajv_1.default.MissingRefError)) { throw e; } validator = await this._ajv.compileAsync(schema); } finally { this._currentCompilationSchemaInfo = undefined; } return async (data, options) => { const validationOptions = { withPrompts: true, applyPostTransforms: true, applyPreTransforms: true, ...options, }; const validationContext = { promptFieldsWithValue: new Set(), }; // Apply pre-validation transforms if (validationOptions.applyPreTransforms) { for (const visitor of this._pre.values()) { data = await (0, rxjs_1.lastValueFrom)((0, visitor_1.visitJson)(data, visitor, schema, this._resolver.bind(this), validator)); } } // Apply smart defaults await this._applySmartDefaults(data, schemaInfo.smartDefaultRecord); // Apply prompts if (validationOptions.withPrompts) { const visitor = (value, pointer) => { if (value !== undefined) { validationContext.promptFieldsWithValue.add(pointer); } return value; }; if (typeof schema === 'object') { await (0, rxjs_1.lastValueFrom)((0, visitor_1.visitJson)(data, visitor, schema, this._resolver.bind(this), validator)); } const definitions = schemaInfo.promptDefinitions.filter((def) => !validationContext.promptFieldsWithValue.has(def.id)); if (definitions.length > 0) { await this._applyPrompts(data, definitions); } } // Validate using ajv try { // eslint-disable-next-line @typescript-eslint/await-thenable const success = await validator.call(validationContext, data); if (!success) { return { data, success, errors: validator.errors ?? [] }; } } catch (error) { if (error instanceof ajv_1.default.ValidationError) { return { data, success: false, errors: error.errors }; } throw error; } // Apply post-validation transforms if (validationOptions.applyPostTransforms) { for (const visitor of this._post.values()) { data = await (0, rxjs_1.lastValueFrom)((0, visitor_1.visitJson)(data, visitor, schema, this._resolver.bind(this), validator)); } } return { data, success: true }; }; } addFormat(format) { this._ajv.addFormat(format.name, format.formatter); } addSmartDefaultProvider(source, provider) { if (this._sourceMap.has(source)) { throw new Error(source); } this._sourceMap.set(source, provider); if (!this._smartDefaultKeyword) { this._smartDefaultKeyword = true; this._ajv.addKeyword({ keyword: '$default', errors: false, valid: true, compile: (schema, _parentSchema, it) => { const compilationSchemInfo = this._currentCompilationSchemaInfo; if (compilationSchemInfo === undefined) { return () => true; } // We cheat, heavily. const pathArray = this.normalizeDataPathArr(it); compilationSchemInfo.smartDefaultRecord.set(JSON.stringify(pathArray), schema); return () => true; }, metaSchema: { type: 'object', properties: { '$source': { type: 'string' }, }, additionalProperties: true, required: ['$source'], }, }); } } registerUriHandler(handler) { this._uriHandlers.add(handler); } usePromptProvider(provider) { const isSetup = !!this._promptProvider; this._promptProvider = provider; if (isSetup) { return; } this._ajv.addKeyword({ keyword: 'x-prompt', errors: false, valid: true, compile: (schema, parentSchema, it) => { const compilationSchemInfo = this._currentCompilationSchemaInfo; if (!compilationSchemInfo) { return () => true; } const path = '/' + this.normalizeDataPathArr(it).join('/'); let type; let items; let message; if (typeof schema == 'string') { message = schema; } else { message = schema.message; type = schema.type; items = schema.items; } const propertyTypes = (0, utility_1.getTypesOfSchema)(parentSchema); if (!type) { if (propertyTypes.size === 1 && propertyTypes.has('boolean')) { type = 'confirmation'; } else if (Array.isArray(parentSchema.enum)) { type = 'list'; } else if (propertyTypes.size === 1 && propertyTypes.has('array') && parentSchema.items && Array.isArray(parentSchema.items.enum)) { type = 'list'; } else { type = 'input'; } } let multiselect; if (type === 'list') { multiselect = schema.multiselect === undefined ? propertyTypes.size === 1 && propertyTypes.has('array') : schema.multiselect; const enumValues = multiselect ? parentSchema.items && parentSchema.items.enum : parentSchema.enum; if (!items && Array.isArray(enumValues)) { items = []; for (const value of enumValues) { if (typeof value == 'string') { items.push(value); } else if (typeof value == 'object') { // Invalid } else { items.push({ label: value.toString(), value }); } } } } const definition = { id: path, type, message, raw: schema, items, multiselect, propertyTypes, default: typeof parentSchema.default == 'object' && parentSchema.default !== null && !Array.isArray(parentSchema.default) ? undefined : parentSchema.default, async validator(data) { try { const result = await it.self.validate(parentSchema, data); // If the schema is sync then false will be returned on validation failure if (result) { return result; } else if (it.self.errors?.length) { // Validation errors will be present on the Ajv instance when sync return it.self.errors[0].message; } } catch (e) { const validationError = e; // If the schema is async then an error will be thrown on validation failure if (Array.isArray(validationError.errors) && validationError.errors.length) { return validationError.errors[0].message; } } return false; }, }; compilationSchemInfo.promptDefinitions.push(definition); return function () { // If 'this' is undefined in the call, then it defaults to the global // 'this'. if (this && this.promptFieldsWithValue) { this.promptFieldsWithValue.add(path); } return true; }; }, metaSchema: { oneOf: [ { type: 'string' }, { type: 'object', properties: { 'type': { type: 'string' }, 'message': { type: 'string' }, }, additionalProperties: true, required: ['message'], }, ], }, }); } async _applyPrompts(data, prompts) { const provider = this._promptProvider; if (!provider) { return; } const answers = await (0, rxjs_1.lastValueFrom)((0, rxjs_1.from)(provider(prompts))); for (const path in answers) { const pathFragments = path.split('/').slice(1); CoreSchemaRegistry._set(data, pathFragments, answers[path], null, undefined, true); } } static _set( // eslint-disable-next-line @typescript-eslint/no-explicit-any data, fragments, value, // eslint-disable-next-line @typescript-eslint/no-explicit-any parent = null, parentProperty, force) { for (let index = 0; index < fragments.length; index++) { const fragment = fragments[index]; if (/^i\d+$/.test(fragment)) { if (!Array.isArray(data)) { return; } for (let dataIndex = 0; dataIndex < data.length; dataIndex++) { CoreSchemaRegistry._set(data[dataIndex], fragments.slice(index + 1), value, data, `${dataIndex}`); } return; } if (!data && parent !== null && parentProperty) { data = parent[parentProperty] = {}; } parent = data; parentProperty = fragment; data = data[fragment]; } if (parent && parentProperty && (force || parent[parentProperty] === undefined)) { parent[parentProperty] = value; } } async _applySmartDefaults(data, smartDefaults) { for (const [pointer, schema] of smartDefaults.entries()) { const fragments = JSON.parse(pointer); const source = this._sourceMap.get(schema.$source); if (!source) { continue; } let value = source(schema); if ((0, rxjs_1.isObservable)(value)) { value = (await (0, rxjs_1.lastValueFrom)(value)); } CoreSchemaRegistry._set(data, fragments, value); } } useXDeprecatedProvider(onUsage) { this._ajv.addKeyword({ keyword: 'x-deprecated', validate: (schema, _data, _parentSchema, dataCxt) => { if (schema) { onUsage(`Option "${dataCxt?.parentDataProperty}" is deprecated${typeof schema == 'string' ? ': ' + schema : '.'}`); } return true; }, errors: false, }); } normalizeDataPathArr(it) { return it.dataPathArr .slice(1, it.dataLevel + 1) .map((p) => (typeof p === 'number' ? p : p.str.replace(/"/g, ''))); } } exports.CoreSchemaRegistry = CoreSchemaRegistry;