UNPKG

22.9 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 var desc = Object.getOwnPropertyDescriptor(m, k);
12 if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13 desc = { enumerable: true, get: function() { return m[k]; } };
14 }
15 Object.defineProperty(o, k2, desc);
16}) : (function(o, m, k, k2) {
17 if (k2 === undefined) k2 = k;
18 o[k2] = m[k];
19}));
20var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21 Object.defineProperty(o, "default", { enumerable: true, value: v });
22}) : function(o, v) {
23 o["default"] = v;
24});
25var __importStar = (this && this.__importStar) || function (mod) {
26 if (mod && mod.__esModule) return mod;
27 var result = {};
28 if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
29 __setModuleDefault(result, mod);
30 return result;
31};
32var __importDefault = (this && this.__importDefault) || function (mod) {
33 return (mod && mod.__esModule) ? mod : { "default": mod };
34};
35Object.defineProperty(exports, "__esModule", { value: true });
36exports.CoreSchemaRegistry = exports.SchemaValidationException = void 0;
37const ajv_1 = __importDefault(require("ajv"));
38const ajv_formats_1 = __importDefault(require("ajv-formats"));
39const http = __importStar(require("http"));
40const https = __importStar(require("https"));
41const rxjs_1 = require("rxjs");
42const operators_1 = require("rxjs/operators");
43const Url = __importStar(require("url"));
44const exception_1 = require("../../exception");
45const utils_1 = require("../../utils");
46const utils_2 = require("../utils");
47const utility_1 = require("./utility");
48const visitor_1 = require("./visitor");
49class SchemaValidationException extends exception_1.BaseException {
50 constructor(errors, baseMessage = 'Schema validation failed with the following errors:') {
51 if (!errors || errors.length === 0) {
52 super('Schema validation failed.');
53 this.errors = [];
54 return;
55 }
56 const messages = SchemaValidationException.createMessages(errors);
57 super(`${baseMessage}\n ${messages.join('\n ')}`);
58 this.errors = errors;
59 }
60 static createMessages(errors) {
61 if (!errors || errors.length === 0) {
62 return [];
63 }
64 const messages = errors.map((err) => {
65 var _a;
66 let message = `Data path ${JSON.stringify(err.instancePath)} ${err.message}`;
67 if (err.params) {
68 switch (err.keyword) {
69 case 'additionalProperties':
70 message += `(${err.params.additionalProperty})`;
71 break;
72 case 'enum':
73 message += `. Allowed values are: ${(_a = err.params.allowedValues) === null || _a === void 0 ? void 0 : _a.map((v) => `"${v}"`).join(', ')}`;
74 break;
75 }
76 }
77 return message + '.';
78 });
79 return messages;
80 }
81}
82exports.SchemaValidationException = SchemaValidationException;
83class CoreSchemaRegistry {
84 constructor(formats = []) {
85 this._uriCache = new Map();
86 this._uriHandlers = new Set();
87 this._pre = new utils_1.PartiallyOrderedSet();
88 this._post = new utils_1.PartiallyOrderedSet();
89 this._smartDefaultKeyword = false;
90 this._sourceMap = new Map();
91 this._ajv = new ajv_1.default({
92 strict: false,
93 loadSchema: (uri) => this._fetch(uri),
94 passContext: true,
95 });
96 (0, ajv_formats_1.default)(this._ajv);
97 for (const format of formats) {
98 this.addFormat(format);
99 }
100 }
101 async _fetch(uri) {
102 const maybeSchema = this._uriCache.get(uri);
103 if (maybeSchema) {
104 return maybeSchema;
105 }
106 // Try all handlers, one after the other.
107 for (const handler of this._uriHandlers) {
108 let handlerResult = handler(uri);
109 if (handlerResult === null || handlerResult === undefined) {
110 continue;
111 }
112 if ((0, rxjs_1.isObservable)(handlerResult)) {
113 handlerResult = handlerResult.toPromise();
114 }
115 const value = await handlerResult;
116 this._uriCache.set(uri, value);
117 return value;
118 }
119 // If none are found, handle using http client.
120 return new Promise((resolve, reject) => {
121 const url = new Url.URL(uri);
122 const client = url.protocol === 'https:' ? https : http;
123 client.get(url, (res) => {
124 if (!res.statusCode || res.statusCode >= 300) {
125 // Consume the rest of the data to free memory.
126 res.resume();
127 reject(new Error(`Request failed. Status Code: ${res.statusCode}`));
128 }
129 else {
130 res.setEncoding('utf8');
131 let data = '';
132 res.on('data', (chunk) => {
133 data += chunk;
134 });
135 res.on('end', () => {
136 try {
137 const json = JSON.parse(data);
138 this._uriCache.set(uri, json);
139 resolve(json);
140 }
141 catch (err) {
142 reject(err);
143 }
144 });
145 }
146 });
147 });
148 }
149 /**
150 * Add a transformation step before the validation of any Json.
151 * @param {JsonVisitor} visitor The visitor to transform every value.
152 * @param {JsonVisitor[]} deps A list of other visitors to run before.
153 */
154 addPreTransform(visitor, deps) {
155 this._pre.add(visitor, deps);
156 }
157 /**
158 * Add a transformation step after the validation of any Json. The JSON will not be validated
159 * after the POST, so if transformations are not compatible with the Schema it will not result
160 * in an error.
161 * @param {JsonVisitor} visitor The visitor to transform every value.
162 * @param {JsonVisitor[]} deps A list of other visitors to run before.
163 */
164 addPostTransform(visitor, deps) {
165 this._post.add(visitor, deps);
166 }
167 _resolver(ref, validate) {
168 if (!validate || !ref) {
169 return {};
170 }
171 const schema = validate.schemaEnv.root.schema;
172 const id = typeof schema === 'object' ? schema.$id : null;
173 let fullReference = ref;
174 if (typeof id === 'string') {
175 fullReference = Url.resolve(id, ref);
176 if (ref.startsWith('#')) {
177 fullReference = id + fullReference;
178 }
179 }
180 const resolvedSchema = this._ajv.getSchema(fullReference);
181 return {
182 context: resolvedSchema === null || resolvedSchema === void 0 ? void 0 : resolvedSchema.schemaEnv.validate,
183 schema: resolvedSchema === null || resolvedSchema === void 0 ? void 0 : resolvedSchema.schema,
184 };
185 }
186 /**
187 * Flatten the Schema, resolving and replacing all the refs. Makes it into a synchronous schema
188 * that is also easier to traverse. Does not cache the result.
189 *
190 * @param schema The schema or URI to flatten.
191 * @returns An Observable of the flattened schema object.
192 * @deprecated since 11.2 without replacement.
193 * Producing a flatten schema document does not in all cases produce a schema with identical behavior to the original.
194 * See: https://json-schema.org/draft/2019-09/json-schema-core.html#rfc.appendix.B.2
195 */
196 flatten(schema) {
197 return (0, rxjs_1.from)(this._flatten(schema));
198 }
199 async _flatten(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 (0, utils_2.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 = (0, utils_1.deepCopy)(validate.schema);
219 (0, 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 (0, rxjs_1.from)(this._compile(schema)).pipe((0, operators_1.map)((validate) => (value, options) => (0, 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 const schemaInfo = {
237 smartDefaultRecord: new Map(),
238 promptDefinitions: [],
239 };
240 this._ajv.removeSchema(schema);
241 let validator;
242 try {
243 this._currentCompilationSchemaInfo = schemaInfo;
244 validator = this._ajv.compile(schema);
245 }
246 catch (e) {
247 // This should eventually be refactored so that we we handle race condition where the same schema is validated at the same time.
248 if (!(e instanceof ajv_1.default.MissingRefError)) {
249 throw e;
250 }
251 validator = await this._ajv.compileAsync(schema);
252 }
253 finally {
254 this._currentCompilationSchemaInfo = undefined;
255 }
256 return async (data, options) => {
257 var _a;
258 const validationOptions = {
259 withPrompts: true,
260 applyPostTransforms: true,
261 applyPreTransforms: true,
262 ...options,
263 };
264 const validationContext = {
265 promptFieldsWithValue: new Set(),
266 };
267 // Apply pre-validation transforms
268 if (validationOptions.applyPreTransforms) {
269 for (const visitor of this._pre.values()) {
270 data = await (0, visitor_1.visitJson)(data, visitor, schema, this._resolver.bind(this), validator).toPromise();
271 }
272 }
273 // Apply smart defaults
274 await this._applySmartDefaults(data, schemaInfo.smartDefaultRecord);
275 // Apply prompts
276 if (validationOptions.withPrompts) {
277 const visitor = (value, pointer) => {
278 if (value !== undefined) {
279 validationContext.promptFieldsWithValue.add(pointer);
280 }
281 return value;
282 };
283 if (typeof schema === 'object') {
284 await (0, visitor_1.visitJson)(data, visitor, schema, this._resolver.bind(this), validator).toPromise();
285 }
286 const definitions = schemaInfo.promptDefinitions.filter((def) => !validationContext.promptFieldsWithValue.has(def.id));
287 if (definitions.length > 0) {
288 await this._applyPrompts(data, definitions);
289 }
290 }
291 // Validate using ajv
292 try {
293 const success = await validator.call(validationContext, data);
294 if (!success) {
295 return { data, success, errors: (_a = validator.errors) !== null && _a !== void 0 ? _a : [] };
296 }
297 }
298 catch (error) {
299 if (error instanceof ajv_1.default.ValidationError) {
300 return { data, success: false, errors: error.errors };
301 }
302 throw error;
303 }
304 // Apply post-validation transforms
305 if (validationOptions.applyPostTransforms) {
306 for (const visitor of this._post.values()) {
307 data = await (0, visitor_1.visitJson)(data, visitor, schema, this._resolver.bind(this), validator).toPromise();
308 }
309 }
310 return { data, success: true };
311 };
312 }
313 addFormat(format) {
314 this._ajv.addFormat(format.name, format.formatter);
315 }
316 addSmartDefaultProvider(source, provider) {
317 if (this._sourceMap.has(source)) {
318 throw new Error(source);
319 }
320 this._sourceMap.set(source, provider);
321 if (!this._smartDefaultKeyword) {
322 this._smartDefaultKeyword = true;
323 this._ajv.addKeyword({
324 keyword: '$default',
325 errors: false,
326 valid: true,
327 compile: (schema, _parentSchema, it) => {
328 const compilationSchemInfo = this._currentCompilationSchemaInfo;
329 if (compilationSchemInfo === undefined) {
330 return () => true;
331 }
332 // We cheat, heavily.
333 const pathArray = this.normalizeDataPathArr(it);
334 compilationSchemInfo.smartDefaultRecord.set(JSON.stringify(pathArray), schema);
335 return () => true;
336 },
337 metaSchema: {
338 type: 'object',
339 properties: {
340 '$source': { type: 'string' },
341 },
342 additionalProperties: true,
343 required: ['$source'],
344 },
345 });
346 }
347 }
348 registerUriHandler(handler) {
349 this._uriHandlers.add(handler);
350 }
351 usePromptProvider(provider) {
352 const isSetup = !!this._promptProvider;
353 this._promptProvider = provider;
354 if (isSetup) {
355 return;
356 }
357 this._ajv.addKeyword({
358 keyword: 'x-prompt',
359 errors: false,
360 valid: true,
361 compile: (schema, parentSchema, it) => {
362 const compilationSchemInfo = this._currentCompilationSchemaInfo;
363 if (!compilationSchemInfo) {
364 return () => true;
365 }
366 const path = '/' + this.normalizeDataPathArr(it).join('/');
367 let type;
368 let items;
369 let message;
370 if (typeof schema == 'string') {
371 message = schema;
372 }
373 else {
374 message = schema.message;
375 type = schema.type;
376 items = schema.items;
377 }
378 const propertyTypes = (0, utility_1.getTypesOfSchema)(parentSchema);
379 if (!type) {
380 if (propertyTypes.size === 1 && propertyTypes.has('boolean')) {
381 type = 'confirmation';
382 }
383 else if (Array.isArray(parentSchema.enum)) {
384 type = 'list';
385 }
386 else if (propertyTypes.size === 1 &&
387 propertyTypes.has('array') &&
388 parentSchema.items &&
389 Array.isArray(parentSchema.items.enum)) {
390 type = 'list';
391 }
392 else {
393 type = 'input';
394 }
395 }
396 let multiselect;
397 if (type === 'list') {
398 multiselect =
399 schema.multiselect === undefined
400 ? propertyTypes.size === 1 && propertyTypes.has('array')
401 : schema.multiselect;
402 const enumValues = multiselect
403 ? parentSchema.items &&
404 parentSchema.items.enum
405 : parentSchema.enum;
406 if (!items && Array.isArray(enumValues)) {
407 items = [];
408 for (const value of enumValues) {
409 if (typeof value == 'string') {
410 items.push(value);
411 }
412 else if (typeof value == 'object') {
413 // Invalid
414 }
415 else {
416 items.push({ label: value.toString(), value });
417 }
418 }
419 }
420 }
421 const definition = {
422 id: path,
423 type,
424 message,
425 raw: schema,
426 items,
427 multiselect,
428 propertyTypes,
429 default: typeof parentSchema.default == 'object' &&
430 parentSchema.default !== null &&
431 !Array.isArray(parentSchema.default)
432 ? undefined
433 : parentSchema.default,
434 async validator(data) {
435 var _a;
436 try {
437 const result = await it.self.validate(parentSchema, data);
438 // If the schema is sync then false will be returned on validation failure
439 if (result) {
440 return result;
441 }
442 else if ((_a = it.self.errors) === null || _a === void 0 ? void 0 : _a.length) {
443 // Validation errors will be present on the Ajv instance when sync
444 return it.self.errors[0].message;
445 }
446 }
447 catch (e) {
448 const validationError = e;
449 // If the schema is async then an error will be thrown on validation failure
450 if (Array.isArray(validationError.errors) && validationError.errors.length) {
451 return validationError.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 (0, 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 ((0, 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 normalizeDataPathArr(it) {
548 return it.dataPathArr
549 .slice(1, it.dataLevel + 1)
550 .map((p) => (typeof p === 'number' ? p : p.str.replace(/"/g, '')));
551 }
552}
553exports.CoreSchemaRegistry = CoreSchemaRegistry;