@dokploy/trpc-openapi 0.0.2 → 0.0.4

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.
Files changed (68) hide show
  1. package/dist/generator/paths.d.ts.map +1 -1
  2. package/dist/generator/paths.js +2 -2
  3. package/dist/generator/paths.js.map +1 -1
  4. package/dist/generator/schema.d.ts.map +1 -1
  5. package/dist/generator/schema.js +3 -13
  6. package/dist/generator/schema.js.map +1 -1
  7. package/dist/types.d.ts +12 -10
  8. package/dist/types.d.ts.map +1 -1
  9. package/dist/utils/procedure.d.ts +4 -3
  10. package/dist/utils/procedure.d.ts.map +1 -1
  11. package/dist/utils/procedure.js +30 -7
  12. package/dist/utils/procedure.js.map +1 -1
  13. package/package.json +14 -9
  14. package/assets/trpc-openapi-graph.png +0 -0
  15. package/assets/trpc-openapi-readme.png +0 -0
  16. package/assets/trpc-openapi.svg +0 -4
  17. package/examples/with-express/README.md +0 -11
  18. package/examples/with-express/package.json +0 -28
  19. package/examples/with-express/src/database.ts +0 -67
  20. package/examples/with-express/src/index.ts +0 -27
  21. package/examples/with-express/src/openapi.ts +0 -13
  22. package/examples/with-express/src/router.ts +0 -424
  23. package/examples/with-express/tsconfig.json +0 -102
  24. package/examples/with-interop/README.md +0 -10
  25. package/examples/with-interop/package.json +0 -13
  26. package/examples/with-interop/src/index.ts +0 -17
  27. package/examples/with-interop/tsconfig.json +0 -103
  28. package/examples/with-nextjs/.eslintrc.json +0 -3
  29. package/examples/with-nextjs/README.md +0 -12
  30. package/examples/with-nextjs/next-env.d.ts +0 -5
  31. package/examples/with-nextjs/next.config.js +0 -6
  32. package/examples/with-nextjs/package.json +0 -33
  33. package/examples/with-nextjs/public/favicon.ico +0 -0
  34. package/examples/with-nextjs/src/pages/_app.tsx +0 -7
  35. package/examples/with-nextjs/src/pages/api/[...trpc].ts +0 -18
  36. package/examples/with-nextjs/src/pages/api/openapi.json.ts +0 -10
  37. package/examples/with-nextjs/src/pages/api/trpc/[...trpc].ts +0 -9
  38. package/examples/with-nextjs/src/pages/index.tsx +0 -12
  39. package/examples/with-nextjs/src/server/database.ts +0 -67
  40. package/examples/with-nextjs/src/server/openapi.ts +0 -13
  41. package/examples/with-nextjs/src/server/router.ts +0 -425
  42. package/examples/with-nextjs/tsconfig.json +0 -24
  43. package/jest.config.ts +0 -12
  44. package/pnpm-workspace.yaml +0 -7
  45. package/src/adapters/express.ts +0 -20
  46. package/src/adapters/index.ts +0 -3
  47. package/src/adapters/next.ts +0 -64
  48. package/src/adapters/node-http/core.ts +0 -203
  49. package/src/adapters/node-http/errors.ts +0 -45
  50. package/src/adapters/node-http/input.ts +0 -76
  51. package/src/adapters/node-http/procedures.ts +0 -64
  52. package/src/adapters/standalone.ts +0 -19
  53. package/src/generator/index.ts +0 -51
  54. package/src/generator/paths.ts +0 -127
  55. package/src/generator/schema.ts +0 -238
  56. package/src/index.ts +0 -42
  57. package/src/types.ts +0 -79
  58. package/src/utils/method.ts +0 -8
  59. package/src/utils/path.ts +0 -12
  60. package/src/utils/procedure.ts +0 -45
  61. package/src/utils/zod.ts +0 -115
  62. package/test/adapters/express.test.ts +0 -150
  63. package/test/adapters/next.test.ts +0 -162
  64. package/test/adapters/standalone.test.ts +0 -1335
  65. package/test/generator.test.ts +0 -2897
  66. package/tsconfig.build.json +0 -4
  67. package/tsconfig.eslint.json +0 -5
  68. package/tsconfig.json +0 -19
