UNPKG

9.33 kBJavaScriptView Raw
1"use strict";
2var __importDefault = (this && this.__importDefault) || function (mod) {
3 return (mod && mod.__esModule) ? mod : { "default": mod };
4};
5Object.defineProperty(exports, "__esModule", { value: true });
6const path_1 = __importDefault(require("path"));
7const fs_1 = __importDefault(require("fs"));
8const lodash_1 = __importDefault(require("lodash"));
9const ajv_1 = __importDefault(require("ajv"));
10const probe_image_size_1 = __importDefault(require("probe-image-size"));
11const read_chunk_1 = __importDefault(require("read-chunk"));
12const json_schema_traverse_1 = __importDefault(require("json-schema-traverse"));
13const Util_1 = require("./Util");
14const Error_1 = require("./Error");
15var Error_2 = require("./Error");
16exports.SchemerError = Error_2.SchemerError;
17exports.ValidationError = Error_2.ValidationError;
18exports.ErrorCodes = Error_2.ErrorCodes;
19class Schemer {
20 // Schema is a JSON Schema object
21 constructor(schema, options = {}) {
22 this.options = lodash_1.default.extend({
23 allErrors: true,
24 verbose: true,
25 format: 'full',
26 metaValidation: true,
27 }, options);
28 this.ajv = new ajv_1.default(this.options);
29 this.schema = schema;
30 this.rootDir = this.options.rootDir || __dirname;
31 this.manualValidationErrors = [];
32 }
33 _formatAjvErrorMessage({ keyword, dataPath, params, parentSchema, data, message, }) {
34 const meta = parentSchema && parentSchema.meta;
35 // This removes the "." in front of a fieldPath
36 dataPath = dataPath.slice(1);
37 switch (keyword) {
38 case 'additionalProperties': {
39 return new Error_1.ValidationError({
40 errorCode: 'SCHEMA_ADDITIONAL_PROPERTY',
41 fieldPath: dataPath,
42 message: `should NOT have additional property '${params.additionalProperty}'`,
43 data,
44 meta,
45 });
46 }
47 case 'required':
48 return new Error_1.ValidationError({
49 errorCode: 'SCHEMA_MISSING_REQUIRED_PROPERTY',
50 fieldPath: dataPath,
51 message: `is missing required property '${params.missingProperty}'`,
52 data,
53 meta,
54 });
55 case 'pattern': {
56 //@TODO Parse the message in a less hacky way. Perhaps for regex validation errors, embed the error message under the meta tag?
57 const regexHuman = lodash_1.default.get(parentSchema, 'meta.regexHuman');
58 const regexErrorMessage = regexHuman
59 ? `'${dataPath}' should be a ${regexHuman[0].toLowerCase() + regexHuman.slice(1)}`
60 : `'${dataPath}' ${message}`;
61 return new Error_1.ValidationError({
62 errorCode: 'SCHEMA_INVALID_PATTERN',
63 fieldPath: dataPath,
64 message: regexErrorMessage,
65 data,
66 meta,
67 });
68 }
69 default:
70 return new Error_1.ValidationError({
71 errorCode: 'SCHEMA_VALIDATION_ERROR',
72 fieldPath: dataPath,
73 message: message || 'Validation error',
74 data,
75 meta,
76 });
77 }
78 }
79 getErrors() {
80 // Convert AJV JSONSchema errors to our ValidationErrors
81 let valErrors = [];
82 if (this.ajv.errors) {
83 valErrors = this.ajv.errors.map(e => this._formatAjvErrorMessage(e));
84 }
85 const bothErrors = lodash_1.default.concat(valErrors, this.manualValidationErrors);
86 return bothErrors;
87 }
88 _throwOnErrors() {
89 // Clean error state after each validation
90 const errors = this.getErrors();
91 if (errors.length > 0) {
92 this.manualValidationErrors = [];
93 this.ajv.errors = [];
94 throw new Error_1.SchemerError(errors);
95 }
96 }
97 async validateAll(data) {
98 await this._validateSchemaAsync(data);
99 await this._validateAssetsAsync(data);
100 this._throwOnErrors();
101 }
102 async validateAssetsAsync(data) {
103 await this._validateAssetsAsync(data);
104 this._throwOnErrors();
105 }
106 async validateSchemaAsync(data) {
107 await this._validateSchemaAsync(data);
108 this._throwOnErrors();
109 }
110 _validateSchemaAsync(data) {
111 this.ajv.validate(this.schema, data);
112 }
113 async _validateAssetsAsync(data) {
114 let assets = [];
115 json_schema_traverse_1.default(this.schema, { allKeys: true }, (subSchema, jsonPointer, a, b, c, d, property) => {
116 if (property && subSchema.meta && subSchema.meta.asset) {
117 const fieldPath = Util_1.schemaPointerToFieldPath(jsonPointer);
118 assets.push({
119 fieldPath,
120 data: lodash_1.default.get(data, fieldPath),
121 meta: subSchema.meta,
122 });
123 }
124 });
125 await Promise.all(assets.map(this._validateAssetAsync.bind(this)));
126 }
127 async _validateImageAsync({ fieldPath, data, meta }) {
128 if (meta && meta.asset && data) {
129 const { dimensions, square, contentTypePattern } = meta;
130 // filePath could be an URL
131 const filePath = path_1.default.resolve(this.rootDir, data);
132 try {
133 // NOTE(nikki): The '4100' below should be enough for most file types, though we
134 // could probably go shorter....
135 // http://www.garykessler.net/library/file_sigs.html
136 // The metadata content for .jpgs might be located a lot farther down the file, so this
137 // may pose problems in the future.
138 // This cases on whether filePath is a remote URL or located on the machine
139 const probeResult = fs_1.default.existsSync(filePath)
140 ? probe_image_size_1.default.sync(await read_chunk_1.default(filePath, 0, 4100))
141 : await probe_image_size_1.default(data, { useElectronNet: false });
142 if (!probeResult) {
143 return;
144 }
145 const { width, height, type, mime } = probeResult;
146 if (contentTypePattern && !mime.match(new RegExp(contentTypePattern))) {
147 this.manualValidationErrors.push(new Error_1.ValidationError({
148 errorCode: 'INVALID_CONTENT_TYPE',
149 fieldPath,
150 message: `field '${fieldPath}' should point to ${meta.contentTypeHuman} but the file at '${data}' has type ${type}`,
151 data,
152 meta,
153 }));
154 }
155 if (dimensions && (dimensions.height !== height || dimensions.width !== width)) {
156 this.manualValidationErrors.push(new Error_1.ValidationError({
157 errorCode: 'INVALID_DIMENSIONS',
158 fieldPath,
159 message: `'${fieldPath}' should have dimensions ${dimensions.width}x${dimensions.height}, but the file at '${data}' has dimensions ${width}x${height}`,
160 data,
161 meta,
162 }));
163 }
164 if (square && width !== height) {
165 this.manualValidationErrors.push(new Error_1.ValidationError({
166 errorCode: 'NOT_SQUARE',
167 fieldPath,
168 message: `image should be square, but the file at '${data}' has dimensions ${width}x${height}`,
169 data,
170 meta,
171 }));
172 }
173 }
174 catch (e) {
175 this.manualValidationErrors.push(new Error_1.ValidationError({
176 errorCode: 'INVALID_ASSET_URI',
177 fieldPath,
178 message: `cannot access file at '${data}'`,
179 data,
180 meta,
181 }));
182 }
183 }
184 }
185 async _validateAssetAsync({ fieldPath, data, meta }) {
186 if (meta && meta.asset && data) {
187 if (meta.contentTypePattern && meta.contentTypePattern.startsWith('^image')) {
188 await this._validateImageAsync({ fieldPath, data, meta });
189 }
190 }
191 }
192 async validateProperty(fieldPath, data) {
193 const subSchema = Util_1.fieldPathToSchema(this.schema, fieldPath);
194 this.ajv.validate(subSchema, data);
195 if (subSchema.meta && subSchema.meta.asset) {
196 await this._validateAssetAsync({ fieldPath, data, meta: subSchema.meta });
197 }
198 this._throwOnErrors();
199 }
200 validateName(name) {
201 return this.validateProperty('name', name);
202 }
203 validateSlug(slug) {
204 return this.validateProperty('slug', slug);
205 }
206 validateSdkVersion(version) {
207 return this.validateProperty('sdkVersion', version);
208 }
209 validateIcon(iconPath) {
210 return this.validateProperty('icon', iconPath);
211 }
212}
213exports.default = Schemer;
214//# sourceMappingURL=index.js.map
\No newline at end of file