UNPKG

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