UNPKG

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