@sapporta/rest-core 3.52.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.
@@ -0,0 +1,935 @@
1
+ import { z } from 'zod';
2
+ import {
3
+ UnknownOrUndefinedObjectValuesToOptionalKeys,
4
+ initContract,
5
+ } from './dsl';
6
+ import { Equal, Expect } from './test-helpers';
7
+ import {
8
+ ClientInferRequest,
9
+ ServerInferRequest,
10
+ ClientInferResponseBody,
11
+ ServerInferResponseBody,
12
+ ClientInferResponses,
13
+ ServerInferResponses,
14
+ InferResponseDefinedStatusCodes,
15
+ InferResponseUndefinedStatusCodes,
16
+ } from './infer-types';
17
+ import {
18
+ ErrorHttpStatusCode,
19
+ HTTPStatusCode,
20
+ SuccessfulHttpStatusCode,
21
+ } from './status-codes';
22
+ import { FetchOptions, OverridableClientArgs, initClient } from './client';
23
+ import { Prettify } from './type-utils';
24
+ import * as v from 'valibot';
25
+
26
+ const c = initContract();
27
+
28
+ const contract = c.router(
29
+ {
30
+ getPost: {
31
+ method: 'GET',
32
+ path: '/posts/:id',
33
+ pathParams: z.object({
34
+ id: z.string().transform((id) => Number(id)),
35
+ }),
36
+ query: z.object({
37
+ includeComments: z.boolean().default(false),
38
+ }),
39
+ responses: {
40
+ 200: z.object({
41
+ id: z.number(),
42
+ title: z.string().default('Untitled'),
43
+ content: z.string(),
44
+ }),
45
+ 404: z.object({
46
+ message: z.string(),
47
+ }),
48
+ },
49
+ },
50
+ createPost: {
51
+ method: 'POST',
52
+ path: '/posts',
53
+ body: z.object({
54
+ title: z.string(),
55
+ content: z.string(),
56
+ }),
57
+ responses: {
58
+ 201: z.object({
59
+ id: z.number(),
60
+ title: z.string(),
61
+ content: z.string(),
62
+ }),
63
+ },
64
+ },
65
+ uploadImage: {
66
+ method: 'POST',
67
+ path: '/images',
68
+ contentType: 'multipart/form-data',
69
+ body: c.type<{ image: File; images: File[] }>(),
70
+ responses: {
71
+ 201: c.otherResponse({
72
+ contentType: 'text/plain',
73
+ body: c.type<'Image uploaded successfully'>(),
74
+ }),
75
+ 500: c.otherResponse({
76
+ contentType: 'text/plain',
77
+ body: z.literal('Image upload failed'),
78
+ }),
79
+ },
80
+ },
81
+ nested: {
82
+ getComments: {
83
+ method: 'GET',
84
+ path: '/posts/:id/comments',
85
+ pathParams: z.object({
86
+ id: z.string().transform((id) => Number(id)),
87
+ }),
88
+ headers: {
89
+ 'pagination-page': z.string().transform(Number),
90
+ },
91
+ responses: {
92
+ 200: z.object({
93
+ comments: z.array(
94
+ z.object({
95
+ id: z.number(),
96
+ content: z.string(),
97
+ }),
98
+ ),
99
+ }),
100
+ 404: c.type<null>(),
101
+ },
102
+ },
103
+ },
104
+ },
105
+ {
106
+ baseHeaders: {
107
+ Authorization: z.string(),
108
+ age: z.coerce.number().optional(),
109
+ },
110
+ },
111
+ );
112
+
113
+ const contractStrict = c.router(contract, {
114
+ strictStatusCodes: true,
115
+ });
116
+
117
+ const headerlessContract = c.router({
118
+ getPost: {
119
+ method: 'GET',
120
+ path: '/posts/:id',
121
+ pathParams: z.object({
122
+ id: z.string().transform((id) => Number(id)),
123
+ }),
124
+ query: z.object({
125
+ includeComments: z.boolean().default(false),
126
+ }),
127
+ responses: {
128
+ 200: z.object({
129
+ id: z.number(),
130
+ title: z.string().default('Untitled'),
131
+ content: z.string(),
132
+ }),
133
+ 404: z.object({
134
+ message: z.string(),
135
+ }),
136
+ },
137
+ },
138
+ });
139
+
140
+ it('type inference helpers', () => {
141
+ /**
142
+ * @name ServerInferResponsesWithUnknownStatusCodes
143
+ * Expect ServerInferResponses to include all defined status codes plus unknown status codes
144
+ * for endpoints that don't have strict status codes enabled
145
+ */
146
+ type ServerInferResponsesWithUnknownStatusCodes = ServerInferResponses<
147
+ typeof contract
148
+ >;
149
+ type TestServerInferResponsesWithUnknownStatusCodes = Expect<
150
+ Equal<
151
+ ServerInferResponsesWithUnknownStatusCodes,
152
+ {
153
+ getPost:
154
+ | {
155
+ status: 200;
156
+ body: { title?: string | undefined; id: number; content: string };
157
+ }
158
+ | { status: 404; body: { message: string } }
159
+ | { status: Exclude<HTTPStatusCode, 200 | 404>; body: unknown };
160
+ createPost:
161
+ | {
162
+ status: 201;
163
+ body: { id: number; title: string; content: string };
164
+ }
165
+ | { status: Exclude<HTTPStatusCode, 201>; body: unknown };
166
+ uploadImage:
167
+ | {
168
+ status: 201;
169
+ body: 'Image uploaded successfully';
170
+ }
171
+ | {
172
+ status: 500;
173
+ body: 'Image upload failed';
174
+ }
175
+ | { status: Exclude<HTTPStatusCode, 201 | 500>; body: unknown };
176
+ nested: {
177
+ getComments:
178
+ | {
179
+ status: 200;
180
+ body: { comments: { id: number; content: string }[] };
181
+ }
182
+ | { status: 404; body: null }
183
+ | { status: Exclude<HTTPStatusCode, 200 | 404>; body: unknown };
184
+ };
185
+ }
186
+ >
187
+ >;
188
+
189
+ /**
190
+ * @name ServerInferResponsesWithStrictStatusCodes
191
+ * Expect ServerInferResponses to only include explicitly defined status codes
192
+ * when strict status codes are enabled, excluding unknown status codes
193
+ */
194
+ type ServerInferResponsesWithStrictStatusCodes = ServerInferResponses<
195
+ typeof contractStrict
196
+ >;
197
+ type TestServerInferResponsesWithStrictStatusCodes = Expect<
198
+ Equal<
199
+ ServerInferResponsesWithStrictStatusCodes,
200
+ {
201
+ getPost:
202
+ | {
203
+ status: 200;
204
+ body: { title?: string | undefined; id: number; content: string };
205
+ }
206
+ | { status: 404; body: { message: string } };
207
+ createPost: {
208
+ status: 201;
209
+ body: { id: number; title: string; content: string };
210
+ };
211
+ uploadImage:
212
+ | {
213
+ status: 201;
214
+ body: 'Image uploaded successfully';
215
+ }
216
+ | {
217
+ status: 500;
218
+ body: 'Image upload failed';
219
+ };
220
+ nested: {
221
+ getComments:
222
+ | {
223
+ status: 200;
224
+ body: { comments: { id: number; content: string }[] };
225
+ }
226
+ | { status: 404; body: null };
227
+ };
228
+ }
229
+ >
230
+ >;
231
+
232
+ /**
233
+ * @name ServerInferResponsesIgnoreStrictMode
234
+ * Expect ServerInferResponses to include unknown status codes even when strict mode is enabled
235
+ * but explicitly ignored via the 'ignore' parameter
236
+ */
237
+ type ServerInferResponsesIgnoreStrictMode = ServerInferResponses<
238
+ typeof contractStrict,
239
+ HTTPStatusCode,
240
+ 'ignore'
241
+ >;
242
+ type TestServerInferResponsesIgnoreStrictMode = Expect<
243
+ Equal<
244
+ ServerInferResponsesIgnoreStrictMode,
245
+ {
246
+ getPost:
247
+ | {
248
+ status: 200;
249
+ body: { title?: string | undefined; id: number; content: string };
250
+ }
251
+ | { status: 404; body: { message: string } }
252
+ | { status: Exclude<HTTPStatusCode, 200 | 404>; body: unknown };
253
+ createPost:
254
+ | {
255
+ status: 201;
256
+ body: { id: number; title: string; content: string };
257
+ }
258
+ | { status: Exclude<HTTPStatusCode, 201>; body: unknown };
259
+ uploadImage:
260
+ | {
261
+ status: 201;
262
+ body: 'Image uploaded successfully';
263
+ }
264
+ | {
265
+ status: 500;
266
+ body: 'Image upload failed';
267
+ }
268
+ | { status: Exclude<HTTPStatusCode, 201 | 500>; body: unknown };
269
+ nested: {
270
+ getComments:
271
+ | {
272
+ status: 200;
273
+ body: { comments: { id: number; content: string }[] };
274
+ }
275
+ | { status: 404; body: null }
276
+ | { status: Exclude<HTTPStatusCode, 200 | 404>; body: unknown };
277
+ };
278
+ }
279
+ >
280
+ >;
281
+
282
+ /**
283
+ * @name ServerInferResponsesSpecificStatusCode
284
+ * Expect ServerInferResponses to filter responses to only include the specified status code (200),
285
+ * returning unknown body for endpoints that don't define that status code
286
+ */
287
+ type ServerInferResponsesSpecificStatusCode = ServerInferResponses<
288
+ typeof contract,
289
+ 200
290
+ >;
291
+ type TestServerInferResponsesSpecificStatusCode = Expect<
292
+ Equal<
293
+ ServerInferResponsesSpecificStatusCode,
294
+ {
295
+ getPost: {
296
+ status: 200;
297
+ body: { title?: string | undefined; id: number; content: string };
298
+ };
299
+ createPost: {
300
+ status: 200;
301
+ body: unknown;
302
+ };
303
+ uploadImage: {
304
+ status: 200;
305
+ body: unknown;
306
+ };
307
+ nested: {
308
+ getComments: {
309
+ status: 200;
310
+ body: { comments: { id: number; content: string }[] };
311
+ };
312
+ };
313
+ }
314
+ >
315
+ >;
316
+
317
+ /**
318
+ * @name ServerInferResponsesUndefinedStatusCode
319
+ * Expect ServerInferResponses to return unknown body for all endpoints
320
+ * when filtering by a status code (401) that is not defined in any endpoint
321
+ */
322
+ type ServerInferResponsesUndefinedStatusCode = ServerInferResponses<
323
+ typeof contract,
324
+ 401
325
+ >;
326
+ type TestServerInferResponsesUndefinedStatusCode = Expect<
327
+ Equal<
328
+ ServerInferResponsesUndefinedStatusCode,
329
+ {
330
+ getPost: {
331
+ status: 401;
332
+ body: unknown;
333
+ };
334
+ createPost: {
335
+ status: 401;
336
+ body: unknown;
337
+ };
338
+ uploadImage: {
339
+ status: 401;
340
+ body: unknown;
341
+ };
342
+ nested: {
343
+ getComments: {
344
+ status: 401;
345
+ body: unknown;
346
+ };
347
+ };
348
+ }
349
+ >
350
+ >;
351
+
352
+ /**
353
+ * @name ServerInferResponsesErrorStatusCodes
354
+ * Expect ServerInferResponses to filter responses to only include error status codes,
355
+ * showing defined error responses and unknown for undefined error codes
356
+ */
357
+ type ServerInferResponsesErrorStatusCodes = ServerInferResponses<
358
+ typeof contractStrict,
359
+ ErrorHttpStatusCode,
360
+ 'ignore'
361
+ >;
362
+ type TestServerInferResponsesErrorStatusCodes = Expect<
363
+ Equal<
364
+ ServerInferResponsesErrorStatusCodes,
365
+ {
366
+ getPost:
367
+ | { status: 404; body: { message: string } }
368
+ | { status: Exclude<ErrorHttpStatusCode, 404>; body: unknown };
369
+ createPost: { status: ErrorHttpStatusCode; body: unknown };
370
+ uploadImage:
371
+ | {
372
+ status: 500;
373
+ body: 'Image upload failed';
374
+ }
375
+ | { status: Exclude<ErrorHttpStatusCode, 500>; body: unknown };
376
+ nested: {
377
+ getComments:
378
+ | { status: 404; body: null }
379
+ | { status: Exclude<ErrorHttpStatusCode, 404>; body: unknown };
380
+ };
381
+ }
382
+ >
383
+ >;
384
+
385
+ /**
386
+ * @name ServerInferResponsesSuccessStatusCodesForced
387
+ * Expect ServerInferResponses to only include successful status codes when forced,
388
+ * filtering out error responses and unknown status codes
389
+ */
390
+ type ServerInferResponsesSuccessStatusCodesForced = ServerInferResponses<
391
+ typeof contract,
392
+ SuccessfulHttpStatusCode,
393
+ 'force'
394
+ >;
395
+ type TestServerInferResponsesSuccessStatusCodesForced = Expect<
396
+ Equal<
397
+ ServerInferResponsesSuccessStatusCodesForced,
398
+ {
399
+ getPost: {
400
+ status: 200;
401
+ body: { title?: string | undefined; id: number; content: string };
402
+ };
403
+ createPost: {
404
+ status: 201;
405
+ body: { id: number; title: string; content: string };
406
+ };
407
+ uploadImage: {
408
+ status: 201;
409
+ body: 'Image uploaded successfully';
410
+ };
411
+ nested: {
412
+ getComments: {
413
+ status: 200;
414
+ body: { comments: { id: number; content: string }[] };
415
+ };
416
+ };
417
+ }
418
+ >
419
+ >;
420
+
421
+ /**
422
+ * @name ClientInferResponsesWithHeaders
423
+ * Expect ClientInferResponses to include Headers object in all response types,
424
+ * distinguishing client-side responses from server-side responses
425
+ */
426
+ type ClientInferResponsesWithHeaders = ClientInferResponses<typeof contract>;
427
+ type TestClientInferResponsesWithHeaders = Expect<
428
+ Equal<
429
+ ClientInferResponsesWithHeaders,
430
+ {
431
+ getPost:
432
+ | {
433
+ status: 200;
434
+ body: { title: string; id: number; content: string };
435
+ headers: Headers;
436
+ }
437
+ | {
438
+ status: 404;
439
+ body: { message: string };
440
+ headers: Headers;
441
+ }
442
+ | {
443
+ status: Exclude<HTTPStatusCode, 200 | 404>;
444
+ body: unknown;
445
+ headers: Headers;
446
+ };
447
+ createPost:
448
+ | {
449
+ status: 201;
450
+ body: { id: number; title: string; content: string };
451
+ headers: Headers;
452
+ }
453
+ | {
454
+ status: Exclude<HTTPStatusCode, 201>;
455
+ body: unknown;
456
+ headers: Headers;
457
+ };
458
+ uploadImage:
459
+ | {
460
+ status: 201;
461
+ body: 'Image uploaded successfully';
462
+ headers: Headers;
463
+ }
464
+ | {
465
+ status: 500;
466
+ body: 'Image upload failed';
467
+ headers: Headers;
468
+ }
469
+ | {
470
+ status: Exclude<HTTPStatusCode, 201 | 500>;
471
+ body: unknown;
472
+ headers: Headers;
473
+ };
474
+ nested: {
475
+ getComments:
476
+ | {
477
+ status: 200;
478
+ body: { comments: { id: number; content: string }[] };
479
+ headers: Headers;
480
+ }
481
+ | {
482
+ status: 404;
483
+ body: null;
484
+ headers: Headers;
485
+ }
486
+ | {
487
+ status: Exclude<HTTPStatusCode, 200 | 404>;
488
+ body: unknown;
489
+ headers: Headers;
490
+ };
491
+ };
492
+ }
493
+ >
494
+ >;
495
+
496
+ /**
497
+ * @name ServerInferResponseBodySpecificEndpoint
498
+ * Expect ServerInferResponseBody to extract the response body type for a specific endpoint and status code,
499
+ * including optional properties from Zod defaults
500
+ */
501
+ type ServerInferResponseBodySpecificEndpoint = ServerInferResponseBody<
502
+ typeof contract.getPost,
503
+ 200
504
+ >;
505
+ type TestServerInferResponseBodySpecificEndpoint = Expect<
506
+ Equal<
507
+ ServerInferResponseBodySpecificEndpoint,
508
+ { title?: string | undefined; id: number; content: string }
509
+ >
510
+ >;
511
+
512
+ /**
513
+ * @name ClientInferResponseBodySpecificEndpoint
514
+ * Expect ClientInferResponseBody to extract the response body type for a specific endpoint and status code,
515
+ * with required properties (no optional from Zod defaults on client side)
516
+ */
517
+ type ClientInferResponseBodySpecificEndpoint = ClientInferResponseBody<
518
+ typeof contract.getPost,
519
+ 200
520
+ >;
521
+ type TestClientInferResponseBodySpecificEndpoint = Expect<
522
+ Equal<
523
+ ClientInferResponseBodySpecificEndpoint,
524
+ { title: string; id: number; content: string }
525
+ >
526
+ >;
527
+
528
+ const commonErrors = c.responses({
529
+ 400: c.type<{ message: string }>(),
530
+ });
531
+
532
+ const contractWithCommonErrors = c.router({
533
+ get: {
534
+ method: 'GET',
535
+ path: '/',
536
+ responses: {
537
+ ...commonErrors,
538
+ },
539
+ },
540
+ });
541
+
542
+ /**
543
+ * @name ClientInferResponseBodyWithCommonResponses
544
+ * Expect ClientInferResponseBody to work with common response definitions,
545
+ * extracting the correct body type from shared response schemas
546
+ */
547
+ type ClientInferResponseBodyWithCommonResponses = ClientInferResponseBody<
548
+ typeof contractWithCommonErrors.get,
549
+ 400
550
+ >;
551
+ type TestClientInferResponseBodyWithCommonResponses = Expect<
552
+ Equal<ClientInferResponseBodyWithCommonResponses, { message: string }>
553
+ >;
554
+
555
+ /**
556
+ * @name ServerInferRequestWithTransforms
557
+ * Expect ServerInferRequest to include transformed types for path params and headers,
558
+ * showing how Zod transforms affect the inferred server-side types
559
+ */
560
+ type ServerInferRequestWithTransforms = ServerInferRequest<typeof contract>;
561
+ type TestServerInferRequestWithTransforms = Expect<
562
+ Equal<
563
+ ServerInferRequestWithTransforms,
564
+ {
565
+ getPost: {
566
+ query: { includeComments: boolean };
567
+ params: { id: number };
568
+ headers: {
569
+ Authorization: string;
570
+ age: number | undefined;
571
+ };
572
+ };
573
+ createPost: {
574
+ body: { title: string; content: string };
575
+ headers: {
576
+ Authorization: string;
577
+ age: number | undefined;
578
+ };
579
+ };
580
+ uploadImage: {
581
+ body: {};
582
+ headers: {
583
+ Authorization: string;
584
+ age: number | undefined;
585
+ };
586
+ };
587
+ nested: {
588
+ getComments: {
589
+ params: { id: number };
590
+ headers: {
591
+ 'pagination-page': number;
592
+ Authorization: string;
593
+ age: number | undefined;
594
+ };
595
+ };
596
+ };
597
+ }
598
+ >
599
+ >;
600
+
601
+ /**
602
+ * @name ServerInferRequestWithOverriddenHeaders
603
+ * Expect ServerInferRequest to merge contract headers with overridden server headers,
604
+ * allowing server-specific header types to be injected
605
+ */
606
+ type ServerInferRequestWithOverriddenHeaders = ServerInferRequest<
607
+ typeof contract,
608
+ {
609
+ authorization: string | undefined;
610
+ age: string | undefined;
611
+ 'content-type': string | undefined;
612
+ }
613
+ >;
614
+ type TestServerInferRequestWithOverriddenHeaders = Expect<
615
+ Equal<
616
+ ServerInferRequestWithOverriddenHeaders,
617
+ {
618
+ getPost: {
619
+ query: { includeComments: boolean };
620
+ params: { id: number };
621
+ headers: {
622
+ Authorization: string;
623
+ age: number | undefined;
624
+ 'content-type': string | undefined;
625
+ };
626
+ };
627
+ createPost: {
628
+ body: { title: string; content: string };
629
+ headers: {
630
+ Authorization: string;
631
+ age: number | undefined;
632
+ 'content-type': string | undefined;
633
+ };
634
+ };
635
+ uploadImage: {
636
+ body: {};
637
+ headers: {
638
+ Authorization: string;
639
+ age: number | undefined;
640
+ 'content-type': string | undefined;
641
+ };
642
+ };
643
+ nested: {
644
+ getComments: {
645
+ params: { id: number };
646
+ headers: {
647
+ 'pagination-page': number;
648
+ Authorization: string;
649
+ age: number | undefined;
650
+ 'content-type': string | undefined;
651
+ };
652
+ };
653
+ };
654
+ }
655
+ >
656
+ >;
657
+
658
+ /**
659
+ * @name ClientInferRequestWithClientOptions
660
+ * Expect ClientInferRequest to include client-specific options like fetchOptions and extraHeaders,
661
+ * showing the difference between client and server request inference
662
+ */
663
+ type ClientInferRequestWithClientOptions = ClientInferRequest<
664
+ typeof contract
665
+ >;
666
+ type TestClientInferRequestWithClientOptions = Expect<
667
+ Equal<
668
+ ClientInferRequestWithClientOptions,
669
+ {
670
+ getPost: {
671
+ query: { includeComments?: boolean | undefined };
672
+ params: { id: string };
673
+ headers: {
674
+ age?: unknown;
675
+ authorization: string;
676
+ };
677
+ extraHeaders?: {
678
+ authorization?: undefined;
679
+ age?: undefined;
680
+ } & Record<string, string>;
681
+ fetchOptions?: FetchOptions;
682
+ overrideClientOptions?: Partial<OverridableClientArgs>;
683
+ cache?: FetchOptions['cache'];
684
+ };
685
+ createPost: {
686
+ body: { title: string; content: string };
687
+ headers: {
688
+ age?: unknown;
689
+ authorization: string;
690
+ };
691
+ extraHeaders?: {
692
+ authorization?: undefined;
693
+ age?: undefined;
694
+ } & Record<string, string>;
695
+ fetchOptions?: FetchOptions;
696
+ overrideClientOptions?: Partial<OverridableClientArgs>;
697
+ cache?: FetchOptions['cache'];
698
+ };
699
+ uploadImage: {
700
+ body:
701
+ | {
702
+ image: File;
703
+ images: File[];
704
+ }
705
+ | FormData;
706
+ headers: {
707
+ age?: unknown;
708
+ authorization: string;
709
+ };
710
+ extraHeaders?: {
711
+ authorization?: undefined;
712
+ age?: undefined;
713
+ } & Record<string, string>;
714
+ fetchOptions?: FetchOptions;
715
+ overrideClientOptions?: Partial<OverridableClientArgs>;
716
+ cache?: FetchOptions['cache'];
717
+ };
718
+ nested: {
719
+ getComments: {
720
+ params: { id: string };
721
+ headers: {
722
+ authorization: string;
723
+ 'pagination-page': string;
724
+ age?: unknown;
725
+ };
726
+ extraHeaders?: {
727
+ authorization?: undefined;
728
+ 'pagination-page'?: undefined;
729
+ age?: undefined;
730
+ } & Record<string, string>;
731
+ fetchOptions?: FetchOptions;
732
+ overrideClientOptions?: Partial<OverridableClientArgs>;
733
+ cache?: FetchOptions['cache'];
734
+ };
735
+ };
736
+ }
737
+ >
738
+ >;
739
+
740
+ /**
741
+ * @name ClientInferRequestWithoutBaseHeaders
742
+ * Expect ClientInferRequest to only include extraHeaders when no base headers are defined,
743
+ * demonstrating how base headers affect the client request interface
744
+ */
745
+ type ClientInferRequestWithoutBaseHeaders = Omit<
746
+ ClientInferRequest<typeof headerlessContract>['getPost'],
747
+ 'next'
748
+ >;
749
+ type TestClientInferRequestWithoutBaseHeaders = Expect<
750
+ Equal<
751
+ ClientInferRequestWithoutBaseHeaders,
752
+ {
753
+ query: { includeComments?: boolean | undefined };
754
+ params: { id: string };
755
+ extraHeaders?: Record<string, string>;
756
+ fetchOptions?: FetchOptions;
757
+ overrideClientOptions?: Partial<OverridableClientArgs>;
758
+ cache?: FetchOptions['cache'];
759
+ }
760
+ >
761
+ >;
762
+
763
+ /**
764
+ * @name InferResponseDefinedStatusCodesBasic
765
+ * Expect InferResponseDefinedStatusCodes to extract all explicitly defined status codes
766
+ * from an endpoint's response definitions
767
+ */
768
+ type InferResponseDefinedStatusCodesBasic = InferResponseDefinedStatusCodes<
769
+ typeof contract.getPost
770
+ >;
771
+ type TestInferResponseDefinedStatusCodesBasic = Expect<
772
+ Equal<InferResponseDefinedStatusCodesBasic, 200 | 404>
773
+ >;
774
+
775
+ /**
776
+ * @name InferResponseDefinedStatusCodesFiltered
777
+ * Expect InferResponseDefinedStatusCodes to filter defined status codes by a specific type,
778
+ * only returning successful status codes that are explicitly defined
779
+ */
780
+ type InferResponseDefinedStatusCodesFiltered =
781
+ InferResponseDefinedStatusCodes<
782
+ typeof contract.getPost,
783
+ SuccessfulHttpStatusCode
784
+ >;
785
+ type TestInferResponseDefinedStatusCodesFiltered = Expect<
786
+ Equal<InferResponseDefinedStatusCodesFiltered, 200>
787
+ >;
788
+
789
+ /**
790
+ * @name InferResponseDefinedStatusCodesErrorsOnly
791
+ * Expect InferResponseDefinedStatusCodes to filter defined status codes by error type,
792
+ * only returning error status codes that are explicitly defined
793
+ */
794
+ type InferResponseDefinedStatusCodesErrorsOnly =
795
+ InferResponseDefinedStatusCodes<
796
+ typeof contract.getPost,
797
+ ErrorHttpStatusCode
798
+ >;
799
+ type TestInferResponseDefinedStatusCodesErrorsOnly = Expect<
800
+ Equal<InferResponseDefinedStatusCodesErrorsOnly, 404>
801
+ >;
802
+
803
+ /**
804
+ * @name InferResponseUndefinedStatusCodesBasic
805
+ * Expect InferResponseUndefinedStatusCodes to extract all status codes that are NOT explicitly defined,
806
+ * representing the complement of defined status codes
807
+ */
808
+ type InferResponseUndefinedStatusCodesBasic =
809
+ InferResponseUndefinedStatusCodes<typeof contract.getPost>;
810
+ type TestInferResponseUndefinedStatusCodesBasic = Expect<
811
+ Equal<
812
+ InferResponseUndefinedStatusCodesBasic,
813
+ Exclude<HTTPStatusCode, 200 | 404>
814
+ >
815
+ >;
816
+
817
+ /**
818
+ * @name InferResponseUndefinedStatusCodesSuccessFiltered
819
+ * Expect InferResponseUndefinedStatusCodes to extract undefined successful status codes,
820
+ * showing which successful codes are not explicitly defined in the endpoint
821
+ */
822
+ type InferResponseUndefinedStatusCodesSuccessFiltered =
823
+ InferResponseUndefinedStatusCodes<
824
+ typeof contract.getPost,
825
+ SuccessfulHttpStatusCode
826
+ >;
827
+ type TestInferResponseUndefinedStatusCodesSuccessFiltered = Expect<
828
+ Equal<
829
+ InferResponseUndefinedStatusCodesSuccessFiltered,
830
+ Exclude<SuccessfulHttpStatusCode, 200>
831
+ >
832
+ >;
833
+
834
+ /**
835
+ * @name InferResponseUndefinedStatusCodesErrorFiltered
836
+ * Expect InferResponseUndefinedStatusCodes to extract undefined error status codes,
837
+ * showing which error codes are not explicitly defined in the endpoint
838
+ */
839
+ type InferResponseUndefinedStatusCodesErrorFiltered =
840
+ InferResponseUndefinedStatusCodes<
841
+ typeof contract.getPost,
842
+ ErrorHttpStatusCode
843
+ >;
844
+ type TestInferResponseUndefinedStatusCodesErrorFiltered = Expect<
845
+ Equal<
846
+ InferResponseUndefinedStatusCodesErrorFiltered,
847
+ Exclude<ErrorHttpStatusCode, 404>
848
+ >
849
+ >;
850
+ });
851
+
852
+ describe('ClientInferRequest', () => {
853
+ it('standard schema - optional headers', () => {
854
+ const contract = c.router({
855
+ getPost: {
856
+ method: 'GET',
857
+ path: '/post',
858
+ headers: {
859
+ 'x-foo': v.optional(v.string()),
860
+ },
861
+ responses: {
862
+ 200: c.noBody(),
863
+ },
864
+ },
865
+ });
866
+
867
+ const client = initClient(contract, { baseUrl: '' });
868
+ const testUsage = () => client.getPost({ headers: { 'x-foo': 'string' } });
869
+
870
+ type Actual = ClientInferRequest<typeof contract.getPost>['headers'];
871
+ type TestResult = Expect<
872
+ Equal<
873
+ Actual,
874
+ {
875
+ 'x-foo'?: string | undefined;
876
+ }
877
+ >
878
+ >;
879
+ });
880
+
881
+ it('headers zod coerce', () => {
882
+ const contract = c.router({
883
+ getPost: {
884
+ method: 'GET',
885
+ path: '/post',
886
+ headers: {
887
+ 'x-foo': z.coerce.number().optional(),
888
+ },
889
+ responses: {
890
+ 200: c.noBody(),
891
+ },
892
+ },
893
+ });
894
+
895
+ const client = initClient(contract, { baseUrl: '' });
896
+ const testUsage = () => client.getPost({ headers: { 'x-foo': 1 } });
897
+
898
+ type Actual = ClientInferRequest<typeof contract.getPost>['headers'];
899
+ type TestResult = Expect<Equal<Actual, { 'x-foo'?: unknown }>>;
900
+ });
901
+ });
902
+
903
+ describe('UnknownOrUndefinedObjectValuesToOptionalKeys', () => {
904
+ it('should make undefined key optional', () => {
905
+ type Actual = Prettify<
906
+ UnknownOrUndefinedObjectValuesToOptionalKeys<{
907
+ foo: string | undefined;
908
+ }>
909
+ >;
910
+ type Assert = Expect<Equal<Actual, { foo?: string | undefined }>>;
911
+ });
912
+
913
+ it('should make unknown key optional', () => {
914
+ type Actual = Prettify<
915
+ UnknownOrUndefinedObjectValuesToOptionalKeys<{
916
+ foo: unknown;
917
+ }>
918
+ >;
919
+ type Assert = Expect<Equal<Actual, { foo?: unknown }>>;
920
+ });
921
+
922
+ it('should not affect a non-empty object', () => {
923
+ type Actual = Prettify<
924
+ UnknownOrUndefinedObjectValuesToOptionalKeys<{
925
+ foo: string;
926
+ }>
927
+ >;
928
+ type Assert = Expect<Equal<Actual, { foo: string }>>;
929
+ });
930
+
931
+ it('should not affect an empty object', () => {
932
+ type Actual = Prettify<UnknownOrUndefinedObjectValuesToOptionalKeys<{}>>;
933
+ type Assert = Expect<Equal<Actual, {}>>;
934
+ });
935
+ });