@openaddresses/batch-schema 9.3.0 → 10.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.
Files changed (45) hide show
  1. package/.eslintrc.json +5 -47
  2. package/.github/workflows/release.yml +5 -2
  3. package/CHANGELOG.md +8 -0
  4. package/LICENSE +1 -1
  5. package/README.md +21 -17
  6. package/{routes/schema.js → lib/api.ts} +11 -4
  7. package/lib/openapi.ts +122 -0
  8. package/lib/schema.ts +293 -0
  9. package/lib/types.ts +32 -0
  10. package/package.json +16 -11
  11. package/test/{schema.srv.test.js → schema.srv.test.ts} +9 -20
  12. package/tsconfig.json +27 -0
  13. package/docs/assets/anchor.js +0 -350
  14. package/docs/assets/bass-addons.css +0 -12
  15. package/docs/assets/bass.css +0 -544
  16. package/docs/assets/fonts/EOT/SourceCodePro-Bold.eot +0 -0
  17. package/docs/assets/fonts/EOT/SourceCodePro-Regular.eot +0 -0
  18. package/docs/assets/fonts/LICENSE.txt +0 -93
  19. package/docs/assets/fonts/OTF/SourceCodePro-Bold.otf +0 -0
  20. package/docs/assets/fonts/OTF/SourceCodePro-Regular.otf +0 -0
  21. package/docs/assets/fonts/TTF/SourceCodePro-Bold.ttf +0 -0
  22. package/docs/assets/fonts/TTF/SourceCodePro-Regular.ttf +0 -0
  23. package/docs/assets/fonts/WOFF/OTF/SourceCodePro-Bold.otf.woff +0 -0
  24. package/docs/assets/fonts/WOFF/OTF/SourceCodePro-Regular.otf.woff +0 -0
  25. package/docs/assets/fonts/WOFF/TTF/SourceCodePro-Bold.ttf.woff +0 -0
  26. package/docs/assets/fonts/WOFF/TTF/SourceCodePro-Regular.ttf.woff +0 -0
  27. package/docs/assets/fonts/WOFF2/OTF/SourceCodePro-Bold.otf.woff2 +0 -0
  28. package/docs/assets/fonts/WOFF2/OTF/SourceCodePro-Regular.otf.woff2 +0 -0
  29. package/docs/assets/fonts/WOFF2/TTF/SourceCodePro-Bold.ttf.woff2 +0 -0
  30. package/docs/assets/fonts/WOFF2/TTF/SourceCodePro-Regular.ttf.woff2 +0 -0
  31. package/docs/assets/fonts/source-code-pro.css +0 -23
  32. package/docs/assets/github.css +0 -123
  33. package/docs/assets/site.js +0 -168
  34. package/docs/assets/split.css +0 -15
  35. package/docs/assets/split.js +0 -782
  36. package/docs/assets/style.css +0 -147
  37. package/docs/index.html +0 -792
  38. package/lib/middleware.js +0 -159
  39. package/lib/openapi.js +0 -96
  40. package/lib/param.js +0 -29
  41. package/lib/schema.js +0 -254
  42. package/schema/req.query.ListSchema.json +0 -25
  43. package/schema/res.ListSchema.json +0 -47
  44. package/test/schema.test.js +0 -33
  45. /package/{index.js → index.ts} +0 -0
