@openaddresses/batch-schema 10.25.0 → 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 +4 -0
- package/README.md +78 -0
- package/dist/lib/openapi.js +16 -6
- package/dist/lib/openapi.js.map +1 -1
- package/dist/lib/schema.d.ts +20 -0
- package/dist/lib/schema.js +78 -10
- package/dist/lib/schema.js.map +1 -1
- package/dist/lib/types.d.ts +48 -1
- package/dist/lib/types.js +48 -1
- package/dist/lib/types.js.map +1 -1
- package/dist/test/schema.srv.test.js +216 -0
- package/dist/test/schema.srv.test.js.map +1 -1
- package/lib/openapi.ts +14 -6
- package/lib/schema.ts +86 -7
- package/lib/types.ts +86 -1
- package/package.json +1 -1
- package/test/schema.srv.test.ts +244 -0
package/test/schema.srv.test.ts
CHANGED
|
@@ -24,6 +24,76 @@ describe('Schema Server', () => {
|
|
|
24
24
|
res.json({ ok: true });
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
+
await schema.post('/json-only', {
|
|
28
|
+
description: 'Backwards compatible JSON body',
|
|
29
|
+
body: Type.Object({
|
|
30
|
+
name: Type.String()
|
|
31
|
+
}),
|
|
32
|
+
res: Type.Object({ ok: Type.Boolean() })
|
|
33
|
+
}, async (req, res) => {
|
|
34
|
+
res.json({ ok: typeof req.body.name === 'string' });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await schema.post('/multi-body', {
|
|
38
|
+
description: 'Multi content-type body',
|
|
39
|
+
body: {
|
|
40
|
+
'application/json': Type.Object({ name: Type.String() }),
|
|
41
|
+
'text/xml': true
|
|
42
|
+
},
|
|
43
|
+
res: Type.Object({
|
|
44
|
+
contentType: Type.String(),
|
|
45
|
+
body: Type.Any()
|
|
46
|
+
})
|
|
47
|
+
}, async (req, res) => {
|
|
48
|
+
res.json({
|
|
49
|
+
contentType: (req.headers['content-type'] || '').split(';')[0],
|
|
50
|
+
body: req.body
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
await schema.post('/wildcard-body', {
|
|
55
|
+
description: 'Wildcard content-type body',
|
|
56
|
+
body: {
|
|
57
|
+
'application/json': Type.Object({ name: Type.String() }),
|
|
58
|
+
'text/*': true
|
|
59
|
+
},
|
|
60
|
+
res: Type.Object({
|
|
61
|
+
contentType: Type.String(),
|
|
62
|
+
body: Type.Any()
|
|
63
|
+
})
|
|
64
|
+
}, async (req, res) => {
|
|
65
|
+
res.json({
|
|
66
|
+
contentType: (req.headers['content-type'] || '').split(';')[0],
|
|
67
|
+
body: req.body
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await schema.post('/optional-body', {
|
|
72
|
+
description: 'Optional body',
|
|
73
|
+
body: Type.Object({ name: Type.String() }),
|
|
74
|
+
bodyRequired: false,
|
|
75
|
+
res: Type.Object({ ok: Type.Boolean() })
|
|
76
|
+
}, async (_req, res) => {
|
|
77
|
+
res.json({ ok: true });
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
await schema.post('/with-examples', {
|
|
81
|
+
description: 'Body with examples in OpenAPI',
|
|
82
|
+
body: {
|
|
83
|
+
'application/json': {
|
|
84
|
+
schema: Type.Object({ name: Type.String() }),
|
|
85
|
+
example: { name: 'sample' },
|
|
86
|
+
examples: {
|
|
87
|
+
primary: { summary: 'Primary', value: { name: 'alice' } },
|
|
88
|
+
secondary: { summary: 'Secondary', value: { name: 'bob' } }
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
res: Type.Object({ ok: Type.Boolean() })
|
|
93
|
+
}, async (_req, res) => {
|
|
94
|
+
res.json({ ok: true });
|
|
95
|
+
});
|
|
96
|
+
|
|
27
97
|
await schema.api();
|
|
28
98
|
done();
|
|
29
99
|
});
|
|
@@ -43,6 +113,31 @@ describe('Schema Server', () => {
|
|
|
43
113
|
query: false,
|
|
44
114
|
res: true
|
|
45
115
|
},
|
|
116
|
+
'POST /json-only': {
|
|
117
|
+
body: true,
|
|
118
|
+
query: false,
|
|
119
|
+
res: true
|
|
120
|
+
},
|
|
121
|
+
'POST /multi-body': {
|
|
122
|
+
body: true,
|
|
123
|
+
query: false,
|
|
124
|
+
res: true
|
|
125
|
+
},
|
|
126
|
+
'POST /wildcard-body': {
|
|
127
|
+
body: true,
|
|
128
|
+
query: false,
|
|
129
|
+
res: true
|
|
130
|
+
},
|
|
131
|
+
'POST /optional-body': {
|
|
132
|
+
body: true,
|
|
133
|
+
query: false,
|
|
134
|
+
res: true
|
|
135
|
+
},
|
|
136
|
+
'POST /with-examples': {
|
|
137
|
+
body: true,
|
|
138
|
+
query: false,
|
|
139
|
+
res: true
|
|
140
|
+
},
|
|
46
141
|
'GET /schema':{
|
|
47
142
|
body: false,
|
|
48
143
|
query: true,
|
|
@@ -123,4 +218,153 @@ describe('Schema Server', () => {
|
|
|
123
218
|
assert.strictEqual(body.paths['/legacy'].get.deprecated, true);
|
|
124
219
|
assert.strictEqual(body.paths['/schema'].get.deprecated, false);
|
|
125
220
|
});
|
|
221
|
+
|
|
222
|
+
it('POST: /json-only with valid JSON body (backwards compat)', async () => {
|
|
223
|
+
const res = await fetch('http://localhost:2000/api/json-only', {
|
|
224
|
+
method: 'POST',
|
|
225
|
+
headers: { 'Content-Type': 'application/json' },
|
|
226
|
+
body: JSON.stringify({ name: 'foo' })
|
|
227
|
+
});
|
|
228
|
+
assert.strictEqual(res.status, 200);
|
|
229
|
+
assert.deepStrictEqual(await res.json(), { ok: true });
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it('POST: /multi-body with application/json', async () => {
|
|
233
|
+
const res = await fetch('http://localhost:2000/api/multi-body', {
|
|
234
|
+
method: 'POST',
|
|
235
|
+
headers: { 'Content-Type': 'application/json' },
|
|
236
|
+
body: JSON.stringify({ name: 'foo' })
|
|
237
|
+
});
|
|
238
|
+
assert.strictEqual(res.status, 200);
|
|
239
|
+
assert.deepStrictEqual(await res.json(), {
|
|
240
|
+
contentType: 'application/json',
|
|
241
|
+
body: { name: 'foo' }
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('POST: /multi-body with text/xml (true => no validation)', async () => {
|
|
246
|
+
const res = await fetch('http://localhost:2000/api/multi-body', {
|
|
247
|
+
method: 'POST',
|
|
248
|
+
headers: { 'Content-Type': 'text/xml' },
|
|
249
|
+
body: '<root><a>1</a></root>'
|
|
250
|
+
});
|
|
251
|
+
assert.strictEqual(res.status, 200);
|
|
252
|
+
assert.deepStrictEqual(await res.json(), {
|
|
253
|
+
contentType: 'text/xml',
|
|
254
|
+
body: '<root><a>1</a></root>'
|
|
255
|
+
});
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('POST: /multi-body with unsupported content-type returns 400', async () => {
|
|
259
|
+
const res = await fetch('http://localhost:2000/api/multi-body', {
|
|
260
|
+
method: 'POST',
|
|
261
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
262
|
+
body: 'hello'
|
|
263
|
+
});
|
|
264
|
+
assert.strictEqual(res.status, 400);
|
|
265
|
+
const body = await res.json();
|
|
266
|
+
assert.strictEqual(body.status, 400);
|
|
267
|
+
assert.match(body.message, /Content-Type text\/plain not supported/);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('POST: /multi-body with invalid JSON body fails validation', async () => {
|
|
271
|
+
const res = await fetch('http://localhost:2000/api/multi-body', {
|
|
272
|
+
method: 'POST',
|
|
273
|
+
headers: { 'Content-Type': 'application/json' },
|
|
274
|
+
body: JSON.stringify({})
|
|
275
|
+
});
|
|
276
|
+
assert.strictEqual(res.status, 400);
|
|
277
|
+
const body = await res.json();
|
|
278
|
+
assert.strictEqual(body.message, 'Validation Error POST /multi-body');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('GET: api/openapi documents multiple content types', async () => {
|
|
282
|
+
const res = await fetch('http://localhost:2000/api/openapi');
|
|
283
|
+
const body = await res.json();
|
|
284
|
+
|
|
285
|
+
assert.deepStrictEqual(
|
|
286
|
+
Object.keys(body.paths['/multi-body'].post.requestBody.content).sort(),
|
|
287
|
+
['application/json', 'text/xml']
|
|
288
|
+
);
|
|
289
|
+
assert.deepStrictEqual(
|
|
290
|
+
body.paths['/multi-body'].post.requestBody.content['text/xml'],
|
|
291
|
+
{ schema: {} }
|
|
292
|
+
);
|
|
293
|
+
assert.strictEqual(
|
|
294
|
+
body.paths['/json-only'].post.requestBody.content['application/json'].schema.type,
|
|
295
|
+
'object'
|
|
296
|
+
);
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it('POST: /wildcard-body matches text/* with text/csv', async () => {
|
|
300
|
+
const res = await fetch('http://localhost:2000/api/wildcard-body', {
|
|
301
|
+
method: 'POST',
|
|
302
|
+
headers: { 'Content-Type': 'text/csv' },
|
|
303
|
+
body: 'a,b,c\n1,2,3'
|
|
304
|
+
});
|
|
305
|
+
assert.strictEqual(res.status, 200);
|
|
306
|
+
assert.deepStrictEqual(await res.json(), {
|
|
307
|
+
contentType: 'text/csv',
|
|
308
|
+
body: 'a,b,c\n1,2,3'
|
|
309
|
+
});
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('POST: /wildcard-body still validates application/json strictly', async () => {
|
|
313
|
+
const res = await fetch('http://localhost:2000/api/wildcard-body', {
|
|
314
|
+
method: 'POST',
|
|
315
|
+
headers: { 'Content-Type': 'application/json' },
|
|
316
|
+
body: JSON.stringify({ name: 'foo' })
|
|
317
|
+
});
|
|
318
|
+
assert.strictEqual(res.status, 200);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('POST: /wildcard-body rejects unsupported family', async () => {
|
|
322
|
+
const res = await fetch('http://localhost:2000/api/wildcard-body', {
|
|
323
|
+
method: 'POST',
|
|
324
|
+
headers: { 'Content-Type': 'image/png' },
|
|
325
|
+
body: 'binary'
|
|
326
|
+
});
|
|
327
|
+
assert.strictEqual(res.status, 400);
|
|
328
|
+
const body = await res.json();
|
|
329
|
+
assert.match(body.message, /Content-Type image\/png not supported/);
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
it('POST: /optional-body accepts no body when bodyRequired=false', async () => {
|
|
333
|
+
const res = await fetch('http://localhost:2000/api/optional-body', {
|
|
334
|
+
method: 'POST'
|
|
335
|
+
});
|
|
336
|
+
assert.strictEqual(res.status, 200);
|
|
337
|
+
assert.deepStrictEqual(await res.json(), { ok: true });
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('POST: /optional-body still validates when a body is sent', async () => {
|
|
341
|
+
const res = await fetch('http://localhost:2000/api/optional-body', {
|
|
342
|
+
method: 'POST',
|
|
343
|
+
headers: { 'Content-Type': 'application/json' },
|
|
344
|
+
body: JSON.stringify({})
|
|
345
|
+
});
|
|
346
|
+
assert.strictEqual(res.status, 400);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it('GET: api/openapi marks optional body required=false', async () => {
|
|
350
|
+
const res = await fetch('http://localhost:2000/api/openapi');
|
|
351
|
+
const body = await res.json();
|
|
352
|
+
assert.strictEqual(body.paths['/optional-body'].post.requestBody.required, false);
|
|
353
|
+
assert.strictEqual(body.paths['/json-only'].post.requestBody.required, true);
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('GET: api/openapi includes example and examples on media type', async () => {
|
|
357
|
+
const res = await fetch('http://localhost:2000/api/openapi');
|
|
358
|
+
const body = await res.json();
|
|
359
|
+
const media = body.paths['/with-examples'].post.requestBody.content['application/json'];
|
|
360
|
+
assert.deepStrictEqual(media.example, { name: 'sample' });
|
|
361
|
+
assert.deepStrictEqual(media.examples.primary, {
|
|
362
|
+
summary: 'Primary',
|
|
363
|
+
value: { name: 'alice' }
|
|
364
|
+
});
|
|
365
|
+
assert.deepStrictEqual(media.examples.secondary, {
|
|
366
|
+
summary: 'Secondary',
|
|
367
|
+
value: { name: 'bob' }
|
|
368
|
+
});
|
|
369
|
+
});
|
|
126
370
|
});
|