UNPKG

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