@openaddresses/batch-schema 10.26.0 → 10.27.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/.github/workflows/test.yml +19 -19
- package/CHANGELOG.md +11 -0
- package/dist/lib/schema.d.ts +35 -12
- package/dist/lib/schema.js +134 -217
- package/dist/lib/schema.js.map +1 -1
- package/dist/test/response.srv.test.d.ts +1 -0
- package/dist/test/response.srv.test.js +95 -0
- package/dist/test/response.srv.test.js.map +1 -0
- package/lib/schema.ts +170 -226
- package/package.json +1 -1
- package/test/response.srv.test.ts +114 -0
|
@@ -1,29 +1,29 @@
|
|
|
1
1
|
name: Test
|
|
2
2
|
|
|
3
3
|
on:
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
4
|
+
pull_request:
|
|
5
|
+
types:
|
|
6
|
+
- opened
|
|
7
|
+
- synchronize
|
|
8
|
+
- reopened
|
|
9
|
+
- ready_for_review
|
|
10
10
|
|
|
11
11
|
jobs:
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
if: github.event.pull_request.draft == false
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v2
|
|
17
|
+
with:
|
|
18
|
+
ref: ${{ github.event.pull_request.head.sha }}
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
- uses: actions/setup-node@v2
|
|
21
|
+
with:
|
|
22
|
+
node-version: '18'
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
- run: npm install
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
- run: npm run lint
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
- run: npm test
|
|
29
29
|
|
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,17 @@
|
|
|
10
10
|
|
|
11
11
|
## Version History
|
|
12
12
|
|
|
13
|
+
### v10.27.0 - 2026-07-06
|
|
14
|
+
|
|
15
|
+
- :rocket: Only wrap `res.json` when a route has a response schema that requires validation — routes without one no longer pay any response-path overhead
|
|
16
|
+
- :rocket: Replace the `JSON.parse(JSON.stringify())` response clone with a single-pass JSON-semantics clone (~1.3-1.6x faster, no intermediate string allocation)
|
|
17
|
+
- :rocket: Skip the response clone entirely for non-200 responses (validation was already skipped)
|
|
18
|
+
- :bug: `Type.Any`/`Type.Unknown` response schemas were still compiled and validated on every request due to a broken `instanceof` check; now detected via the TypeBox `Kind` symbol and skipped
|
|
19
|
+
- :rocket: Precompute the body content-type matcher and supported-types error string at route registration instead of rebuilding them per request
|
|
20
|
+
- :rocket: Consolidate the five per-method route registration paths into a single shared implementation
|
|
21
|
+
- :rocket: Truncate response-validation error logging to 4KB
|
|
22
|
+
- :white_check_mark: Add response validation test suite
|
|
23
|
+
|
|
13
24
|
### v10.26.0
|
|
14
25
|
|
|
15
26
|
- :rocket: Allow non-json content-type
|
package/dist/lib/schema.d.ts
CHANGED
|
@@ -5,16 +5,25 @@ import type { Static, TSchema } from '@sinclair/typebox';
|
|
|
5
5
|
import { OpenAPIV3 as Doc } from 'openapi-types';
|
|
6
6
|
import { Router } from 'express';
|
|
7
7
|
import type { RequestHandler } from 'express';
|
|
8
|
-
import {
|
|
9
|
-
import type {
|
|
10
|
-
import type { RequestValidation } from './types.js';
|
|
8
|
+
import type { ErrorObject, ValidateFunction } from 'ajv';
|
|
9
|
+
import type { RequestValidation, NormalizedBody } from './types.js';
|
|
11
10
|
import type { OpenAPIDocumentInput } from './openapi.js';
|
|
12
11
|
import Docs from './openapi.js';
|
|
13
|
-
declare const ajv: Ajv;
|
|
14
12
|
export type ErrorListItem = {
|
|
15
13
|
type: 'Body' | 'Query' | 'Params';
|
|
16
14
|
errors: ErrorObject[];
|
|
17
15
|
};
|
|
16
|
+
/**
|
|
17
|
+
* Per-route body validation state, compiled once at route registration:
|
|
18
|
+
* ajv validators keyed by content-type, the normalized map used for
|
|
19
|
+
* content-type matching, and the precomputed supported-types string
|
|
20
|
+
* used in error messages.
|
|
21
|
+
*/
|
|
22
|
+
export type CompiledBodyValidation = {
|
|
23
|
+
validators: Map<string, true | ValidateFunction>;
|
|
24
|
+
matcher: NormalizedBody;
|
|
25
|
+
supported: string;
|
|
26
|
+
};
|
|
18
27
|
/**
|
|
19
28
|
* @class
|
|
20
29
|
*
|
|
@@ -37,19 +46,21 @@ export default class Schemas {
|
|
|
37
46
|
validationError(method: Doc.HttpMethods, path: string): Err;
|
|
38
47
|
/**
|
|
39
48
|
* Compile body validators keyed by content-type. Returns null when no
|
|
40
|
-
* body validation is configured. Each entry is either `true`
|
|
41
|
-
* accepted for this content-type) or a compiled ajv validator
|
|
49
|
+
* body validation is configured. Each validator entry is either `true`
|
|
50
|
+
* (any body accepted for this content-type) or a compiled ajv validator.
|
|
51
|
+
* The content-type matcher and supported-types error string are
|
|
52
|
+
* precomputed here so no per-request allocation is needed.
|
|
42
53
|
*/
|
|
43
|
-
compileBodyValidators(body: RequestValidation<TSchema, TSchema, TSchema, TSchema>['body']):
|
|
54
|
+
compileBodyValidators(body: RequestValidation<TSchema, TSchema, TSchema, TSchema>['body']): CompiledBodyValidation | null;
|
|
44
55
|
/**
|
|
45
|
-
* Validate the incoming request body against the compiled
|
|
46
|
-
*
|
|
56
|
+
* Validate the incoming request body against the compiled validation
|
|
57
|
+
* state for the route. Pushes a Body error onto `errors` if validation
|
|
47
58
|
* fails; returns an Err to respond with for an unsupported content-type.
|
|
48
59
|
*
|
|
49
|
-
* If `
|
|
60
|
+
* If `opts.required` is false and the request has no body / no
|
|
50
61
|
* content-type, validation is skipped.
|
|
51
62
|
*/
|
|
52
|
-
validateBody(req: Request,
|
|
63
|
+
validateBody(req: Request, compiled: CompiledBodyValidation, errors: Array<ErrorListItem>, opts?: {
|
|
53
64
|
required?: boolean;
|
|
54
65
|
}): Err | null;
|
|
55
66
|
constructor(router: Router, opts?: {
|
|
@@ -84,6 +95,19 @@ export default class Schemas {
|
|
|
84
95
|
blueprint<T>(bp_fn: (schema: Schemas, config?: T) => Promise<void>, config?: T, opts?: {
|
|
85
96
|
silent: boolean;
|
|
86
97
|
}): Promise<void>;
|
|
98
|
+
/**
|
|
99
|
+
* Compile the response validator for a route. Type.Any and Type.Unknown
|
|
100
|
+
* impose no constraints, so they are skipped entirely — routes with an
|
|
101
|
+
* open response schema (or none) bypass response validation and its
|
|
102
|
+
* clone at request time.
|
|
103
|
+
*/
|
|
104
|
+
private compileResponseValidation;
|
|
105
|
+
/**
|
|
106
|
+
* Shared route registration: compiles all validators once, then installs
|
|
107
|
+
* a request handler that validates params/query/body and — only when a
|
|
108
|
+
* response schema requires it — wraps res.json with response validation.
|
|
109
|
+
*/
|
|
110
|
+
private route;
|
|
87
111
|
get<TParams extends TSchema, TQuery extends TSchema, TBody extends TSchema, TResponse extends TSchema>(path: string, opts: RequestValidation<TParams, TQuery, TBody, TResponse> | undefined, handler: RequestHandler<Static<TParams>, Static<TResponse>, Static<TBody>, Static<TQuery>>): Promise<void>;
|
|
88
112
|
delete<TParams extends TSchema, TQuery extends TSchema, TBody extends TSchema, TResponse extends TSchema>(path: string, opts: RequestValidation<TParams, TQuery, TBody, TResponse> | undefined, handler: RequestHandler<Static<TParams>, Static<TResponse>, Static<TBody>, Static<TQuery>>): Promise<void>;
|
|
89
113
|
post<TParams extends TSchema, TQuery extends TSchema, TBody extends TSchema, TResponse extends TSchema>(path: string, opts: RequestValidation<TParams, TQuery, TBody, TResponse> | undefined, handler: RequestHandler<Static<TParams>, Static<TResponse>, Static<TBody>, Static<TQuery>>): Promise<void>;
|
|
@@ -104,4 +128,3 @@ export default class Schemas {
|
|
|
104
128
|
};
|
|
105
129
|
};
|
|
106
130
|
}
|
|
107
|
-
export {};
|
package/dist/lib/schema.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import morgan from 'morgan';
|
|
4
4
|
import bodyparser from 'body-parser';
|
|
5
5
|
import Err from '@openaddresses/batch-error';
|
|
6
|
-
import {
|
|
6
|
+
import { Kind } from '@sinclair/typebox';
|
|
7
7
|
import { OpenAPIV3 as Doc } from 'openapi-types';
|
|
8
8
|
import { Router } from 'express';
|
|
9
9
|
import { Ajv } from 'ajv';
|
|
@@ -19,6 +19,61 @@ const ajv = new Ajv({
|
|
|
19
19
|
coerceTypes: true
|
|
20
20
|
});
|
|
21
21
|
addFormatsModule.default(ajv);
|
|
22
|
+
const MAX_LOGGED_RESPONSE = 4096;
|
|
23
|
+
/**
|
|
24
|
+
* Deep-clone a value following JSON.stringify semantics: toJSON() support
|
|
25
|
+
* (Date => ISO string), undefined/function/symbol omission, NaN/Infinity
|
|
26
|
+
* => null, and circular reference detection. Response validation needs the
|
|
27
|
+
* serialized shape of the payload (Dates must already be strings), and ajv's
|
|
28
|
+
* removeAdditional/useDefaults mutate the object they validate, so the
|
|
29
|
+
* caller's object must be protected. A single-pass clone does both without
|
|
30
|
+
* the cost of JSON.parse(JSON.stringify()) building an intermediate string.
|
|
31
|
+
*/
|
|
32
|
+
function jsonClone(value, seen) {
|
|
33
|
+
if (value !== null && typeof value === 'object' && typeof value.toJSON === 'function') {
|
|
34
|
+
value = value.toJSON();
|
|
35
|
+
}
|
|
36
|
+
if (value === null)
|
|
37
|
+
return null;
|
|
38
|
+
switch (typeof value) {
|
|
39
|
+
case 'string':
|
|
40
|
+
case 'boolean':
|
|
41
|
+
return value;
|
|
42
|
+
case 'number':
|
|
43
|
+
return Number.isFinite(value) ? value : null;
|
|
44
|
+
case 'bigint':
|
|
45
|
+
throw new TypeError('Do not know how to serialize a BigInt');
|
|
46
|
+
case 'object':
|
|
47
|
+
break;
|
|
48
|
+
default:
|
|
49
|
+
return undefined; // undefined | function | symbol are omitted, as in JSON.stringify
|
|
50
|
+
}
|
|
51
|
+
if (!seen)
|
|
52
|
+
seen = new Set();
|
|
53
|
+
if (seen.has(value))
|
|
54
|
+
throw new TypeError('Converting circular structure to JSON');
|
|
55
|
+
seen.add(value);
|
|
56
|
+
let out;
|
|
57
|
+
if (Array.isArray(value)) {
|
|
58
|
+
const arr = new Array(value.length);
|
|
59
|
+
for (let i = 0; i < value.length; i++) {
|
|
60
|
+
const v = jsonClone(value[i], seen);
|
|
61
|
+
arr[i] = v === undefined ? null : v;
|
|
62
|
+
}
|
|
63
|
+
out = arr;
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
const obj = {};
|
|
67
|
+
for (const key of Object.keys(value)) {
|
|
68
|
+
const v = jsonClone(value[key], seen);
|
|
69
|
+
if (v !== undefined)
|
|
70
|
+
obj[key] = v;
|
|
71
|
+
}
|
|
72
|
+
out = obj;
|
|
73
|
+
}
|
|
74
|
+
seen.delete(value);
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
22
77
|
/**
|
|
23
78
|
* @class
|
|
24
79
|
*
|
|
@@ -43,46 +98,48 @@ export default class Schemas {
|
|
|
43
98
|
}
|
|
44
99
|
/**
|
|
45
100
|
* Compile body validators keyed by content-type. Returns null when no
|
|
46
|
-
* body validation is configured. Each entry is either `true`
|
|
47
|
-
* accepted for this content-type) or a compiled ajv validator
|
|
101
|
+
* body validation is configured. Each validator entry is either `true`
|
|
102
|
+
* (any body accepted for this content-type) or a compiled ajv validator.
|
|
103
|
+
* The content-type matcher and supported-types error string are
|
|
104
|
+
* precomputed here so no per-request allocation is needed.
|
|
48
105
|
*/
|
|
49
106
|
compileBodyValidators(body) {
|
|
50
107
|
const normalized = normalizeBody(body);
|
|
51
108
|
if (!normalized)
|
|
52
109
|
return null;
|
|
53
|
-
const
|
|
110
|
+
const validators = new Map();
|
|
54
111
|
for (const ct of Object.keys(normalized)) {
|
|
55
112
|
const schema = normalized[ct].schema;
|
|
56
113
|
if (schema === true || schema === undefined) {
|
|
57
|
-
|
|
114
|
+
validators.set(ct, true);
|
|
58
115
|
}
|
|
59
116
|
else {
|
|
60
|
-
|
|
117
|
+
validators.set(ct, ajv.compile(schema));
|
|
61
118
|
}
|
|
62
119
|
}
|
|
63
|
-
return
|
|
120
|
+
return {
|
|
121
|
+
validators,
|
|
122
|
+
matcher: normalized,
|
|
123
|
+
supported: Object.keys(normalized).join(', ')
|
|
124
|
+
};
|
|
64
125
|
}
|
|
65
126
|
/**
|
|
66
|
-
* Validate the incoming request body against the compiled
|
|
67
|
-
*
|
|
127
|
+
* Validate the incoming request body against the compiled validation
|
|
128
|
+
* state for the route. Pushes a Body error onto `errors` if validation
|
|
68
129
|
* fails; returns an Err to respond with for an unsupported content-type.
|
|
69
130
|
*
|
|
70
|
-
* If `
|
|
131
|
+
* If `opts.required` is false and the request has no body / no
|
|
71
132
|
* content-type, validation is skipped.
|
|
72
133
|
*/
|
|
73
|
-
validateBody(req,
|
|
134
|
+
validateBody(req, compiled, errors, opts = {}) {
|
|
74
135
|
const raw = (req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
|
|
75
136
|
const required = opts.required !== false;
|
|
76
137
|
if (!raw && !required)
|
|
77
138
|
return null;
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
normalizedMap[key] = { schema: true };
|
|
81
|
-
const matched = matchContentType(raw, normalizedMap);
|
|
82
|
-
const validator = matched ? validators.get(matched) : undefined;
|
|
139
|
+
const matched = matchContentType(raw, compiled.matcher);
|
|
140
|
+
const validator = matched ? compiled.validators.get(matched) : undefined;
|
|
83
141
|
if (!validator) {
|
|
84
|
-
|
|
85
|
-
return new Err(400, null, `Content-Type ${raw || '(none)'} not supported. Supported: ${supported}`);
|
|
142
|
+
return new Err(400, null, `Content-Type ${raw || '(none)'} not supported. Supported: ${compiled.supported}`);
|
|
86
143
|
}
|
|
87
144
|
if (validator !== true) {
|
|
88
145
|
if (!validator(req.body)) {
|
|
@@ -172,60 +229,40 @@ export default class Schemas {
|
|
|
172
229
|
console.log(`ok - loaded blueprint`);
|
|
173
230
|
await bp_fn(this, config);
|
|
174
231
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
Object.defineProperty(req, 'query', {
|
|
188
|
-
writable: true,
|
|
189
|
-
value: { ...req.query },
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
const errors = [];
|
|
193
|
-
if (paramsValidation && !paramsValidation(req.params))
|
|
194
|
-
errors.push({ type: 'Params', errors: paramsValidation.errors });
|
|
195
|
-
if (queryValidation && !queryValidation(req.query))
|
|
196
|
-
errors.push({ type: 'Query', errors: queryValidation.errors });
|
|
197
|
-
if (errors.length)
|
|
198
|
-
return Err.respond(this.validationError(Doc.HttpMethods.GET, path), res, errors);
|
|
199
|
-
const json = res.json;
|
|
200
|
-
res.json = function (obj) {
|
|
201
|
-
obj = JSON.parse(JSON.stringify(obj)); // Here as Date => String needs to happen
|
|
202
|
-
if ((res.statusCode === null || res.statusCode === 200) && resValidation && !resValidation(obj)) {
|
|
203
|
-
res.status(400);
|
|
204
|
-
console.error(`Response Validation Error: ${JSON.stringify(obj)}`);
|
|
205
|
-
return json.call(this, { type: 'Response', errors: resValidation.errors });
|
|
206
|
-
}
|
|
207
|
-
else {
|
|
208
|
-
return json.call(this, obj);
|
|
209
|
-
}
|
|
210
|
-
};
|
|
211
|
-
return handler(req, res, next);
|
|
212
|
-
};
|
|
213
|
-
this.router.get(path, _handler);
|
|
214
|
-
}
|
|
215
|
-
catch (err) {
|
|
216
|
-
throw new Error(`Get: ${path}: ` + err, { cause: err });
|
|
217
|
-
}
|
|
232
|
+
/**
|
|
233
|
+
* Compile the response validator for a route. Type.Any and Type.Unknown
|
|
234
|
+
* impose no constraints, so they are skipped entirely — routes with an
|
|
235
|
+
* open response schema (or none) bypass response validation and its
|
|
236
|
+
* clone at request time.
|
|
237
|
+
*/
|
|
238
|
+
compileResponseValidation(res) {
|
|
239
|
+
if (!res)
|
|
240
|
+
return null;
|
|
241
|
+
if (res[Kind] === 'Any' || res[Kind] === 'Unknown')
|
|
242
|
+
return null;
|
|
243
|
+
return ajv.compile(res);
|
|
218
244
|
}
|
|
219
|
-
|
|
245
|
+
/**
|
|
246
|
+
* Shared route registration: compiles all validators once, then installs
|
|
247
|
+
* a request handler that validates params/query/body and — only when a
|
|
248
|
+
* response schema requires it — wraps res.json with response validation.
|
|
249
|
+
*/
|
|
250
|
+
route(method, path, opts, handler, allowBody) {
|
|
251
|
+
const methodUpper = String(method).toUpperCase();
|
|
220
252
|
try {
|
|
221
253
|
const validation = this.normalizeValidation(opts);
|
|
222
|
-
this.docs.push({ method
|
|
223
|
-
this.schemas.set(
|
|
224
|
-
const resValidation =
|
|
254
|
+
this.docs.push({ method, path }, validation);
|
|
255
|
+
this.schemas.set(`${methodUpper} ${path}`, validation);
|
|
256
|
+
const resValidation = this.compileResponseValidation(validation.res);
|
|
225
257
|
const paramsValidation = validation.params && ajv.compile(validation.params);
|
|
226
258
|
const queryValidation = validation.query && ajv.compile(validation.query);
|
|
227
|
-
|
|
259
|
+
let bodyValidation = null;
|
|
260
|
+
if (allowBody) {
|
|
261
|
+
bodyValidation = this.compileBodyValidators(validation.body);
|
|
262
|
+
}
|
|
263
|
+
else if (validation.body) {
|
|
228
264
|
throw new Error(`Body not allowed`);
|
|
265
|
+
}
|
|
229
266
|
const _handler = (req, res, next) => {
|
|
230
267
|
if (req.query) { // Ref: https://github.com/cdimascio/express-openapi-validator/issues/969
|
|
231
268
|
Object.defineProperty(req, 'query', {
|
|
@@ -238,171 +275,51 @@ export default class Schemas {
|
|
|
238
275
|
errors.push({ type: 'Params', errors: paramsValidation.errors });
|
|
239
276
|
if (queryValidation && !queryValidation(req.query))
|
|
240
277
|
errors.push({ type: 'Query', errors: queryValidation.errors });
|
|
241
|
-
if (
|
|
242
|
-
|
|
243
|
-
const json = res.json;
|
|
244
|
-
res.json = function (obj) {
|
|
245
|
-
obj = JSON.parse(JSON.stringify(obj)); // Here as Date => String needs to happen
|
|
246
|
-
if ((res.statusCode === null || res.statusCode === 200) && resValidation && !resValidation(obj)) {
|
|
247
|
-
res.status(400);
|
|
248
|
-
console.error(`Response Validation Error: ${JSON.stringify(obj)}`);
|
|
249
|
-
return json.call(this, { type: 'Response', errors: resValidation.errors });
|
|
250
|
-
}
|
|
251
|
-
else {
|
|
252
|
-
return json.call(this, obj);
|
|
253
|
-
}
|
|
254
|
-
};
|
|
255
|
-
return handler(req, res, next);
|
|
256
|
-
};
|
|
257
|
-
this.router.delete(path, _handler);
|
|
258
|
-
}
|
|
259
|
-
catch (err) {
|
|
260
|
-
throw new Error(`Delete: ${path}: ` + String(err), { cause: err });
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
async post(path, opts = {}, handler) {
|
|
264
|
-
try {
|
|
265
|
-
const validation = this.normalizeValidation(opts);
|
|
266
|
-
this.docs.push({ method: Doc.HttpMethods.POST, path: path }, validation);
|
|
267
|
-
this.schemas.set(`POST ${path}`, validation);
|
|
268
|
-
const resValidation = validation.res && !(validation.res instanceof Type.Any) && !(validation.res instanceof Type.Unknown) && ajv.compile(validation.res);
|
|
269
|
-
const paramsValidation = validation.params && ajv.compile(validation.params);
|
|
270
|
-
const queryValidation = validation.query && ajv.compile(validation.query);
|
|
271
|
-
const bodyValidators = this.compileBodyValidators(validation.body);
|
|
272
|
-
const _handler = (req, res, next) => {
|
|
273
|
-
if (req.query) { // Ref: https://github.com/cdimascio/express-openapi-validator/issues/969
|
|
274
|
-
Object.defineProperty(req, 'query', {
|
|
275
|
-
writable: true,
|
|
276
|
-
value: { ...req.query },
|
|
277
|
-
});
|
|
278
|
-
}
|
|
279
|
-
const errors = [];
|
|
280
|
-
if (paramsValidation && !paramsValidation(req.params))
|
|
281
|
-
errors.push({ type: 'Params', errors: paramsValidation.errors });
|
|
282
|
-
if (queryValidation && !queryValidation(req.query))
|
|
283
|
-
errors.push({ type: 'Query', errors: queryValidation.errors });
|
|
284
|
-
if (bodyValidators) {
|
|
285
|
-
const ctErr = this.validateBody(req, bodyValidators, errors, { required: validation.bodyRequired });
|
|
278
|
+
if (bodyValidation) {
|
|
279
|
+
const ctErr = this.validateBody(req, bodyValidation, errors, { required: validation.bodyRequired });
|
|
286
280
|
if (ctErr)
|
|
287
281
|
return Err.respond(ctErr, res);
|
|
288
282
|
}
|
|
289
283
|
if (errors.length)
|
|
290
|
-
return Err.respond(this.validationError(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
284
|
+
return Err.respond(this.validationError(method, path), res, errors);
|
|
285
|
+
if (resValidation) {
|
|
286
|
+
const json = res.json;
|
|
287
|
+
res.json = function (obj) {
|
|
288
|
+
if (res.statusCode === null || res.statusCode === 200) {
|
|
289
|
+
obj = jsonClone(obj); // Date => String needs to happen before validation
|
|
290
|
+
if (!resValidation(obj)) {
|
|
291
|
+
res.status(400);
|
|
292
|
+
const logged = JSON.stringify(obj);
|
|
293
|
+
console.error(`Response Validation Error: ${methodUpper} ${path}: ${logged && logged.length > MAX_LOGGED_RESPONSE ? logged.slice(0, MAX_LOGGED_RESPONSE) + '... (truncated)' : logged}`);
|
|
294
|
+
return json.call(this, { type: 'Response', errors: resValidation.errors });
|
|
295
|
+
}
|
|
296
|
+
}
|
|
300
297
|
return json.call(this, obj);
|
|
301
|
-
}
|
|
302
|
-
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
303
300
|
return handler(req, res, next);
|
|
304
301
|
};
|
|
305
|
-
this.router
|
|
302
|
+
this.router[method](path, _handler);
|
|
306
303
|
}
|
|
307
304
|
catch (err) {
|
|
308
|
-
|
|
305
|
+
const label = methodUpper.charAt(0) + methodUpper.slice(1).toLowerCase();
|
|
306
|
+
throw new Error(`${label}: ${path}: ` + String(err), { cause: err });
|
|
309
307
|
}
|
|
310
308
|
}
|
|
309
|
+
async get(path, opts = {}, handler) {
|
|
310
|
+
this.route(Doc.HttpMethods.GET, path, opts, handler, false);
|
|
311
|
+
}
|
|
312
|
+
async delete(path, opts = {}, handler) {
|
|
313
|
+
this.route(Doc.HttpMethods.DELETE, path, opts, handler, false);
|
|
314
|
+
}
|
|
315
|
+
async post(path, opts = {}, handler) {
|
|
316
|
+
this.route(Doc.HttpMethods.POST, path, opts, handler, true);
|
|
317
|
+
}
|
|
311
318
|
async patch(path, opts = {}, handler) {
|
|
312
|
-
|
|
313
|
-
const validation = this.normalizeValidation(opts);
|
|
314
|
-
this.docs.push({ method: Doc.HttpMethods.PATCH, path: path }, validation);
|
|
315
|
-
this.schemas.set(`PATCH ${path}`, validation);
|
|
316
|
-
const resValidation = validation.res && !(validation.res instanceof Type.Any) && !(validation.res instanceof Type.Unknown) && ajv.compile(validation.res);
|
|
317
|
-
const paramsValidation = validation.params && ajv.compile(validation.params);
|
|
318
|
-
const queryValidation = validation.query && ajv.compile(validation.query);
|
|
319
|
-
const bodyValidators = this.compileBodyValidators(validation.body);
|
|
320
|
-
const _handler = (req, res, next) => {
|
|
321
|
-
if (req.query) { // Ref: https://github.com/cdimascio/express-openapi-validator/issues/969
|
|
322
|
-
Object.defineProperty(req, 'query', {
|
|
323
|
-
writable: true,
|
|
324
|
-
value: { ...req.query },
|
|
325
|
-
});
|
|
326
|
-
}
|
|
327
|
-
const errors = [];
|
|
328
|
-
if (paramsValidation && !paramsValidation(req.params))
|
|
329
|
-
errors.push({ type: 'Params', errors: paramsValidation.errors });
|
|
330
|
-
if (queryValidation && !queryValidation(req.query))
|
|
331
|
-
errors.push({ type: 'Query', errors: queryValidation.errors });
|
|
332
|
-
if (bodyValidators) {
|
|
333
|
-
const ctErr = this.validateBody(req, bodyValidators, errors, { required: validation.bodyRequired });
|
|
334
|
-
if (ctErr)
|
|
335
|
-
return Err.respond(ctErr, res);
|
|
336
|
-
}
|
|
337
|
-
if (errors.length)
|
|
338
|
-
return Err.respond(this.validationError(Doc.HttpMethods.PATCH, path), res, errors);
|
|
339
|
-
const json = res.json;
|
|
340
|
-
res.json = function (obj) {
|
|
341
|
-
obj = JSON.parse(JSON.stringify(obj)); // Here as Date => String needs to happen
|
|
342
|
-
if ((res.statusCode === null || res.statusCode === 200) && resValidation && !resValidation(obj)) {
|
|
343
|
-
res.status(400);
|
|
344
|
-
console.error(`Response Validation Error: ${JSON.stringify(obj)}`);
|
|
345
|
-
return json.call(this, { type: 'Response', errors: resValidation.errors });
|
|
346
|
-
}
|
|
347
|
-
else {
|
|
348
|
-
return json.call(this, obj);
|
|
349
|
-
}
|
|
350
|
-
};
|
|
351
|
-
return handler(req, res, next);
|
|
352
|
-
};
|
|
353
|
-
this.router.patch(path, _handler);
|
|
354
|
-
}
|
|
355
|
-
catch (err) {
|
|
356
|
-
throw new Error(`Patch: ${path}: ` + String(err), { cause: err });
|
|
357
|
-
}
|
|
319
|
+
this.route(Doc.HttpMethods.PATCH, path, opts, handler, true);
|
|
358
320
|
}
|
|
359
321
|
async put(path, opts = {}, handler) {
|
|
360
|
-
|
|
361
|
-
const validation = this.normalizeValidation(opts);
|
|
362
|
-
this.docs.push({ method: Doc.HttpMethods.PUT, path: path }, validation);
|
|
363
|
-
this.schemas.set(`PUT ${path}`, validation);
|
|
364
|
-
const resValidation = validation.res && !(validation.res instanceof Type.Any) && !(validation.res instanceof Type.Unknown) && ajv.compile(validation.res);
|
|
365
|
-
const paramsValidation = validation.params && ajv.compile(validation.params);
|
|
366
|
-
const queryValidation = validation.query && ajv.compile(validation.query);
|
|
367
|
-
const bodyValidators = this.compileBodyValidators(validation.body);
|
|
368
|
-
const _handler = (req, res, next) => {
|
|
369
|
-
if (req.query) { // Ref: https://github.com/cdimascio/express-openapi-validator/issues/969
|
|
370
|
-
Object.defineProperty(req, 'query', {
|
|
371
|
-
writable: true,
|
|
372
|
-
value: { ...req.query },
|
|
373
|
-
});
|
|
374
|
-
}
|
|
375
|
-
const errors = [];
|
|
376
|
-
if (paramsValidation && !paramsValidation(req.params))
|
|
377
|
-
errors.push({ type: 'Params', errors: paramsValidation.errors });
|
|
378
|
-
if (queryValidation && !queryValidation(req.query))
|
|
379
|
-
errors.push({ type: 'Query', errors: queryValidation.errors });
|
|
380
|
-
if (bodyValidators) {
|
|
381
|
-
const ctErr = this.validateBody(req, bodyValidators, errors, { required: validation.bodyRequired });
|
|
382
|
-
if (ctErr)
|
|
383
|
-
return Err.respond(ctErr, res);
|
|
384
|
-
}
|
|
385
|
-
if (errors.length)
|
|
386
|
-
return Err.respond(this.validationError(Doc.HttpMethods.PUT, path), res, errors);
|
|
387
|
-
const json = res.json;
|
|
388
|
-
res.json = function (obj) {
|
|
389
|
-
obj = JSON.parse(JSON.stringify(obj)); // Here as Date => String needs to happen
|
|
390
|
-
if ((res.statusCode === null || res.statusCode === 200) && resValidation && !resValidation(obj)) {
|
|
391
|
-
res.status(400);
|
|
392
|
-
console.error(`Response Validation Error: ${JSON.stringify(obj)}`);
|
|
393
|
-
return json.call(this, { type: 'Response', errors: resValidation.errors });
|
|
394
|
-
}
|
|
395
|
-
else {
|
|
396
|
-
return json.call(this, obj);
|
|
397
|
-
}
|
|
398
|
-
};
|
|
399
|
-
return handler(req, res, next);
|
|
400
|
-
};
|
|
401
|
-
this.router.put(path, _handler);
|
|
402
|
-
}
|
|
403
|
-
catch (err) {
|
|
404
|
-
throw new Error(`Put: ${path}: ` + String(err), { cause: err });
|
|
405
|
-
}
|
|
322
|
+
this.route(Doc.HttpMethods.PUT, path, opts, handler, true);
|
|
406
323
|
}
|
|
407
324
|
not_found() {
|
|
408
325
|
this.router.all('*path', (req, res) => {
|