@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.
@@ -1,29 +1,29 @@
1
1
  name: Test
2
2
 
3
3
  on:
4
- pull_request:
5
- types:
6
- - opened
7
- - synchronize
8
- - reopened
9
- - ready_for_review
4
+ pull_request:
5
+ types:
6
+ - opened
7
+ - synchronize
8
+ - reopened
9
+ - ready_for_review
10
10
 
11
11
  jobs:
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 }}
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
- - uses: actions/setup-node@v2
21
- with:
22
- node-version: '18'
20
+ - uses: actions/setup-node@v2
21
+ with:
22
+ node-version: '18'
23
23
 
24
- - run: npm install
24
+ - run: npm install
25
25
 
26
- - run: npm run lint
26
+ - run: npm run lint
27
27
 
28
- - run: npm test
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
@@ -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 { Ajv } from 'ajv';
9
- import type { ErrorObject } from 'ajv';
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` (any body
41
- * accepted for this content-type) or a compiled ajv validator function.
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']): Map<string, true | ReturnType<typeof ajv.compile>> | null;
54
+ compileBodyValidators(body: RequestValidation<TSchema, TSchema, TSchema, TSchema>['body']): CompiledBodyValidation | null;
44
55
  /**
45
- * Validate the incoming request body against the compiled validator
46
- * map for the route. Pushes a Body error onto `errors` if validation
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 `bodyRequired` is false and the request has no body / no
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, validators: Map<string, true | ReturnType<typeof ajv.compile>>, errors: Array<ErrorListItem>, opts?: {
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 {};
@@ -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 { Type } from '@sinclair/typebox';
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` (any body
47
- * accepted for this content-type) or a compiled ajv validator function.
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 map = new Map();
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
- map.set(ct, true);
114
+ validators.set(ct, true);
58
115
  }
59
116
  else {
60
- map.set(ct, ajv.compile(schema));
117
+ validators.set(ct, ajv.compile(schema));
61
118
  }
62
119
  }
63
- return map;
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 validator
67
- * map for the route. Pushes a Body error onto `errors` if validation
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 `bodyRequired` is false and the request has no body / no
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, validators, errors, opts = {}) {
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 normalizedMap = {};
79
- for (const key of validators.keys())
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
- const supported = Array.from(validators.keys()).join(', ');
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
- async get(path, opts = {}, handler) {
176
- try {
177
- const validation = this.normalizeValidation(opts);
178
- this.docs.push({ method: Doc.HttpMethods.GET, path: path }, validation);
179
- this.schemas.set(`GET ${path}`, validation);
180
- const resValidation = validation.res && !(validation.res instanceof Type.Any) && !(validation.res instanceof Type.Unknown) && ajv.compile(validation.res);
181
- const paramsValidation = validation.params && ajv.compile(validation.params);
182
- const queryValidation = validation.query && ajv.compile(validation.query);
183
- if (validation.body)
184
- throw new Error(`Body not allowed`);
185
- const _handler = (req, res, next) => {
186
- if (req.query) { // Ref: https://github.com/cdimascio/express-openapi-validator/issues/969
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
- async delete(path, opts = {}, handler) {
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: Doc.HttpMethods.DELETE, path: path }, validation);
223
- this.schemas.set(`DELETE ${path}`, validation);
224
- const resValidation = validation.res && !(validation.res instanceof Type.Any) && !(validation.res instanceof Type.Unknown) && ajv.compile(validation.res);
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
- if (validation.body)
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 (errors.length)
242
- return Err.respond(this.validationError(Doc.HttpMethods.DELETE, path), res, errors);
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(Doc.HttpMethods.POST, path), res, errors);
291
- const json = res.json;
292
- res.json = function (obj) {
293
- obj = JSON.parse(JSON.stringify(obj)); // Here as Date => String needs to happen
294
- if ((res.statusCode === null || res.statusCode === 200) && resValidation && !resValidation(obj)) {
295
- res.status(400);
296
- console.error(`Response Validation Error: ${JSON.stringify(obj)}`);
297
- return json.call(this, { type: 'Response', errors: resValidation.errors });
298
- }
299
- else {
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.post(path, _handler);
302
+ this.router[method](path, _handler);
306
303
  }
307
304
  catch (err) {
308
- throw new Error(`Post: ${path}: ` + String(err), { cause: err });
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
- try {
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
- try {
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) => {