@salesforce/core
Version:
Core libraries to interact with SFDX projects, orgs, and APIs.
191 lines • 8.25 kB
JavaScript
;
/*
* Copyright (c) 2020, salesforce.com, inc.
* All rights reserved.
* Licensed under the BSD 3-Clause license.
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SchemaValidator = void 0;
const path = require("path");
const kit_1 = require("@salesforce/kit");
const ts_types_1 = require("@salesforce/ts-types");
const validator = require("jsen");
const sfdxError_1 = require("../sfdxError");
const fs_1 = require("../util/fs");
/**
* Loads a JSON schema and performs validations against JSON objects.
*/
class SchemaValidator {
/**
* Creates a new `SchemaValidator` instance given a logger and path to a schema file.
*
* @param logger An {@link Logger} instance on which to base this class's logger.
* @param schemaPath The path to the schema file to load and use for validation.
*/
constructor(logger, schemaPath) {
this.schemaPath = schemaPath;
this.logger = logger.child('SchemaValidator');
this.schemasDir = path.dirname(this.schemaPath);
}
/**
* Loads a JSON schema from the `schemaPath` parameter provided at instantiation.
*/
async load() {
if (!this.schema) {
this.schema = await fs_1.fs.readJsonMap(this.schemaPath);
this.logger.debug(`Schema loaded for ${this.schemaPath}`);
}
return this.schema;
}
/**
* Loads a JSON schema from the `schemaPath` parameter provided at instantiation.
*/
loadSync() {
if (!this.schema) {
this.schema = fs_1.fs.readJsonMapSync(this.schemaPath);
this.logger.debug(`Schema loaded for ${this.schemaPath}`);
}
return this.schema;
}
/**
* Performs validation of JSON data against the schema located at the `schemaPath` value provided
* at instantiation.
*
* **Throws** *{@link SfdxError}{ name: 'ValidationSchemaFieldError' }* If there are known validations errors.
* **Throws** *{@link SfdxError}{ name: 'ValidationSchemaUnknownError' }* If there are unknown validations errors.
*
* @param json A JSON value to validate against this instance's target schema.
* @returns The validated JSON data.
*/
// eslint-disable-next-line @typescript-eslint/require-await
async validate(json) {
return this.validateSync(json);
}
/**
* Performs validation of JSON data against the schema located at the `schemaPath` value provided
* at instantiation.
*
* **Throws** *{@link SfdxError}{ name: 'ValidationSchemaFieldError' }* If there are known validations errors.
* **Throws** *{@link SfdxError}{ name: 'ValidationSchemaUnknownError' }* If there are unknown validations errors.
*
* @param json A JSON value to validate against this instance's target schema.
* @returns The validated JSON data.
*/
validateSync(json) {
const schema = this.loadSync();
const externalSchemas = this.loadExternalSchemas(schema);
// TODO: We should default to throw an error when a property is specified
// that is not in the schema, but the only option to do this right now is
// to specify "removeAdditional: false" in every object.
const validate = validator(schema, {
greedy: true,
schemas: externalSchemas,
});
if (!validate(json)) {
if (validate.errors) {
const errors = this.getErrorsText(validate.errors, schema);
throw new sfdxError_1.SfdxError(`Validation errors:\n${errors}`, 'ValidationSchemaFieldError');
}
else {
throw new sfdxError_1.SfdxError('Unknown schema validation error', 'ValidationSchemaUnknownError');
}
}
return validate.build(json);
}
/**
* Loads local, external schemas from URIs relative to the local schema file. Does not support loading from
* remote URIs. Returns a map of external schema local URIs to loaded schema JSON objects.
*
* @param schema The main schema to validate against.
*/
loadExternalSchemas(schema) {
const externalSchemas = {};
const schemas = kit_1.getJsonValuesByName(schema, '$ref')
// eslint-disable-next-line no-useless-escape
.map((ref) => ref && RegExp(/([\w\.]+)#/).exec(ref))
.map((match) => match && match[1])
.filter((uri) => !!uri)
.map((uri) => this.loadExternalSchema(uri));
schemas.forEach((externalSchema) => {
if (ts_types_1.isString(externalSchema.id)) {
externalSchemas[externalSchema.id] = externalSchema;
}
else {
throw new sfdxError_1.SfdxError(`Unexpected external schema id type: ${typeof externalSchema.id}`, 'ValidationSchemaTypeError');
}
});
return externalSchemas;
}
/**
* Load another schema relative to the primary schema when referenced. Only supports local schema URIs.
*
* @param uri The first segment of the $ref schema.
*/
loadExternalSchema(uri) {
const schemaPath = path.join(this.schemasDir, `${uri}.json`);
try {
return fs_1.fs.readJsonMapSync(schemaPath);
}
catch (err) {
if (err.code === 'ENOENT') {
throw new sfdxError_1.SfdxError(`Schema not found: ${schemaPath}`, 'ValidationSchemaNotFound');
}
throw err;
}
}
/**
* Get a string representation of the schema validation errors.
*
* @param errors An array of JsenValidateError objects.
* @param schema The validation schema.
*/
getErrorsText(errors, schema) {
return errors
.map((error) => {
// eslint-disable-next-line no-useless-escape
const property = RegExp(/^([a-zA-Z0-9\.]+)\.([a-zA-Z0-9]+)$/).exec(error.path);
const getPropValue = (prop) => {
const reducer = (obj, name) => {
if (!ts_types_1.isJsonMap(obj))
return;
if (ts_types_1.isJsonMap(obj.properties))
return obj.properties[name];
if (name === '0')
return ts_types_1.asJsonArray(obj.items);
return obj[name] || obj[prop];
};
return error.path.split('.').reduce(reducer, schema);
};
const getEnumValues = () => {
const enumSchema = ts_types_1.asJsonMap(getPropValue('enum'));
return (enumSchema && ts_types_1.getJsonArray(enumSchema, 'enum', []).join(', ')) || '';
};
switch (error.keyword) {
case 'additionalProperties':
// Missing Typing
// eslint-disable-next-line no-case-declarations
const additionalProperties = ts_types_1.get(error, 'additionalProperties');
return `${error.path} should NOT have additional properties '${additionalProperties}'`;
case 'required':
if (property) {
return `${property[1]} should have required property ${property[2]}`;
}
return `should have required property '${error.path}'`;
case 'oneOf':
return `${error.path} should match exactly one schema in oneOf`;
case 'enum':
return `${error.path} should be equal to one of the allowed values ${getEnumValues()}`;
case 'type': {
const _path = error.path === '' ? 'Root of JSON object' : error.path;
return `${_path} is an invalid type. Expected type [${getPropValue('type')}]`;
}
default:
return `${error.path} invalid ${error.keyword}`;
}
})
.join('\n');
}
}
exports.SchemaValidator = SchemaValidator;
//# sourceMappingURL=validator.js.map