"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const path_1 = __importDefault(require("path")); const fs_1 = __importDefault(require("fs")); const lodash_1 = __importDefault(require("lodash")); const ajv_1 = __importDefault(require("ajv")); const probe_image_size_1 = __importDefault(require("probe-image-size")); const read_chunk_1 = __importDefault(require("read-chunk")); const json_schema_traverse_1 = __importDefault(require("json-schema-traverse")); const Util_1 = require("./Util"); const Error_1 = require("./Error"); var Error_2 = require("./Error"); exports.SchemerError = Error_2.SchemerError; exports.ValidationError = Error_2.ValidationError; exports.ErrorCodes = Error_2.ErrorCodes; class Schemer { // Schema is a JSON Schema object constructor(schema, options = {}) { this.options = lodash_1.default.extend({ allErrors: true, verbose: true, format: 'full', metaValidation: true, }, options); this.ajv = new ajv_1.default(this.options); this.schema = schema; this.rootDir = this.options.rootDir || __dirname; this.manualValidationErrors = []; } _formatAjvErrorMessage({ keyword, dataPath, params, parentSchema, data, message, }) { const meta = parentSchema && parentSchema.meta; // This removes the "." in front of a fieldPath dataPath = dataPath.slice(1); switch (keyword) { case 'additionalProperties': { return new Error_1.ValidationError({ errorCode: 'SCHEMA_ADDITIONAL_PROPERTY', fieldPath: dataPath, message: `should NOT have additional property '${params.additionalProperty}'`, data, meta, }); } case 'required': return new Error_1.ValidationError({ errorCode: 'SCHEMA_MISSING_REQUIRED_PROPERTY', fieldPath: dataPath, message: `is missing required property '${params.missingProperty}'`, data, meta, }); case 'pattern': { //@TODO Parse the message in a less hacky way. Perhaps for regex validation errors, embed the error message under the meta tag? const regexHuman = lodash_1.default.get(parentSchema, 'meta.regexHuman'); const regexErrorMessage = regexHuman ? `'${dataPath}' should be a ${regexHuman[0].toLowerCase() + regexHuman.slice(1)}` : `'${dataPath}' ${message}`; return new Error_1.ValidationError({ errorCode: 'SCHEMA_INVALID_PATTERN', fieldPath: dataPath, message: regexErrorMessage, data, meta, }); } default: return new Error_1.ValidationError({ errorCode: 'SCHEMA_VALIDATION_ERROR', fieldPath: dataPath, message: message || 'Validation error', data, meta, }); } } getErrors() { // Convert AJV JSONSchema errors to our ValidationErrors let valErrors = []; if (this.ajv.errors) { valErrors = this.ajv.errors.map(e => this._formatAjvErrorMessage(e)); } const bothErrors = lodash_1.default.concat(valErrors, this.manualValidationErrors); return bothErrors; } _throwOnErrors() { // Clean error state after each validation const errors = this.getErrors(); if (errors.length > 0) { this.manualValidationErrors = []; this.ajv.errors = []; throw new Error_1.SchemerError(errors); } } validateAll(data) { return __awaiter(this, void 0, void 0, function* () { yield this._validateSchemaAsync(data); yield this._validateAssetsAsync(data); this._throwOnErrors(); }); } validateAssetsAsync(data) { return __awaiter(this, void 0, void 0, function* () { yield this._validateAssetsAsync(data); this._throwOnErrors(); }); } validateSchemaAsync(data) { return __awaiter(this, void 0, void 0, function* () { yield this._validateSchemaAsync(data); this._throwOnErrors(); }); } _validateSchemaAsync(data) { this.ajv.validate(this.schema, data); } _validateAssetsAsync(data) { return __awaiter(this, void 0, void 0, function* () { let assets = []; json_schema_traverse_1.default(this.schema, { allKeys: true }, (subSchema, jsonPointer, a, b, c, d, property) => { if (property && subSchema.meta && subSchema.meta.asset) { const fieldPath = Util_1.schemaPointerToFieldPath(jsonPointer); assets.push({ fieldPath, data: lodash_1.default.get(data, fieldPath), meta: subSchema.meta, }); } }); yield Promise.all(assets.map(this._validateAssetAsync.bind(this))); }); } _validateImageAsync({ fieldPath, data, meta }) { return __awaiter(this, void 0, void 0, function* () { if (meta && meta.asset && data) { const { dimensions, square, contentTypePattern } = meta; // filePath could be an URL const filePath = path_1.default.resolve(this.rootDir, data); try { // NOTE(nikki): The '4100' below should be enough for most file types, though we // could probably go shorter.... // http://www.garykessler.net/library/file_sigs.html // The metadata content for .jpgs might be located a lot farther down the file, so this // may pose problems in the future. // This cases on whether filePath is a remote URL or located on the machine const probeResult = fs_1.default.existsSync(filePath) ? probe_image_size_1.default.sync(yield read_chunk_1.default(filePath, 0, 4100)) : yield probe_image_size_1.default(data, { useElectronNet: false }); if (!probeResult) { return; } const { width, height, type, mime } = probeResult; if (contentTypePattern && !mime.match(new RegExp(contentTypePattern))) { this.manualValidationErrors.push(new Error_1.ValidationError({ errorCode: 'INVALID_CONTENT_TYPE', fieldPath, message: `field '${fieldPath}' should point to ${meta.contentTypeHuman} but the file at '${data}' has type ${type}`, data, meta, })); } if (dimensions && (dimensions.height !== height || dimensions.width !== width)) { this.manualValidationErrors.push(new Error_1.ValidationError({ errorCode: 'INVALID_DIMENSIONS', fieldPath, message: `'${fieldPath}' should have dimensions ${dimensions.width}x${dimensions.height}, but the file at '${data}' has dimensions ${width}x${height}`, data, meta, })); } if (square && width !== height) { this.manualValidationErrors.push(new Error_1.ValidationError({ errorCode: 'NOT_SQUARE', fieldPath, message: `image should be square, but the file at '${data}' has dimensions ${width}x${height}`, data, meta, })); } } catch (e) { this.manualValidationErrors.push(new Error_1.ValidationError({ errorCode: 'INVALID_ASSET_URI', fieldPath, message: `cannot access file at '${data}'`, data, meta, })); } } }); } _validateAssetAsync({ fieldPath, data, meta }) { return __awaiter(this, void 0, void 0, function* () { if (meta && meta.asset && data) { if (meta.contentTypePattern && meta.contentTypePattern.startsWith('^image')) { yield this._validateImageAsync({ fieldPath, data, meta }); } } }); } validateProperty(fieldPath, data) { return __awaiter(this, void 0, void 0, function* () { const subSchema = Util_1.fieldPathToSchema(this.schema, fieldPath); this.ajv.validate(subSchema, data); if (subSchema.meta && subSchema.meta.asset) { yield this._validateAssetAsync({ fieldPath, data, meta: subSchema.meta }); } this._throwOnErrors(); }); } validateName(name) { return this.validateProperty('name', name); } validateSlug(slug) { return this.validateProperty('slug', slug); } validateSdkVersion(version) { return this.validateProperty('sdkVersion', version); } validateIcon(iconPath) { return this.validateProperty('icon', iconPath); } } exports.default = Schemer; //# sourceMappingURL=index.js.map