UNPKG

23.3 kBJavaScriptView Raw
1"use strict";
2/**
3 * @license
4 * Copyright Google LLC All Rights Reserved.
5 *
6 * Use of this source code is governed by an MIT-style license that can be
7 * found in the LICENSE file at https://angular.io/license
8 */
9var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10 if (k2 === undefined) k2 = k;
11 Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
12}) : (function(o, m, k, k2) {
13 if (k2 === undefined) k2 = k;
14 o[k2] = m[k];
15}));
16var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
17 Object.defineProperty(o, "default", { enumerable: true, value: v });
18}) : function(o, v) {
19 o["default"] = v;
20});
21var __importStar = (this && this.__importStar) || function (mod) {
22 if (mod && mod.__esModule) return mod;
23 var result = {};
24 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
25 __setModuleDefault(result, mod);
26 return result;
27};
28var __importDefault = (this && this.__importDefault) || function (mod) {
29 return (mod && mod.__esModule) ? mod : { "default": mod };
30};
31Object.defineProperty(exports, "__esModule", { value: true });
32exports.CoreSchemaRegistry = exports.SchemaValidationException = void 0;
33const ajv_1 = __importDefault(require("ajv"));
34const ajv_formats_1 = __importDefault(require("ajv-formats"));
35const http = __importStar(require("http"));
36const https = __importStar(require("https"));
37const rxjs_1 = require("rxjs");
38const operators_1 = require("rxjs/operators");
39const Url = __importStar(require("url"));
40const exception_1 = require("../../exception/exception");
41const utils_1 = require("../../utils");
42const interface_1 = require("../interface");
43const utility_1 = require("./utility");
44const visitor_1 = require("./visitor");
45class SchemaValidationException extends exception_1.BaseException {
46 constructor(errors, baseMessage = 'Schema validation failed with the following errors:') {
47 if (!errors || errors.length === 0) {
48 super('Schema validation failed.');
49 this.errors = [];
50 return;
51 }
52 const messages = SchemaValidationException.createMessages(errors);
53 super(`${baseMessage}\n ${messages.join('\n ')}`);
54 this.errors = errors;
55 }
56 static createMessages(errors) {
57 if (!errors || errors.length === 0) {
58 return [];
59 }
60 const messages = errors.map((err) => {
61 var _a;
62 let message = `Data path ${JSON.stringify(err.instancePath)} ${err.message}`;
63 if (err.params) {
64 switch (err.keyword) {
65 case 'additionalProperties':
66 message += `(${err.params.additionalProperty})`;
67 break;
68 case 'enum':
69 message += `. Allowed values are: ${(_a = err.params.allowedValues) === null || _a === void 0 ? void 0 : _a.map((v) => `"${v}"`).join(', ')}`;
70 break;
71 }
72 }
73 return message + '.';
74 });
75 return messages;
76 }
77}
78exports.SchemaValidationException = SchemaValidationException;
79class CoreSchemaRegistry {
80 constructor(formats = []) {
81 this._uriCache = new Map();
82 this._uriHandlers = new Set();
83 this._pre = new utils_1.PartiallyOrderedSet();
84 this._post = new utils_1.PartiallyOrderedSet();
85 this._smartDefaultKeyword = false;
86 this._sourceMap = new Map();
87 this._ajv = new ajv_1.default({
88 strict: false,
89 loadSchema: (uri) => this._fetch(uri),
90 passContext: true,
91 });
92 ajv_formats_1.default(this._ajv);
93 for (const format of formats) {
94 this.addFormat(format);
95 }
96 }
97 async _fetch(uri) {
98 const maybeSchema = this._uriCache.get(uri);
99 if (maybeSchema) {
100 return maybeSchema;
101 }
102 // Try all handlers, one after the other.
103 for (const handler of this._uriHandlers) {
104 let handlerResult = handler(uri);
105 if (handlerResult === null || handlerResult === undefined) {
106 continue;
107 }
108 if (rxjs_1.isObservable(handlerResult)) {
109 handlerResult = handlerResult.toPromise();
110 }
111 const value = await handlerResult;
112 this._uriCache.set(uri, value);
113 return value;
114 }
115 // If none are found, handle using http client.
116 return new Promise((resolve, reject) => {
117 const url = new Url.URL(uri);
118 const client = url.protocol === 'https:' ? https : http;
119 client.get(url, (res) => {
120 if (!res.statusCode || res.statusCode >= 300) {
121 // Consume the rest of the data to free memory.
122 res.resume();
123 reject(new Error(`Request failed. Status Code: ${res.statusCode}`));
124 }
125 else {
126 res.setEncoding('utf8');
127 let data = '';
128 res.on('data', (chunk) => {
129 data += chunk;
130 });
131 res.on('end', () => {
132 try {
133 const json = JSON.parse(data);
134 this._uriCache.set(uri, json);
135 resolve(json);
136 }
137 catch (err) {
138 reject(err);
139 }
140 });
141 }
142 });
143 });
144 }
145 /**
146 * Add a transformation step before the validation of any Json.
147 * @param {JsonVisitor} visitor The visitor to transform every value.
148 * @param {JsonVisitor[]} deps A list of other visitors to run before.
149 */
150 addPreTransform(visitor, deps) {
151 this._pre.add(visitor, deps);
152 }
153 /**
154 * Add a transformation step after the validation of any Json. The JSON will not be validated
155 * after the POST, so if transformations are not compatible with the Schema it will not result
156 * in an error.
157 * @param {JsonVisitor} visitor The visitor to transform every value.
158 * @param {JsonVisitor[]} deps A list of other visitors to run before.
159 */
160 addPostTransform(visitor, deps) {
161 this._post.add(visitor, deps);
162 }
163 _resolver(ref, validate) {
164 if (!validate || !ref) {
165 return {};
166 }
167 const schema = validate.schemaEnv.root.schema;
168 const id = typeof schema === 'object' ? schema.$id : null;
169 let fullReference = ref;
170 if (typeof id === 'string') {
171 fullReference = Url.resolve(id, ref);
172 if (ref.startsWith('#')) {
173 fullReference = id + fullReference;
174 }
175 }
176 if (fullReference.startsWith('#')) {
177 fullReference = fullReference.slice(0, -1);
178 }
179 const resolvedSchema = this._ajv.getSchema(fullReference);
180 return {
181 context: resolvedSchema === null || resolvedSchema === void 0 ? void 0 : resolvedSchema.schemaEnv.validate,
182 schema: resolvedSchema === null || resolvedSchema === void 0 ? void 0 : resolvedSchema.schema,
183 };
184 }
185 /**
186 * Flatten the Schema, resolving and replacing all the refs. Makes it into a synchronous schema
187 * that is also easier to traverse. Does not cache the result.
188 *
189 * @param schema The schema or URI to flatten.
190 * @returns An Observable of the flattened schema object.
191 * @deprecated since 11.2 without replacement.
192 * Producing a flatten schema document does not in all cases produce a schema with identical behavior to the original.
193 * See: https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.appendix.B.2
194 */
195 flatten(schema) {
196 return rxjs_1.from(this._flatten(schema));
197 }
198 async _flatten(schema) {
199 this._replaceDeprecatedSchemaIdKeyword(schema);
200 this._ajv.removeSchema(schema);
201 this._currentCompilationSchemaInfo = undefined;
202 const validate = await this._ajv.compileAsync(schema);
203 // eslint-disable-next-line @typescript-eslint/no-this-alias
204 const self = this;
205 function visitor(current, pointer, parentSchema, index) {
206 if (current &&
207 parentSchema &&
208 index &&
209 interface_1.isJsonObject(current) &&
210 Object.prototype.hasOwnProperty.call(current, '$ref') &&
211 typeof current['$ref'] == 'string') {
212 const resolved = self._resolver(current['$ref'], validate);
213 if (resolved.schema) {
214 parentSchema[index] = resolved.schema;
215 }
216 }
217 }
218 const schemaCopy = utils_1.deepCopy(validate.schema);
219 visitor_1.visitJsonSchema(schemaCopy, visitor);
220 return schemaCopy;
221 }
222 /**
223 * Compile and return a validation function for the Schema.
224 *
225 * @param schema The schema to validate. If a string, will fetch the schema before compiling it
226 * (using schema as a URI).
227 * @returns An Observable of the Validation function.
228 */
229 compile(schema) {
230 return rxjs_1.from(this._compile(schema)).pipe(operators_1.map((validate) => (value, options) => rxjs_1.from(validate(value, options))));
231 }
232 async _compile(schema) {
233 if (typeof schema === 'boolean') {
234 return async (data) => ({ success: schema, data });
235 }
236 this._replaceDeprecatedSchemaIdKeyword(schema);
237 const schemaInfo = {
238 smartDefaultRecord: new Map(),
239 promptDefinitions: [],
240 };
241 this._ajv.removeSchema(schema);
242 let validator;
243 try {
244 this._currentCompilationSchemaInfo = schemaInfo;
245 validator = this._ajv.compile(schema);
246 }
247 catch (e) {
248 // This should eventually be refactored so that we we handle race condition where the same schema is validated at the same time.
249 if (!(e instanceof ajv_1.default.MissingRefError)) {
250 throw e;
251 }
252 validator = await this._ajv.compileAsync(schema);
253 }
254 finally {
255 this._currentCompilationSchemaInfo = undefined;
256 }
257 return async (data, options) => {
258 var _a;
259 const validationOptions = {
260 withPrompts: true,
261 applyPostTransforms: true,
262 applyPreTransforms: true,
263 ...options,
264 };
265 const validationContext = {
266 promptFieldsWithValue: new Set(),
267 };
268 // Apply pre-validation transforms
269 if (validationOptions.applyPreTransforms) {
270 for (const visitor of this._pre.values()) {
271 data = await visitor_1.visitJson(data, visitor, schema, this._resolver.bind(this), validator).toPromise();
272 }
273 }
274 // Apply smart defaults
275 await this._applySmartDefaults(data, schemaInfo.smartDefaultRecord);
276 // Apply prompts
277 if (validationOptions.withPrompts) {
278 const visitor = (value, pointer) => {
279 if (value !== undefined) {
280 validationContext.promptFieldsWithValue.add(pointer);
281 }
282 return value;
283 };
284 if (typeof schema === 'object') {
285 await visitor_1.visitJson(data, visitor, schema, this._resolver.bind(this), validator).toPromise();
286 }
287 const definitions = schemaInfo.promptDefinitions.filter((def) => !validationContext.promptFieldsWithValue.has(def.id));
288 if (definitions.length > 0) {
289 await this._applyPrompts(data, definitions);
290 }
291 }
292 // Validate using ajv
293 try {
294 const success = await validator.call(validationContext, data);
295 if (!success) {
296 return { data, success, errors: (_a = validator.errors) !== null && _a !== void 0 ? _a : [] };
297 }
298 }
299 catch (error) {
300 if (error instanceof ajv_1.default.ValidationError) {
301 return { data, success: false, errors: error.errors };
302 }
303 throw error;
304 }
305 // Apply post-validation transforms
306 if (validationOptions.applyPostTransforms) {
307 for (const visitor of this._post.values()) {
308 data = await visitor_1.visitJson(data, visitor, schema, this._resolver.bind(this), validator).toPromise();
309 }
310 }
311 return { data, success: true };
312 };
313 }
314 addFormat(format) {
315 this._ajv.addFormat(format.name, format.formatter);
316 }
317 addSmartDefaultProvider(source, provider) {
318 if (this._sourceMap.has(source)) {
319 throw new Error(source);
320 }
321 this._sourceMap.set(source, provider);
322 if (!this._smartDefaultKeyword) {
323 this._smartDefaultKeyword = true;
324 this._ajv.addKeyword({
325 keyword: '$default',
326 errors: false,
327 valid: true,
328 compile: (schema, _parentSchema, it) => {
329 const compilationSchemInfo = this._currentCompilationSchemaInfo;
330 if (compilationSchemInfo === undefined) {
331 return () => true;
332 }
333 // We cheat, heavily.
334 const pathArray = this.normalizeDataPathArr(it);
335 compilationSchemInfo.smartDefaultRecord.set(JSON.stringify(pathArray), schema);
336 return () => true;
337 },
338 metaSchema: {
339 type: 'object',
340 properties: {
341 '$source': { type: 'string' },
342 },
343 additionalProperties: true,
344 required: ['$source'],
345 },
346 });
347 }
348 }
349 registerUriHandler(handler) {
350 this._uriHandlers.add(handler);
351 }
352 usePromptProvider(provider) {
353 const isSetup = !!this._promptProvider;
354 this._promptProvider = provider;
355 if (isSetup) {
356 return;
357 }
358 this._ajv.addKeyword({
359 keyword: 'x-prompt',
360 errors: false,
361 valid: true,
362 compile: (schema, parentSchema, it) => {
363 const compilationSchemInfo = this._currentCompilationSchemaInfo;
364 if (!compilationSchemInfo) {
365 return () => true;
366 }
367 const path = '/' + this.normalizeDataPathArr(it).join('/');
368 let type;
369 let items;
370 let message;
371 if (typeof schema == 'string') {
372 message = schema;
373 }
374 else {
375 message = schema.message;
376 type = schema.type;
377 items = schema.items;
378 }
379 const propertyTypes = utility_1.getTypesOfSchema(parentSchema);
380 if (!type) {
381 if (propertyTypes.size === 1 && propertyTypes.has('boolean')) {
382 type = 'confirmation';
383 }
384 else if (Array.isArray(parentSchema.enum)) {
385 type = 'list';
386 }
387 else if (propertyTypes.size === 1 &&
388 propertyTypes.has('array') &&
389 parentSchema.items &&
390 Array.isArray(parentSchema.items.enum)) {
391 type = 'list';
392 }
393 else {
394 type = 'input';
395 }
396 }
397 let multiselect;
398 if (type === 'list') {
399 multiselect =
400 schema.multiselect === undefined
401 ? propertyTypes.size === 1 && propertyTypes.has('array')
402 : schema.multiselect;
403 const enumValues = multiselect
404 ? parentSchema.items &&
405 parentSchema.items.enum
406 : parentSchema.enum;
407 if (!items && Array.isArray(enumValues)) {
408 items = [];
409 for (const value of enumValues) {
410 if (typeof value == 'string') {
411 items.push(value);
412 }
413 else if (typeof value == 'object') {
414 // Invalid
415 }
416 else {
417 items.push({ label: value.toString(), value });
418 }
419 }
420 }
421 }
422 const definition = {
423 id: path,
424 type,
425 message,
426 raw: schema,
427 items,
428 multiselect,
429 propertyTypes,
430 default: typeof parentSchema.default == 'object' &&
431 parentSchema.default !== null &&
432 !Array.isArray(parentSchema.default)
433 ? undefined
434 : parentSchema.default,
435 async validator(data) {
436 var _a;
437 try {
438 const result = await it.self.validate(parentSchema, data);
439 // If the schema is sync then false will be returned on validation failure
440 if (result) {
441 return result;
442 }
443 else if ((_a = it.self.errors) === null || _a === void 0 ? void 0 : _a.length) {
444 // Validation errors will be present on the Ajv instance when sync
445 return it.self.errors[0].message;
446 }
447 }
448 catch (e) {
449 // If the schema is async then an error will be thrown on validation failure
450 if (Array.isArray(e.errors) && e.errors.length) {
451 return e.errors[0].message;
452 }
453 }
454 return false;
455 },
456 };
457 compilationSchemInfo.promptDefinitions.push(definition);
458 return function () {
459 // If 'this' is undefined in the call, then it defaults to the global
460 // 'this'.
461 if (this && this.promptFieldsWithValue) {
462 this.promptFieldsWithValue.add(path);
463 }
464 return true;
465 };
466 },
467 metaSchema: {
468 oneOf: [
469 { type: 'string' },
470 {
471 type: 'object',
472 properties: {
473 'type': { type: 'string' },
474 'message': { type: 'string' },
475 },
476 additionalProperties: true,
477 required: ['message'],
478 },
479 ],
480 },
481 });
482 }
483 async _applyPrompts(data, prompts) {
484 const provider = this._promptProvider;
485 if (!provider) {
486 return;
487 }
488 const answers = await rxjs_1.from(provider(prompts)).toPromise();
489 for (const path in answers) {
490 const pathFragments = path.split('/').slice(1);
491 CoreSchemaRegistry._set(data, pathFragments, answers[path], null, undefined, true);
492 }
493 }
494 static _set(
495 // eslint-disable-next-line @typescript-eslint/no-explicit-any
496 data, fragments, value,
497 // eslint-disable-next-line @typescript-eslint/no-explicit-any
498 parent = null, parentProperty, force) {
499 for (let index = 0; index < fragments.length; index++) {
500 const fragment = fragments[index];
501 if (/^i\d+$/.test(fragment)) {
502 if (!Array.isArray(data)) {
503 return;
504 }
505 for (let dataIndex = 0; dataIndex < data.length; dataIndex++) {
506 CoreSchemaRegistry._set(data[dataIndex], fragments.slice(index + 1), value, data, `${dataIndex}`);
507 }
508 return;
509 }
510 if (!data && parent !== null && parentProperty) {
511 data = parent[parentProperty] = {};
512 }
513 parent = data;
514 parentProperty = fragment;
515 data = data[fragment];
516 }
517 if (parent && parentProperty && (force || parent[parentProperty] === undefined)) {
518 parent[parentProperty] = value;
519 }
520 }
521 async _applySmartDefaults(data, smartDefaults) {
522 for (const [pointer, schema] of smartDefaults.entries()) {
523 const fragments = JSON.parse(pointer);
524 const source = this._sourceMap.get(schema.$source);
525 if (!source) {
526 continue;
527 }
528 let value = source(schema);
529 if (rxjs_1.isObservable(value)) {
530 value = await value.toPromise();
531 }
532 CoreSchemaRegistry._set(data, fragments, value);
533 }
534 }
535 useXDeprecatedProvider(onUsage) {
536 this._ajv.addKeyword({
537 keyword: 'x-deprecated',
538 validate: (schema, _data, _parentSchema, dataCxt) => {
539 if (schema) {
540 onUsage(`Option "${dataCxt === null || dataCxt === void 0 ? void 0 : dataCxt.parentDataProperty}" is deprecated${typeof schema == 'string' ? ': ' + schema : '.'}`);
541 }
542 return true;
543 },
544 errors: false,
545 });
546 }
547 /**
548 * Workaround to avoid a breaking change in downstream schematics.
549 * @deprecated will be removed in version 13.
550 */
551 _replaceDeprecatedSchemaIdKeyword(schema) {
552 if (typeof schema.id === 'string') {
553 schema.$id = schema.id;
554 delete schema.id;
555 // eslint-disable-next-line no-console
556 console.warn(`"${schema.$id}" schema is using the keyword "id" which its support is deprecated. Use "$id" for schema ID.`);
557 }
558 }
559 normalizeDataPathArr(it) {
560 return it.dataPathArr
561 .slice(1, it.dataLevel + 1)
562 .map((p) => (typeof p === 'number' ? p : p.str.replace(/\"/g, '')));
563 }
564}
565exports.CoreSchemaRegistry = CoreSchemaRegistry;