1 | "use strict";
|
2 | var __importDefault = (this && this.__importDefault) || function (mod) {
|
3 | return (mod && mod.__esModule) ? mod : { "default": mod };
|
4 | };
|
5 | Object.defineProperty(exports, "__esModule", { value: true });
|
6 | exports.ErrorCodes = exports.ValidationError = exports.SchemerError = void 0;
|
7 | const ajv_1 = __importDefault(require("ajv"));
|
8 | const ajv_formats_1 = __importDefault(require("ajv-formats"));
|
9 | const fs_1 = __importDefault(require("fs"));
|
10 | const json_schema_traverse_1 = __importDefault(require("json-schema-traverse"));
|
11 | const path_1 = __importDefault(require("path"));
|
12 | const probe_image_size_1 = __importDefault(require("probe-image-size"));
|
13 | const Error_1 = require("./Error");
|
14 | const Util_1 = require("./Util");
|
15 | function lowerFirst(str) {
|
16 | return str.charAt(0).toLowerCase() + str.slice(1);
|
17 | }
|
18 | var Error_2 = require("./Error");
|
19 | Object.defineProperty(exports, "SchemerError", { enumerable: true, get: function () { return Error_2.SchemerError; } });
|
20 | Object.defineProperty(exports, "ValidationError", { enumerable: true, get: function () { return Error_2.ValidationError; } });
|
21 | Object.defineProperty(exports, "ErrorCodes", { enumerable: true, get: function () { return Error_2.ErrorCodes; } });
|
22 | class Schemer {
|
23 | options;
|
24 | ajv;
|
25 | schema;
|
26 | rootDir;
|
27 | manualValidationErrors;
|
28 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
154 | const filePath = path_1.default.resolve(this.rootDir, data);
|
155 | try {
|
156 |
|
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 | }
|
242 | exports.default = Schemer;
|
243 |
|
\ | No newline at end of file |