@openaddresses/batch-schema 5.0.0 → 7.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,19 @@
10
10
 
11
11
  ## Version History
12
12
 
13
+ ### v7.0.0
14
+
15
+ - :rocket: Remove PublicError class to it's own package
16
+ - :rocket: `Schema` is now exposed as the default
17
+
18
+ ### v6.0.0
19
+
20
+ - :rocket: Remove additional properties if they are not present in `res` and `additionalProperties: false` is set
21
+
22
+ ### v5.1.0
23
+
24
+ - :rocket: Support `URL` in `Schema.load`
25
+
13
26
  ### v5.0.0
14
27
 
15
28
  - :tada: Logging and body parsing enabled by default
package/index.js CHANGED
@@ -1,7 +1,3 @@
1
1
  import Schema from './lib/schema.js';
2
- import Err from './lib/error.js';
3
2
 
4
- export {
5
- Schema,
6
- Err
7
- };
3
+ export default Schema;
@@ -0,0 +1,148 @@
1
+ import Err from '@openaddresses/batch-error';
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 '@openaddresses/batch-error';
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,41 +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';
6
5
  import morgan from 'morgan';
7
6
  import bodyparser from 'body-parser';
8
7
 
9
- import Err from './error.js';
10
8
  import SchemaRoute from '../routes/schema.js';
11
-
12
- /**
13
- * @class
14
- */
15
- class Param {
16
- static integer(req, name) {
17
- req.params[name] = parseInt(req.params[name]);
18
- if (isNaN(req.params[name])) {
19
- throw new Err(400, null, `${name} param must be an integer`);
20
- }
21
- }
22
-
23
- static number(req, name) {
24
- req.params[name] = Number(req.params[name]);
25
- if (isNaN(req.params[name])) {
26
- throw new Err(400, null, `${name} param must be numeric`);
27
- }
28
- }
29
-
30
- static boolean(req, name) {
31
- if (!['true', 'false'].includes(req.params[name])) throw new Error(`${name} param must be a boolean`);
32
- req.params[name] = req.params[name] === true ? true : false;
33
- }
34
-
35
- static string(req, name) {
36
- req.params[name] = String(req.params[name]);
37
- }
38
- }
9
+ import Param from './param.js';
10
+ import Middleware from './middleware.js';
39
11
 
40
12
  /**
41
13
  * @class
@@ -89,6 +61,9 @@ export default class Schemas {
89
61
  * @param {boolean} opts.silent Squelch StdOut
90
62
  */
