@openaddresses/batch-schema 4.4.0 → 6.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/lib/middleware.js +148 -0
- package/lib/param.js +29 -0
- package/lib/schema.js +24 -141
- package/package.json +4 -1
- package/schema/res.ListSchema.json +0 -3
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,18 @@
|
|
|
10
10
|
|
|
11
11
|
## Version History
|
|
12
12
|
|
|
13
|
+
### v6.0.0
|
|
14
|
+
|
|
15
|
+
- :rocket: Remove additional properties if they are not present in `res` and `additionalProperties: false` is set
|
|
16
|
+
|
|
17
|
+
### v5.1.0
|
|
18
|
+
|
|
19
|
+
- :rocket: Support `URL` in `Schema.load`
|
|
20
|
+
|
|
21
|
+
### v5.0.0
|
|
22
|
+
|
|
23
|
+
- :tada: Logging and body parsing enabled by default
|
|
24
|
+
|
|
13
25
|
### v4.4.0
|
|
14
26
|
|
|
15
27
|
- :rocket: Add ability to take a `URL` as a schemas directory
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import Err from './error.js';
|
|
2
|
+
import Param from './param.js';
|
|
3
|
+
import Ajv from 'ajv';
|
|
4
|
+
import addFormats from 'ajv-formats';
|
|
5
|
+
import { ValidationError } from 'express-json-validator-middleware';
|
|
6
|
+
|
|
7
|
+
const ajv = addFormats(new Ajv({
|
|
8
|
+
allErrors: true,
|
|
9
|
+
removeAdditional: true
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @class
|
|
14
|
+
*/
|
|
15
|
+
export default class Middleware {
|
|
16
|
+
/**
|
|
17
|
+
* Express middleware to identify express params that should be integers/number/booleans
|
|
18
|
+
* according to the schema and attempt to cast them as such to ensure they pass the schema
|
|
19
|
+
*
|
|
20
|
+
* @param {String} match Express Param
|
|
21
|
+
* @param {String} type Type to coerce it to
|
|
22
|
+
*
|
|
23
|
+
* @returns {Function}
|
|
24
|
+
*/
|
|
25
|
+
static param(match, type) {
|
|
26
|
+
return function (req, res, next) {
|
|
27
|
+
try {
|
|
28
|
+
Param[type](req, match.replace(':', ''));
|
|
29
|
+
} catch (err) {
|
|
30
|
+
return Err.respond(err, res);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return next();
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Express middleware to identify query params that should be integers/booleans according to the schema
|
|
39
|
+
* and attempt to cast them as such to ensure they pass the schema
|
|
40
|
+
*
|
|
41
|
+
* @param {Object} schema JSON Schema
|
|
42
|
+
*
|
|
43
|
+
* @returns {Function}
|
|
44
|
+
*/
|
|
45
|
+
static query(schema) {
|
|
46
|
+
return function (req, res, next) {
|
|
47
|
+
for (const key in req.query) {
|
|
48
|
+
if (!schema.properties[key] || !schema.properties[key].type) continue;
|
|
49
|
+
|
|
50
|
+
// For easier processing use consistent array format IE: `type: ["integer", "boolean"]` vs type: "integer"
|
|
51
|
+
if (!Array.isArray(schema.properties[key].type)) {
|
|
52
|
+
schema.properties[key].type = [schema.properties[key].type];
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
for (const type of schema.properties[key].type) {
|
|
56
|
+
if (type === 'array') {
|
|
57
|
+
req.query[key] = req.query[key].split(',');
|
|
58
|
+
|
|
59
|
+
if (schema.properties[key].items && schema.properties[key].items.type === 'number') {
|
|
60
|
+
req.query[key] = req.query[key].map((k) => {
|
|
61
|
+
return Number(k);
|
|
62
|
+
});
|
|
63
|
+
} else if (schema.properties[key].items && schema.properties[key].items.type === 'integer') {
|
|
64
|
+
req.query[key] = req.query[key].map((k) => {
|
|
65
|
+
return parseInt(k);
|
|
66
|
+
});
|
|
67
|
+
} else if (schema.properties[key].items && schema.properties[key].items.type === 'boolean') {
|
|
68
|
+
req.query[key] = req.query[key].map((k) => {
|
|
69
|
+
if (k === 'true') {
|
|
70
|
+
return true;
|
|
71
|
+
} else if (k === 'false') {
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
} else if (type === 'integer' && !isNaN(parseInt(req.query[key]))) {
|
|
77
|
+
req.query[key] = parseInt(req.query[key]);
|
|
78
|
+
} else if (type === 'number' && !isNaN(Number(req.query[key]))) {
|
|
79
|
+
req.query[key] = Number(req.query[key]);
|
|
80
|
+
} else if (type === 'boolean') {
|
|
81
|
+
if (req.query[key] === 'true') {
|
|
82
|
+
req.query[key] = true;
|
|
83
|
+
} else if (req.query[key] === 'false') {
|
|
84
|
+
req.query[key] = false;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
for (const key in schema.properties) {
|
|
91
|
+
if (req.query[key] === undefined && schema.properties[key].default !== undefined) {
|
|
92
|
+
req.query[key] = schema.properties[key].default;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return next();
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Express middleware to validate the response body
|
|
102
|
+
*
|
|
103
|
+
* @param {Object} schema JSON Schema
|
|
104
|
+
*
|
|
105
|
+
* @returns {Function}
|
|
106
|
+
*/
|
|
107
|
+
static res(schema) {
|
|
108
|
+
const validate = ajv.compile(schema);
|
|
109
|
+
|
|
110
|
+
return function (req, res, next) {
|
|
111
|
+
const json = res.json;
|
|
112
|
+
res.json = function(obj) {
|
|
113
|
+
validate(obj);
|
|
114
|
+
|
|
115
|
+
json.call(this, obj);
|
|
116
|
+
};
|
|
117
|
+
next();
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Convert validation errors into standardized JSON Error Messages
|
|
123
|
+
*
|
|
124
|
+
* @returns {Function}
|
|
125
|
+
*/
|
|
126
|
+
static error() {
|
|
127
|
+
return function(err, req, res, next) {
|
|
128
|
+
if (err instanceof ValidationError) {
|
|
129
|
+
let errs = [];
|
|
130
|
+
if (err.validationErrors.body) {
|
|
131
|
+
errs = errs.concat(err.validationErrors.body);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (err.validationErrors.query) {
|
|
135
|
+
errs = errs.concat(err.validationErrors.query);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return Err.respond(
|
|
139
|
+
new Err(400, null, 'validation error'),
|
|
140
|
+
res,
|
|
141
|
+
errs
|
|
142
|
+
);
|
|
143
|
+
} else {
|
|
144
|
+
next(err);
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
}
|
package/lib/param.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import Err from './error.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @class
|
|
5
|
+
*/
|
|
6
|
+
export default class Param {
|
|
7
|
+
static integer(req, name) {
|
|
8
|
+
req.params[name] = parseInt(req.params[name]);
|
|
9
|
+
if (isNaN(req.params[name])) {
|
|
10
|
+
throw new Err(400, null, `${name} param must be an integer`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
static number(req, name) {
|
|
15
|
+
req.params[name] = Number(req.params[name]);
|
|
16
|
+
if (isNaN(req.params[name])) {
|
|
17
|
+
throw new Err(400, null, `${name} param must be numeric`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
static boolean(req, name) {
|
|
22
|
+
if (!['true', 'false'].includes(req.params[name])) throw new Error(`${name} param must be a boolean`);
|
|
23
|
+
req.params[name] = req.params[name] === true ? true : false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
static string(req, name) {
|
|
27
|
+
req.params[name] = String(req.params[name]);
|
|
28
|
+
}
|
|
29
|
+
}
|
package/lib/schema.js
CHANGED
|
@@ -1,39 +1,13 @@
|
|
|
1
1
|
import { Validator } from 'express-json-validator-middleware';
|
|
2
|
-
import { ValidationError } from 'express-json-validator-middleware';
|
|
3
2
|
import $RefParser from 'json-schema-ref-parser';
|
|
4
3
|
import fs from 'fs';
|
|
5
4
|
import path from 'path';
|
|
5
|
+
import morgan from 'morgan';
|
|
6
|
+
import bodyparser from 'body-parser';
|
|
6
7
|
|
|
7
|
-
import Err from './error.js';
|
|
8
8
|
import SchemaRoute from '../routes/schema.js';
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* @class
|
|
12
|
-
*/
|
|
13
|
-
class Param {
|
|
14
|
-
static integer(req, name) {
|
|
15
|
-
req.params[name] = parseInt(req.params[name]);
|
|
16
|
-
if (isNaN(req.params[name])) {
|
|
17
|
-
throw new Err(400, null, `${name} param must be an integer`);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
static number(req, name) {
|
|
22
|
-
req.params[name] = Number(req.params[name]);
|
|
23
|
-
if (isNaN(req.params[name])) {
|
|
24
|
-
throw new Err(400, null, `${name} param must be numeric`);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
static boolean(req, name) {
|
|
29
|
-
if (!['true', 'false'].includes(req.params[name])) throw new Error(`${name} param must be a boolean`);
|
|
30
|
-
req.params[name] = req.params[name] === true ? true : false;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
static string(req, name) {
|
|
34
|
-
req.params[name] = String(req.params[name]);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
9
|
+
import Param from './param.js';
|
|
10
|
+
import Middleware from './middleware.js';
|
|
37
11
|
|
|
38
12
|
/**
|
|
39
13
|
* @class
|
|
@@ -41,6 +15,8 @@ class Param {
|
|
|
41
15
|
* @param {Object} router Express Router Object
|
|
42
16
|
* @param {Object} opts Options Object
|
|
43
17
|
* @param {String} opts.schemas Schemas Path
|
|
18
|
+
* @param {boolean|string} [opts.logging=true] disable logging with false
|
|
19
|
+
* @param {number} [opts.limit=50] body size limit in mb
|
|
44
20
|
*/
|
|
45
21
|
export default class Schemas {
|
|
46
22
|
constructor(router, opts = {}) {
|
|
@@ -49,8 +25,8 @@ export default class Schemas {
|
|
|
49
25
|
if (!opts.schemas) {
|
|
50
26
|
this.schemas_path = new URL('../schema/', import.meta.url).pathname;
|
|
51
27
|
} else {
|
|
52
|
-
if (
|
|
53
|
-
else
|
|
28
|
+
if (opts.schemas instanceof URL) opts.schemas = opts.schemas.pathname;
|
|
29
|
+
else opts.schemas = String(opts.schemas);
|
|
54
30
|
|
|
55
31
|
this.schemas_path = opts.schemas;
|
|
56
32
|
}
|
|
@@ -62,6 +38,11 @@ export default class Schemas {
|
|
|
62
38
|
});
|
|
63
39
|
|
|
64
40
|
this.router = router;
|
|
41
|
+
|
|
42
|
+
if (opts.morgan !== false) this.router.use(morgan('combined'));
|
|
43
|
+
this.router.use(bodyparser.urlencoded({ extended: true }));
|
|
44
|
+
this.router.use(bodyparser.json({ limit: `${opts.limit}mb` }));
|
|
45
|
+
|
|
65
46
|
this.schemas = new Map();
|
|
66
47
|
this.validate = this.validator.validate;
|
|
67
48
|
}
|
|
@@ -80,6 +61,9 @@ export default class Schemas {
|
|
|
80
61
|
* @param {boolean} opts.silent Squelch StdOut
|
|
81
62
|
*/
|
|
82
63
|
async load(dirpath, config, opts = {}) {
|
|
64
|
+
if (dirpath instanceof URL) dirpath = dirpath.pathname;
|
|
65
|
+
else dirpath = String(dirpath);
|
|
66
|
+
|
|
83
67
|
// Load dynamic routes directory
|
|
84
68
|
for (const r of fs.readdirSync(dirpath)) {
|
|
85
69
|
if (!opts.silent) console.error(`ok - loaded routes/${r}`);
|
|
@@ -152,104 +136,29 @@ export default class Schemas {
|
|
|
152
136
|
|
|
153
137
|
const flow = [parsed[1], []];
|
|
154
138
|
|
|
155
|
-
if (schemas.query) flow[1].push(
|
|
139
|
+
if (schemas.query) flow[1].push(Middleware.query(schemas.query));
|
|
156
140
|
|
|
157
141
|
// Make sure express params are validated/coerced into proper type
|
|
158
142
|
const matches = url.match(/(:.+?)(?=\/|\.|$)/g);
|
|
159
143
|
if (matches) for (const match of matches) {
|
|
160
144
|
if (!schemas[match]) throw new Error(`${match} type is not defined in schema`);
|
|
161
145
|
|
|
162
|
-
flow[1].push(
|
|
146
|
+
flow[1].push(Middleware.param(match, schemas[match]));
|
|
163
147
|
}
|
|
164
148
|
|
|
165
149
|
flow[1].push(this.validate(opts));
|
|
166
150
|
|
|
167
|
-
|
|
168
|
-
|
|
151
|
+
console.error(schemas);
|
|
152
|
+
if (schemas.res) flow[1].push(Middleware.res(schemas.res));
|
|
169
153
|
|
|
170
|
-
|
|
171
|
-
* Express middleware to identify express params that should be integers/number/booleans
|
|
172
|
-
* according to the schema and attempt to cast them as such to ensure they pass the schema
|
|
173
|
-
*
|
|
174
|
-
* @param {String} match Express Param
|
|
175
|
-
* @param {String} type Type to coerce it to
|
|
176
|
-
*
|
|
177
|
-
* @returns {Function}
|
|
178
|
-
*/
|
|
179
|
-
static param(match, type) {
|
|
180
|
-
return function (req, res, next) {
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
Param[type](req, match.replace(':', ''));
|
|
184
|
-
} catch (err) {
|
|
185
|
-
return Err.respond(err, res);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return next();
|
|
189
|
-
};
|
|
154
|
+
return flow;
|
|
190
155
|
}
|
|
191
156
|
|
|
192
157
|
/**
|
|
193
|
-
*
|
|
194
|
-
* and attempt to cast them as such to ensure they pass the schema
|
|
195
|
-
*
|
|
196
|
-
* @param {Object} schema JSON Schema
|
|
197
|
-
*
|
|
198
|
-
* @returns {Function}
|
|
158
|
+
* Convert validation errors into standardized JSON Error Messages
|
|
199
159
|
*/
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
for (const key in req.query) {
|
|
203
|
-
if (!schema.properties[key] || !schema.properties[key].type) continue;
|
|
204
|
-
|
|
205
|
-
// For easier processing use consistent array format IE: `type: ["integer", "boolean"]` vs type: "integer"
|
|
206
|
-
if (!Array.isArray(schema.properties[key].type)) {
|
|
207
|
-
schema.properties[key].type = [schema.properties[key].type];
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
for (const type of schema.properties[key].type) {
|
|
211
|
-
if (type === 'array') {
|
|
212
|
-
req.query[key] = req.query[key].split(',');
|
|
213
|
-
|
|
214
|
-
if (schema.properties[key].items && schema.properties[key].items.type === 'number') {
|
|
215
|
-
req.query[key] = req.query[key].map((k) => {
|
|
216
|
-
return Number(k);
|
|
217
|
-
});
|
|
218
|
-
} else if (schema.properties[key].items && schema.properties[key].items.type === 'integer') {
|
|
219
|
-
req.query[key] = req.query[key].map((k) => {
|
|
220
|
-
return parseInt(k);
|
|
221
|
-
});
|
|
222
|
-
} else if (schema.properties[key].items && schema.properties[key].items.type === 'boolean') {
|
|
223
|
-
req.query[key] = req.query[key].map((k) => {
|
|
224
|
-
if (k === 'true') {
|
|
225
|
-
return true;
|
|
226
|
-
} else if (k === 'false') {
|
|
227
|
-
return false;
|
|
228
|
-
}
|
|
229
|
-
});
|
|
230
|
-
}
|
|
231
|
-
} else if (type === 'integer' && !isNaN(parseInt(req.query[key]))) {
|
|
232
|
-
req.query[key] = parseInt(req.query[key]);
|
|
233
|
-
} else if (type === 'number' && !isNaN(Number(req.query[key]))) {
|
|
234
|
-
req.query[key] = Number(req.query[key]);
|
|
235
|
-
} else if (type === 'boolean') {
|
|
236
|
-
if (req.query[key] === 'true') {
|
|
237
|
-
req.query[key] = true;
|
|
238
|
-
} else if (req.query[key] === 'false') {
|
|
239
|
-
req.query[key] = false;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
for (const key in schema.properties) {
|
|
246
|
-
if (req.query[key] === undefined && schema.properties[key].default !== undefined) {
|
|
247
|
-
req.query[key] = schema.properties[key].default;
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
return next();
|
|
252
|
-
};
|
|
160
|
+
error() {
|
|
161
|
+
this.router.use(Middleware.error());
|
|
253
162
|
}
|
|
254
163
|
|
|
255
164
|
/**
|
|
@@ -286,32 +195,6 @@ export default class Schemas {
|
|
|
286
195
|
});
|
|
287
196
|
}
|
|
288
197
|
|
|
289
|
-
/**
|
|
290
|
-
* Convert validation errors into standardized JSON Error Messages
|
|
291
|
-
*/
|
|
292
|
-
error() {
|
|
293
|
-
this.router.use((err, req, res, next) => {
|
|
294
|
-
if (err instanceof ValidationError) {
|
|
295
|
-
let errs = [];
|
|
296
|
-
if (err.validationErrors.body) {
|
|
297
|
-
errs = errs.concat(err.validationErrors.body);
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (err.validationErrors.query) {
|
|
301
|
-
errs = errs.concat(err.validationErrors.query);
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return Err.respond(
|
|
305
|
-
new Err(400, null, 'validation error'),
|
|
306
|
-
res,
|
|
307
|
-
errs
|
|
308
|
-
);
|
|
309
|
-
} else {
|
|
310
|
-
next(err);
|
|
311
|
-
}
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
198
|
/**
|
|
316
199
|
* Return a list of endpoints with schemas
|
|
317
200
|
*
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"type": "module",
|
|
4
4
|
"root": true,
|
|
5
5
|
"main": "index.js",
|
|
6
|
-
"version": "
|
|
6
|
+
"version": "6.0.0",
|
|
7
7
|
"description": "Strongly Validated JSON Schema support for address",
|
|
8
8
|
"repository": "git@github.com:openaddresses/batch-schema",
|
|
9
9
|
"author": "ingalls <nick@ingalls.ca>",
|
|
@@ -19,10 +19,13 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"ajv": "^8.6.3",
|
|
22
|
+
"ajv-formats": "^2.1.1",
|
|
23
|
+
"body-parser": "^1.20.0",
|
|
22
24
|
"express": "^4.17.1",
|
|
23
25
|
"express-json-validator-middleware": "^3.0.0",
|
|
24
26
|
"glob": "^8.0.0",
|
|
25
27
|
"json-schema-ref-parser": "^9.0.9",
|
|
28
|
+
"morgan": "^1.10.0",
|
|
26
29
|
"tape": "^5.3.1"
|
|
27
30
|
},
|
|
28
31
|
"devDependencies": {
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"oneOf": [{
|
|
3
3
|
"type": "object",
|
|
4
|
-
"additionalProperties": false,
|
|
5
4
|
"patternProperties": {
|
|
6
5
|
".*": {
|
|
7
6
|
"type": "object",
|
|
@@ -10,7 +9,6 @@
|
|
|
10
9
|
"query",
|
|
11
10
|
"res"
|
|
12
11
|
],
|
|
13
|
-
"additionalProperties": false,
|
|
14
12
|
"properties": {
|
|
15
13
|
"body": {
|
|
16
14
|
"type": "boolean",
|
|
@@ -34,7 +32,6 @@
|
|
|
34
32
|
"res",
|
|
35
33
|
"query"
|
|
36
34
|
],
|
|
37
|
-
"additionalProperties": false,
|
|
38
35
|
"properties": {
|
|
39
36
|
"body": {
|
|
40
37
|
"type": ["null", "object"]
|