@angular-devkit/core
Version:
Angular DevKit - Core Utility Library
565 lines (564 loc) • 22.9 kB
JavaScript
/**
* @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;
;