91
63
  async load(dirpath, config, opts = {}) {
64
+ if (dirpath instanceof URL) dirpath = dirpath.pathname;
65
+ else dirpath = String(dirpath);
66
+
92
67
  // Load dynamic routes directory
93
68
  for (const r of fs.readdirSync(dirpath)) {
94
69
  if (!opts.silent) console.error(`ok - loaded routes/${r}`);
@@ -161,104 +136,28 @@ export default class Schemas {
161
136
 
162
137
  const flow = [parsed[1], []];
163
138
 
164
- if (schemas.query) flow[1].push(Schemas.query(schemas.query));
139
+ if (schemas.query) flow[1].push(Middleware.query(schemas.query));
165
140
 
166
141
  // Make sure express params are validated/coerced into proper type
167
142
  const matches = url.match(/(:.+?)(?=\/|\.|$)/g);
168
143
  if (matches) for (const match of matches) {
169
144
  if (!schemas[match]) throw new Error(`${match} type is not defined in schema`);
170
145
 
171
- flow[1].push(Schemas.param(match, schemas[match]));
146
+ flow[1].push(Middleware.param(match, schemas[match]));
172
147
  }
173
148
 
174
149
  flow[1].push(this.validate(opts));
175
150
 
176
- return flow;
177
- }
178
-
179
- /**
180
- * Express middleware to identify express params that should be integers/number/booleans
181
- * according to the schema and attempt to cast them as such to ensure they pass the schema
182
- *
183
- * @param {String} match Express Param
184
- * @param {String} type Type to coerce it to
185
- *
186
- * @returns {Function}
187
- */
188
- static param(match, type) {
189
- return function (req, res, next) {
151
+ if (schemas.res) flow[1].push(Middleware.res(schemas.res));
190
152
 
191
- try {
192
- Param[type](req, match.replace(':', ''));
193
- } catch (err) {
194
- return Err.respond(err, res);
195
- }
196
-
197
- return next();
198
- };
153
+ return flow;
199
154
  }
200
155
 
201
156
  /**
202
- * Express middleware to identify query params that should be integers/booleans according to the schema
203
- * and attempt to cast them as such to ensure they pass the schema
204
- *
205
- * @param {Object} schema JSON Schema
206
- *
207
- * @returns {Function}
157
+ * Convert validation errors into standardized JSON Error Messages
208
158
  */
209
- static query(schema) {
210
- return function (req, res, next) {
211
- for (const key in req.query) {
212
- if (!schema.properties[key] || !schema.properties[key].type) continue;
213
-
214
- // For easier processing use consistent array format IE: `type: ["integer", "boolean"]` vs type: "integer"
215
- if (!Array.isArray(schema.properties[key].type)) {
216
- schema.properties[key].type = [schema.properties[key].type];
217
- }
218
-
219
- for (const type of schema.properties[key].type) {
220
- if (type === 'array') {
221
- req.query[key] = req.query[key].split(',');
222
-
223
- if (schema.properties[key].items && schema.properties[key].items.type === 'number') {
224
- req.query[key] = req.query[key].map((k) => {
225
- return Number(k);
226
- });
227
- } else if (schema.properties[key].items && schema.properties[key].items.type === 'integer') {
228
- req.query[key] = req.query[key].map((k) => {
229
- return parseInt(k);
230
- });
231
- } else if (schema.properties[key].items && schema.properties[key].items.type === 'boolean') {
232
- req.query[key] = req.query[key].map((k) => {
233
- if (k === 'true') {
234
- return true;
235
- } else if (k === 'false') {
236
- return false;
237
- }
238
- });
239
- }
240
- } else if (type === 'integer' && !isNaN(parseInt(req.query[key]))) {
241
- req.query[key] = parseInt(req.query[key]);
242
- } else if (type === 'number' && !isNaN(Number(req.query[key]))) {
243
- req.query[key] = Number(req.query[key]);
244
- } else if (type === 'boolean') {
245
- if (req.query[key] === 'true') {
246
- req.query[key] = true;
247
- } else if (req.query[key] === 'false') {
248
- req.query[key] = false;
249
- }
250
- }
251
- }
252
- }
253
-
254
- for (const key in schema.properties) {
255
- if (req.query[key] === undefined && schema.properties[key].default !== undefined) {
256
- req.query[key] = schema.properties[key].default;
257
- }
258
- }
259
-
260
- return next();
261
- };
159
+ error() {
160
+ this.router.use(Middleware.error());
262
161
  }
263
162
 
264
163
  /**
@@ -295,32 +194,6 @@ export default class Schemas {
295
194
  });
296
195
  }
297
196
 
298
- /**
299
- * Convert validation errors into standardized JSON Error Messages
300
- */
301
- error() {
302
- this.router.use((err, req, res, next) => {
303
- if (err instanceof ValidationError) {
304
- let errs = [];
305
- if (err.validationErrors.body) {
306
- errs = errs.concat(err.validationErrors.body);
307
- }
308
-
309
- if (err.validationErrors.query) {
310
- errs = errs.concat(err.validationErrors.query);
311
- }
312
-
313
- return Err.respond(
314
- new Err(400, null, 'validation error'),
315
- res,
316
- errs
317
- );
318
- } else {
319
- next(err);
320
- }
321
- });
322
- }
323
-
324
197
  /**
325
198
  * Return a list of endpoints with schemas
326
199
  *
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "type": "module",
4
4
  "root": true,
5
5
  "main": "index.js",
6
- "version": "5.0.0",
6
+ "version": "7.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>",
@@ -18,7 +18,9 @@
18
18
  "doc": "documentation build index.js -f html -o docs && documentation lint index.js"
19
19
  },
20
20
  "dependencies": {
21
+ "@openaddresses/batch-error": "^1.0.1",
21
22
  "ajv": "^8.6.3",
23
+ "ajv-formats": "^2.1.1",
22
24
  "body-parser": "^1.20.0",
23
25
  "express": "^4.17.1",
24
26
  "express-json-validator-middleware": "^3.0.0",
package/routes/schema.js CHANGED
@@ -1,4 +1,4 @@
1
- import Err from '../lib/error.js';
1
+ import Err from '@openaddresses/batch-error';
2
2
 
3
3
  export default async function router(schema) {
4
4
  /**
@@ -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"]
@@ -1,6 +1,6 @@
1
1
  import test from 'tape';
2
2
  import express from 'express';
3
- import { Schema } from '../index.js';
3
+ import Schema from '../index.js';
4
4
 
5
5
  const app = express();
6
6
  const schema = new Schema(express.Router(), {
package/lib/error.js DELETED
@@ -1,43 +0,0 @@
1
- /**
2
- * @class
3
- */
4
- export default class PublicError {
5
- constructor(status, err, safe, print = true) {
6
- // Wrap postgres errors to ensure stack trace (line nums) are returned
7
- if (err && err.severity) err = new Error(err);
8
-
9
- if (print && ![400, 401, 402, 403, 404].includes(status)) console.error(err ? err : 'Error: ' + safe);
10
-
11
- this.status = status;
12
- this.err = err;
13
- this.safe = safe;
14
- }
15
-
16
- static respond(err, res, messages = []) {
17
- if (err instanceof PublicError) {
18
- res.status(err.status).json({
19
- status: err.status,
20
- message: err.safe,
21
- messages: messages
22
- });
23
-
24
- // Duplicate Express supported Error extensions
25
- } else if (err instanceof Error && (err.status || err.statusCode)) {
26
- res.status(err.status || err.statusCode).json({
27
- status: err.status || err.statusCode,
28
- message: err.message || 'Generic Error',
29
- messages: messages
30
- });
31
-
32
- // Handle generic errors
33
- } else {
34
- console.error(err);
35
-
36
- res.status(500).json({
37
- status: 500,
38
- message: 'Internal Server Error',
39
- messages: messages
40
- });
41
- }
42
- }
43
- }