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 | const path_1 = __importDefault(require("path"));
|
7 | const fs_1 = __importDefault(require("fs"));
|
8 | const lodash_1 = __importDefault(require("lodash"));
|
9 | const ajv_1 = __importDefault(require("ajv"));
|
10 | const probe_image_size_1 = __importDefault(require("probe-image-size"));
|
11 | const read_chunk_1 = __importDefault(require("read-chunk"));
|
12 | const json_schema_traverse_1 = __importDefault(require("json-schema-traverse"));
|
13 | const Util_1 = require("./Util");
|
14 | const Error_1 = require("./Error");
|
15 | var Error_2 = require("./Error");
|
16 | exports.SchemerError = Error_2.SchemerError;
|
17 | exports.ValidationError = Error_2.ValidationError;
|
18 | exports.ErrorCodes = Error_2.ErrorCodes;
|
19 | class Schemer {
|
20 |
|
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 |
|
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 |
|
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 |
|
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 |
|
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 |
|
131 | const filePath = path_1.default.resolve(this.rootDir, data);
|
132 | try {
|
133 |
|
134 |
|
135 |
|
136 |
|
137 |
|
138 |
|
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 | }
|
213 | exports.default = Schemer;
|
214 |
|
\ | No newline at end of file |