@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,282 @@
1
+ import {
2
+ AppRoute,
3
+ AppRouteMutation,
4
+ AppRouter,
5
+ AppRouteStrictStatusCodes,
6
+ ContractAnyType,
7
+ ContractNoBodyType,
8
+ ContractOtherResponse,
9
+ InferHeadersInput,
10
+ InferHeadersOutput,
11
+ } from './dsl';
12
+ import { HTTPStatusCode } from './status-codes';
13
+ import {
14
+ And,
15
+ Extends,
16
+ LowercaseKeys,
17
+ Merge,
18
+ Not,
19
+ OptionalIfAllOptional,
20
+ Or,
21
+ PartialByLooseKeys,
22
+ Prettify,
23
+ Without,
24
+ SchemaOutputOrType,
25
+ SchemaInputOrType,
26
+ } from './type-utils';
27
+ import {
28
+ ApiFetcher,
29
+ ClientArgs,
30
+ OverridableClientArgs,
31
+ FetchOptions,
32
+ } from './client';
33
+ import { ParamsFromUrl } from './paths';
34
+
35
+ type ExtractExtraParametersFromClientArgs<
36
+ TClientArgs extends Pick<ClientArgs, 'api'>,
37
+ > = TClientArgs['api'] extends ApiFetcher
38
+ ? Omit<Parameters<TClientArgs['api']>[0], keyof Parameters<ApiFetcher>[0]>
39
+ : {};
40
+
41
+ /**
42
+ * Extract the path params from the path in the contract
43
+ */
44
+ type PathParamsFromUrl<T extends AppRoute> = ParamsFromUrl<
45
+ T['path']
46
+ > extends infer U
47
+ ? U
48
+ : never;
49
+
50
+ /**
51
+ * Merge `PathParamsFromUrl<T>` with pathParams schema if it exists
52
+ */
53
+ type PathParamsWithCustomValidators<
54
+ T extends AppRoute,
55
+ TClientOrServer extends 'client' | 'server' = 'server',
56
+ > = 'pathParams' extends keyof T
57
+ ? Merge<
58
+ PathParamsFromUrl<T>,
59
+ TClientOrServer extends 'server'
60
+ ? SchemaOutputOrType<T['pathParams']>
61
+ : SchemaInputOrType<T['pathParams']>
62
+ >
63
+ : PathParamsFromUrl<T>;
64
+
65
+ export type ResolveResponseType<
66
+ T extends
67
+ | ContractAnyType
68
+ | ContractNoBodyType
69
+ | ContractOtherResponse<ContractAnyType>,
70
+ > = T extends ContractOtherResponse<infer U> ? U : T;
71
+
72
+ export type InferResponseDefinedStatusCodes<
73
+ T extends AppRoute,
74
+ TStatus extends HTTPStatusCode = HTTPStatusCode,
75
+ > = {
76
+ [K in keyof T['responses'] & TStatus]: K;
77
+ }[keyof T['responses'] & TStatus];
78
+
79
+ export type InferResponseUndefinedStatusCodes<
80
+ T extends AppRoute,
81
+ TStatus extends HTTPStatusCode = HTTPStatusCode,
82
+ > = Exclude<TStatus, InferResponseDefinedStatusCodes<T, TStatus>>;
83
+
84
+ type AppRouteResponses<
85
+ T extends AppRoute,
86
+ TStatus extends HTTPStatusCode,
87
+ TClientOrServer extends 'client' | 'server',
88
+ TStrictStatusCodes extends 'default' | 'ignore' | 'force' = 'default',
89
+ > =
90
+ | {
91
+ [K in keyof T['responses'] & TStatus]: {
92
+ status: K;
93
+ body: TClientOrServer extends 'server'
94
+ ? SchemaInputOrType<ResolveResponseType<T['responses'][K]>>
95
+ : SchemaOutputOrType<ResolveResponseType<T['responses'][K]>>;
96
+ } & (TClientOrServer extends 'client'
97
+ ? {
98
+ headers: Headers;
99
+ }
100
+ : {});
101
+ }[keyof T['responses'] & TStatus]
102
+ | (Or<
103
+ Extends<TStrictStatusCodes, 'force'>,
104
+ And<
105
+ Extends<T, AppRouteStrictStatusCodes>,
106
+ Not<Extends<TStrictStatusCodes, 'ignore'>>
107
+ >
108
+ > extends true
109
+ ? never
110
+ : Exclude<TStatus, keyof T['responses']> extends never
111
+ ? never
112
+ : {
113
+ status: Exclude<TStatus, keyof T['responses']>;
114
+ body: unknown;
115
+ } & (TClientOrServer extends 'client'
116
+ ? {
117
+ headers: Headers;
118
+ }
119
+ : {}));
120
+
121
+ export type ServerInferResponses<
122
+ T extends AppRoute | AppRouter,
123
+ TStatus extends HTTPStatusCode = HTTPStatusCode,
124
+ TStrictStatusCodes extends 'default' | 'ignore' | 'force' = 'default',
125
+ > = T extends AppRoute
126
+ ? Prettify<AppRouteResponses<T, TStatus, 'server', TStrictStatusCodes>>
127
+ : T extends AppRouter
128
+ ? {
129
+ [TKey in keyof T]: ServerInferResponses<
130
+ T[TKey],
131
+ TStatus,
132
+ TStrictStatusCodes
133
+ >;
134
+ }
135
+ : never;
136
+
137
+ export type ClientInferResponses<
138
+ T extends AppRoute | AppRouter,
139
+ TStatus extends HTTPStatusCode = HTTPStatusCode,
140
+ TStrictStatusCodes extends 'default' | 'ignore' | 'force' = 'default',
141
+ > = T extends AppRoute
142
+ ? Prettify<AppRouteResponses<T, TStatus, 'client', TStrictStatusCodes>>
143
+ : T extends AppRouter
144
+ ? {
145
+ [TKey in keyof T]: ClientInferResponses<
146
+ T[TKey],
147
+ TStatus,
148
+ TStrictStatusCodes
149
+ >;
150
+ }
151
+ : never;
152
+
153
+ export type ServerInferResponseBody<
154
+ T extends AppRoute,
155
+ TStatus extends keyof T['responses'] = keyof T['responses'],
156
+ > = Prettify<AppRouteResponses<T, TStatus & HTTPStatusCode, 'server'>['body']>;
157
+
158
+ export type ClientInferResponseBody<
159
+ T extends AppRoute,
160
+ TStatus extends keyof T['responses'] = keyof T['responses'],
161
+ > = Prettify<AppRouteResponses<T, TStatus & HTTPStatusCode, 'client'>['body']>;
162
+
163
+ type BodyWithoutFileIfMultiPart<T extends AppRouteMutation> =
164
+ T['contentType'] extends 'multipart/form-data'
165
+ ? Without<SchemaOutputOrType<T['body']>, File | File[]>
166
+ : SchemaOutputOrType<T['body']>;
167
+
168
+ export type ServerInferRequest<
169
+ T extends AppRoute | AppRouter,
170
+ TServerHeaders = never,
171
+ > = T extends AppRoute
172
+ ? Prettify<
173
+ Without<
174
+ {
175
+ params: [keyof PathParamsWithCustomValidators<T>] extends [never]
176
+ ? never
177
+ : Prettify<PathParamsWithCustomValidators<T>>;
178
+ body: T extends AppRouteMutation
179
+ ? BodyWithoutFileIfMultiPart<T>
180
+ : never;
181
+ query: 'query' extends keyof T
182
+ ? SchemaOutputOrType<T['query']>
183
+ : never;
184
+ headers: 'headers' extends keyof T
185
+ ? Prettify<
186
+ InferHeadersOutput<T> &
187
+ ([TServerHeaders] extends [never]
188
+ ? {}
189
+ : Omit<
190
+ TServerHeaders,
191
+ keyof LowercaseKeys<InferHeadersOutput<T>>
192
+ >)
193
+ >
194
+ : TServerHeaders;
195
+ },
196
+ never
197
+ >
198
+ >
199
+ : T extends AppRouter
200
+ ? { [TKey in keyof T]: ServerInferRequest<T[TKey], TServerHeaders> }
201
+ : never;
202
+
203
+ type ClientInferRequestBase<
204
+ T extends AppRoute,
205
+ TClientArgs extends Omit<ClientArgs, 'baseUrl'> = {},
206
+ THeaders = 'headers' extends keyof T
207
+ ? /**
208
+ * We want to only require headers that the user did not provide
209
+ * in the base headers of the client
210
+ */
211
+ Prettify<
212
+ PartialByLooseKeys<
213
+ LowercaseKeys<InferHeadersInput<T>>,
214
+ keyof LowercaseKeys<TClientArgs['baseHeaders']>
215
+ >
216
+ >
217
+ : never,
218
+ TFetchOptions extends FetchOptions = FetchOptions,
219
+ > = Prettify<
220
+ Without<
221
+ {
222
+ params: [keyof PathParamsWithCustomValidators<T, 'client'>] extends [
223
+ never,
224
+ ]
225
+ ? never
226
+ : Prettify<PathParamsWithCustomValidators<T, 'client'>>;
227
+ body: T extends AppRouteMutation
228
+ ? T['body'] extends null
229
+ ? never
230
+ : T['contentType'] extends 'multipart/form-data'
231
+ ? FormData | SchemaInputOrType<T['body']>
232
+ : T['contentType'] extends 'application/x-www-form-urlencoded'
233
+ ? string | SchemaInputOrType<T['body']>
234
+ : SchemaInputOrType<T['body']>
235
+ : never;
236
+ query: 'query' extends keyof T
237
+ ? T['query'] extends null
238
+ ? never
239
+ : SchemaInputOrType<T['query']>
240
+ : never;
241
+ headers: THeaders;
242
+ extraHeaders?: [THeaders] extends [never]
243
+ ? Record<string, string>
244
+ : {
245
+ [K in keyof RequiredKeys<InferHeadersInput<T>>]?: never;
246
+ } & Record<string, string>;
247
+ fetchOptions?: FetchOptions;
248
+ overrideClientOptions?: Partial<OverridableClientArgs>;
249
+
250
+ /**
251
+ * @deprecated Use `fetchOptions.cache` instead
252
+ */
253
+ cache?: 'cache' extends keyof TFetchOptions
254
+ ? TFetchOptions['cache']
255
+ : never;
256
+
257
+ /**
258
+ * @deprecated Use `fetchOptions.next` instead
259
+ */
260
+ next?: 'next' extends keyof TFetchOptions ? TFetchOptions['next'] : never;
261
+ } & ExtractExtraParametersFromClientArgs<TClientArgs>,
262
+ never
263
+ >
264
+ >;
265
+
266
+ type RequiredKeys<T> = {
267
+ [K in keyof T]-?: T[K];
268
+ };
269
+
270
+ export type ClientInferRequest<
271
+ T extends AppRoute | AppRouter,
272
+ TClientArgs extends Omit<ClientArgs, 'baseUrl'> = {},
273
+ > = T extends AppRoute
274
+ ? ClientInferRequestBase<T, TClientArgs>
275
+ : T extends AppRouter
276
+ ? { [TKey in keyof T]: ClientInferRequest<T[TKey]> }
277
+ : never;
278
+
279
+ export type PartialClientInferRequest<
280
+ TRoute extends AppRoute,
281
+ TClientArgs extends Omit<ClientArgs, 'baseUrl'> = {},
282
+ > = OptionalIfAllOptional<ClientInferRequest<TRoute, TClientArgs>>;
@@ -0,0 +1,138 @@
1
+ import { expectType } from 'tsd';
2
+ import { insertParamsIntoPath, ParamsFromUrl } from './paths';
3
+
4
+ const type = <T>() => '' as unknown as T;
5
+
6
+ const url = '/post/:id/comments/:commentId';
7
+ expectType<{ id: string; commentId: string }>(
8
+ type<ParamsFromUrl<typeof url>>(),
9
+ );
10
+
11
+ const url2 = '/post/:id/comments';
12
+ expectType<{ id: string }>(type<ParamsFromUrl<typeof url2>>());
13
+
14
+ const url3 = '/post/:id';
15
+ expectType<{ id: string }>(type<ParamsFromUrl<typeof url3>>());
16
+
17
+ const urlNoParams = '/posts';
18
+ expectType<{}>(type<ParamsFromUrl<typeof urlNoParams>>());
19
+
20
+ const urlManyParams = '/post/:id/comments/:commentId/:commentId2';
21
+ expectType<{ id: string; commentId: string; commentId2: string }>(
22
+ type<ParamsFromUrl<typeof urlManyParams>>(),
23
+ );
24
+
25
+ const urlOptional = '/post/:id?';
26
+ expectType<{
27
+ id?: string;
28
+ }>(type<ParamsFromUrl<typeof urlOptional>>());
29
+
30
+ const urlManyOptional = '/post/:id?/comments/:commentId?';
31
+ expectType<{
32
+ id?: string;
33
+ commentId?: string;
34
+ }>(type<ParamsFromUrl<typeof urlManyOptional>>());
35
+
36
+ const urlMixedOptional = '/post/:id/comments/:commentId?';
37
+ expectType<{
38
+ id: string;
39
+ commentId?: string;
40
+ }>(type<ParamsFromUrl<typeof urlMixedOptional>>());
41
+
42
+ const urlMixedOptional2 = '/post/:id?/comments/:commentId';
43
+ expectType<{
44
+ id?: string;
45
+ commentId: string;
46
+ }>(type<ParamsFromUrl<typeof urlMixedOptional2>>());
47
+
48
+ describe('insertParamsIntoPath', () => {
49
+ it('should insert params into path', () => {
50
+ expect(
51
+ insertParamsIntoPath({
52
+ path: '/post/:id/comments/:commentId',
53
+ params: { commentId: '2', id: '1' },
54
+ }),
55
+ ).toBe('/post/1/comments/2');
56
+ });
57
+
58
+ it('should insert params into path with no params', () => {
59
+ expect(
60
+ insertParamsIntoPath({
61
+ path: '/posts',
62
+ params: { a: '1' },
63
+ }),
64
+ ).toBe('/posts');
65
+ });
66
+
67
+ it('should insert params into path with many params', () => {
68
+ expect(
69
+ insertParamsIntoPath({
70
+ path: '/post/:id/comments/:commentId/:commentId2',
71
+ params: { commentId: '2', commentId2: '3', id: '1' },
72
+ }),
73
+ ).toBe('/post/1/comments/2/3');
74
+ });
75
+
76
+ it('should insert into paths with only one param', () => {
77
+ expect(
78
+ insertParamsIntoPath({
79
+ path: '/:id',
80
+ params: { id: '1' },
81
+ }),
82
+ ).toBe('/1');
83
+ });
84
+
85
+ it('should insert optional params into path with many params', () => {
86
+ expect(
87
+ insertParamsIntoPath({
88
+ path: '/post/:id?/comments/:commentId?/:commentId2?',
89
+ params: { commentId: '2', commentId2: '3', id: '1' },
90
+ }),
91
+ ).toBe('/post/1/comments/2/3');
92
+ });
93
+
94
+ it('should insert optional params into path with no params', () => {
95
+ expect(
96
+ insertParamsIntoPath({
97
+ path: '/post/:id?/comments/:commentId?/:commentId2?',
98
+ params: {},
99
+ }),
100
+ ).toBe('/post/comments');
101
+ });
102
+
103
+ it('should insert not have trailing slashes', () => {
104
+ expect(
105
+ insertParamsIntoPath({
106
+ path: '/post/:id?/comments/:commentId?/:commentId2?/:commentId3?',
107
+ params: {},
108
+ }),
109
+ ).toBe('/post/comments');
110
+ });
111
+
112
+ it('should insert optional params into paths with only one param', () => {
113
+ expect(
114
+ insertParamsIntoPath({
115
+ path: '/:id?',
116
+ params: { id: '1' },
117
+ }),
118
+ ).toBe('/1');
119
+ });
120
+
121
+ it('should preserve trailing slashes in path', () => {
122
+ expect(
123
+ insertParamsIntoPath({
124
+ path: '/:id/',
125
+ params: { id: '1' },
126
+ }),
127
+ ).toBe('/1/');
128
+ });
129
+
130
+ it('should preserve trailing slashes in path with optional params', () => {
131
+ expect(
132
+ insertParamsIntoPath({
133
+ path: '/post/:id?/comments/:commentId?/:commentId2?/:commentId3?/',
134
+ params: {},
135
+ }),
136
+ ).toBe('/post/comments/');
137
+ });
138
+ });
@@ -0,0 +1,61 @@
1
+ type ResolveOptionalPathParam<T extends string> =
2
+ T extends `${infer PathParam}?`
3
+ ? {
4
+ [key in PathParam]?: string | undefined;
5
+ }
6
+ : {
7
+ [key in T]: string;
8
+ };
9
+
10
+ /**
11
+ * @params T - The URL e.g. /posts/:id
12
+ * @params TAcc - Accumulator object
13
+ */
14
+ type RecursivelyExtractPathParams<T extends string> = T extends ''
15
+ ? Record<never, never>
16
+ : T extends `${infer Left}/:${infer PathParam}/${infer Right}`
17
+ ? ResolveOptionalPathParam<PathParam> &
18
+ RecursivelyExtractPathParams<Left> &
19
+ RecursivelyExtractPathParams<Right>
20
+ : T extends `:${infer PathParam}/${infer Right}`
21
+ ? ResolveOptionalPathParam<PathParam> & RecursivelyExtractPathParams<Right>
22
+ : T extends `${infer Left}/:${infer PathParam}`
23
+ ? ResolveOptionalPathParam<PathParam> & RecursivelyExtractPathParams<Left>
24
+ : T extends `:${infer PathParam}`
25
+ ? ResolveOptionalPathParam<PathParam>
26
+ : Record<never, never>;
27
+
28
+ /**
29
+ * Extract path params from path function
30
+ *
31
+ * `{ id: string, commentId: string }`
32
+ *
33
+ * @params T - The URL e.g. /posts/:id
34
+ */
35
+ export type ParamsFromUrl<T extends string> =
36
+ RecursivelyExtractPathParams<T> extends infer U
37
+ ? {
38
+ [key in keyof U]: U[key];
39
+ }
40
+ : never;
41
+
42
+ /**
43
+ * @param path - The URL e.g. /posts/:id
44
+ * @param params - The params e.g. `{ id: string }`
45
+ * @returns - The URL with the params e.g. /posts/123
46
+ */
47
+ export const insertParamsIntoPath = <T extends string>({
48
+ path,
49
+ params,
50
+ }: {
51
+ path: T;
52
+ params: ParamsFromUrl<T>;
53
+ }) => {
54
+ const pathParams: Record<string, string> = params;
55
+
56
+ return path.replace(/\/?:([^/?]+)\??/g, (matched, p) =>
57
+ pathParams[p]
58
+ ? `${matched.startsWith('/') ? '/' : ''}${pathParams[p]}`
59
+ : '',
60
+ );
61
+ };