UNPKG

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