@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.
- package/.eslintrc.json +5 -47
- package/.github/workflows/release.yml +5 -2
- package/CHANGELOG.md +8 -0
- package/LICENSE +1 -1
- package/README.md +21 -17
- package/{routes/schema.js → lib/api.ts} +11 -4
- package/lib/openapi.ts +122 -0
- package/lib/schema.ts +293 -0
- package/lib/types.ts +32 -0
- package/package.json +16 -11
- package/test/{schema.srv.test.js → schema.srv.test.ts} +9 -20
- package/tsconfig.json +27 -0
- package/docs/assets/anchor.js +0 -350
- package/docs/assets/bass-addons.css +0 -12
- package/docs/assets/bass.css +0 -544
- package/docs/assets/fonts/EOT/SourceCodePro-Bold.eot +0 -0
- package/docs/assets/fonts/EOT/SourceCodePro-Regular.eot +0 -0
- package/docs/assets/fonts/LICENSE.txt +0 -93
- package/docs/assets/fonts/OTF/SourceCodePro-Bold.otf +0 -0
- package/docs/assets/fonts/OTF/SourceCodePro-Regular.otf +0 -0
- package/docs/assets/fonts/TTF/SourceCodePro-Bold.ttf +0 -0
- package/docs/assets/fonts/TTF/SourceCodePro-Regular.ttf +0 -0
- package/docs/assets/fonts/WOFF/OTF/SourceCodePro-Bold.otf.woff +0 -0
- package/docs/assets/fonts/WOFF/OTF/SourceCodePro-Regular.otf.woff +0 -0
- package/docs/assets/fonts/WOFF/TTF/SourceCodePro-Bold.ttf.woff +0 -0
- package/docs/assets/fonts/WOFF/TTF/SourceCodePro-Regular.ttf.woff +0 -0
- package/docs/assets/fonts/WOFF2/OTF/SourceCodePro-Bold.otf.woff2 +0 -0
- package/docs/assets/fonts/WOFF2/OTF/SourceCodePro-Regular.otf.woff2 +0 -0
- package/docs/assets/fonts/WOFF2/TTF/SourceCodePro-Bold.ttf.woff2 +0 -0
- package/docs/assets/fonts/WOFF2/TTF/SourceCodePro-Regular.ttf.woff2 +0 -0
- package/docs/assets/fonts/source-code-pro.css +0 -23
- package/docs/assets/github.css +0 -123
- package/docs/assets/site.js +0 -168
- package/docs/assets/split.css +0 -15
- package/docs/assets/split.js +0 -782
- package/docs/assets/style.css +0 -147
- package/docs/index.html +0 -792
- package/lib/middleware.js +0 -159
- package/lib/openapi.js +0 -96
- package/lib/param.js +0 -29
- package/lib/schema.js +0 -254
- package/schema/req.query.ListSchema.json +0 -25
- package/schema/res.ListSchema.json +0 -47
- package/test/schema.test.js +0 -33
- /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
|
-
}
|
package/test/schema.test.js
DELETED
|
@@ -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
|
-
}
|
/package/{index.js → index.ts}
RENAMED
|
File without changes
|