@@ -1,2897 +0,0 @@
1
- import { initTRPC } from '@trpc/server';
2
- import { observable } from '@trpc/server/observable';
3
- import openAPISchemaValidator from 'openapi-schema-validator';
4
- import { z } from 'zod';
5
-
6
- import {
7
- GenerateOpenApiDocumentOptions,
8
- OpenApiMeta,
9
- generateOpenApiDocument,
10
- openApiVersion,
11
- } from '../src';
12
- import * as zodUtils from '../src/utils/zod';
13
-
14
- // TODO: test for duplicate paths (using getPathRegExp)
15
-
16
- const openApiSchemaValidator = new openAPISchemaValidator({ version: openApiVersion });
17
-
18
- const t = initTRPC.meta<OpenApiMeta>().context<any>().create();
19
-
20
- const defaultDocOpts: GenerateOpenApiDocumentOptions = {
21
- title: 'tRPC OpenAPI',
22
- version: '1.0.0',
23
- baseUrl: 'http://localhost:3000/api',
24
- };
25
-
26
- describe('generator', () => {
27
- test('open api version', () => {
28
- expect(openApiVersion).toBe('3.0.3');
29
- });
30
-
31
- test('with empty router', () => {
32
- const appRouter = t.router({});
33
-
34
- const openApiDocument = generateOpenApiDocument(appRouter, {
35
- title: 'tRPC OpenAPI',
36
- version: '1.0.0',
37
- description: 'API documentation',
38
- baseUrl: 'http://localhost:3000/api',
39
- docsUrl: 'http://localhost:3000/docs',
40
- tags: [],
41
- });
42
-
43
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
44
- expect(openApiDocument).toMatchInlineSnapshot(`
45
- Object {
46
- "components": Object {
47
- "responses": Object {
48
- "error": Object {
49
- "content": Object {
50
- "application/json": Object {
51
- "schema": Object {
52
- "additionalProperties": false,
53
- "properties": Object {
54
- "code": Object {
55
- "type": "string",
56
- },
57
- "issues": Object {
58
- "items": Object {
59
- "additionalProperties": false,
60
- "properties": Object {
61
- "message": Object {
62
- "type": "string",
63
- },
64
- },
65
- "required": Array [
66
- "message",
67
- ],
68
- "type": "object",
69
- },
70
- "type": "array",
71
- },
72
- "message": Object {
73
- "type": "string",
74
- },
75
- },
76
- "required": Array [
77
- "message",
78
- "code",
79
- ],
80
- "type": "object",
81
- },
82
- },
83
- },
84
- "description": "Error response",
85
- },
86
- },
87
- "securitySchemes": Object {
88
- "Authorization": Object {
89
- "scheme": "bearer",
90
- "type": "http",
91
- },
92
- },
93
- },
94
- "externalDocs": Object {
95
- "url": "http://localhost:3000/docs",
96
- },
97
- "info": Object {
98
- "description": "API documentation",
99
- "title": "tRPC OpenAPI",
100
- "version": "1.0.0",
101
- },
102
- "openapi": "3.0.3",
103
- "paths": Object {},
104
- "servers": Array [
105
- Object {
106
- "url": "http://localhost:3000/api",
107
- },
108
- ],
109
- "tags": Array [],
110
- }
111
- `);
112
- });
113
-
114
- test('with missing input', () => {
115
- {
116
- const appRouter = t.router({
117
- noInput: t.procedure
118
- .meta({ openapi: { method: 'GET', path: '/no-input' } })
119
- .output(z.object({ name: z.string() }))
120
- .query(() => ({ name: 'jlalmes' })),
121
- });
122
-
123
- expect(() => {
124
- generateOpenApiDocument(appRouter, defaultDocOpts);
125
- }).toThrowError('[query.noInput] - Input parser expects a Zod validator');
126
- }
127
- {
128
- const appRouter = t.router({
129
- noInput: t.procedure
130
- .meta({ openapi: { method: 'POST', path: '/no-input' } })
131
- .output(z.object({ name: z.string() }))
132
- .mutation(() => ({ name: 'jlalmes' })),
133
- });
134
-
135
- expect(() => {
136
- generateOpenApiDocument(appRouter, defaultDocOpts);
137
- }).toThrowError('[mutation.noInput] - Input parser expects a Zod validator');
138
- }
139
- });
140
-
141
- test('with missing output', () => {
142
- {
143
- const appRouter = t.router({
144
- noOutput: t.procedure
145
- .meta({ openapi: { method: 'GET', path: '/no-output' } })
146
- .input(z.object({ name: z.string() }))
147
- .query(({ input }) => ({ name: input.name })),
148
- });
149
-
150
- expect(() => {
151
- generateOpenApiDocument(appRouter, defaultDocOpts);
152
- }).toThrowError('[query.noOutput] - Output parser expects a Zod validator');
153
- }
154
- {
155
- const appRouter = t.router({
156
- noOutput: t.procedure
157
- .meta({ openapi: { method: 'POST', path: '/no-output' } })
158
- .input(z.object({ name: z.string() }))
159
- .mutation(({ input }) => ({ name: input.name })),
160
- });
161
-
162
- expect(() => {
163
- generateOpenApiDocument(appRouter, defaultDocOpts);
164
- }).toThrowError('[mutation.noOutput] - Output parser expects a Zod validator');
165
- }
166
- });
167
-
168
- test('with non-zod parser', () => {
169
- {
170
- const appRouter = t.router({
171
- badInput: t.procedure
172
- .meta({ openapi: { method: 'GET', path: '/bad-input' } })
173
- .input((arg) => ({ payload: typeof arg === 'string' ? arg : String(arg) }))
174
- .output(z.object({ payload: z.string() }))
175
- .query(({ input }) => ({ payload: 'Hello world!' })),
176
- });
177
-
178
- expect(() => {
179
- generateOpenApiDocument(appRouter, defaultDocOpts);
180
- }).toThrowError('[query.badInput] - Input parser expects a Zod validator');
181
- }
182
- {
183
- const appRouter = t.router({
184
- badInput: t.procedure
185
- .meta({ openapi: { method: 'GET', path: '/bad-input' } })
186
- .input(z.object({ payload: z.string() }))
187
- .output((arg) => ({ payload: typeof arg === 'string' ? arg : String(arg) }))
188
- .query(({ input }) => ({ payload: input.payload })),
189
- });
190
-
191
- expect(() => {
192
- generateOpenApiDocument(appRouter, defaultDocOpts);
193
- }).toThrowError('[query.badInput] - Output parser expects a Zod validator');
194
- }
195
- });
196
-
197
- test('with non-object input', () => {
198
- {
199
- const appRouter = t.router({
200
- badInput: t.procedure
201
- .meta({ openapi: { method: 'GET', path: '/bad-input' } })
202
- .input(z.string())
203
- .output(z.null())
204
- .query(() => null),
205
- });
206
-
207
- expect(() => {
208
- generateOpenApiDocument(appRouter, defaultDocOpts);
209
- }).toThrowError('[query.badInput] - Input parser must be a ZodObject');
210
- }
211
- {
212
- const appRouter = t.router({
213
- badInput: t.procedure
214
- .meta({ openapi: { method: 'POST', path: '/bad-input' } })
215
- .input(z.string())
216
- .output(z.null())
217
- .mutation(() => null),
218
- });
219
-
220
- expect(() => {
221
- generateOpenApiDocument(appRouter, defaultDocOpts);
222
- }).toThrowError('[mutation.badInput] - Input parser must be a ZodObject');
223
- }
224
- });
225
-
226
- test('with object non-string input', () => {
227
- // only applies when zod does not support (below version v3.20.0)
228
-
229
- // @ts-expect-error - hack to disable zodSupportsCoerce
230
- // eslint-disable-next-line import/namespace
231
- zodUtils.zodSupportsCoerce = false;
232
-
233
- {
234
- const appRouter = t.router({
235
- badInput: t.procedure
236
- .meta({ openapi: { method: 'GET', path: '/bad-input' } })
237
- .input(z.object({ age: z.number().min(0).max(122) })) // RIP Jeanne Calment
238
- .output(z.object({ name: z.string() }))
239
- .query(() => ({ name: 'jlalmes' })),
240
- });
241
-
242
- expect(() => {
243
- generateOpenApiDocument(appRouter, defaultDocOpts);
244
- }).toThrowError('[query.badInput] - Input parser key: "age" must be ZodString');
245
- }
246
- {
247
- const appRouter = t.router({
248
- okInput: t.procedure
249
- .meta({ openapi: { method: 'POST', path: '/ok-input' } })
250
- .input(z.object({ age: z.number().min(0).max(122) }))
251
- .output(z.object({ name: z.string() }))
252
- .mutation(() => ({ name: 'jlalmes' })),
253
- });
254
-
255
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
256
-
257
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
258
- expect(openApiDocument.paths['/ok-input']!.post!.requestBody).toMatchInlineSnapshot(`
259
- Object {
260
- "content": Object {
261
- "application/json": Object {
262
- "example": undefined,
263
- "schema": Object {
264
- "additionalProperties": false,
265
- "properties": Object {
266
- "age": Object {
267
- "maximum": 122,
268
- "minimum": 0,
269
- "type": "number",
270
- },
271
- },
272
- "required": Array [
273
- "age",
274
- ],
275
- "type": "object",
276
- },
277
- },
278
- },
279
- "required": true,
280
- }
281
- `);
282
- }
283
-
284
- // @ts-expect-error - hack to re-enable zodSupportsCoerce
285
- // eslint-disable-next-line import/namespace
286
- zodUtils.zodSupportsCoerce = true;
287
- });
288
-
289
- test('with bad method', () => {
290
- const appRouter = t.router({
291
- badMethod: t.procedure
292
- // @ts-expect-error - bad method
293
- .meta({ openapi: { method: 'BAD_METHOD', path: '/bad-method' } })
294
- .input(z.object({ name: z.string() }))
295
- .output(z.object({ name: z.string() }))
296
- .query(({ input }) => ({ name: input.name })),
297
- });
298
-
299
- expect(() => {
300
- generateOpenApiDocument(appRouter, defaultDocOpts);
301
- }).toThrowError('[query.badMethod] - Method must be GET, POST, PATCH, PUT or DELETE');
302
- });
303
-
304
- test('with duplicate routes', () => {
305
- {
306
- const appRouter = t.router({
307
- procedure1: t.procedure
308
- .meta({ openapi: { method: 'GET', path: '/procedure' } })
309
- .input(z.object({ name: z.string() }))
310
- .output(z.object({ name: z.string() }))
311
- .query(({ input }) => ({ name: input.name })),
312
- procedure2: t.procedure
313
- .meta({ openapi: { method: 'GET', path: '/procedure' } })
314
- .input(z.object({ name: z.string() }))
315
- .output(z.object({ name: z.string() }))
316
- .query(({ input }) => ({ name: input.name })),
317
- });
318
-
319
- expect(() => {
320
- generateOpenApiDocument(appRouter, defaultDocOpts);
321
- }).toThrowError('[query.procedure2] - Duplicate procedure defined for route GET /procedure');
322
- }
323
- {
324
- const appRouter = t.router({
325
- procedure1: t.procedure
326
- .meta({ openapi: { method: 'GET', path: '/procedure/' } })
327
- .input(z.object({ name: z.string() }))
328
- .output(z.object({ name: z.string() }))
329
- .query(({ input }) => ({ name: input.name })),
330
- procedure2: t.procedure
331
- .meta({ openapi: { method: 'GET', path: '/procedure' } })
332
- .input(z.object({ name: z.string() }))
333
- .output(z.object({ name: z.string() }))
334
- .query(({ input }) => ({ name: input.name })),
335
- });
336
-
337
- expect(() => {
338
- generateOpenApiDocument(appRouter, defaultDocOpts);
339
- }).toThrowError('[query.procedure2] - Duplicate procedure defined for route GET /procedure');
340
- }
341
- });
342
-
343
- test('with unsupported subscription', () => {
344
- const appRouter = t.router({
345
- currentName: t.procedure
346
- .meta({ openapi: { method: 'PATCH', path: '/current-name' } })
347
- .input(z.object({ name: z.string() }))
348
- .subscription(({ input }) => {
349
- return observable((emit) => {
350
- emit.next(input.name);
351
- return () => null;
352
- });
353
- }),
354
- });
355
-
356
- expect(() => {
357
- generateOpenApiDocument(appRouter, defaultDocOpts);
358
- }).toThrowError('[subscription.currentName] - Subscriptions are not supported by OpenAPI v3');
359
- });
360
-
361
- test('with void and path parameters', () => {
362
- const appRouter = t.router({
363
- pathParameters: t.procedure
364
- .meta({ openapi: { method: 'GET', path: '/path-parameters/{name}' } })
365
- .input(z.void())
366
- .output(z.object({ name: z.string() }))
367
- .query(() => ({ name: 'asdf' })),
368
- });
369
-
370
- expect(() => {
371
- generateOpenApiDocument(appRouter, defaultDocOpts);
372
- }).toThrowError('[query.pathParameters] - Input parser must be a ZodObject');
373
- });
374
-
375
- test('with optional path parameters', () => {
376
- const appRouter = t.router({
377
- pathParameters: t.procedure
378
- .meta({ openapi: { method: 'GET', path: '/path-parameters/{name}' } })
379
- .input(z.object({ name: z.string().optional() }))
380
- .output(z.object({ name: z.string() }))
381
- .query(() => ({ name: 'asdf' })),
382
- });
383
-
384
- expect(() => {
385
- generateOpenApiDocument(appRouter, defaultDocOpts);
386
- }).toThrowError('[query.pathParameters] - Path parameter: "name" must not be optional');
387
- });
388
-
389
- test('with missing path parameters', () => {
390
- const appRouter = t.router({
391
- pathParameters: t.procedure
392
- .meta({ openapi: { method: 'GET', path: '/path-parameters/{name}' } })
393
- .input(z.object({}))
394
- .output(z.object({ name: z.string() }))
395
- .query(() => ({ name: 'asdf' })),
396
- });
397
-
398
- expect(() => {
399
- generateOpenApiDocument(appRouter, defaultDocOpts);
400
- }).toThrowError('[query.pathParameters] - Input parser expects key from path: "name"');
401
- });
402
-
403
- // test for https://github.com/jlalmes/trpc-openapi/issues/296
404
- test('with post & only path paramters', () => {
405
- const appRouter = t.router({
406
- noBody: t.procedure
407
- .meta({ openapi: { method: 'POST', path: '/no-body/{name}' } })
408
- .input(z.object({ name: z.string() }))
409
- .output(z.object({ name: z.string() }))
410
- .mutation(({ input }) => ({ name: input.name })),
411
- emptyBody: t.procedure
412
- .meta({ openapi: { method: 'POST', path: '/empty-body' } })
413
- .input(z.object({}))
414
- .output(z.object({ name: z.string() }))
415
- .mutation(() => ({ name: 'James' })),
416
- });
417
-
418
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
419
-
420
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
421
- expect(openApiDocument.paths['/no-body/{name}']!.post!.requestBody).toBe(undefined);
422
- expect(openApiDocument.paths['/empty-body']!.post!.requestBody).toMatchInlineSnapshot(`
423
- Object {
424
- "content": Object {
425
- "application/json": Object {
426
- "example": undefined,
427
- "schema": Object {
428
- "additionalProperties": false,
429
- "properties": Object {},
430
- "type": "object",
431
- },
432
- },
433
- },
434
- "required": true,
435
- }
436
- `);
437
- });
438
-
439
- test('with valid procedures', () => {
440
- const appRouter = t.router({
441
- createUser: t.procedure
442
- .meta({ openapi: { method: 'POST', path: '/users' } })
443
- .input(z.object({ name: z.string() }))
444
- .output(z.object({ id: z.string(), name: z.string() }))
445
- .mutation(({ input }) => ({ id: 'user-id', name: input.name })),
446
- readUsers: t.procedure
447
- .meta({ openapi: { method: 'GET', path: '/users' } })
448
- .input(z.void())
449
- .output(z.array(z.object({ id: z.string(), name: z.string() })))
450
- .query(() => [{ id: 'user-id', name: 'name' }]),
451
- readUser: t.procedure
452
- .meta({ openapi: { method: 'GET', path: '/users/{id}' } })
453
- .input(z.object({ id: z.string() }))
454
- .output(z.object({ id: z.string(), name: z.string() }))
455
- .query(({ input }) => ({ id: input.id, name: 'name' })),
456
- updateUser: t.procedure
457
- .meta({ openapi: { method: 'PATCH', path: '/users/{id}' } })
458
- .input(z.object({ id: z.string(), name: z.string().optional() }))
459
- .output(z.object({ id: z.string(), name: z.string() }))
460
- .mutation(({ input }) => ({ id: input.id, name: input.name ?? 'name' })),
461
- deleteUser: t.procedure
462
- .meta({ openapi: { method: 'DELETE', path: '/users/{id}' } })
463
- .input(z.object({ id: z.string() }))
464
- .output(z.void())
465
- .mutation(() => undefined),
466
- });
467
-
468
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
469
-
470
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
471
- expect(openApiDocument).toMatchInlineSnapshot(`
472
- Object {
473
- "components": Object {
474
- "responses": Object {
475
- "error": Object {
476
- "content": Object {
477
- "application/json": Object {
478
- "schema": Object {
479
- "additionalProperties": false,
480
- "properties": Object {
481
- "code": Object {
482
- "type": "string",
483
- },
484
- "issues": Object {
485
- "items": Object {
486
- "additionalProperties": false,
487
- "properties": Object {
488
- "message": Object {
489
- "type": "string",
490
- },
491
- },
492
- "required": Array [
493
- "message",
494
- ],
495
- "type": "object",
496
- },
497
- "type": "array",
498
- },
499
- "message": Object {
500
- "type": "string",
501
- },
502
- },
503
- "required": Array [
504
- "message",
505
- "code",
506
- ],
507
- "type": "object",
508
- },
509
- },
510
- },
511
- "description": "Error response",
512
- },
513
- },
514
- "securitySchemes": Object {
515
- "Authorization": Object {
516
- "scheme": "bearer",
517
- "type": "http",
518
- },
519
- },
520
- },
521
- "externalDocs": undefined,
522
- "info": Object {
523
- "description": undefined,
524
- "title": "tRPC OpenAPI",
525
- "version": "1.0.0",
526
- },
527
- "openapi": "3.0.3",
528
- "paths": Object {
529
- "/users": Object {
530
- "get": Object {
531
- "description": undefined,
532
- "operationId": "readUsers",
533
- "parameters": Array [],
534
- "requestBody": undefined,
535
- "responses": Object {
536
- "200": Object {
537
- "content": Object {
538
- "application/json": Object {
539
- "example": undefined,
540
- "schema": Object {
541
- "items": Object {
542
- "additionalProperties": false,
543
- "properties": Object {
544
- "id": Object {
545
- "type": "string",
546
- },
547
- "name": Object {
548
- "type": "string",
549
- },
550
- },
551
- "required": Array [
552
- "id",
553
- "name",
554
- ],
555
- "type": "object",
556
- },
557
- "type": "array",
558
- },
559
- },
560
- },
561
- "description": "Successful response",
562
- "headers": undefined,
563
- },
564
- "default": Object {
565
- "$ref": "#/components/responses/error",
566
- },
567
- },
568
- "security": undefined,
569
- "summary": undefined,
570
- "tags": undefined,
571
- },
572
- "post": Object {
573
- "description": undefined,
574
- "operationId": "createUser",
575
- "parameters": Array [],
576
- "requestBody": Object {
577
- "content": Object {
578
- "application/json": Object {
579
- "example": undefined,
580
- "schema": Object {
581
- "additionalProperties": false,
582
- "properties": Object {
583
- "name": Object {
584
- "type": "string",
585
- },
586
- },
587
- "required": Array [
588
- "name",
589
- ],
590
- "type": "object",
591
- },
592
- },
593
- },
594
- "required": true,
595
- },
596
- "responses": Object {
597
- "200": Object {
598
- "content": Object {
599
- "application/json": Object {
600
- "example": undefined,
601
- "schema": Object {
602
- "additionalProperties": false,
603
- "properties": Object {
604
- "id": Object {
605
- "type": "string",
606
- },
607
- "name": Object {
608
- "type": "string",
609
- },
610
- },
611
- "required": Array [
612
- "id",
613
- "name",
614
- ],
615
- "type": "object",
616
- },
617
- },
618
- },
619
- "description": "Successful response",
620
- "headers": undefined,
621
- },
622
- "default": Object {
623
- "$ref": "#/components/responses/error",
624
- },
625
- },
626
- "security": undefined,
627
- "summary": undefined,
628
- "tags": undefined,
629
- },
630
- },
631
- "/users/{id}": Object {
632
- "delete": Object {
633
- "description": undefined,
634
- "operationId": "deleteUser",
635
- "parameters": Array [
636
- Object {
637
- "description": undefined,
638
- "example": undefined,
639
- "in": "path",
640
- "name": "id",
641
- "required": true,
642
- "schema": Object {
643
- "type": "string",
644
- },
645
- },
646
- ],
647
- "requestBody": undefined,
648
- "responses": Object {
649
- "200": Object {
650
- "content": Object {
651
- "application/json": Object {
652
- "example": undefined,
653
- "schema": Object {},
654
- },
655
- },
656
- "description": "Successful response",
657
- "headers": undefined,
658
- },
659
- "default": Object {
660
- "$ref": "#/components/responses/error",
661
- },
662
- },
663
- "security": undefined,
664
- "summary": undefined,
665
- "tags": undefined,
666
- },
667
- "get": Object {
668
- "description": undefined,
669
- "operationId": "readUser",
670
- "parameters": Array [
671
- Object {
672
- "description": undefined,
673
- "example": undefined,
674
- "in": "path",
675
- "name": "id",
676
- "required": true,
677
- "schema": Object {
678
- "type": "string",
679
- },
680
- },
681
- ],
682
- "requestBody": undefined,
683
- "responses": Object {
684
- "200": Object {
685
- "content": Object {
686
- "application/json": Object {
687
- "example": undefined,
688
- "schema": Object {
689
- "additionalProperties": false,
690
- "properties": Object {
691
- "id": Object {
692
- "type": "string",
693
- },
694
- "name": Object {
695
- "type": "string",
696
- },
697
- },
698
- "required": Array [
699
- "id",
700
- "name",
701
- ],
702
- "type": "object",
703
- },
704
- },
705
- },
706
- "description": "Successful response",
707
- "headers": undefined,
708
- },
709
- "default": Object {
710
- "$ref": "#/components/responses/error",
711
- },
712
- },
713
- "security": undefined,
714
- "summary": undefined,
715
- "tags": undefined,
716
- },
717
- "patch": Object {
718
- "description": undefined,
719
- "operationId": "updateUser",
720
- "parameters": Array [
721
- Object {
722
- "description": undefined,
723
- "example": undefined,
724
- "in": "path",
725
- "name": "id",
726
- "required": true,
727
- "schema": Object {
728
- "type": "string",
729
- },
730
- },
731
- ],
732
- "requestBody": Object {
733
- "content": Object {
734
- "application/json": Object {
735
- "example": undefined,
736
- "schema": Object {
737
- "additionalProperties": false,
738
- "properties": Object {
739
- "name": Object {
740
- "type": "string",
741
- },
742
- },
743
- "type": "object",
744
- },
745
- },
746
- },
747
- "required": true,
748
- },
749
- "responses": Object {
750
- "200": Object {
751
- "content": Object {
752
- "application/json": Object {
753
- "example": undefined,
754
- "schema": Object {
755
- "additionalProperties": false,
756
- "properties": Object {
757
- "id": Object {
758
- "type": "string",
759
- },
760
- "name": Object {
761
- "type": "string",
762
- },
763
- },
764
- "required": Array [
765
- "id",
766
- "name",
767
- ],
768
- "type": "object",
769
- },
770
- },
771
- },
772
- "description": "Successful response",
773
- "headers": undefined,
774
- },
775
- "default": Object {
776
- "$ref": "#/components/responses/error",
777
- },
778
- },
779
- "security": undefined,
780
- "summary": undefined,
781
- "tags": undefined,
782
- },
783
- },
784
- },
785
- "servers": Array [
786
- Object {
787
- "url": "http://localhost:3000/api",
788
- },
789
- ],
790
- "tags": undefined,
791
- }
792
- `);
793
- });
794
-
795
- test('with disabled', () => {
796
- const appRouter = t.router({
797
- getMe: t.procedure
798
- .meta({ openapi: { enabled: false, method: 'GET', path: '/me' } })
799
- .input(z.object({ id: z.string() }))
800
- .output(z.object({ id: z.string() }))
801
- .query(({ input }) => ({ id: input.id })),
802
- });
803
-
804
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
805
-
806
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
807
- expect(Object.keys(openApiDocument.paths).length).toBe(0);
808
- });
809
-
810
- test('with summary, description & tags', () => {
811
- const appRouter = t.router({
812
- getMe: t.procedure
813
- .meta({
814
- openapi: {
815
- method: 'GET',
816
- path: '/metadata/all',
817
- summary: 'Short summary',
818
- description: 'Verbose description',
819
- tags: ['tagA', 'tagB'],
820
- },
821
- })
822
- .input(z.object({ name: z.string() }))
823
- .output(z.object({ name: z.string() }))
824
- .query(({ input }) => ({ name: input.name })),
825
- });
826
-
827
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
828
-
829
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
830
- expect(openApiDocument.paths['/metadata/all']!.get!.summary).toBe('Short summary');
831
- expect(openApiDocument.paths['/metadata/all']!.get!.description).toBe('Verbose description');
832
- expect(openApiDocument.paths['/metadata/all']!.get!.tags).toEqual(['tagA', 'tagB']);
833
- });
834
-
835
- test('with security', () => {
836
- const appRouter = t.router({
837
- protectedEndpoint: t.procedure
838
- .meta({ openapi: { method: 'POST', path: '/secure/endpoint', protect: true } })
839
- .input(z.object({ name: z.string() }))
840
- .output(z.object({ name: z.string() }))
841
- .query(({ input }) => ({ name: input.name })),
842
- });
843
-
844
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
845
-
846
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
847
- expect(openApiDocument.paths['/secure/endpoint']!.post!.security).toEqual([
848
- { Authorization: [] },
849
- ]);
850
- });
851
-
852
- test('with schema descriptions', () => {
853
- const appRouter = t.router({
854
- createUser: t.procedure
855
- .meta({ openapi: { method: 'POST', path: '/user' } })
856
- .input(
857
- z
858
- .object({
859
- id: z.string().uuid().describe('User ID'),
860
- name: z.string().describe('User name'),
861
- })
862
- .describe('Request body input'),
863
- )
864
- .output(
865
- z
866
- .object({
867
- id: z.string().uuid().describe('User ID'),
868
- name: z.string().describe('User name'),
869
- })
870
- .describe('User data'),
871
- )
872
- .mutation(({ input }) => ({ id: input.id, name: 'James' })),
873
- getUser: t.procedure
874
- .meta({ openapi: { method: 'GET', path: '/user' } })
875
- .input(
876
- z.object({ id: z.string().uuid().describe('User ID') }).describe('Query string inputs'),
877
- )
878
- .output(
879
- z
880
- .object({
881
- id: z.string().uuid().describe('User ID'),
882
- name: z.string().describe('User name'),
883
- })
884
- .describe('User data'),
885
- )
886
- .query(({ input }) => ({ id: input.id, name: 'James' })),
887
- });
888
-
889
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
890
-
891
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
892
- expect(openApiDocument.paths['/user']!.post!).toMatchInlineSnapshot(`
893
- Object {
894
- "description": undefined,
895
- "operationId": "createUser",
896
- "parameters": Array [],
897
- "requestBody": Object {
898
- "content": Object {
899
- "application/json": Object {
900
- "example": undefined,
901
- "schema": Object {
902
- "additionalProperties": false,
903
- "description": "Request body input",
904
- "properties": Object {
905
- "id": Object {
906
- "description": "User ID",
907
- "format": "uuid",
908
- "type": "string",
909
- },
910
- "name": Object {
911
- "description": "User name",
912
- "type": "string",
913
- },
914
- },
915
- "required": Array [
916
- "id",
917
- "name",
918
- ],
919
- "type": "object",
920
- },
921
- },
922
- },
923
- "required": true,
924
- },
925
- "responses": Object {
926
- "200": Object {
927
- "content": Object {
928
- "application/json": Object {
929
- "example": undefined,
930
- "schema": Object {
931
- "additionalProperties": false,
932
- "description": "User data",
933
- "properties": Object {
934
- "id": Object {
935
- "description": "User ID",
936
- "format": "uuid",
937
- "type": "string",
938
- },
939
- "name": Object {
940
- "description": "User name",
941
- "type": "string",
942
- },
943
- },
944
- "required": Array [
945
- "id",
946
- "name",
947
- ],
948
- "type": "object",
949
- },
950
- },
951
- },
952
- "description": "Successful response",
953
- "headers": undefined,
954
- },
955
- "default": Object {
956
- "$ref": "#/components/responses/error",
957
- },
958
- },
959
- "security": undefined,
960
- "summary": undefined,
961
- "tags": undefined,
962
- }
963
- `);
964
- expect(openApiDocument.paths['/user']!.get!).toMatchInlineSnapshot(`
965
- Object {
966
- "description": undefined,
967
- "operationId": "getUser",
968
- "parameters": Array [
969
- Object {
970
- "description": "User ID",
971
- "example": undefined,
972
- "in": "query",
973
- "name": "id",
974
- "required": true,
975
- "schema": Object {
976
- "format": "uuid",
977
- "type": "string",
978
- },
979
- },
980
- ],
981
- "requestBody": undefined,
982
- "responses": Object {
983
- "200": Object {
984
- "content": Object {
985
- "application/json": Object {
986
- "example": undefined,
987
- "schema": Object {
988
- "additionalProperties": false,
989
- "description": "User data",
990
- "properties": Object {
991
- "id": Object {
992
- "description": "User ID",
993
- "format": "uuid",
994
- "type": "string",
995
- },
996
- "name": Object {
997
- "description": "User name",
998
- "type": "string",
999
- },
1000
- },
1001
- "required": Array [
1002
- "id",
1003
- "name",
1004
- ],
1005
- "type": "object",
1006
- },
1007
- },
1008
- },
1009
- "description": "Successful response",
1010
- "headers": undefined,
1011
- },
1012
- "default": Object {
1013
- "$ref": "#/components/responses/error",
1014
- },
1015
- },
1016
- "security": undefined,
1017
- "summary": undefined,
1018
- "tags": undefined,
1019
- }
1020
- `);
1021
- });
1022
-
1023
- test('with void', () => {
1024
- {
1025
- const appRouter = t.router({
1026
- void: t.procedure
1027
- .meta({ openapi: { method: 'GET', path: '/void' } })
1028
- .input(z.void())
1029
- .output(z.void())
1030
- .query(() => undefined),
1031
- });
1032
-
1033
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1034
-
1035
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1036
- expect(openApiDocument.paths['/void']!.get!.parameters).toEqual([]);
1037
- expect(openApiDocument.paths['/void']!.get!.responses[200]).toMatchInlineSnapshot(`
1038
- Object {
1039
- "content": Object {
1040
- "application/json": Object {
1041
- "example": undefined,
1042
- "schema": Object {},
1043
- },
1044
- },
1045
- "description": "Successful response",
1046
- "headers": undefined,
1047
- }
1048
- `);
1049
- }
1050
- {
1051
- const appRouter = t.router({
1052
- void: t.procedure
1053
- .meta({ openapi: { method: 'POST', path: '/void' } })
1054
- .input(z.void())
1055
- .output(z.void())
1056
- .mutation(() => undefined),
1057
- });
1058
-
1059
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1060
-
1061
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1062
- expect(openApiDocument.paths['/void']!.post!.requestBody).toMatchInlineSnapshot(`undefined`);
1063
- expect(openApiDocument.paths['/void']!.post!.responses[200]).toMatchInlineSnapshot(`
1064
- Object {
1065
- "content": Object {
1066
- "application/json": Object {
1067
- "example": undefined,
1068
- "schema": Object {},
1069
- },
1070
- },
1071
- "description": "Successful response",
1072
- "headers": undefined,
1073
- }
1074
- `);
1075
- }
1076
- });
1077
-
1078
- test('with null', () => {
1079
- const appRouter = t.router({
1080
- null: t.procedure
1081
- .meta({ openapi: { method: 'POST', path: '/null' } })
1082
- .input(z.void())
1083
- .output(z.null())
1084
- .mutation(() => null),
1085
- });
1086
-
1087
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1088
-
1089
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1090
- expect(openApiDocument.paths['/null']!.post!.responses[200]).toMatchInlineSnapshot(`
1091
- Object {
1092
- "content": Object {
1093
- "application/json": Object {
1094
- "example": undefined,
1095
- "schema": Object {
1096
- "enum": Array [
1097
- "null",
1098
- ],
1099
- "nullable": true,
1100
- },
1101
- },
1102
- },
1103
- "description": "Successful response",
1104
- "headers": undefined,
1105
- }
1106
- `);
1107
- });
1108
-
1109
- test('with undefined', () => {
1110
- const appRouter = t.router({
1111
- undefined: t.procedure
1112
- .meta({ openapi: { method: 'POST', path: '/undefined' } })
1113
- .input(z.undefined())
1114
- .output(z.undefined())
1115
- .mutation(() => undefined),
1116
- });
1117
-
1118
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1119
-
1120
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1121
- expect(openApiDocument.paths['/undefined']!.post!.requestBody).toMatchInlineSnapshot(
1122
- `undefined`,
1123
- );
1124
- expect(openApiDocument.paths['/undefined']!.post!.responses[200]).toMatchInlineSnapshot(`
1125
- Object {
1126
- "content": Object {
1127
- "application/json": Object {
1128
- "example": undefined,
1129
- "schema": Object {
1130
- "not": Object {},
1131
- },
1132
- },
1133
- },
1134
- "description": "Successful response",
1135
- "headers": undefined,
1136
- }
1137
- `);
1138
- });
1139
-
1140
- test('with nullish', () => {
1141
- const appRouter = t.router({
1142
- nullish: t.procedure
1143
- .meta({ openapi: { method: 'POST', path: '/nullish' } })
1144
- .input(z.void())
1145
- .output(z.string().nullish())
1146
- .mutation(() => null),
1147
- });
1148
-
1149
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1150
-
1151
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1152
- expect(openApiDocument.paths['/nullish']!.post!.responses[200]).toMatchInlineSnapshot(`
1153
- Object {
1154
- "content": Object {
1155
- "application/json": Object {
1156
- "example": undefined,
1157
- "schema": Object {
1158
- "anyOf": Array [
1159
- Object {
1160
- "not": Object {},
1161
- },
1162
- Object {
1163
- "nullable": true,
1164
- "type": "string",
1165
- },
1166
- ],
1167
- },
1168
- },
1169
- },
1170
- "description": "Successful response",
1171
- "headers": undefined,
1172
- }
1173
- `);
1174
- });
1175
-
1176
- test('with never', () => {
1177
- const appRouter = t.router({
1178
- never: t.procedure
1179
- .meta({ openapi: { method: 'POST', path: '/never' } })
1180
- .input(z.never())
1181
- .output(z.never())
1182
- // @ts-expect-error - cannot return never
1183
- .mutation(() => undefined),
1184
- });
1185
-
1186
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1187
-
1188
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1189
- expect(openApiDocument.paths['/never']!.post!.requestBody).toMatchInlineSnapshot(`undefined`);
1190
- expect(openApiDocument.paths['/never']!.post!.responses[200]).toMatchInlineSnapshot(`
1191
- Object {
1192
- "content": Object {
1193
- "application/json": Object {
1194
- "example": undefined,
1195
- "schema": Object {
1196
- "not": Object {},
1197
- },
1198
- },
1199
- },
1200
- "description": "Successful response",
1201
- "headers": undefined,
1202
- }
1203
- `);
1204
- });
1205
-
1206
- test('with optional query param', () => {
1207
- const appRouter = t.router({
1208
- optionalParam: t.procedure
1209
- .meta({ openapi: { method: 'GET', path: '/optional-param' } })
1210
- .input(z.object({ one: z.string().optional(), two: z.string() }))
1211
- .output(z.string().optional())
1212
- .query(({ input }) => input.one),
1213
- optionalObject: t.procedure
1214
- .meta({ openapi: { method: 'GET', path: '/optional-object' } })
1215
- .input(z.object({ one: z.string().optional(), two: z.string() }).optional())
1216
- .output(z.string().optional())
1217
- .query(({ input }) => input?.two),
1218
- });
1219
-
1220
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1221
-
1222
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1223
- expect(openApiDocument.paths['/optional-param']!.get!.parameters).toMatchInlineSnapshot(`
1224
- Array [
1225
- Object {
1226
- "description": undefined,
1227
- "example": undefined,
1228
- "in": "query",
1229
- "name": "one",
1230
- "required": false,
1231
- "schema": Object {
1232
- "type": "string",
1233
- },
1234
- },
1235
- Object {
1236
- "description": undefined,
1237
- "example": undefined,
1238
- "in": "query",
1239
- "name": "two",
1240
- "required": true,
1241
- "schema": Object {
1242
- "type": "string",
1243
- },
1244
- },
1245
- ]
1246
- `);
1247
- expect(openApiDocument.paths['/optional-param']!.get!.responses[200]).toMatchInlineSnapshot(`
1248
- Object {
1249
- "content": Object {
1250
- "application/json": Object {
1251
- "example": undefined,
1252
- "schema": Object {
1253
- "anyOf": Array [
1254
- Object {
1255
- "not": Object {},
1256
- },
1257
- Object {
1258
- "type": "string",
1259
- },
1260
- ],
1261
- },
1262
- },
1263
- },
1264
- "description": "Successful response",
1265
- "headers": undefined,
1266
- }
1267
- `);
1268
- expect(openApiDocument.paths['/optional-object']!.get!.parameters).toMatchInlineSnapshot(`
1269
- Array [
1270
- Object {
1271
- "description": undefined,
1272
- "example": undefined,
1273
- "in": "query",
1274
- "name": "one",
1275
- "required": false,
1276
- "schema": Object {
1277
- "type": "string",
1278
- },
1279
- },
1280
- Object {
1281
- "description": undefined,
1282
- "example": undefined,
1283
- "in": "query",
1284
- "name": "two",
1285
- "required": false,
1286
- "schema": Object {
1287
- "type": "string",
1288
- },
1289
- },
1290
- ]
1291
- `);
1292
- expect(openApiDocument.paths['/optional-object']!.get!.responses[200]).toMatchInlineSnapshot(`
1293
- Object {
1294
- "content": Object {
1295
- "application/json": Object {
1296
- "example": undefined,
1297
- "schema": Object {
1298
- "anyOf": Array [
1299
- Object {
1300
- "not": Object {},
1301
- },
1302
- Object {
1303
- "type": "string",
1304
- },
1305
- ],
1306
- },
1307
- },
1308
- },
1309
- "description": "Successful response",
1310
- "headers": undefined,
1311
- }
1312
- `);
1313
- });
1314
-
1315
- test('with optional request body', () => {
1316
- const appRouter = t.router({
1317
- optionalParam: t.procedure
1318
- .meta({ openapi: { method: 'POST', path: '/optional-param' } })
1319
- .input(z.object({ one: z.string().optional(), two: z.string() }))
1320
- .output(z.string().optional())
1321
- .query(({ input }) => input.one),
1322
- optionalObject: t.procedure
1323
- .meta({ openapi: { method: 'POST', path: '/optional-object' } })
1324
- .input(z.object({ one: z.string().optional(), two: z.string() }).optional())
1325
- .output(z.string().optional())
1326
- .query(({ input }) => input?.two),
1327
- });
1328
-
1329
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1330
-
1331
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1332
- expect(openApiDocument.paths['/optional-param']!.post!.requestBody).toMatchInlineSnapshot(`
1333
- Object {
1334
- "content": Object {
1335
- "application/json": Object {
1336
- "example": undefined,
1337
- "schema": Object {
1338
- "additionalProperties": false,
1339
- "properties": Object {
1340
- "one": Object {
1341
- "type": "string",
1342
- },
1343
- "two": Object {
1344
- "type": "string",
1345
- },
1346
- },
1347
- "required": Array [
1348
- "two",
1349
- ],
1350
- "type": "object",
1351
- },
1352
- },
1353
- },
1354
- "required": true,
1355
- }
1356
- `);
1357
- expect(openApiDocument.paths['/optional-param']!.post!.responses[200]).toMatchInlineSnapshot(`
1358
- Object {
1359
- "content": Object {
1360
- "application/json": Object {
1361
- "example": undefined,
1362
- "schema": Object {
1363
- "anyOf": Array [
1364
- Object {
1365
- "not": Object {},
1366
- },
1367
- Object {
1368
- "type": "string",
1369
- },
1370
- ],
1371
- },
1372
- },
1373
- },
1374
- "description": "Successful response",
1375
- "headers": undefined,
1376
- }
1377
- `);
1378
- expect(openApiDocument.paths['/optional-object']!.post!.requestBody).toMatchInlineSnapshot(`
1379
- Object {
1380
- "content": Object {
1381
- "application/json": Object {
1382
- "example": undefined,
1383
- "schema": Object {
1384
- "additionalProperties": false,
1385
- "properties": Object {
1386
- "one": Object {
1387
- "type": "string",
1388
- },
1389
- "two": Object {
1390
- "type": "string",
1391
- },
1392
- },
1393
- "required": Array [
1394
- "two",
1395
- ],
1396
- "type": "object",
1397
- },
1398
- },
1399
- },
1400
- "required": false,
1401
- }
1402
- `);
1403
- expect(openApiDocument.paths['/optional-object']!.post!.responses[200]).toMatchInlineSnapshot(`
1404
- Object {
1405
- "content": Object {
1406
- "application/json": Object {
1407
- "example": undefined,
1408
- "schema": Object {
1409
- "anyOf": Array [
1410
- Object {
1411
- "not": Object {},
1412
- },
1413
- Object {
1414
- "type": "string",
1415
- },
1416
- ],
1417
- },
1418
- },
1419
- },
1420
- "description": "Successful response",
1421
- "headers": undefined,
1422
- }
1423
- `);
1424
- });
1425
-
1426
- test('with default', () => {
1427
- const appRouter = t.router({
1428
- default: t.procedure
1429
- .meta({ openapi: { method: 'GET', path: '/default' } })
1430
- .input(z.object({ payload: z.string().default('James') }))
1431
- .output(z.string().default('James'))
1432
- .query(({ input }) => input.payload),
1433
- });
1434
-
1435
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1436
-
1437
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1438
- expect(openApiDocument.paths['/default']!.get!.parameters).toMatchInlineSnapshot(`
1439
- Array [
1440
- Object {
1441
- "description": undefined,
1442
- "example": undefined,
1443
- "in": "query",
1444
- "name": "payload",
1445
- "required": false,
1446
- "schema": Object {
1447
- "default": "James",
1448
- "type": "string",
1449
- },
1450
- },
1451
- ]
1452
- `);
1453
- expect(openApiDocument.paths['/default']!.get!.responses[200]).toMatchInlineSnapshot(`
1454
- Object {
1455
- "content": Object {
1456
- "application/json": Object {
1457
- "example": undefined,
1458
- "schema": Object {
1459
- "default": "James",
1460
- "type": "string",
1461
- },
1462
- },
1463
- },
1464
- "description": "Successful response",
1465
- "headers": undefined,
1466
- }
1467
- `);
1468
- });
1469
-
1470
- test('with refine', () => {
1471
- {
1472
- const appRouter = t.router({
1473
- refine: t.procedure
1474
- .meta({ openapi: { method: 'POST', path: '/refine' } })
1475
- .input(z.object({ a: z.string().refine((arg) => arg.length > 10) }))
1476
- .output(z.null())
1477
- .mutation(() => null),
1478
- });
1479
-
1480
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1481
-
1482
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1483
- expect(openApiDocument.paths['/refine']!.post!.requestBody).toMatchInlineSnapshot(`
1484
- Object {
1485
- "content": Object {
1486
- "application/json": Object {
1487
- "example": undefined,
1488
- "schema": Object {
1489
- "additionalProperties": false,
1490
- "properties": Object {
1491
- "a": Object {
1492
- "type": "string",
1493
- },
1494
- },
1495
- "required": Array [
1496
- "a",
1497
- ],
1498
- "type": "object",
1499
- },
1500
- },
1501
- },
1502
- "required": true,
1503
- }
1504
- `);
1505
- }
1506
- {
1507
- const appRouter = t.router({
1508
- objectRefine: t.procedure
1509
- .meta({ openapi: { method: 'POST', path: '/object-refine' } })
1510
- .input(z.object({ a: z.string(), b: z.string() }).refine((data) => data.a === data.b))
1511
- .output(z.null())
1512
- .mutation(() => null),
1513
- });
1514
-
1515
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1516
-
1517
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1518
- expect(openApiDocument.paths['/object-refine']!.post!.requestBody).toMatchInlineSnapshot(`
1519
- Object {
1520
- "content": Object {
1521
- "application/json": Object {
1522
- "example": undefined,
1523
- "schema": Object {
1524
- "additionalProperties": false,
1525
- "properties": Object {
1526
- "a": Object {
1527
- "type": "string",
1528
- },
1529
- "b": Object {
1530
- "type": "string",
1531
- },
1532
- },
1533
- "required": Array [
1534
- "a",
1535
- "b",
1536
- ],
1537
- "type": "object",
1538
- },
1539
- },
1540
- },
1541
- "required": true,
1542
- }
1543
- `);
1544
- }
1545
- });
1546
-
1547
- test('with async refine', () => {
1548
- {
1549
- const appRouter = t.router({
1550
- refine: t.procedure
1551
- .meta({ openapi: { method: 'POST', path: '/refine' } })
1552
- // eslint-disable-next-line @typescript-eslint/require-await
1553
- .input(z.object({ a: z.string().refine(async (arg) => arg.length > 10) }))
1554
- .output(z.null())
1555
- .mutation(() => null),
1556
- });
1557
-
1558
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1559
-
1560
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1561
- expect(openApiDocument.paths['/refine']!.post!.requestBody).toMatchInlineSnapshot(`
1562
- Object {
1563
- "content": Object {
1564
- "application/json": Object {
1565
- "example": undefined,
1566
- "schema": Object {
1567
- "additionalProperties": false,
1568
- "properties": Object {
1569
- "a": Object {
1570
- "type": "string",
1571
- },
1572
- },
1573
- "required": Array [
1574
- "a",
1575
- ],
1576
- "type": "object",
1577
- },
1578
- },
1579
- },
1580
- "required": true,
1581
- }
1582
- `);
1583
- }
1584
- {
1585
- const appRouter = t.router({
1586
- objectRefine: t.procedure
1587
- .meta({ openapi: { method: 'POST', path: '/object-refine' } })
1588
- .input(
1589
- // eslint-disable-next-line @typescript-eslint/require-await
1590
- z.object({ a: z.string(), b: z.string() }).refine(async (data) => data.a === data.b),
1591
- )
1592
- .output(z.null())
1593
- .mutation(() => null),
1594
- });
1595
-
1596
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1597
-
1598
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1599
- expect(openApiDocument.paths['/object-refine']!.post!.requestBody).toMatchInlineSnapshot(`
1600
- Object {
1601
- "content": Object {
1602
- "application/json": Object {
1603
- "example": undefined,
1604
- "schema": Object {
1605
- "additionalProperties": false,
1606
- "properties": Object {
1607
- "a": Object {
1608
- "type": "string",
1609
- },
1610
- "b": Object {
1611
- "type": "string",
1612
- },
1613
- },
1614
- "required": Array [
1615
- "a",
1616
- "b",
1617
- ],
1618
- "type": "object",
1619
- },
1620
- },
1621
- },
1622
- "required": true,
1623
- }
1624
- `);
1625
- }
1626
- });
1627
-
1628
- test('with transform', () => {
1629
- const appRouter = t.router({
1630
- transform: t.procedure
1631
- .meta({ openapi: { method: 'GET', path: '/transform' } })
1632
- .input(z.object({ age: z.string().transform((input) => parseInt(input)) }))
1633
- .output(z.object({ age: z.string().transform((input) => parseInt(input)) }))
1634
- .query(({ input }) => ({ age: input.age.toString() })),
1635
- });
1636
-
1637
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1638
-
1639
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1640
- expect(openApiDocument.paths['/transform']!.get!.parameters).toMatchInlineSnapshot(`
1641
- Array [
1642
- Object {
1643
- "description": undefined,
1644
- "example": undefined,
1645
- "in": "query",
1646
- "name": "age",
1647
- "required": true,
1648
- "schema": Object {
1649
- "type": "string",
1650
- },
1651
- },
1652
- ]
1653
- `);
1654
- });
1655
-
1656
- test('with preprocess', () => {
1657
- const appRouter = t.router({
1658
- transform: t.procedure
1659
- .meta({ openapi: { method: 'GET', path: '/preprocess' } })
1660
- .input(
1661
- z.object({
1662
- payload: z.preprocess((arg) => {
1663
- if (typeof arg === 'string') {
1664
- return parseInt(arg);
1665
- }
1666
- return arg;
1667
- }, z.number()),
1668
- }),
1669
- )
1670
- .output(z.number())
1671
- .query(({ input }) => input.payload),
1672
- });
1673
-
1674
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1675
-
1676
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1677
- expect(openApiDocument.paths['/preprocess']!.get!.parameters).toMatchInlineSnapshot(`
1678
- Array [
1679
- Object {
1680
- "description": undefined,
1681
- "example": undefined,
1682
- "in": "query",
1683
- "name": "payload",
1684
- "required": true,
1685
- "schema": Object {
1686
- "type": "number",
1687
- },
1688
- },
1689
- ]
1690
- `);
1691
- expect(openApiDocument.paths['/preprocess']!.get!.responses[200]).toMatchInlineSnapshot(`
1692
- Object {
1693
- "content": Object {
1694
- "application/json": Object {
1695
- "example": undefined,
1696
- "schema": Object {
1697
- "type": "number",
1698
- },
1699
- },
1700
- },
1701
- "description": "Successful response",
1702
- "headers": undefined,
1703
- }
1704
- `);
1705
- });
1706
-
1707
- test('with coerce', () => {
1708
- const appRouter = t.router({
1709
- transform: t.procedure
1710
- .meta({ openapi: { method: 'GET', path: '/coerce' } })
1711
- .input(z.object({ payload: z.number() }))
1712
- .output(z.number())
1713
- .query(({ input }) => input.payload),
1714
- });
1715
-
1716
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1717
-
1718
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1719
- expect(openApiDocument.paths['/coerce']!.get!.parameters).toMatchInlineSnapshot(`
1720
- Array [
1721
- Object {
1722
- "description": undefined,
1723
- "example": undefined,
1724
- "in": "query",
1725
- "name": "payload",
1726
- "required": true,
1727
- "schema": Object {
1728
- "type": "number",
1729
- },
1730
- },
1731
- ]
1732
- `);
1733
- expect(openApiDocument.paths['/coerce']!.get!.responses[200]).toMatchInlineSnapshot(`
1734
- Object {
1735
- "content": Object {
1736
- "application/json": Object {
1737
- "example": undefined,
1738
- "schema": Object {
1739
- "type": "number",
1740
- },
1741
- },
1742
- },
1743
- "description": "Successful response",
1744
- "headers": undefined,
1745
- }
1746
- `);
1747
- });
1748
-
1749
- test('with union', () => {
1750
- {
1751
- const appRouter = t.router({
1752
- union: t.procedure
1753
- .meta({ openapi: { method: 'GET', path: '/union' } })
1754
- .input(z.object({ payload: z.string().or(z.object({})) }))
1755
- .output(z.null())
1756
- .query(() => null),
1757
- });
1758
-
1759
- expect(() => {
1760
- generateOpenApiDocument(appRouter, defaultDocOpts);
1761
- }).toThrowError('[query.union] - Input parser key: "payload" must be ZodString');
1762
- }
1763
- {
1764
- const appRouter = t.router({
1765
- union: t.procedure
1766
- .meta({ openapi: { method: 'GET', path: '/union' } })
1767
- .input(z.object({ payload: z.string().or(z.literal('James')) }))
1768
- .output(z.null())
1769
- .query(() => null),
1770
- });
1771
-
1772
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1773
-
1774
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1775
- expect(openApiDocument.paths['/union']!.get!.parameters).toMatchInlineSnapshot(`
1776
- Array [
1777
- Object {
1778
- "description": undefined,
1779
- "example": undefined,
1780
- "in": "query",
1781
- "name": "payload",
1782
- "required": true,
1783
- "schema": Object {
1784
- "anyOf": Array [
1785
- Object {
1786
- "type": "string",
1787
- },
1788
- Object {
1789
- "enum": Array [
1790
- "James",
1791
- ],
1792
- "type": "string",
1793
- },
1794
- ],
1795
- },
1796
- },
1797
- ]
1798
- `);
1799
- }
1800
- });
1801
-
1802
- test('with intersection', () => {
1803
- const appRouter = t.router({
1804
- intersection: t.procedure
1805
- .meta({ openapi: { method: 'GET', path: '/intersection' } })
1806
- .input(
1807
- z.object({
1808
- payload: z.intersection(
1809
- z.union([z.literal('a'), z.literal('b')]),
1810
- z.union([z.literal('b'), z.literal('c')]),
1811
- ),
1812
- }),
1813
- )
1814
- .output(z.null())
1815
- .query(() => null),
1816
- });
1817
-
1818
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1819
-
1820
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1821
- expect(openApiDocument.paths['/intersection']!.get!.parameters).toMatchInlineSnapshot(`
1822
- Array [
1823
- Object {
1824
- "description": undefined,
1825
- "example": undefined,
1826
- "in": "query",
1827
- "name": "payload",
1828
- "required": true,
1829
- "schema": Object {
1830
- "allOf": Array [
1831
- Object {
1832
- "anyOf": Array [
1833
- Object {
1834
- "enum": Array [
1835
- "a",
1836
- ],
1837
- "type": "string",
1838
- },
1839
- Object {
1840
- "enum": Array [
1841
- "b",
1842
- ],
1843
- "type": "string",
1844
- },
1845
- ],
1846
- },
1847
- Object {
1848
- "anyOf": Array [
1849
- Object {
1850
- "enum": Array [
1851
- "b",
1852
- ],
1853
- "type": "string",
1854
- },
1855
- Object {
1856
- "enum": Array [
1857
- "c",
1858
- ],
1859
- "type": "string",
1860
- },
1861
- ],
1862
- },
1863
- ],
1864
- },
1865
- },
1866
- ]
1867
- `);
1868
- });
1869
-
1870
- test('with lazy', () => {
1871
- const appRouter = t.router({
1872
- lazy: t.procedure
1873
- .meta({ openapi: { method: 'GET', path: '/lazy' } })
1874
- .input(z.object({ payload: z.lazy(() => z.string()) }))
1875
- .output(z.null())
1876
- .query(() => null),
1877
- });
1878
-
1879
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1880
-
1881
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1882
- expect(openApiDocument.paths['/lazy']!.get!.parameters).toMatchInlineSnapshot(`
1883
- Array [
1884
- Object {
1885
- "description": undefined,
1886
- "example": undefined,
1887
- "in": "query",
1888
- "name": "payload",
1889
- "required": true,
1890
- "schema": Object {
1891
- "type": "string",
1892
- },
1893
- },
1894
- ]
1895
- `);
1896
- });
1897
-
1898
- test('with literal', () => {
1899
- const appRouter = t.router({
1900
- literal: t.procedure
1901
- .meta({ openapi: { method: 'GET', path: '/literal' } })
1902
- .input(z.object({ payload: z.literal('literal') }))
1903
- .output(z.null())
1904
- .query(() => null),
1905
- });
1906
-
1907
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1908
-
1909
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1910
- expect(openApiDocument.paths['/literal']!.get!.parameters).toMatchInlineSnapshot(`
1911
- Array [
1912
- Object {
1913
- "description": undefined,
1914
- "example": undefined,
1915
- "in": "query",
1916
- "name": "payload",
1917
- "required": true,
1918
- "schema": Object {
1919
- "enum": Array [
1920
- "literal",
1921
- ],
1922
- "type": "string",
1923
- },
1924
- },
1925
- ]
1926
- `);
1927
- });
1928
-
1929
- test('with enum', () => {
1930
- const appRouter = t.router({
1931
- enum: t.procedure
1932
- .meta({ openapi: { method: 'GET', path: '/enum' } })
1933
- .input(z.object({ name: z.enum(['James', 'jlalmes']) }))
1934
- .output(z.null())
1935
- .query(() => null),
1936
- });
1937
-
1938
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1939
-
1940
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1941
- expect(openApiDocument.paths['/enum']!.get!.parameters).toMatchInlineSnapshot(`
1942
- Array [
1943
- Object {
1944
- "description": undefined,
1945
- "example": undefined,
1946
- "in": "query",
1947
- "name": "name",
1948
- "required": true,
1949
- "schema": Object {
1950
- "enum": Array [
1951
- "James",
1952
- "jlalmes",
1953
- ],
1954
- "type": "string",
1955
- },
1956
- },
1957
- ]
1958
- `);
1959
- });
1960
-
1961
- test('with native-enum', () => {
1962
- {
1963
- enum InvalidEnum {
1964
- James,
1965
- jlalmes,
1966
- }
1967
-
1968
- const appRouter = t.router({
1969
- nativeEnum: t.procedure
1970
- .meta({ openapi: { method: 'GET', path: '/nativeEnum' } })
1971
- .input(z.object({ name: z.nativeEnum(InvalidEnum) }))
1972
- .output(z.null())
1973
- .query(() => null),
1974
- });
1975
-
1976
- expect(() => {
1977
- generateOpenApiDocument(appRouter, defaultDocOpts);
1978
- }).toThrowError('[query.nativeEnum] - Input parser key: "name" must be ZodString');
1979
- }
1980
- {
1981
- enum ValidEnum {
1982
- James = 'James',
1983
- jlalmes = 'jlalmes',
1984
- }
1985
-
1986
- const appRouter = t.router({
1987
- nativeEnum: t.procedure
1988
- .meta({ openapi: { method: 'GET', path: '/nativeEnum' } })
1989
- .input(z.object({ name: z.nativeEnum(ValidEnum) }))
1990
- .output(z.null())
1991
- .query(() => null),
1992
- });
1993
-
1994
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
1995
-
1996
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
1997
- expect(openApiDocument.paths['/nativeEnum']!.get!.parameters).toMatchInlineSnapshot(`
1998
- Array [
1999
- Object {
2000
- "description": undefined,
2001
- "example": undefined,
2002
- "in": "query",
2003
- "name": "name",
2004
- "required": true,
2005
- "schema": Object {
2006
- "enum": Array [
2007
- "James",
2008
- "jlalmes",
2009
- ],
2010
- "type": "string",
2011
- },
2012
- },
2013
- ]
2014
- `);
2015
- }
2016
- });
2017
-
2018
- test('with no refs', () => {
2019
- const schemas = { emails: z.array(z.string().email()) };
2020
-
2021
- const appRouter = t.router({
2022
- refs: t.procedure
2023
- .meta({ openapi: { method: 'POST', path: '/refs' } })
2024
- .input(z.object({ allowed: schemas.emails, blocked: schemas.emails }))
2025
- .output(z.object({ allowed: schemas.emails, blocked: schemas.emails }))
2026
- .mutation(() => ({ allowed: [], blocked: [] })),
2027
- });
2028
-
2029
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
2030
-
2031
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
2032
- expect(openApiDocument.paths['/refs']!.post!.requestBody).toMatchInlineSnapshot(`
2033
- Object {
2034
- "content": Object {
2035
- "application/json": Object {
2036
- "example": undefined,
2037
- "schema": Object {
2038
- "additionalProperties": false,
2039
- "properties": Object {
2040
- "allowed": Object {
2041
- "items": Object {
2042
- "format": "email",
2043
- "type": "string",
2044
- },
2045
- "type": "array",
2046
- },
2047
- "blocked": Object {
2048
- "items": Object {
2049
- "format": "email",
2050
- "type": "string",
2051
- },
2052
- "type": "array",
2053
- },
2054
- },
2055
- "required": Array [
2056
- "allowed",
2057
- "blocked",
2058
- ],
2059
- "type": "object",
2060
- },
2061
- },
2062
- },
2063
- "required": true,
2064
- }
2065
- `);
2066
- expect(openApiDocument.paths['/refs']!.post!.responses[200]).toMatchInlineSnapshot(`
2067
- Object {
2068
- "content": Object {
2069
- "application/json": Object {
2070
- "example": undefined,
2071
- "schema": Object {
2072
- "additionalProperties": false,
2073
- "properties": Object {
2074
- "allowed": Object {
2075
- "items": Object {
2076
- "format": "email",
2077
- "type": "string",
2078
- },
2079
- "type": "array",
2080
- },
2081
- "blocked": Object {
2082
- "items": Object {
2083
- "format": "email",
2084
- "type": "string",
2085
- },
2086
- "type": "array",
2087
- },
2088
- },
2089
- "required": Array [
2090
- "allowed",
2091
- "blocked",
2092
- ],
2093
- "type": "object",
2094
- },
2095
- },
2096
- },
2097
- "description": "Successful response",
2098
- "headers": undefined,
2099
- }
2100
- `);
2101
- });
2102
-
2103
- test('with custom header', () => {
2104
- const appRouter = t.router({
2105
- echo: t.procedure
2106
- .meta({
2107
- openapi: {
2108
- method: 'GET',
2109
- path: '/echo',
2110
- headers: [
2111
- {
2112
- name: 'x-custom-header',
2113
- required: true,
2114
- description: 'Some custom header',
2115
- },
2116
- ],
2117
- },
2118
- })
2119
- .input(z.object({ id: z.string() }))
2120
- .output(z.object({ id: z.string() }))
2121
- .query(({ input }) => ({ id: input.id })),
2122
- });
2123
-
2124
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
2125
-
2126
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
2127
- expect(openApiDocument.paths['/echo']!.get!.parameters).toMatchInlineSnapshot(`
2128
- Array [
2129
- Object {
2130
- "description": "Some custom header",
2131
- "in": "header",
2132
- "name": "x-custom-header",
2133
- "required": true,
2134
- },
2135
- Object {
2136
- "description": undefined,
2137
- "example": undefined,
2138
- "in": "query",
2139
- "name": "id",
2140
- "required": true,
2141
- "schema": Object {
2142
- "type": "string",
2143
- },
2144
- },
2145
- ]
2146
- `);
2147
- });
2148
-
2149
- test('with DELETE mutation', () => {
2150
- const appRouter = t.router({
2151
- deleteMutation: t.procedure
2152
- .meta({ openapi: { method: 'DELETE', path: '/mutation/delete' } })
2153
- .input(z.object({ id: z.string() }))
2154
- .output(z.object({ id: z.string() }))
2155
- .mutation(({ input }) => ({ id: input.id })),
2156
- });
2157
-
2158
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
2159
-
2160
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
2161
- expect(openApiDocument.paths['/mutation/delete']!.delete!.requestBody).toMatchInlineSnapshot(
2162
- `undefined`,
2163
- );
2164
- expect(openApiDocument.paths['/mutation/delete']!.delete!.parameters).toMatchInlineSnapshot(`
2165
- Array [
2166
- Object {
2167
- "description": undefined,
2168
- "example": undefined,
2169
- "in": "query",
2170
- "name": "id",
2171
- "required": true,
2172
- "schema": Object {
2173
- "type": "string",
2174
- },
2175
- },
2176
- ]
2177
- `);
2178
- });
2179
-
2180
- test('with POST query', () => {
2181
- const appRouter = t.router({
2182
- postQuery: t.procedure
2183
- .meta({ openapi: { method: 'POST', path: '/query/post' } })
2184
- .input(z.object({ id: z.string() }))
2185
- .output(z.object({ id: z.string() }))
2186
- .query(({ input }) => ({ id: input.id })),
2187
- });
2188
-
2189
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
2190
-
2191
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
2192
- expect(openApiDocument.paths['/query/post']!.post!.requestBody).toMatchInlineSnapshot(`
2193
- Object {
2194
- "content": Object {
2195
- "application/json": Object {
2196
- "example": undefined,
2197
- "schema": Object {
2198
- "additionalProperties": false,
2199
- "properties": Object {
2200
- "id": Object {
2201
- "type": "string",
2202
- },
2203
- },
2204
- "required": Array [
2205
- "id",
2206
- ],
2207
- "type": "object",
2208
- },
2209
- },
2210
- },
2211
- "required": true,
2212
- }
2213
- `);
2214
- expect(openApiDocument.paths['/query/post']!.post!.parameters).toMatchInlineSnapshot(
2215
- `Array []`,
2216
- );
2217
- });
2218
-
2219
- test('with top-level preprocess', () => {
2220
- const appRouter = t.router({
2221
- topLevelPreprocessQuery: t.procedure
2222
- .meta({ openapi: { method: 'GET', path: '/top-level-preprocess' } })
2223
- .input(z.preprocess((arg) => arg, z.object({ id: z.string() })))
2224
- .output(z.preprocess((arg) => arg, z.object({ id: z.string() })))
2225
- .query(({ input }) => ({ id: input.id })),
2226
- topLevelPreprocessMutation: t.procedure
2227
- .meta({ openapi: { method: 'POST', path: '/top-level-preprocess' } })
2228
- .input(z.preprocess((arg) => arg, z.object({ id: z.string() })))
2229
- .output(z.preprocess((arg) => arg, z.object({ id: z.string() })))
2230
- .mutation(({ input }) => ({ id: input.id })),
2231
- });
2232
-
2233
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
2234
-
2235
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
2236
- expect(openApiDocument.paths['/top-level-preprocess']!.get!.parameters).toMatchInlineSnapshot(`
2237
- Array [
2238
- Object {
2239
- "description": undefined,
2240
- "example": undefined,
2241
- "in": "query",
2242
- "name": "id",
2243
- "required": true,
2244
- "schema": Object {
2245
- "type": "string",
2246
- },
2247
- },
2248
- ]
2249
- `);
2250
- expect(openApiDocument.paths['/top-level-preprocess']!.post!.requestBody)
2251
- .toMatchInlineSnapshot(`
2252
- Object {
2253
- "content": Object {
2254
- "application/json": Object {
2255
- "example": undefined,
2256
- "schema": Object {
2257
- "additionalProperties": false,
2258
- "properties": Object {
2259
- "id": Object {
2260
- "type": "string",
2261
- },
2262
- },
2263
- "required": Array [
2264
- "id",
2265
- ],
2266
- "type": "object",
2267
- },
2268
- },
2269
- },
2270
- "required": true,
2271
- }
2272
- `);
2273
- });
2274
-
2275
- test('with nested routers', () => {
2276
- const appRouter = t.router({
2277
- procedure: t.procedure
2278
- .meta({ openapi: { method: 'GET', path: '/procedure' } })
2279
- .input(z.object({ payload: z.string() }))
2280
- .output(z.object({ payload: z.string() }))
2281
- .query(({ input }) => ({ payload: input.payload })),
2282
- router: t.router({
2283
- procedure: t.procedure
2284
- .meta({ openapi: { method: 'GET', path: '/router/procedure' } })
2285
- .input(z.object({ payload: z.string() }))
2286
- .output(z.object({ payload: z.string() }))
2287
- .query(({ input }) => ({ payload: input.payload })),
2288
- router: t.router({
2289
- procedure: t.procedure
2290
- .meta({ openapi: { method: 'GET', path: '/router/router/procedure' } })
2291
- .input(z.object({ payload: z.string() }))
2292
- .output(z.object({ payload: z.string() }))
2293
- .query(({ input }) => ({ payload: input.payload })),
2294
- }),
2295
- }),
2296
- });
2297
-
2298
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
2299
-
2300
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
2301
- expect(openApiDocument.paths).toMatchInlineSnapshot(`
2302
- Object {
2303
- "/procedure": Object {
2304
- "get": Object {
2305
- "description": undefined,
2306
- "operationId": "procedure",
2307
- "parameters": Array [
2308
- Object {
2309
- "description": undefined,
2310
- "example": undefined,
2311
- "in": "query",
2312
- "name": "payload",
2313
- "required": true,
2314
- "schema": Object {
2315
- "type": "string",
2316
- },
2317
- },
2318
- ],
2319
- "requestBody": undefined,
2320
- "responses": Object {
2321
- "200": Object {
2322
- "content": Object {
2323
- "application/json": Object {
2324
- "example": undefined,
2325
- "schema": Object {
2326
- "additionalProperties": false,
2327
- "properties": Object {
2328
- "payload": Object {
2329
- "type": "string",
2330
- },
2331
- },
2332
- "required": Array [
2333
- "payload",
2334
- ],
2335
- "type": "object",
2336
- },
2337
- },
2338
- },
2339
- "description": "Successful response",
2340
- "headers": undefined,
2341
- },
2342
- "default": Object {
2343
- "$ref": "#/components/responses/error",
2344
- },
2345
- },
2346
- "security": undefined,
2347
- "summary": undefined,
2348
- "tags": undefined,
2349
- },
2350
- },
2351
- "/router/procedure": Object {
2352
- "get": Object {
2353
- "description": undefined,
2354
- "operationId": "router-procedure",
2355
- "parameters": Array [
2356
- Object {
2357
- "description": undefined,
2358
- "example": undefined,
2359
- "in": "query",
2360
- "name": "payload",
2361
- "required": true,
2362
- "schema": Object {
2363
- "type": "string",
2364
- },
2365
- },
2366
- ],
2367
- "requestBody": undefined,
2368
- "responses": Object {
2369
- "200": Object {
2370
- "content": Object {
2371
- "application/json": Object {
2372
- "example": undefined,
2373
- "schema": Object {
2374
- "additionalProperties": false,
2375
- "properties": Object {
2376
- "payload": Object {
2377
- "type": "string",
2378
- },
2379
- },
2380
- "required": Array [
2381
- "payload",
2382
- ],
2383
- "type": "object",
2384
- },
2385
- },
2386
- },
2387
- "description": "Successful response",
2388
- "headers": undefined,
2389
- },
2390
- "default": Object {
2391
- "$ref": "#/components/responses/error",
2392
- },
2393
- },
2394
- "security": undefined,
2395
- "summary": undefined,
2396
- "tags": undefined,
2397
- },
2398
- },
2399
- "/router/router/procedure": Object {
2400
- "get": Object {
2401
- "description": undefined,
2402
- "operationId": "router-router-procedure",
2403
- "parameters": Array [
2404
- Object {
2405
- "description": undefined,
2406
- "example": undefined,
2407
- "in": "query",
2408
- "name": "payload",
2409
- "required": true,
2410
- "schema": Object {
2411
- "type": "string",
2412
- },
2413
- },
2414
- ],
2415
- "requestBody": undefined,
2416
- "responses": Object {
2417
- "200": Object {
2418
- "content": Object {
2419
- "application/json": Object {
2420
- "example": undefined,
2421
- "schema": Object {
2422
- "additionalProperties": false,
2423
- "properties": Object {
2424
- "payload": Object {
2425
- "type": "string",
2426
- },
2427
- },
2428
- "required": Array [
2429
- "payload",
2430
- ],
2431
- "type": "object",
2432
- },
2433
- },
2434
- },
2435
- "description": "Successful response",
2436
- "headers": undefined,
2437
- },
2438
- "default": Object {
2439
- "$ref": "#/components/responses/error",
2440
- },
2441
- },
2442
- "security": undefined,
2443
- "summary": undefined,
2444
- "tags": undefined,
2445
- },
2446
- },
2447
- }
2448
- `);
2449
- });
2450
-
2451
- test('with multiple inputs', () => {
2452
- const appRouter = t.router({
2453
- query: t.procedure
2454
- .meta({ openapi: { method: 'GET', path: '/query' } })
2455
- .input(z.object({ id: z.string() }))
2456
- .input(z.object({ payload: z.string() }))
2457
- .output(z.object({ id: z.string(), payload: z.string() }))
2458
- .query(({ input }) => ({ id: input.id, payload: input.payload })),
2459
- mutation: t.procedure
2460
- .meta({ openapi: { method: 'POST', path: '/mutation' } })
2461
- .input(z.object({ id: z.string() }))
2462
- .input(z.object({ payload: z.string() }))
2463
- .output(z.object({ id: z.string(), payload: z.string() }))
2464
- .mutation(({ input }) => ({ id: input.id, payload: input.payload })),
2465
- });
2466
-
2467
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
2468
-
2469
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
2470
- expect(openApiDocument.paths['/query']!.get!.parameters).toMatchInlineSnapshot(`
2471
- Array [
2472
- Object {
2473
- "description": undefined,
2474
- "example": undefined,
2475
- "in": "query",
2476
- "name": "id",
2477
- "required": true,
2478
- "schema": Object {
2479
- "type": "string",
2480
- },
2481
- },
2482
- Object {
2483
- "description": undefined,
2484
- "example": undefined,
2485
- "in": "query",
2486
- "name": "payload",
2487
- "required": true,
2488
- "schema": Object {
2489
- "type": "string",
2490
- },
2491
- },
2492
- ]
2493
- `);
2494
- expect(openApiDocument.paths['/mutation']!.post!.requestBody).toMatchInlineSnapshot(`
2495
- Object {
2496
- "content": Object {
2497
- "application/json": Object {
2498
- "example": undefined,
2499
- "schema": Object {
2500
- "additionalProperties": false,
2501
- "properties": Object {
2502
- "id": Object {
2503
- "type": "string",
2504
- },
2505
- "payload": Object {
2506
- "type": "string",
2507
- },
2508
- },
2509
- "required": Array [
2510
- "id",
2511
- "payload",
2512
- ],
2513
- "type": "object",
2514
- },
2515
- },
2516
- },
2517
- "required": true,
2518
- }
2519
- `);
2520
- });
2521
-
2522
- test('with content types', () => {
2523
- {
2524
- const appRouter = t.router({
2525
- withNone: t.procedure
2526
- .meta({ openapi: { method: 'POST', path: '/with-none', contentTypes: [] } })
2527
- .input(z.object({ payload: z.string() }))
2528
- .output(z.object({ payload: z.string() }))
2529
- .mutation(({ input }) => ({ payload: input.payload })),
2530
- });
2531
-
2532
- expect(() => {
2533
- generateOpenApiDocument(appRouter, defaultDocOpts);
2534
- }).toThrowError('[mutation.withNone] - At least one content type must be specified');
2535
- }
2536
- {
2537
- const appRouter = t.router({
2538
- withUrlencoded: t.procedure
2539
- .meta({
2540
- openapi: {
2541
- method: 'POST',
2542
- path: '/with-urlencoded',
2543
- contentTypes: ['application/x-www-form-urlencoded'],
2544
- },
2545
- })
2546
- .input(z.object({ payload: z.string() }))
2547
- .output(z.object({ payload: z.string() }))
2548
- .mutation(({ input }) => ({ payload: input.payload })),
2549
- withJson: t.procedure
2550
- .meta({
2551
- openapi: { method: 'POST', path: '/with-json', contentTypes: ['application/json'] },
2552
- })
2553
- .input(z.object({ payload: z.string() }))
2554
- .output(z.object({ payload: z.string() }))
2555
- .mutation(({ input }) => ({ payload: input.payload })),
2556
- withAll: t.procedure
2557
- .meta({
2558
- openapi: {
2559
- method: 'POST',
2560
- path: '/with-all',
2561
- contentTypes: ['application/json', 'application/x-www-form-urlencoded'],
2562
- },
2563
- })
2564
- .input(z.object({ payload: z.string() }))
2565
- .output(z.object({ payload: z.string() }))
2566
- .mutation(({ input }) => ({ payload: input.payload })),
2567
- withDefault: t.procedure
2568
- .meta({ openapi: { method: 'POST', path: '/with-default' } })
2569
- .input(z.object({ payload: z.string() }))
2570
- .output(z.object({ payload: z.string() }))
2571
- .mutation(({ input }) => ({ payload: input.payload })),
2572
- });
2573
-
2574
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
2575
-
2576
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
2577
- expect(
2578
- Object.keys((openApiDocument.paths['/with-urlencoded']!.post!.requestBody as any).content),
2579
- ).toEqual(['application/x-www-form-urlencoded']);
2580
- expect(
2581
- Object.keys((openApiDocument.paths['/with-json']!.post!.requestBody as any).content),
2582
- ).toEqual(['application/json']);
2583
- expect(
2584
- Object.keys((openApiDocument.paths['/with-all']!.post!.requestBody as any).content),
2585
- ).toEqual(['application/json', 'application/x-www-form-urlencoded']);
2586
- expect(
2587
- (openApiDocument.paths['/with-all']!.post!.requestBody as any).content['application/json'],
2588
- ).toEqual(
2589
- (openApiDocument.paths['/with-all']!.post!.requestBody as any).content[
2590
- 'application/x-www-form-urlencoded'
2591
- ],
2592
- );
2593
- expect(
2594
- Object.keys((openApiDocument.paths['/with-default']!.post!.requestBody as any).content),
2595
- ).toEqual(['application/json']);
2596
- }
2597
- });
2598
-
2599
- test('with deprecated', () => {
2600
- const appRouter = t.router({
2601
- deprecated: t.procedure
2602
- .meta({ openapi: { method: 'POST', path: '/deprecated', deprecated: true } })
2603
- .input(z.object({ payload: z.string() }))
2604
- .output(z.object({ payload: z.string() }))
2605
- .mutation(({ input }) => ({ payload: input.payload })),
2606
- });
2607
-
2608
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
2609
-
2610
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
2611
- expect(openApiDocument.paths['/deprecated']!.post!.deprecated).toEqual(true);
2612
- });
2613
-
2614
- test('with security schemes', () => {
2615
- const appRouter = t.router({
2616
- protected: t.procedure
2617
- .meta({ openapi: { method: 'POST', path: '/protected', protect: true } })
2618
- .input(z.object({ payload: z.string() }))
2619
- .output(z.object({ payload: z.string() }))
2620
- .mutation(({ input }) => ({ payload: input.payload })),
2621
- });
2622
-
2623
- const openApiDocument = generateOpenApiDocument(appRouter, {
2624
- ...defaultDocOpts,
2625
- securitySchemes: {
2626
- ApiKey: {
2627
- type: 'apiKey',
2628
- in: 'header',
2629
- name: 'X-API-Key',
2630
- },
2631
- },
2632
- });
2633
-
2634
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
2635
- expect(openApiDocument.components!.securitySchemes).toEqual({
2636
- ApiKey: {
2637
- type: 'apiKey',
2638
- in: 'header',
2639
- name: 'X-API-Key',
2640
- },
2641
- });
2642
- expect(openApiDocument.paths['/protected']!.post!.security).toEqual([{ ApiKey: [] }]);
2643
- });
2644
-
2645
- test('with examples', () => {
2646
- const appRouter = t.router({
2647
- queryExample: t.procedure
2648
- .meta({
2649
- openapi: {
2650
- method: 'GET',
2651
- path: '/query-example/{name}',
2652
- example: {
2653
- request: { name: 'James', greeting: 'Hello' },
2654
- response: { output: 'Hello James' },
2655
- },
2656
- },
2657
- })
2658
- .input(z.object({ name: z.string(), greeting: z.string() }))
2659
- .output(z.object({ output: z.string() }))
2660
- .query(({ input }) => ({
2661
- output: `${input.greeting} ${input.name}`,
2662
- })),
2663
- mutationExample: t.procedure
2664
- .meta({
2665
- openapi: {
2666
- method: 'POST',
2667
- path: '/mutation-example/{name}',
2668
- example: {
2669
- request: { name: 'James', greeting: 'Hello' },
2670
- response: { output: 'Hello James' },
2671
- },
2672
- },
2673
- })
2674
- .input(z.object({ name: z.string(), greeting: z.string() }))
2675
- .output(z.object({ output: z.string() }))
2676
- .mutation(({ input }) => ({
2677
- output: `${input.greeting} ${input.name}`,
2678
- })),
2679
- });
2680
-
2681
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
2682
-
2683
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
2684
- expect(openApiDocument.paths['/query-example/{name}']!.get!.parameters).toMatchInlineSnapshot(`
2685
- Array [
2686
- Object {
2687
- "description": undefined,
2688
- "example": "James",
2689
- "in": "path",
2690
- "name": "name",
2691
- "required": true,
2692
- "schema": Object {
2693
- "type": "string",
2694
- },
2695
- },
2696
- Object {
2697
- "description": undefined,
2698
- "example": "Hello",
2699
- "in": "query",
2700
- "name": "greeting",
2701
- "required": true,
2702
- "schema": Object {
2703
- "type": "string",
2704
- },
2705
- },
2706
- ]
2707
- `);
2708
- expect(openApiDocument.paths['/query-example/{name}']!.get!.responses[200])
2709
- .toMatchInlineSnapshot(`
2710
- Object {
2711
- "content": Object {
2712
- "application/json": Object {
2713
- "example": Object {
2714
- "output": "Hello James",
2715
- },
2716
- "schema": Object {
2717
- "additionalProperties": false,
2718
- "properties": Object {
2719
- "output": Object {
2720
- "type": "string",
2721
- },
2722
- },
2723
- "required": Array [
2724
- "output",
2725
- ],
2726
- "type": "object",
2727
- },
2728
- },
2729
- },
2730
- "description": "Successful response",
2731
- "headers": undefined,
2732
- }
2733
- `);
2734
- expect(openApiDocument.paths['/mutation-example/{name}']!.post!.parameters)
2735
- .toMatchInlineSnapshot(`
2736
- Array [
2737
- Object {
2738
- "description": undefined,
2739
- "example": "James",
2740
- "in": "path",
2741
- "name": "name",
2742
- "required": true,
2743
- "schema": Object {
2744
- "type": "string",
2745
- },
2746
- },
2747
- ]
2748
- `);
2749
- expect(openApiDocument.paths['/mutation-example/{name}']!.post!.requestBody)
2750
- .toMatchInlineSnapshot(`
2751
- Object {
2752
- "content": Object {
2753
- "application/json": Object {
2754
- "example": Object {
2755
- "greeting": "Hello",
2756
- },
2757
- "schema": Object {
2758
- "additionalProperties": false,
2759
- "properties": Object {
2760
- "greeting": Object {
2761
- "type": "string",
2762
- },
2763
- },
2764
- "required": Array [
2765
- "greeting",
2766
- ],
2767
- "type": "object",
2768
- },
2769
- },
2770
- },
2771
- "required": true,
2772
- }
2773
- `);
2774
- expect(openApiDocument.paths['/mutation-example/{name}']!.post!.responses[200])
2775
- .toMatchInlineSnapshot(`
2776
- Object {
2777
- "content": Object {
2778
- "application/json": Object {
2779
- "example": Object {
2780
- "output": "Hello James",
2781
- },
2782
- "schema": Object {
2783
- "additionalProperties": false,
2784
- "properties": Object {
2785
- "output": Object {
2786
- "type": "string",
2787
- },
2788
- },
2789
- "required": Array [
2790
- "output",
2791
- ],
2792
- "type": "object",
2793
- },
2794
- },
2795
- },
2796
- "description": "Successful response",
2797
- "headers": undefined,
2798
- }
2799
- `);
2800
- });
2801
-
2802
- test('with response headers', () => {
2803
- const appRouter = t.router({
2804
- queryExample: t.procedure
2805
- .meta({
2806
- openapi: {
2807
- method: 'GET',
2808
- path: '/query-example/{name}',
2809
- responseHeaders: {
2810
- "X-RateLimit-Limit": {
2811
- description: "Request limit per hour.",
2812
- schema: {
2813
- type: "integer"
2814
- }
2815
- },
2816
- "X-RateLimit-Remaining": {
2817
- description: "The number of requests left for the time window.",
2818
- schema: {
2819
- type: "integer"
2820
- }
2821
- }
2822
- }
2823
- },
2824
- })
2825
- .input(z.object({ name: z.string(), greeting: z.string() }))
2826
- .output(z.object({ output: z.string() }))
2827
- .query(({ input }) => ({
2828
- output: `${input.greeting} ${input.name}`,
2829
- }))
2830
- });
2831
-
2832
- const openApiDocument = generateOpenApiDocument(appRouter, defaultDocOpts);
2833
-
2834
- expect(openApiSchemaValidator.validate(openApiDocument).errors).toEqual([]);
2835
- expect(openApiDocument.paths['/query-example/{name}']!.get!.parameters).toMatchInlineSnapshot(`
2836
- Array [
2837
- Object {
2838
- "description": undefined,
2839
- "example": undefined,
2840
- "in": "path",
2841
- "name": "name",
2842
- "required": true,
2843
- "schema": Object {
2844
- "type": "string",
2845
- },
2846
- },
2847
- Object {
2848
- "description": undefined,
2849
- "example": undefined,
2850
- "in": "query",
2851
- "name": "greeting",
2852
- "required": true,
2853
- "schema": Object {
2854
- "type": "string",
2855
- },
2856
- },
2857
- ]
2858
- `);
2859
- expect(openApiDocument.paths['/query-example/{name}']!.get!.responses[200])
2860
- .toMatchInlineSnapshot(`
2861
- Object {
2862
- "content": Object {
2863
- "application/json": Object {
2864
- "example": undefined,
2865
- "schema": Object {
2866
- "additionalProperties": false,
2867
- "properties": Object {
2868
- "output": Object {
2869
- "type": "string",
2870
- },
2871
- },
2872
- "required": Array [
2873
- "output",
2874
- ],
2875
- "type": "object",
2876
- },
2877
- },
2878
- },
2879
- "description": "Successful response",
2880
- "headers": Object {
2881
- "X-RateLimit-Limit": Object {
2882
- "description": "Request limit per hour.",
2883
- "schema": Object {
2884
- "type": "integer",
2885
- },
2886
- },
2887
- "X-RateLimit-Remaining": Object {
2888
- "description": "The number of requests left for the time window.",
2889
- "schema": Object {
2890
- "type": "integer",
2891
- },
2892
- },
2893
- },
2894
- }
2895
- `);
2896
- });
2897
- });