@openaddresses/batch-schema 10.24.1 → 10.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/README.md +81 -0
- package/dist/lib/openapi.js +17 -6
- package/dist/lib/openapi.js.map +1 -1
- package/dist/lib/schema.d.ts +22 -0
- package/dist/lib/schema.js +117 -37
- package/dist/lib/schema.js.map +1 -1
- package/dist/lib/types.d.ts +49 -1
- package/dist/lib/types.js +48 -1
- package/dist/lib/types.js.map +1 -1
- package/dist/test/schema.srv.test.js +255 -1
- package/dist/test/schema.srv.test.js.map +1 -1
- package/lib/openapi.ts +15 -6
- package/lib/schema.ts +134 -34
- package/lib/types.ts +87 -1
- package/package.json +1 -1
- package/test/schema.srv.test.ts +288 -1
package/lib/schema.ts
CHANGED
|
@@ -13,6 +13,7 @@ import { Ajv } from 'ajv'
|
|
|
13
13
|
import type { ErrorObject } from 'ajv'
|
|
14
14
|
import addFormatsModule from 'ajv-formats'
|
|
15
15
|
import type { RequestValidation } from './types.js';
|
|
16
|
+
import { normalizeBody, matchContentType } from './types.js';
|
|
16
17
|
import type { OpenAPIDocumentInput } from './openapi.js'
|
|
17
18
|
import SchemaAPI from './api.js';
|
|
18
19
|
import Docs from './openapi.js';
|
|
@@ -53,6 +54,67 @@ export default class Schemas {
|
|
|
53
54
|
return new Err(400, null, `Validation Error ${String(method).toUpperCase()} ${path}`);
|
|
54
55
|
}
|
|
55
56
|
|
|
57
|
+
/**
|
|
58
|
+
* Compile body validators keyed by content-type. Returns null when no
|
|
59
|
+
* body validation is configured. Each entry is either `true` (any body
|
|
60
|
+
* accepted for this content-type) or a compiled ajv validator function.
|
|
61
|
+
*/
|
|
62
|
+
compileBodyValidators(
|
|
63
|
+
body: RequestValidation<TSchema, TSchema, TSchema, TSchema>['body']
|
|
64
|
+
): Map<string, true | ReturnType<typeof ajv.compile>> | null {
|
|
65
|
+
const normalized = normalizeBody(body);
|
|
66
|
+
if (!normalized) return null;
|
|
67
|
+
|
|
68
|
+
const map = new Map<string, true | ReturnType<typeof ajv.compile>>();
|
|
69
|
+
for (const ct of Object.keys(normalized)) {
|
|
70
|
+
const schema = normalized[ct].schema;
|
|
71
|
+
if (schema === true || schema === undefined) {
|
|
72
|
+
map.set(ct, true);
|
|
73
|
+
} else {
|
|
74
|
+
map.set(ct, ajv.compile(schema));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return map;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Validate the incoming request body against the compiled validator
|
|
82
|
+
* map for the route. Pushes a Body error onto `errors` if validation
|
|
83
|
+
* fails; returns an Err to respond with for an unsupported content-type.
|
|
84
|
+
*
|
|
85
|
+
* If `bodyRequired` is false and the request has no body / no
|
|
86
|
+
* content-type, validation is skipped.
|
|
87
|
+
*/
|
|
88
|
+
validateBody(
|
|
89
|
+
req: Request,
|
|
90
|
+
validators: Map<string, true | ReturnType<typeof ajv.compile>>,
|
|
91
|
+
errors: Array<ErrorListItem>,
|
|
92
|
+
opts: { required?: boolean } = {}
|
|
93
|
+
): Err | null {
|
|
94
|
+
const raw = (req.headers['content-type'] || '').split(';')[0].trim().toLowerCase();
|
|
95
|
+
const required = opts.required !== false;
|
|
96
|
+
|
|
97
|
+
if (!raw && !required) return null;
|
|
98
|
+
|
|
99
|
+
const normalizedMap: Record<string, { schema: true }> = {};
|
|
100
|
+
for (const key of validators.keys()) normalizedMap[key] = { schema: true };
|
|
101
|
+
const matched = matchContentType(raw, normalizedMap);
|
|
102
|
+
const validator = matched ? validators.get(matched) : undefined;
|
|
103
|
+
|
|
104
|
+
if (!validator) {
|
|
105
|
+
const supported = Array.from(validators.keys()).join(', ');
|
|
106
|
+
return new Err(400, null, `Content-Type ${raw || '(none)'} not supported. Supported: ${supported}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (validator !== true) {
|
|
110
|
+
if (!validator(req.body)) {
|
|
111
|
+
errors.push({ type: 'Body', errors: validator.errors as ErrorObject[] });
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
56
118
|
constructor(
|
|
57
119
|
router: Router,
|
|
58
120
|
opts: {
|
|
@@ -74,8 +136,16 @@ export default class Schemas {
|
|
|
74
136
|
this.router.use(morgan('combined', morganOptions));
|
|
75
137
|
}
|
|
76
138
|
|
|
77
|
-
this.router.use(bodyparser.urlencoded({ extended: true }));
|
|
139
|
+
this.router.use(bodyparser.urlencoded({ extended: true, limit: `${opts.limit || 50}mb` }));
|
|
78
140
|
this.router.use(bodyparser.json({ limit: `${opts.limit || 50}mb` }));
|
|
141
|
+
this.router.use(bodyparser.text({
|
|
142
|
+
limit: `${opts.limit || 50}mb`,
|
|
143
|
+
type: ['text/*', 'application/xml', 'application/*+xml']
|
|
144
|
+
}));
|
|
145
|
+
this.router.use(bodyparser.raw({
|
|
146
|
+
limit: `${opts.limit || 50}mb`,
|
|
147
|
+
type: ['application/octet-stream']
|
|
148
|
+
}));
|
|
79
149
|
|
|
80
150
|
if (opts.error) {
|
|
81
151
|
this.error = opts.error;
|
|
@@ -91,6 +161,15 @@ export default class Schemas {
|
|
|
91
161
|
this.schemas = new Map();
|
|
92
162
|
}
|
|
93
163
|
|
|
164
|
+
normalizeValidation<TParams extends TSchema, TQuery extends TSchema, TBody extends TSchema, TResponse extends TSchema>(
|
|
165
|
+
opts: RequestValidation<TParams, TQuery, TBody, TResponse> = {}
|
|
166
|
+
): RequestValidation<TParams, TQuery, TBody, TResponse> {
|
|
167
|
+
return {
|
|
168
|
+
deprecated: false,
|
|
169
|
+
...opts
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
94
173
|
async api() {
|
|
95
174
|
await SchemaAPI(this);
|
|
96
175
|
}
|
|
@@ -147,13 +226,15 @@ export default class Schemas {
|
|
|
147
226
|
handler: RequestHandler<Static<TParams>, Static<TResponse>, Static<TBody>, Static<TQuery>>
|
|
148
227
|
) {
|
|
149
228
|
try {
|
|
150
|
-
this.
|
|
151
|
-
|
|
229
|
+
const validation = this.normalizeValidation(opts);
|
|
230
|
+
|
|
231
|
+
this.docs.push({ method: Doc.HttpMethods.GET, path: path }, validation);
|
|
232
|
+
this.schemas.set(`GET ${path}`, validation);
|
|
152
233
|
|
|
153
|
-
const resValidation =
|
|
154
|
-
const paramsValidation =
|
|
155
|
-
const queryValidation =
|
|
156
|
-
if (
|
|
234
|
+
const resValidation = validation.res && !(validation.res instanceof Type.Any) && !(validation.res instanceof Type.Unknown) && ajv.compile(validation.res);
|
|
235
|
+
const paramsValidation = validation.params && ajv.compile(validation.params);
|
|
236
|
+
const queryValidation = validation.query && ajv.compile(validation.query);
|
|
237
|
+
if (validation.body) throw new Error(`Body not allowed`);
|
|
157
238
|
|
|
158
239
|
const _handler: RequestHandler = (req, res, next) => {
|
|
159
240
|
if (req.query) { // Ref: https://github.com/cdimascio/express-openapi-validator/issues/969
|
|
@@ -195,13 +276,15 @@ export default class Schemas {
|
|
|
195
276
|
handler: RequestHandler<Static<TParams>, Static<TResponse>, Static<TBody>, Static<TQuery>>
|
|
196
277
|
) {
|
|
197
278
|
try {
|
|
198
|
-
this.
|
|
199
|
-
this.schemas.set(`DELETE ${path}`, opts);
|
|
279
|
+
const validation = this.normalizeValidation(opts);
|
|
200
280
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
281
|
+
this.docs.push({ method: Doc.HttpMethods.DELETE, path: path }, validation);
|
|
282
|
+
this.schemas.set(`DELETE ${path}`, validation);
|
|
283
|
+
|
|
284
|
+
const resValidation = validation.res && !(validation.res instanceof Type.Any) && !(validation.res instanceof Type.Unknown) && ajv.compile(validation.res);
|
|
285
|
+
const paramsValidation = validation.params && ajv.compile(validation.params);
|
|
286
|
+
const queryValidation = validation.query && ajv.compile(validation.query);
|
|
287
|
+
if (validation.body) throw new Error(`Body not allowed`);
|
|
205
288
|
|
|
206
289
|
const _handler: RequestHandler = (req, res, next) => {
|
|
207
290
|
if (req.query) { // Ref: https://github.com/cdimascio/express-openapi-validator/issues/969
|
|
@@ -243,13 +326,15 @@ export default class Schemas {
|
|
|
243
326
|
handler: RequestHandler<Static<TParams>, Static<TResponse>, Static<TBody>, Static<TQuery>>
|
|
244
327
|
) {
|
|
245
328
|
try {
|
|
246
|
-
this.
|
|
247
|
-
|
|
329
|
+
const validation = this.normalizeValidation(opts);
|
|
330
|
+
|
|
331
|
+
this.docs.push({ method: Doc.HttpMethods.POST, path: path }, validation);
|
|
332
|
+
this.schemas.set(`POST ${path}`, validation);
|
|
248
333
|
|
|
249
|
-
const resValidation =
|
|
250
|
-
const paramsValidation =
|
|
251
|
-
const queryValidation =
|
|
252
|
-
const
|
|
334
|
+
const resValidation = validation.res && !(validation.res instanceof Type.Any) && !(validation.res instanceof Type.Unknown) && ajv.compile(validation.res);
|
|
335
|
+
const paramsValidation = validation.params && ajv.compile(validation.params);
|
|
336
|
+
const queryValidation = validation.query && ajv.compile(validation.query);
|
|
337
|
+
const bodyValidators = this.compileBodyValidators(validation.body);
|
|
253
338
|
|
|
254
339
|
const _handler: RequestHandler = (req, res, next) => {
|
|
255
340
|
if (req.query) { // Ref: https://github.com/cdimascio/express-openapi-validator/issues/969
|
|
@@ -262,7 +347,10 @@ export default class Schemas {
|
|
|
262
347
|
const errors: Array<ErrorListItem> = [];
|
|
263
348
|
if (paramsValidation && !paramsValidation(req.params)) errors.push({ type: 'Params', errors: paramsValidation.errors as ErrorObject[] });
|
|
264
349
|
if (queryValidation && !queryValidation(req.query)) errors.push({ type: 'Query', errors: queryValidation.errors as ErrorObject[] });
|
|
265
|
-
if (
|
|
350
|
+
if (bodyValidators) {
|
|
351
|
+
const ctErr = this.validateBody(req, bodyValidators, errors, { required: validation.bodyRequired });
|
|
352
|
+
if (ctErr) return Err.respond(ctErr, res);
|
|
353
|
+
}
|
|
266
354
|
if (errors.length) return Err.respond(this.validationError(Doc.HttpMethods.POST, path), res, errors);
|
|
267
355
|
|
|
268
356
|
const json = res.json;
|
|
@@ -292,13 +380,15 @@ export default class Schemas {
|
|
|
292
380
|
handler: RequestHandler<Static<TParams>, Static<TResponse>, Static<TBody>, Static<TQuery>>
|
|
293
381
|
) {
|
|
294
382
|
try {
|
|
295
|
-
this.
|
|
296
|
-
|
|
383
|
+
const validation = this.normalizeValidation(opts);
|
|
384
|
+
|
|
385
|
+
this.docs.push({ method: Doc.HttpMethods.PATCH, path: path }, validation);
|
|
386
|
+
this.schemas.set(`PATCH ${path}`, validation);
|
|
297
387
|
|
|
298
|
-
const resValidation =
|
|
299
|
-
const paramsValidation =
|
|
300
|
-
const queryValidation =
|
|
301
|
-
const
|
|
388
|
+
const resValidation = validation.res && !(validation.res instanceof Type.Any) && !(validation.res instanceof Type.Unknown) && ajv.compile(validation.res);
|
|
389
|
+
const paramsValidation = validation.params && ajv.compile(validation.params);
|
|
390
|
+
const queryValidation = validation.query && ajv.compile(validation.query);
|
|
391
|
+
const bodyValidators = this.compileBodyValidators(validation.body);
|
|
302
392
|
|
|
303
393
|
const _handler: RequestHandler = (req, res, next) => {
|
|
304
394
|
if (req.query) { // Ref: https://github.com/cdimascio/express-openapi-validator/issues/969
|
|
@@ -311,7 +401,10 @@ export default class Schemas {
|
|
|
311
401
|
const errors: Array<ErrorListItem> = [];
|
|
312
402
|
if (paramsValidation && !paramsValidation(req.params)) errors.push({ type: 'Params', errors: paramsValidation.errors as ErrorObject[] });
|
|
313
403
|
if (queryValidation && !queryValidation(req.query)) errors.push({ type: 'Query', errors: queryValidation.errors as ErrorObject[] });
|
|
314
|
-
if (
|
|
404
|
+
if (bodyValidators) {
|
|
405
|
+
const ctErr = this.validateBody(req, bodyValidators, errors, { required: validation.bodyRequired });
|
|
406
|
+
if (ctErr) return Err.respond(ctErr, res);
|
|
407
|
+
}
|
|
315
408
|
if (errors.length) return Err.respond(this.validationError(Doc.HttpMethods.PATCH, path), res, errors);
|
|
316
409
|
|
|
317
410
|
const json = res.json;
|
|
@@ -341,13 +434,15 @@ export default class Schemas {
|
|
|
341
434
|
handler: RequestHandler<Static<TParams>, Static<TResponse>, Static<TBody>, Static<TQuery>>
|
|
342
435
|
) {
|
|
343
436
|
try {
|
|
344
|
-
this.
|
|
345
|
-
|
|
437
|
+
const validation = this.normalizeValidation(opts);
|
|
438
|
+
|
|
439
|
+
this.docs.push({ method: Doc.HttpMethods.PUT, path: path }, validation);
|
|
440
|
+
this.schemas.set(`PUT ${path}`, validation);
|
|
346
441
|
|
|
347
|
-
const resValidation =
|
|
348
|
-
const paramsValidation =
|
|
349
|
-
const queryValidation =
|
|
350
|
-
const
|
|
442
|
+
const resValidation = validation.res && !(validation.res instanceof Type.Any) && !(validation.res instanceof Type.Unknown) && ajv.compile(validation.res);
|
|
443
|
+
const paramsValidation = validation.params && ajv.compile(validation.params);
|
|
444
|
+
const queryValidation = validation.query && ajv.compile(validation.query);
|
|
445
|
+
const bodyValidators = this.compileBodyValidators(validation.body);
|
|
351
446
|
|
|
352
447
|
const _handler: RequestHandler = (req, res, next) => {
|
|
353
448
|
if (req.query) { // Ref: https://github.com/cdimascio/express-openapi-validator/issues/969
|
|
@@ -360,7 +455,10 @@ export default class Schemas {
|
|
|
360
455
|
const errors: Array<ErrorListItem> = [];
|
|
361
456
|
if (paramsValidation && !paramsValidation(req.params)) errors.push({ type: 'Params', errors: paramsValidation.errors as ErrorObject[] });
|
|
362
457
|
if (queryValidation && !queryValidation(req.query)) errors.push({ type: 'Query', errors: queryValidation.errors as ErrorObject[] });
|
|
363
|
-
if (
|
|
458
|
+
if (bodyValidators) {
|
|
459
|
+
const ctErr = this.validateBody(req, bodyValidators, errors, { required: validation.bodyRequired });
|
|
460
|
+
if (ctErr) return Err.respond(ctErr, res);
|
|
461
|
+
}
|
|
364
462
|
if (errors.length) return Err.respond(this.validationError(Doc.HttpMethods.PUT, path), res, errors);
|
|
365
463
|
|
|
366
464
|
const json = res.json;
|
|
@@ -396,6 +494,7 @@ export default class Schemas {
|
|
|
396
494
|
|
|
397
495
|
|
|
398
496
|
query(method: Doc.HttpMethods, url: string): {
|
|
497
|
+
deprecated?: boolean;
|
|
399
498
|
query?: object;
|
|
400
499
|
body?: object;
|
|
401
500
|
res?: object;
|
|
@@ -407,6 +506,7 @@ export default class Schemas {
|
|
|
407
506
|
const schema = JSON.parse(JSON.stringify(this.schemas.get(`${method} ${url}`)));
|
|
408
507
|
|
|
409
508
|
return {
|
|
509
|
+
deprecated: schema.deprecated,
|
|
410
510
|
query: schema.query,
|
|
411
511
|
body: schema.body,
|
|
412
512
|
res: schema.res
|
package/lib/types.ts
CHANGED
|
@@ -1,5 +1,88 @@
|
|
|
1
|
+
import { Kind } from '@sinclair/typebox';
|
|
1
2
|
import type { TSchema } from '@sinclair/typebox';
|
|
2
3
|
|
|
4
|
+
/**
|
|
5
|
+
* Per-content-type body entry. May be:
|
|
6
|
+
* - a TypeBox schema (validated, no example)
|
|
7
|
+
* - `true` (any body accepted, no validation)
|
|
8
|
+
* - an object with optional `schema` (TSchema | true), `example`, `examples`
|
|
9
|
+
* (forwarded to the OpenAPI media type object)
|
|
10
|
+
*/
|
|
11
|
+
export type BodyEntryConfig = {
|
|
12
|
+
schema?: TSchema | true;
|
|
13
|
+
example?: unknown;
|
|
14
|
+
examples?: Record<string, { summary?: string; description?: string; value: unknown }>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type BodyEntry = TSchema | true | BodyEntryConfig;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Body validation can either be:
|
|
21
|
+
* - a TypeBox schema (treated as `application/json` for backwards compatibility)
|
|
22
|
+
* - a record mapping content-type strings to a `BodyEntry`. Content-type
|
|
23
|
+
* keys may include wildcards: `text/*`, `application/*`, or `* / *`.
|
|
24
|
+
*/
|
|
25
|
+
export type BodyValidation<TBody extends TSchema> =
|
|
26
|
+
| TBody
|
|
27
|
+
| Record<string, BodyEntry>;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Normalized internal representation: every entry has the expanded
|
|
31
|
+
* `BodyEntryConfig` shape with `schema` always set (defaulting to `true`).
|
|
32
|
+
*/
|
|
33
|
+
export type NormalizedBody = Record<string, Required<Pick<BodyEntryConfig, 'schema'>> & BodyEntryConfig>;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Detect whether a given value is a TypeBox schema. TypeBox attaches a
|
|
37
|
+
* `[Kind]` symbol to every schema instance which is what we use to
|
|
38
|
+
* distinguish a schema from a content-type-keyed body record.
|
|
39
|
+
*/
|
|
40
|
+
export function isTSchema(value: unknown): value is TSchema {
|
|
41
|
+
return !!value && typeof value === 'object' && Kind in (value as object);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeEntry(entry: BodyEntry): NormalizedBody[string] {
|
|
45
|
+
if (entry === true) return { schema: true };
|
|
46
|
+
if (isTSchema(entry)) return { schema: entry };
|
|
47
|
+
return { schema: true, ...entry };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Normalize the body option into a `NormalizedBody` map. A bare TypeBox
|
|
52
|
+
* schema is mapped to `application/json` for backwards compatibility.
|
|
53
|
+
*/
|
|
54
|
+
export function normalizeBody(
|
|
55
|
+
body: TSchema | Record<string, BodyEntry> | undefined
|
|
56
|
+
): NormalizedBody | undefined {
|
|
57
|
+
if (!body) return undefined;
|
|
58
|
+
if (isTSchema(body)) return { 'application/json': { schema: body } };
|
|
59
|
+
|
|
60
|
+
const out: NormalizedBody = {};
|
|
61
|
+
for (const ct of Object.keys(body)) {
|
|
62
|
+
out[ct.toLowerCase()] = normalizeEntry(body[ct]);
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Match an incoming content-type against a normalized body map, honoring
|
|
69
|
+
* wildcard entries like `text/*` or `*\/*`. Exact matches always win.
|
|
70
|
+
*/
|
|
71
|
+
export function matchContentType(
|
|
72
|
+
incoming: string,
|
|
73
|
+
normalized: NormalizedBody
|
|
74
|
+
): string | undefined {
|
|
75
|
+
if (!incoming) {
|
|
76
|
+
return normalized['*/*'] ? '*/*' : undefined;
|
|
77
|
+
}
|
|
78
|
+
if (normalized[incoming]) return incoming;
|
|
79
|
+
|
|
80
|
+
const [type] = incoming.split('/');
|
|
81
|
+
if (type && normalized[`${type}/*`]) return `${type}/*`;
|
|
82
|
+
if (normalized['*/*']) return '*/*';
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
3
86
|
export type RequestValidation<
|
|
4
87
|
TParams extends TSchema,
|
|
5
88
|
TQuery extends TSchema,
|
|
@@ -7,11 +90,14 @@ export type RequestValidation<
|
|
|
7
90
|
TResponse extends TSchema
|
|
8
91
|
> = {
|
|
9
92
|
private?: boolean;
|
|
93
|
+
deprecated?: boolean;
|
|
10
94
|
name?: string;
|
|
11
95
|
group?: string;
|
|
12
96
|
description?: string;
|
|
13
97
|
params?: TParams;
|
|
14
98
|
query?: TQuery;
|
|
15
|
-
body?: TBody
|
|
99
|
+
body?: BodyValidation<TBody>;
|
|
100
|
+
/** Whether the request body is required. Defaults to true when `body` is set. */
|
|
101
|
+
bodyRequired?: boolean;
|
|
16
102
|
res?: TResponse
|
|
17
103
|
};
|