package/lib/middleware.js DELETED
@@ -1,159 +0,0 @@
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
- import Generic from '@openaddresses/batch-generic';
7
-
8
- const ajv = addFormats(new Ajv({
9
- strict: false,
10
- allErrors: true,
11
- useDefaults: true,
12
- removeAdditional: true,
13
- coerceTypes: true
14
- }));
15
-
16
- /**
17
- * @class
18
- */
19
- export default class Middleware {
20
- /**
21
- * Express middleware to identify express params that should be integers/number/booleans
22
- * according to the schema and attempt to cast them as such to ensure they pass the schema
23
- *
24
- * @param {String} match Express Param
25
- * @param {String} type Type to coerce it to
26
- *
27
- * @returns {Function}
28
- */
29
- static param(match, type) {
30
- return function (req, res, next) {
31
- try {
32
- Param[type](req, match.replace(':', ''));
33
- } catch (err) {
34
- return Err.respond(err, res);
35
- }
36
-
37
- return next();
38
- };
39
- }
40
-
41
- /**
42
- * Express middleware to identify query params that should be integers/booleans according to the schema
43
- * and attempt to cast them as such to ensure they pass the schema
44
- *
45
- * @param {Object} schema JSON Schema
46
- *
47
- * @returns {Function}
48
- */
49
- static query(schema) {
50
- return function (req, res, next) {
51
- for (const key in req.query) {
52
- if (!schema.properties[key] || !schema.properties[key].type) continue;
53
-
54
- // For easier processing use consistent array format IE: `type: ["integer", "boolean"]` vs type: "integer"
55
- if (!Array.isArray(schema.properties[key].type)) {
56
- schema.properties[key].type = [schema.properties[key].type];
57
- }
58
-
59
- for (const type of schema.properties[key].type) {
60
- if (type === 'array') {
61
- req.query[key] = req.query[key].split(',');
62
-
63
- if (schema.properties[key].items && schema.properties[key].items.type === 'number') {
64
- req.query[key] = req.query[key].map((k) => {
65
- return Number(k);
66
- });
67
- } else if (schema.properties[key].items && schema.properties[key].items.type === 'integer') {
68
- req.query[key] = req.query[key].map((k) => {
69
- return parseInt(k);
70
- });
71
- } else if (schema.properties[key].items && schema.properties[key].items.type === 'boolean') {
72
- req.query[key] = req.query[key].map((k) => {
73
- if (k === 'true') {
74
- return true;
75
- } else if (k === 'false') {
76
- return false;
77
- }
78
- });
79
- }
80
- } else if (type === 'integer' && !isNaN(parseInt(req.query[key]))) {
81
- req.query[key] = parseInt(req.query[key]);
82
- } else if (type === 'number' && !isNaN(Number(req.query[key]))) {
83
- req.query[key] = Number(req.query[key]);
84
- } else if (type === 'boolean') {
85
- if (req.query[key] === 'true') {
86
- req.query[key] = true;
87
- } else if (req.query[key] === 'false') {
88
- req.query[key] = false;
89
- }
90
- }
91
- }
92
- }
93
-
94
- for (const key in schema.properties) {
95
- if (req.query[key] === undefined && schema.properties[key].default !== undefined) {
96
- req.query[key] = schema.properties[key].default;
97
- }
98
- }
99
-
100
- return next();
101
- };
102
- }
103
-
104
- /**
105
- * Express middleware to validate the response body
106
- * and optionally serialize Generic object to JSON
107
- *
108
- * @param {Object} schema JSON Schema
109
- *
110
- * @returns {Function}
111
- */
112
- static res(schema) {
113
- const validate = ajv.compile(schema);
114
-
115
- return function (req, res, next) {
116
- const json = res.json;
117
- res.json = function(obj) {
118
- if (obj instanceof Generic && typeof obj.serialize === 'function') {
119
- obj = obj.serialize();
120
- }
121
-
122
- if (res.statusCode === null || res.statusCode === 200) {
123
- validate(obj);
124
- }
125
-
126
- json.call(this, obj);
127
- };
128
- next();
129
- };
130
- }
131
-
132
- /**
133
- * Convert validation errors into standardized JSON Error Messages
134
- *
135
- * @returns {Function}
136
- */
137
- static error() {
138
- return function(err, req, res, next) {
139
- if (err instanceof ValidationError) {
140
- let errs = [];
141
- if (err.validationErrors.body) {
142
- errs = errs.concat(err.validationErrors.body);
143
- }
144
-
145
- if (err.validationErrors.query) {
146
- errs = errs.concat(err.validationErrors.query);
147
- }
148
-
149
- return Err.respond(
150
- new Err(400, null, 'validation error'),
151
- res,
152
- errs
153
- );
154
- } else {
155
- next(err);
156
- }
157
- };
158
- }
159
- }
package/lib/openapi.js DELETED
@@ -1,96 +0,0 @@
1
- /**
2
- * @class
3
- */
4
- export default class Docs {
5
- constructor(info) {
6
- this.base = {
7
- openapi: '3.0.3',
8
- info: info || {
9
- title: 'OpenAPI Schema',
10
- version: '1.0.0'
11
- },
12
- paths: {}
13
- };
14
- }
15
-
16
- push(parsed, schemas) {
17
- let path = [];
18
- const method = parsed[0].toLowerCase();
19
-
20
- for (let sec of parsed[1].split('/')) {
21
- for (const lit of ['.', '-']) { // These chars are used literally and can't be part of a param name
22
- const mini = [];
23
- for (const s of sec.split(lit)) {
24
- if (s[0] === ':') mini.push(`{${s}}`);
25
- else mini.push(s);
26
- }
27
- sec = mini.join(lit);
28
- }
29
- path.push(sec);
30
- }
31
- path = path.join('/');
32
-
33
-
34
- if (!this.base.paths[path]) this.base.paths[path] = {};
35
-
36
- if (this.base.paths[path][method]) throw new Error(`Duplicate Path: ${parsed.join(' ')}`);
37
-
38
- const document = {
39
- summary: schemas.description || 'No Description',
40
- tags: [schemas.group || 'Default'],
41
- parameters: [],
42
- responses: {
43
- 200: {
44
- description: 'Successful Response'
45
- }
46
- }
47
- };
48
-
49
- for (const key in schemas) {
50
- if (key[0] === ':') {
51
- document.parameters.push({
52
- in: 'path',
53
- name: key,
54
- schema: {
55
- type: schemas[key]
56
- },
57
- required: true
58
- });
59
- }
60
- }
61
-
62
- if (schemas.query && schemas.query.type === 'object') {
63
- for (const name in (schemas.query.properties || {})) {
64
- const query = schemas.query.properties[name];
65
- document.parameters.push({
66
- in: 'query',
67
- name,
68
- schema: query,
69
- required: query.required || (schemas.query.required || []).includes(name),
70
- description: query.description || 'No Description'
71
- });
72
- }
73
- }
74
-
75
- if (schemas.res) {
76
- document.responses['200'].content = {
77
- 'application/json': {
78
- schema: schemas.res
79
- }
80
- };
81
- }
82
-
83
- if (schemas.body) {
84
- document.requestBody = {
85
- required: true,
86
- content: {
87
- 'application/json': {
88
- schema: schemas.body
89
- }
90
- }
91
- };
92
- }
93
-
94
- this.base.paths[path][method] = document;
95
- }
96
- }
package/lib/param.js DELETED
@@ -1,29 +0,0 @@
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 DELETED
@@ -1,254 +0,0 @@
1
- import { Validator } from 'express-json-validator-middleware';
2
- import $RefParser from '@apidevtools/json-schema-ref-parser';
3
- import fs from 'node:fs';
4
- import path from 'node:path';
5
- import morgan from 'morgan';
6
- import bodyparser from 'body-parser';
7
-
8
- import SchemaRoute from '../routes/schema.js';
9
- import Param from './param.js';
10
- import Docs from './openapi.js';
11
- import Middleware from './middleware.js';
12
-
13
- /**
14
- * @class
15
- *
16
- * @param {Object} router Express Router Object
17
- * @param {Object} opts Options Object
18
- * @param {String} opts.schemas Schemas Path
19
- * @param {boolean|string} [opts.logging=true] disable logging with false
20
- * @param {number} [opts.limit=50] body size limit in mb
21
- *
22
- * @param {URL} [opts.apidoc] apidoc file location
23
- */
24
- export default class Schemas {
25
- constructor(router, opts = {}) {
26
- if (!router) throw new Error('Router Param Required');
27
-
28
- if (!opts.schemas) {
29
- this.schemas_path = new URL('../schema/', import.meta.url).pathname;
30
- } else {
31
- if (opts.schemas instanceof URL) opts.schemas = opts.schemas.pathname;
32
- else opts.schemas = String(opts.schemas);
33
-
34
- this.schemas_path = opts.schemas;
35
- }
36
-
37
- this.validator = new Validator({
38
- removeAdditional: true,
39
- useDefaults: true,
40
- allErrors: true
41
- });
42
-
43
- this.router = router;
44
-
45
- if (opts.morgan !== false) this.router.use(morgan('combined'));
46
- this.router.use(bodyparser.urlencoded({ extended: true }));
47
- this.jsonparser = bodyparser.json({ limit: `${opts.limit}mb` });
48
-
49
- this.schemas = new Map();
50
- this.validate = this.validator.validate;
51
-
52
- this.docs = new Docs();
53
- }
54
-
55
- async api() {
56
- await SchemaRoute(this);
57
- }
58
-
59
- /**
60
- * Load routes directory. All .js files in the given directory will be loaded (ES Module)
61
- * and the default method run
62
- *
63
- * @param {String} dirpath Directory to load
64
- * @param {object} config Config Object
65
- * @param {Object} opts Options Object
66
- * @param {boolean} opts.silent Squelch StdOut
67
- */
68
- async load(dirpath, config, opts = {}) {
69
- if (dirpath instanceof URL) dirpath = dirpath.pathname;
70
- else dirpath = String(dirpath);
71
-
72
- const routes = [];
73
-
74
- // Load dynamic routes directory
75
- for (const r of fs.readdirSync(dirpath)) {
76
- if (!opts.silent) console.log(`ok - loaded routes/${r}`);
77
- if (!['.ts', '.js'].includes(path.parse(r).ext)) continue;
78
- routes.push((await import(path.resolve(dirpath, r))).default(this, config));
79
- }
80
-
81
- await Promise.all(routes);
82
- }
83
-
84
- /**
85
- * Load a blueprint into the router
86
- *
87
- * @param {Object} bp_class Blueprint Class with blueprint() fn
88
- * @param {object} config Config Object
89
- * @param {Object} opts Options Object
90
- * @param {boolean} opts.silent Squelch StdOut
91
- */
92
- async blueprint(bp_class, config, opts = {}) {
93
- if (!opts.silent) console.log(`ok - loaded ${bp_class.name}`);
94
- await bp_class.blueprint(this, config);
95
- }
96
-
97
- check(url, schemas, fns) {
98
- if (typeof url !== 'string') throw new Error('URL should be string');
99
-
100
- if (schemas === null) schemas = {};
101
- if (typeof schemas !== 'object') throw new Error('Schemas should be object');
102
-
103
- // Make sure express params are validated/coerced into proper type
104
- const matches = url.match(/(:.+?)(?=\/|\.|$)/g);
105
- if (matches) for (const match of matches) {
106
- if (!schemas[match]) throw new Error(`${match} type is not defined in schema`);
107
- if (!Param[schemas[match]]) throw new Error(`${schemas[match]} is not a supported type for ${match}`);
108
- }
109
-
110
- if (!fns.length) throw new Error('At least 1 route function should be defined');
111
- }
112
-
113
- async get(url, schemas, ...fns) {
114
- this.check(url, schemas, fns);
115
- this.router.get(...await this.generic(`GET ${url}`, schemas), ...fns);
116
- }
117
-
118
- async delete(url, schemas, ...fns) {
119
- this.check(url, schemas, fns);
120
- this.router.delete(...await this.generic(`DELETE ${url}`, schemas), ...fns);
121
- }
122
-
123
- async post(url, schemas, ...fns) {
124
- this.check(url, schemas, fns);
125
- this.router.post(...await this.generic(`POST ${url}`, schemas), ...fns);
126
- }
127
-
128
- async patch(url, schemas, ...fns) {
129
- this.check(url, schemas, fns);
130
- this.router.patch(...await this.generic(`PATCH ${url}`, schemas), ...fns);
131
- }
132
-
133
- async put(url, schemas, ...fns) {
134
- this.check(url, schemas, fns);
135
- this.router.put(...await this.generic(`PUT ${url}`, schemas), ...fns);
136
- }
137
-
138
- async generic(url, schemas = {}) {
139
- if (!schemas) schemas = {};
140
-
141
- const parsed = url.split(' ');
142
- if (parsed.length !== 2) throw new Error('schema.generic() must be of format "<VERB> <URL>"');
143
-
144
- for (const type of ['body', 'query', 'res']) {
145
- if (!schemas[type]) continue;
146
-
147
- // TODO: Write schemas to a tmp directory so blueprints & main schemas are in the same place
148
- if (typeof schemas[type] === 'object') {
149
- schemas[`${type}_url`] = null;
150
- } else if (typeof schemas[type] === 'string') {
151
- schemas[`${type}_url`] = '../schema/' + schemas[type];
152
- try {
153
- schemas[type] = await $RefParser.dereference((new URL(`../schema/${schemas[type]}`, import.meta.url)).pathname);
154
- } catch (err) {
155
- schemas[type] = await $RefParser.dereference(`${this.schemas_path}/${schemas[type]}`);
156
- }
157
- } else {
158
- throw new Error(`Unsupported Value in ${type} value for ${url}`);
159
- }
160
- }
161
-
162
- this.docs.push(parsed, schemas);
163
-
164
- this.schemas.set(parsed.join(' '), schemas);
165
-
166
- const opts = {};
167
- if (schemas.query) opts.query = schemas.query;
168
- if (schemas.body) opts.body = schemas.body;
169
-
170
- const flow = [parsed[1], []];
171
-
172
- if (schemas.body) flow[1].push(this.jsonparser);
173
-
174
- if (schemas.query) flow[1].push(Middleware.query(schemas.query));
175
-
176
- // Make sure express params are validated/coerced into proper type
177
- const matches = url.match(/(:.+?)(?=\/|\.|$)/g);
178
- if (matches) for (const match of matches) {
179
- if (!schemas[match]) throw new Error(`${match} type is not defined in schema`);
180
-
181
- flow[1].push(Middleware.param(match, schemas[match]));
182
- }
183
-
184
- flow[1].push(this.validate(opts));
185
-
186
- if (schemas.res) flow[1].push(Middleware.res(schemas.res));
187
-
188
- return flow;
189
- }
190
-
191
- /**
192
- * Convert validation errors into standardized JSON Error Messages
193
- */
194
- error() {
195
- this.router.use(Middleware.error());
196
- }
197
-
198
- /**
199
- * Return all schemas (body, query, etc) for a given method + url
200
- *
201
- * @param {String} method HTTP Method
202
- * @param {String} url URL
203
- *
204
- * @returns {Object}
205
- */
206
- query(method, url) {
207
- if (!this.schemas.has(`${method} ${url}`)) {
208
- return { body: null, schema: null };
209
- }
210
-
211
- const schema = JSON.parse(JSON.stringify(this.schemas.get(`${method} ${url}`)));
212
- if (!schema.query) schema.query = null;
213
- if (!schema.body) schema.body = null;
214
- if (!schema.res) schema.res = null;
215
-
216
- return {
217
- query: schema.query,
218
- body: schema.body,
219
- res: schema.res
220
- };
221
- }
222
-
223
- /**
224
- * Catch all for unmatched routes
225
- */
226
- not_found() {
227
- this.router.all('*', (req, res) => {
228
- return res.status(404).json({
229
- status: 404,
230
- message: 'API endpoint does not exist!',
231
- messages: []
232
- });
233
- });
234
- }
235
-
236
- /**
237
- * Return a list of endpoints with schemas
238
- *
239
- * @returns {Object}
240
- */
241
- list() {
242
- const lite = {};
243
-
244
- for (const key of this.schemas.keys()) {
245
- lite[key] = {
246
- body: !!this.schemas.get(key).body,
247
- query: !!this.schemas.get(key).query,
248
- res: !!this.schemas.get(key).res
249
- };
250
- }
251
-
252
- return lite;
253
- }
254
- }
@@ -1,25 +0,0 @@
1
- {
2
- "type": "object",
3
- "additionalProperties": false,
4
- "properties": {
5
- "method": {
6
- "type": "string",
7
- "description": "HTTP Verb",
8
- "enum": [
9
- "GET",
10
- "HEAD",
11
- "POST",
12
- "PUT",
13
- "DELETE",
14
- "CONNECT",
15
- "OPTIONS",
16
- "TRACE",
17
- "PATCH"
18
- ]
19
- },
20
- "url": {
21
- "type": "string",
22
- "description": "URLEncoded URL that you want to fetch"
23
- }
24
- }
25
- }
@@ -1,47 +0,0 @@
1
- {
2
- "oneOf": [{
3
- "type": "object",
4
- "patternProperties": {
5
- ".*": {
6
- "type": "object",
7
- "required": [
8
- "body",
9
- "query",
10
- "res"
11
- ],
12
- "properties": {
13
- "body": {
14
- "type": "boolean",
15
- "description": "Does the request body have a schema"
16
- },
17
- "query": {
18
- "type": "boolean",
19
- "description": "Do the request query params have a schema"
20
- },
21
- "res": {
22
- "type": "boolean",
23
- "description": "Does the response have a schema"
24
- }
25
- }
26
- }
27
- }
28
- }, {
29
- "type": "object",
30
- "required": [
31
- "body",
32
- "res",
33
- "query"
34
- ],
35
- "properties": {
36
- "body": {
37
- "type": ["null", "object"]
38
- },
39
- "res": {
40
- "type": ["null", "object"]
41
- },
42
- "query": {
43
- "type": ["null", "object"]
44
- }
45
- }
46
- }]
47
- }
@@ -1,33 +0,0 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- import test from 'tape';
4
-
5
- import glob from 'glob';
6
- import $RefParser from '@apidevtools/json-schema-ref-parser';
7
- import Ajv from 'ajv';
8
-
9
- const ajv = new Ajv({
10
- allErrors: true
11
- });
12
-
13
- for (const source of glob.sync('../schema/**/*.json')) {
14
- test(`schema/${path.parse(source).base}`, async (t) => {
15
- try {
16
- const file = fs.readFileSync(source);
17
- t.ok(file.length, 'file loaded');
18
-
19
- JSON.parse(file);
20
- } catch (err) {
21
- t.error(err, 'no JSON errors');
22
- }
23
-
24
- try {
25
- const schema = await $RefParser.dereference(source);
26
-
27
- ajv.compile(schema);
28
- } catch (err) {
29
- t.error(err, 'no errors');
30
- }
31
- t.end();
32
- });
33
- }
File without changes