@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 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 (dir instanceof URL) dir = dir.pathname;
53
- else dir = String(dir)
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(Schemas.query(schemas.query));
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(Schemas.param(match, schemas[match]));
146
+ flow[1].push(Middleware.param(match, schemas[match]));
163
147
  }
164
148
 
165
149
  flow[1].push(this.validate(opts));
166
150
 
167
- return flow;
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
- * Express middleware to identify query params that should be integers/booleans according to the schema
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
- static query(schema) {
201
- return function (req, res, next) {
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": "4.4.0",
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"]