@sapporta/rest-core 3.52.1 → 3.52.2

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 (60) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +75 -10
  3. package/index.cjs.d.ts +1 -0
  4. package/index.cjs.default.js +1 -0
  5. package/index.cjs.js +807 -0
  6. package/index.cjs.mjs +2 -0
  7. package/index.esm.js +762 -0
  8. package/package.json +13 -3
  9. package/src/lib/client.d.ts +107 -0
  10. package/src/lib/dsl.d.ts +222 -0
  11. package/src/lib/infer-types.d.ts +78 -0
  12. package/src/lib/paths.d.ts +30 -0
  13. package/src/lib/query.d.ts +17 -0
  14. package/src/lib/response-error.d.ts +20 -0
  15. package/src/lib/response-validation-error.d.ts +14 -0
  16. package/src/lib/server.d.ts +18 -0
  17. package/src/lib/standard-schema-utils.d.ts +68 -0
  18. package/src/lib/standard-schema.d.ts +55 -0
  19. package/src/lib/status-codes.d.ts +6 -0
  20. package/src/lib/{test-helpers.ts → test-helpers.d.ts} +1 -6
  21. package/src/lib/type-guards.d.ts +12 -0
  22. package/src/lib/type-utils.d.ts +96 -0
  23. package/src/lib/unknown-status-error.d.ts +10 -0
  24. package/src/lib/validation-error.d.ts +11 -0
  25. package/.babelrc +0 -10
  26. package/.eslintrc.json +0 -21
  27. package/LICENCE +0 -21
  28. package/jest.config.ts +0 -16
  29. package/project.json +0 -51
  30. package/src/lib/client.spec.ts +0 -1330
  31. package/src/lib/client.ts +0 -481
  32. package/src/lib/dsl.spec.ts +0 -1308
  33. package/src/lib/dsl.ts +0 -472
  34. package/src/lib/fetch.spec.ts +0 -102
  35. package/src/lib/infer-types.spec.ts +0 -935
  36. package/src/lib/infer-types.ts +0 -282
  37. package/src/lib/paths.spec.ts +0 -138
  38. package/src/lib/paths.ts +0 -61
  39. package/src/lib/query.spec.ts +0 -329
  40. package/src/lib/query.ts +0 -114
  41. package/src/lib/response-error.spec.ts +0 -67
  42. package/src/lib/response-error.ts +0 -61
  43. package/src/lib/response-validation-error.ts +0 -24
  44. package/src/lib/server.spec.ts +0 -163
  45. package/src/lib/server.ts +0 -83
  46. package/src/lib/standard-schema-utils.spec.ts +0 -218
  47. package/src/lib/standard-schema-utils.ts +0 -280
  48. package/src/lib/standard-schema.ts +0 -71
  49. package/src/lib/status-codes.ts +0 -75
  50. package/src/lib/type-guards.spec.ts +0 -355
  51. package/src/lib/type-guards.ts +0 -99
  52. package/src/lib/type-utils.spec.ts +0 -59
  53. package/src/lib/type-utils.ts +0 -234
  54. package/src/lib/unknown-status-error.ts +0 -15
  55. package/src/lib/validation-error.ts +0 -36
  56. package/tsconfig.json +0 -22
  57. package/tsconfig.lib.json +0 -10
  58. package/tsconfig.spec.json +0 -9
  59. package/typedoc.json +0 -5
  60. /package/src/{index.ts → index.d.ts} +0 -0
package/src/lib/dsl.ts DELETED
@@ -1,472 +0,0 @@
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
- };
@@ -1,102 +0,0 @@
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
- });