@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.
- package/.babelrc +10 -0
- package/.eslintrc.json +21 -0
- package/CHANGELOG.md +3 -0
- package/LICENCE +21 -0
- package/README.md +19 -0
- package/jest.config.ts +16 -0
- package/package.json +33 -0
- package/project.json +51 -0
- package/src/index.ts +15 -0
- package/src/lib/client.spec.ts +1330 -0
- package/src/lib/client.ts +481 -0
- package/src/lib/dsl.spec.ts +1308 -0
- package/src/lib/dsl.ts +472 -0
- package/src/lib/fetch.spec.ts +102 -0
- package/src/lib/infer-types.spec.ts +935 -0
- package/src/lib/infer-types.ts +282 -0
- package/src/lib/paths.spec.ts +138 -0
- package/src/lib/paths.ts +61 -0
- package/src/lib/query.spec.ts +329 -0
- package/src/lib/query.ts +114 -0
- package/src/lib/response-error.spec.ts +67 -0
- package/src/lib/response-error.ts +61 -0
- package/src/lib/response-validation-error.ts +24 -0
- package/src/lib/server.spec.ts +163 -0
- package/src/lib/server.ts +83 -0
- package/src/lib/standard-schema-utils.spec.ts +218 -0
- package/src/lib/standard-schema-utils.ts +280 -0
- package/src/lib/standard-schema.ts +71 -0
- package/src/lib/status-codes.ts +75 -0
- package/src/lib/test-helpers.ts +7 -0
- package/src/lib/type-guards.spec.ts +355 -0
- package/src/lib/type-guards.ts +99 -0
- package/src/lib/type-utils.spec.ts +59 -0
- package/src/lib/type-utils.ts +234 -0
- package/src/lib/unknown-status-error.ts +15 -0
- package/src/lib/validation-error.ts +36 -0
- package/tsconfig.json +22 -0
- package/tsconfig.lib.json +10 -0
- package/tsconfig.spec.json +9 -0
- package/typedoc.json +5 -0
package/src/lib/dsl.ts
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { StandardSchemaV1 } from './standard-schema';
|
|
2
|
+
import {
|
|
3
|
+
LowercaseKeys,
|
|
4
|
+
Merge,
|
|
5
|
+
Opaque,
|
|
6
|
+
Prettify,
|
|
7
|
+
SchemaInputOrType,
|
|
8
|
+
SchemaOutputOrType,
|
|
9
|
+
WithoutUnknown,
|
|
10
|
+
} from './type-utils';
|
|
11
|
+
import { mergeHeaderSchemasForRoute } from './standard-schema-utils';
|
|
12
|
+
|
|
13
|
+
type MixedSchemaError<A, B> = Opaque<{ a: A; b: B }, 'MixedSchemaError'>;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The path with colon-prefixed parameters
|
|
17
|
+
* e.g. "/posts/:id".
|
|
18
|
+
*/
|
|
19
|
+
type Path = string;
|
|
20
|
+
|
|
21
|
+
declare const NullSymbol: unique symbol;
|
|
22
|
+
export const ContractNoBody = Symbol('ContractNoBody');
|
|
23
|
+
|
|
24
|
+
export type ContractPlainType<T> = Opaque<T, 'ContractPlainType'>;
|
|
25
|
+
export type ContractNullType = Opaque<typeof NullSymbol, 'ContractNullType'>;
|
|
26
|
+
export type ContractNoBodyType = typeof ContractNoBody;
|
|
27
|
+
export type ContractAnyType =
|
|
28
|
+
| StandardSchemaV1<any>
|
|
29
|
+
| ContractPlainType<unknown>
|
|
30
|
+
| ContractNullType
|
|
31
|
+
| null;
|
|
32
|
+
|
|
33
|
+
export type ContractOtherResponse<T extends ContractAnyType> = Opaque<
|
|
34
|
+
{ contentType: string; body: T },
|
|
35
|
+
'ContractOtherResponse'
|
|
36
|
+
>;
|
|
37
|
+
|
|
38
|
+
export type AppRouteResponse =
|
|
39
|
+
| ContractAnyType
|
|
40
|
+
| ContractNoBodyType
|
|
41
|
+
| ContractOtherResponse<ContractAnyType>;
|
|
42
|
+
|
|
43
|
+
type AppRouteCommon = {
|
|
44
|
+
path: Path;
|
|
45
|
+
pathParams?: ContractAnyType;
|
|
46
|
+
query?: ContractAnyType;
|
|
47
|
+
headers?: Record<string, ContractAnyType>;
|
|
48
|
+
summary?: string;
|
|
49
|
+
description?: string;
|
|
50
|
+
deprecated?: boolean;
|
|
51
|
+
responses: Record<number, AppRouteResponse>;
|
|
52
|
+
strictStatusCodes?: boolean;
|
|
53
|
+
metadata?: unknown;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @deprecated Use `validateResponse` on the client options
|
|
57
|
+
*/
|
|
58
|
+
validateResponseOnClient?: boolean;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* A query endpoint. In REST terms, one using GET.
|
|
63
|
+
*/
|
|
64
|
+
export type AppRouteQuery = AppRouteCommon & {
|
|
65
|
+
method: 'GET';
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* A mutation endpoint. In REST terms, one using POST, PUT,
|
|
70
|
+
* PATCH, or DELETE.
|
|
71
|
+
*/
|
|
72
|
+
export type AppRouteMutation = AppRouteCommon & {
|
|
73
|
+
method: 'POST' | 'DELETE' | 'PUT' | 'PATCH';
|
|
74
|
+
contentType?:
|
|
75
|
+
| 'application/json'
|
|
76
|
+
| 'multipart/form-data'
|
|
77
|
+
| 'application/x-www-form-urlencoded';
|
|
78
|
+
body: ContractAnyType | ContractNoBodyType;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A mutation endpoint. In REST terms, one using POST, PUT,
|
|
83
|
+
* PATCH, or DELETE.
|
|
84
|
+
*/
|
|
85
|
+
export type AppRouteDeleteNoBody = AppRouteCommon & {
|
|
86
|
+
method: 'DELETE';
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type ValidatedHeaders<
|
|
90
|
+
T extends AppRoute,
|
|
91
|
+
TOptions extends RouterOptions,
|
|
92
|
+
TOptionsApplied = ApplyOptions<T, TOptions>,
|
|
93
|
+
> = 'headers' extends keyof TOptionsApplied
|
|
94
|
+
? TOptionsApplied['headers'] extends MixedSchemaError<infer A, infer B>
|
|
95
|
+
? {
|
|
96
|
+
_error: 'Cannot mix plain object types with StandardSchemaV1 objects for headers';
|
|
97
|
+
a: A;
|
|
98
|
+
b: B;
|
|
99
|
+
}
|
|
100
|
+
: T
|
|
101
|
+
: T;
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Recursively process a router, allowing for you to define nested routers.
|
|
105
|
+
*
|
|
106
|
+
* The main purpose of this is to convert all path strings into string constants so we can infer the path
|
|
107
|
+
*/
|
|
108
|
+
type RecursivelyProcessAppRouter<
|
|
109
|
+
T extends AppRouter,
|
|
110
|
+
TOptions extends RouterOptions,
|
|
111
|
+
> = {
|
|
112
|
+
[K in keyof T]: T[K] extends AppRoute
|
|
113
|
+
? ValidatedHeaders<T[K], TOptions>
|
|
114
|
+
: T[K] extends AppRouter
|
|
115
|
+
? RecursivelyProcessAppRouter<T[K], TOptions>
|
|
116
|
+
: T[K];
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
type RecursivelyApplyOptions<
|
|
120
|
+
TRouter extends AppRouter,
|
|
121
|
+
TOptions extends RouterOptions,
|
|
122
|
+
> = {
|
|
123
|
+
[TRouterKey in keyof TRouter]: TRouter[TRouterKey] extends AppRoute
|
|
124
|
+
? Prettify<ApplyOptions<TRouter[TRouterKey], TOptions>>
|
|
125
|
+
: TRouter[TRouterKey] extends AppRouter
|
|
126
|
+
? RecursivelyApplyOptions<TRouter[TRouterKey], TOptions>
|
|
127
|
+
: TRouter[TRouterKey];
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Merge headers together
|
|
132
|
+
*/
|
|
133
|
+
export type MergeHeaders<
|
|
134
|
+
A extends AppRouteCommon['headers'],
|
|
135
|
+
B extends AppRouteCommon['headers'],
|
|
136
|
+
> = [A, B] extends [undefined, undefined]
|
|
137
|
+
? unknown
|
|
138
|
+
: A extends undefined
|
|
139
|
+
? B
|
|
140
|
+
: B extends undefined
|
|
141
|
+
? A
|
|
142
|
+
: A extends Record<string, ContractAnyType>
|
|
143
|
+
? B extends Record<string, ContractAnyType>
|
|
144
|
+
? MergeObjectBasedHeaders<A, B>
|
|
145
|
+
: unknown
|
|
146
|
+
: unknown;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Headers are typed as a Record<string, ContractAnyType>
|
|
150
|
+
*
|
|
151
|
+
* We need to be able to merge together base headers and route headers, this smushes them together taking precedence to route headers
|
|
152
|
+
*/
|
|
153
|
+
type MergeObjectBasedHeaders<
|
|
154
|
+
T extends Record<string, ContractAnyType>,
|
|
155
|
+
U extends Record<string, ContractAnyType>,
|
|
156
|
+
> = {
|
|
157
|
+
[K in keyof T | keyof U]: K extends keyof U
|
|
158
|
+
? U[K]
|
|
159
|
+
: K extends keyof T
|
|
160
|
+
? T[K]
|
|
161
|
+
: never;
|
|
162
|
+
} extends infer M
|
|
163
|
+
? {
|
|
164
|
+
[K in keyof M as M[K] extends null ? never : K]: M[K];
|
|
165
|
+
}
|
|
166
|
+
: never;
|
|
167
|
+
|
|
168
|
+
type IsEmptyObject<T> = keyof T extends never
|
|
169
|
+
? {} extends T
|
|
170
|
+
? true
|
|
171
|
+
: false
|
|
172
|
+
: false;
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* For a given app route, infer the headers input type
|
|
176
|
+
*/
|
|
177
|
+
export type InferHeadersInput<
|
|
178
|
+
T extends AppRoute,
|
|
179
|
+
THeaders = T['headers'],
|
|
180
|
+
> = unknown extends THeaders
|
|
181
|
+
? undefined
|
|
182
|
+
: // if empty object
|
|
183
|
+
IsEmptyObject<THeaders> extends true
|
|
184
|
+
? {}
|
|
185
|
+
: // if modern object-based headers
|
|
186
|
+
THeaders extends Record<string, ContractAnyType>
|
|
187
|
+
? LowercaseKeys<
|
|
188
|
+
UnknownOrUndefinedObjectValuesToOptionalKeys<{
|
|
189
|
+
[K in keyof THeaders]: THeaders[K] extends ContractAnyType
|
|
190
|
+
? SchemaInputOrType<THeaders[K]>
|
|
191
|
+
: never;
|
|
192
|
+
}>
|
|
193
|
+
>
|
|
194
|
+
: // else
|
|
195
|
+
undefined;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* { foo: string | undefined } => { foo?: string | undefined }
|
|
199
|
+
* { foo: unknown } => { foo?: unknown }
|
|
200
|
+
*
|
|
201
|
+
* @internal
|
|
202
|
+
*/
|
|
203
|
+
export type UnknownOrUndefinedObjectValuesToOptionalKeys<T> = {
|
|
204
|
+
[K in keyof T as undefined extends T[K]
|
|
205
|
+
? K
|
|
206
|
+
: unknown extends T[K]
|
|
207
|
+
? K
|
|
208
|
+
: never]?: T[K];
|
|
209
|
+
} & {
|
|
210
|
+
[K in keyof T as undefined extends T[K]
|
|
211
|
+
? never
|
|
212
|
+
: unknown extends T[K]
|
|
213
|
+
? never
|
|
214
|
+
: K]: T[K];
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* For a given app route, infer the headers output type
|
|
219
|
+
*/
|
|
220
|
+
export type InferHeadersOutput<
|
|
221
|
+
T extends AppRoute,
|
|
222
|
+
THeaders = T['headers'],
|
|
223
|
+
> = unknown extends THeaders
|
|
224
|
+
? '1'
|
|
225
|
+
: // if empty object
|
|
226
|
+
IsEmptyObject<THeaders> extends true
|
|
227
|
+
? {}
|
|
228
|
+
: // if modern object-based headers
|
|
229
|
+
THeaders extends Record<string, ContractAnyType>
|
|
230
|
+
? {
|
|
231
|
+
[K in keyof THeaders]: THeaders[K] extends ContractAnyType
|
|
232
|
+
? LowercaseKeys<SchemaOutputOrType<THeaders[K]>>
|
|
233
|
+
: never;
|
|
234
|
+
}
|
|
235
|
+
: '3';
|
|
236
|
+
|
|
237
|
+
type ApplyOptions<
|
|
238
|
+
TRoute extends AppRoute,
|
|
239
|
+
TOptions extends RouterOptions,
|
|
240
|
+
> = Omit<TRoute, 'headers' | 'path' | 'responses'> &
|
|
241
|
+
WithoutUnknown<{
|
|
242
|
+
path: TOptions['pathPrefix'] extends string
|
|
243
|
+
? `${TOptions['pathPrefix']}${TRoute['path']}`
|
|
244
|
+
: TRoute['path'];
|
|
245
|
+
headers: MergeHeaders<
|
|
246
|
+
UnknownToUndefined<TOptions['baseHeaders']>,
|
|
247
|
+
UnknownToUndefined<TRoute['headers']>
|
|
248
|
+
>;
|
|
249
|
+
strictStatusCodes: TRoute['strictStatusCodes'] extends boolean
|
|
250
|
+
? TRoute['strictStatusCodes']
|
|
251
|
+
: TOptions['strictStatusCodes'] extends boolean
|
|
252
|
+
? TOptions['strictStatusCodes']
|
|
253
|
+
: unknown;
|
|
254
|
+
responses: 'commonResponses' extends keyof TOptions
|
|
255
|
+
? Prettify<Merge<TOptions['commonResponses'], TRoute['responses']>>
|
|
256
|
+
: TRoute['responses'];
|
|
257
|
+
metadata: 'metadata' extends keyof TOptions
|
|
258
|
+
? Prettify<Merge<TOptions['metadata'], TRoute['metadata']>>
|
|
259
|
+
: TRoute['metadata'];
|
|
260
|
+
}>;
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* This was needed as **for some reason** headers above end up being `unknown`, our
|
|
264
|
+
* `MergeHeadersWithLegacySupport` function expends undefined, so we need to normalize it
|
|
265
|
+
*
|
|
266
|
+
* Can be moved in V4 when the legacy polyfill is removed
|
|
267
|
+
*/
|
|
268
|
+
export type UnknownToUndefined<T> = unknown extends T
|
|
269
|
+
? T extends unknown
|
|
270
|
+
? undefined
|
|
271
|
+
: T
|
|
272
|
+
: T;
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* A union of all possible endpoint types.
|
|
276
|
+
*/
|
|
277
|
+
export type AppRoute = AppRouteQuery | AppRouteMutation | AppRouteDeleteNoBody;
|
|
278
|
+
export type AppRouteStrictStatusCodes = Omit<AppRoute, 'strictStatusCodes'> & {
|
|
279
|
+
strictStatusCodes: true;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* A router (or contract) in @ts-rest is a collection of more routers or
|
|
284
|
+
* individual routes
|
|
285
|
+
*/
|
|
286
|
+
export type AppRouter = {
|
|
287
|
+
[key: string]: AppRouter | AppRoute;
|
|
288
|
+
};
|
|
289
|
+
|
|
290
|
+
export type FlattenAppRouter<T extends AppRouter | AppRoute> =
|
|
291
|
+
T extends AppRoute
|
|
292
|
+
? T
|
|
293
|
+
: {
|
|
294
|
+
[TKey in keyof T]: T[TKey] extends AppRoute
|
|
295
|
+
? T[TKey]
|
|
296
|
+
: T[TKey] extends AppRouter
|
|
297
|
+
? FlattenAppRouter<T[TKey]>
|
|
298
|
+
: never;
|
|
299
|
+
}[keyof T];
|
|
300
|
+
|
|
301
|
+
export type RouterOptions<TPrefix extends string = string> = {
|
|
302
|
+
baseHeaders?: Record<string, ContractAnyType>;
|
|
303
|
+
strictStatusCodes?: boolean;
|
|
304
|
+
pathPrefix?: TPrefix;
|
|
305
|
+
commonResponses?: Record<number, AppRouteResponse>;
|
|
306
|
+
metadata?: unknown;
|
|
307
|
+
|
|
308
|
+
/**
|
|
309
|
+
* @deprecated Use `validateResponse` on the client options
|
|
310
|
+
*/
|
|
311
|
+
validateResponseOnClient?: boolean;
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Differentiate between a route and a router
|
|
316
|
+
*
|
|
317
|
+
* @param obj
|
|
318
|
+
* @returns
|
|
319
|
+
*/
|
|
320
|
+
export const isAppRoute = (obj: AppRoute | AppRouter): obj is AppRoute => {
|
|
321
|
+
return 'method' in obj && 'path' in obj;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
export const isAppRouteQuery = (route: AppRoute): route is AppRouteQuery => {
|
|
325
|
+
return route.method === 'GET';
|
|
326
|
+
};
|
|
327
|
+
|
|
328
|
+
export const isAppRouteMutation = (
|
|
329
|
+
route: AppRoute,
|
|
330
|
+
): route is AppRouteMutation => {
|
|
331
|
+
return !isAppRouteQuery(route);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
type NarrowObject<T> = {
|
|
335
|
+
[K in keyof T]: T[K];
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* The instantiated ts-rest client
|
|
340
|
+
*/
|
|
341
|
+
type ContractInstance = {
|
|
342
|
+
/**
|
|
343
|
+
* A collection of routes or routers
|
|
344
|
+
*/
|
|
345
|
+
router: <
|
|
346
|
+
TRouter extends AppRouter,
|
|
347
|
+
TPrefix extends string,
|
|
348
|
+
TOptions extends RouterOptions<TPrefix> = {},
|
|
349
|
+
>(
|
|
350
|
+
endpoints: RecursivelyProcessAppRouter<TRouter, TOptions>,
|
|
351
|
+
options?: TOptions,
|
|
352
|
+
) => RecursivelyApplyOptions<TRouter, TOptions>;
|
|
353
|
+
/**
|
|
354
|
+
* A single query route, should exist within
|
|
355
|
+
* a {@link AppRouter}
|
|
356
|
+
*/
|
|
357
|
+
query: <T extends AppRouteQuery>(query: NarrowObject<T>) => T;
|
|
358
|
+
/**
|
|
359
|
+
* A single mutation route, should exist within
|
|
360
|
+
* a {@link AppRouter}
|
|
361
|
+
*/
|
|
362
|
+
mutation: <T extends AppRouteMutation>(mutation: NarrowObject<T>) => T;
|
|
363
|
+
responses: <TResponses extends Record<number, AppRouteResponse>>(
|
|
364
|
+
responses: TResponses,
|
|
365
|
+
) => TResponses;
|
|
366
|
+
/**
|
|
367
|
+
* @deprecated Please use type() instead.
|
|
368
|
+
*/
|
|
369
|
+
response: <T>() => T extends null ? ContractNullType : ContractPlainType<T>;
|
|
370
|
+
/**
|
|
371
|
+
* @deprecated Please use type() instead.
|
|
372
|
+
*/
|
|
373
|
+
body: <T>() => T extends null ? ContractNullType : ContractPlainType<T>;
|
|
374
|
+
/**
|
|
375
|
+
* Exists to allow storing a Type in the contract (at compile time only)
|
|
376
|
+
*/
|
|
377
|
+
type: <T>() => T extends null ? ContractNullType : ContractPlainType<T>;
|
|
378
|
+
/**
|
|
379
|
+
* Define a custom response type
|
|
380
|
+
*/
|
|
381
|
+
otherResponse: <T extends ContractAnyType>({
|
|
382
|
+
contentType,
|
|
383
|
+
body,
|
|
384
|
+
}: {
|
|
385
|
+
contentType: string;
|
|
386
|
+
body: T;
|
|
387
|
+
}) => ContractOtherResponse<T>;
|
|
388
|
+
/** Use to indicate that a route takes no body or responds with no body */
|
|
389
|
+
noBody: () => ContractNoBodyType;
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
*
|
|
394
|
+
* @deprecated Please use {@link initContract} instead.
|
|
395
|
+
*/
|
|
396
|
+
export const initTsRest = (): ContractInstance => initContract();
|
|
397
|
+
|
|
398
|
+
const recursivelyApplyOptions = <T extends AppRouter>(
|
|
399
|
+
router: T,
|
|
400
|
+
options?: RouterOptions,
|
|
401
|
+
): T => {
|
|
402
|
+
return Object.fromEntries(
|
|
403
|
+
Object.entries(router).map(([key, value]) => {
|
|
404
|
+
if (isAppRoute(value)) {
|
|
405
|
+
return [
|
|
406
|
+
key,
|
|
407
|
+
{
|
|
408
|
+
...value,
|
|
409
|
+
path: options?.pathPrefix
|
|
410
|
+
? options.pathPrefix + value.path
|
|
411
|
+
: value.path,
|
|
412
|
+
headers: mergeHeaderSchemasForRoute(
|
|
413
|
+
options?.baseHeaders,
|
|
414
|
+
value.headers,
|
|
415
|
+
),
|
|
416
|
+
strictStatusCodes:
|
|
417
|
+
value.strictStatusCodes ?? options?.strictStatusCodes,
|
|
418
|
+
validateResponseOnClient:
|
|
419
|
+
value.validateResponseOnClient ??
|
|
420
|
+
options?.validateResponseOnClient,
|
|
421
|
+
responses: {
|
|
422
|
+
...options?.commonResponses,
|
|
423
|
+
...value.responses,
|
|
424
|
+
},
|
|
425
|
+
metadata: options?.metadata
|
|
426
|
+
? {
|
|
427
|
+
...options?.metadata,
|
|
428
|
+
...(value.metadata ?? {}),
|
|
429
|
+
}
|
|
430
|
+
: value.metadata,
|
|
431
|
+
},
|
|
432
|
+
];
|
|
433
|
+
} else {
|
|
434
|
+
return [key, recursivelyApplyOptions(value, options)];
|
|
435
|
+
}
|
|
436
|
+
}),
|
|
437
|
+
);
|
|
438
|
+
};
|
|
439
|
+
|
|
440
|
+
export const ContractPlainTypeRuntimeSymbol = Symbol(
|
|
441
|
+
'ContractPlainType',
|
|
442
|
+
) as any;
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Instantiate a ts-rest client, primarily to access `router`, `response`, and `body`
|
|
446
|
+
*
|
|
447
|
+
* @returns {ContractInstance}
|
|
448
|
+
*/
|
|
449
|
+
export const initContract = (): ContractInstance => {
|
|
450
|
+
return {
|
|
451
|
+
// @ts-expect-error - this is a type error, but it's not clear how to fix it
|
|
452
|
+
router: (endpoints, options) => recursivelyApplyOptions(endpoints, options),
|
|
453
|
+
query: (args) => args,
|
|
454
|
+
mutation: (args) => args,
|
|
455
|
+
responses: (args) => args,
|
|
456
|
+
response: () => ContractPlainTypeRuntimeSymbol,
|
|
457
|
+
body: () => ContractPlainTypeRuntimeSymbol,
|
|
458
|
+
type: () => ContractPlainTypeRuntimeSymbol,
|
|
459
|
+
otherResponse: <T extends ContractAnyType>({
|
|
460
|
+
contentType,
|
|
461
|
+
body,
|
|
462
|
+
}: {
|
|
463
|
+
contentType: string;
|
|
464
|
+
body: T;
|
|
465
|
+
}) =>
|
|
466
|
+
({
|
|
467
|
+
contentType,
|
|
468
|
+
body,
|
|
469
|
+
}) as ContractOtherResponse<T>,
|
|
470
|
+
noBody: () => ContractNoBody,
|
|
471
|
+
};
|
|
472
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as clientModule from './client';
|
|
2
|
+
import { initContract } from './dsl';
|
|
3
|
+
const c = initContract();
|
|
4
|
+
describe('fetchApi', () => {
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
jest.restoreAllMocks();
|
|
7
|
+
});
|
|
8
|
+
it('should not include content-type application/json if body is undefined', async () => {
|
|
9
|
+
const tsRestApiStub = jest
|
|
10
|
+
.spyOn(clientModule, 'tsRestFetchApi')
|
|
11
|
+
.mockResolvedValue({
|
|
12
|
+
status: 200,
|
|
13
|
+
body: { message: 'never gonna give you up, never gonna let you down' },
|
|
14
|
+
headers: new Headers(),
|
|
15
|
+
});
|
|
16
|
+
await clientModule.fetchApi({
|
|
17
|
+
body: undefined,
|
|
18
|
+
headers: {},
|
|
19
|
+
path: '/rick-astley',
|
|
20
|
+
clientArgs: {
|
|
21
|
+
baseUrl: 'https://api.com',
|
|
22
|
+
baseHeaders: {},
|
|
23
|
+
},
|
|
24
|
+
route: {
|
|
25
|
+
method: 'POST',
|
|
26
|
+
body: null,
|
|
27
|
+
path: '/rick-astley',
|
|
28
|
+
responses: {
|
|
29
|
+
200: c.type<{ message: string }>(),
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
query: {},
|
|
33
|
+
extraInputArgs: {},
|
|
34
|
+
});
|
|
35
|
+
expect(tsRestApiStub).toHaveBeenCalledWith({
|
|
36
|
+
body: undefined,
|
|
37
|
+
contentType: undefined,
|
|
38
|
+
headers: {},
|
|
39
|
+
method: 'POST',
|
|
40
|
+
path: '/rick-astley',
|
|
41
|
+
rawBody: undefined,
|
|
42
|
+
rawQuery: {},
|
|
43
|
+
route: {
|
|
44
|
+
body: null,
|
|
45
|
+
method: 'POST',
|
|
46
|
+
path: '/rick-astley',
|
|
47
|
+
responses: {
|
|
48
|
+
'200': expect.anything(),
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
fetchOptions: {},
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should include content-type application/json if body is defined', async () => {
|
|
56
|
+
const tsRestApiStub = jest
|
|
57
|
+
.spyOn(clientModule, 'tsRestFetchApi')
|
|
58
|
+
.mockResolvedValue({
|
|
59
|
+
status: 200,
|
|
60
|
+
body: { message: 'never gonna give you up, never gonna let you down' },
|
|
61
|
+
headers: new Headers(),
|
|
62
|
+
});
|
|
63
|
+
await clientModule.fetchApi({
|
|
64
|
+
body: {
|
|
65
|
+
message: 'never gonna say goodbye, never gonna tell a lie and hurt you',
|
|
66
|
+
},
|
|
67
|
+
headers: {},
|
|
68
|
+
path: '/rick-astley',
|
|
69
|
+
clientArgs: {
|
|
70
|
+
baseUrl: 'https://api.com',
|
|
71
|
+
baseHeaders: {},
|
|
72
|
+
},
|
|
73
|
+
route: {
|
|
74
|
+
method: 'POST',
|
|
75
|
+
body: null,
|
|
76
|
+
path: '/rick-astley',
|
|
77
|
+
responses: {
|
|
78
|
+
200: c.type<{ message: string }>(),
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
query: {},
|
|
82
|
+
extraInputArgs: {},
|
|
83
|
+
});
|
|
84
|
+
expect(tsRestApiStub).toHaveBeenCalledWith(
|
|
85
|
+
expect.objectContaining({
|
|
86
|
+
body: '{"message":"never gonna say goodbye, never gonna tell a lie and hurt you"}',
|
|
87
|
+
contentType: 'application/json',
|
|
88
|
+
headers: {
|
|
89
|
+
'content-type': 'application/json',
|
|
90
|
+
},
|
|
91
|
+
method: 'POST',
|
|
92
|
+
path: '/rick-astley',
|
|
93
|
+
rawBody: {
|
|
94
|
+
message:
|
|
95
|
+
'never gonna say goodbye, never gonna tell a lie and hurt you',
|
|
96
|
+
},
|
|
97
|
+
rawQuery: {},
|
|
98
|
+
fetchOptions: {},
|
|
99
|
+
}),
|
|
100
|
+
);
|
|
101
|
+
});
|
|
102
|
+
});
|