@dokploy/trpc-openapi 0.0.1

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 (126) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +369 -0
  3. package/assets/trpc-openapi-graph.png +0 -0
  4. package/assets/trpc-openapi-readme.png +0 -0
  5. package/assets/trpc-openapi.svg +4 -0
  6. package/dist/adapters/express.d.ts +6 -0
  7. package/dist/adapters/express.d.ts.map +1 -0
  8. package/dist/adapters/express.js +12 -0
  9. package/dist/adapters/express.js.map +1 -0
  10. package/dist/adapters/index.d.ts +4 -0
  11. package/dist/adapters/index.d.ts.map +1 -0
  12. package/dist/adapters/index.js +20 -0
  13. package/dist/adapters/index.js.map +1 -0
  14. package/dist/adapters/next.d.ts +6 -0
  15. package/dist/adapters/next.d.ts.map +1 -0
  16. package/dist/adapters/next.js +45 -0
  17. package/dist/adapters/next.js.map +1 -0
  18. package/dist/adapters/node-http/core.d.ts +6 -0
  19. package/dist/adapters/node-http/core.d.ts.map +1 -0
  20. package/dist/adapters/node-http/core.js +140 -0
  21. package/dist/adapters/node-http/core.js.map +1 -0
  22. package/dist/adapters/node-http/errors.d.ts +4 -0
  23. package/dist/adapters/node-http/errors.d.ts.map +1 -0
  24. package/dist/adapters/node-http/errors.js +43 -0
  25. package/dist/adapters/node-http/errors.js.map +1 -0
  26. package/dist/adapters/node-http/input.d.ts +4 -0
  27. package/dist/adapters/node-http/input.d.ts.map +1 -0
  28. package/dist/adapters/node-http/input.js +76 -0
  29. package/dist/adapters/node-http/input.js.map +1 -0
  30. package/dist/adapters/node-http/procedures.d.ts +12 -0
  31. package/dist/adapters/node-http/procedures.d.ts.map +1 -0
  32. package/dist/adapters/node-http/procedures.js +51 -0
  33. package/dist/adapters/node-http/procedures.js.map +1 -0
  34. package/dist/adapters/standalone.d.ts +6 -0
  35. package/dist/adapters/standalone.d.ts.map +1 -0
  36. package/dist/adapters/standalone.js +12 -0
  37. package/dist/adapters/standalone.js.map +1 -0
  38. package/dist/generator/index.d.ts +14 -0
  39. package/dist/generator/index.d.ts.map +1 -0
  40. package/dist/generator/index.js +39 -0
  41. package/dist/generator/index.js.map +1 -0
  42. package/dist/generator/paths.d.ts +4 -0
  43. package/dist/generator/paths.d.ts.map +1 -0
  44. package/dist/generator/paths.js +76 -0
  45. package/dist/generator/paths.js.map +1 -0
  46. package/dist/generator/schema.d.ts +7 -0
  47. package/dist/generator/schema.d.ts.map +1 -0
  48. package/dist/generator/schema.js +195 -0
  49. package/dist/generator/schema.js.map +1 -0
  50. package/dist/index.d.ts +6 -0
  51. package/dist/index.d.ts.map +1 -0
  52. package/dist/index.js +11 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/types.d.ts +53 -0
  55. package/dist/types.d.ts.map +1 -0
  56. package/dist/types.js +3 -0
  57. package/dist/types.js.map +1 -0
  58. package/dist/utils/method.d.ts +3 -0
  59. package/dist/utils/method.d.ts.map +1 -0
  60. package/dist/utils/method.js +11 -0
  61. package/dist/utils/method.js.map +1 -0
  62. package/dist/utils/path.d.ts +4 -0
  63. package/dist/utils/path.d.ts.map +1 -0
  64. package/dist/utils/path.js +17 -0
  65. package/dist/utils/path.js.map +1 -0
  66. package/dist/utils/procedure.d.ts +14 -0
  67. package/dist/utils/procedure.d.ts.map +1 -0
  68. package/dist/utils/procedure.js +39 -0
  69. package/dist/utils/procedure.js.map +1 -0
  70. package/dist/utils/zod.d.ts +19 -0
  71. package/dist/utils/zod.d.ts.map +1 -0
  72. package/dist/utils/zod.js +87 -0
  73. package/dist/utils/zod.js.map +1 -0
  74. package/examples/with-express/README.md +11 -0
  75. package/examples/with-express/package.json +28 -0
  76. package/examples/with-express/src/database.ts +67 -0
  77. package/examples/with-express/src/index.ts +27 -0
  78. package/examples/with-express/src/openapi.ts +13 -0
  79. package/examples/with-express/src/router.ts +424 -0
  80. package/examples/with-express/tsconfig.json +102 -0
  81. package/examples/with-interop/README.md +10 -0
  82. package/examples/with-interop/package.json +13 -0
  83. package/examples/with-interop/src/index.ts +17 -0
  84. package/examples/with-interop/tsconfig.json +103 -0
  85. package/examples/with-nextjs/.eslintrc.json +3 -0
  86. package/examples/with-nextjs/README.md +12 -0
  87. package/examples/with-nextjs/next-env.d.ts +5 -0
  88. package/examples/with-nextjs/next.config.js +6 -0
  89. package/examples/with-nextjs/package.json +33 -0
  90. package/examples/with-nextjs/public/favicon.ico +0 -0
  91. package/examples/with-nextjs/src/pages/_app.tsx +7 -0
  92. package/examples/with-nextjs/src/pages/api/[...trpc].ts +18 -0
  93. package/examples/with-nextjs/src/pages/api/openapi.json.ts +10 -0
  94. package/examples/with-nextjs/src/pages/api/trpc/[...trpc].ts +9 -0
  95. package/examples/with-nextjs/src/pages/index.tsx +12 -0
  96. package/examples/with-nextjs/src/server/database.ts +67 -0
  97. package/examples/with-nextjs/src/server/openapi.ts +13 -0
  98. package/examples/with-nextjs/src/server/router.ts +426 -0
  99. package/examples/with-nextjs/tsconfig.json +24 -0
  100. package/jest.config.ts +12 -0
  101. package/package.json +74 -0
  102. package/pnpm-workspace.yaml +7 -0
  103. package/src/adapters/express.ts +20 -0
  104. package/src/adapters/index.ts +3 -0
  105. package/src/adapters/next.ts +64 -0
  106. package/src/adapters/node-http/core.ts +203 -0
  107. package/src/adapters/node-http/errors.ts +45 -0
  108. package/src/adapters/node-http/input.ts +76 -0
  109. package/src/adapters/node-http/procedures.ts +64 -0
  110. package/src/adapters/standalone.ts +19 -0
  111. package/src/generator/index.ts +51 -0
  112. package/src/generator/paths.ts +127 -0
  113. package/src/generator/schema.ts +238 -0
  114. package/src/index.ts +42 -0
  115. package/src/types.ts +79 -0
  116. package/src/utils/method.ts +8 -0
  117. package/src/utils/path.ts +12 -0
  118. package/src/utils/procedure.ts +45 -0
  119. package/src/utils/zod.ts +115 -0
  120. package/test/adapters/express.test.ts +150 -0
  121. package/test/adapters/next.test.ts +162 -0
  122. package/test/adapters/standalone.test.ts +1335 -0
  123. package/test/generator.test.ts +2897 -0
  124. package/tsconfig.build.json +4 -0
  125. package/tsconfig.eslint.json +5 -0
  126. package/tsconfig.json +19 -0
@@ -0,0 +1,2897 @@
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
+ });