@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
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import {
|
|
2
|
+
convertQueryParamsToUrlString,
|
|
3
|
+
encodeQueryParams,
|
|
4
|
+
encodeQueryParamsJson,
|
|
5
|
+
parseJsonQueryObject,
|
|
6
|
+
} from './query';
|
|
7
|
+
import { parse as qsParse, stringify as qsStringify } from 'qs';
|
|
8
|
+
|
|
9
|
+
describe('convertQueryParamsToUrlString', () => {
|
|
10
|
+
it('should convert query params to url string', () => {
|
|
11
|
+
const query = {
|
|
12
|
+
id: 'abc',
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
expect(convertQueryParamsToUrlString(query)).toBe('?id=abc');
|
|
16
|
+
expect(convertQueryParamsToUrlString(query, true)).toBe(`?id=abc`);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('should convert query params to url string with no params', () => {
|
|
20
|
+
const query = {};
|
|
21
|
+
|
|
22
|
+
expect(convertQueryParamsToUrlString(query)).toBe('');
|
|
23
|
+
expect(convertQueryParamsToUrlString(query, true)).toBe('');
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it('should convert query params to url string with primitives', () => {
|
|
27
|
+
expect(convertQueryParamsToUrlString(null)).toBe('');
|
|
28
|
+
expect(convertQueryParamsToUrlString(undefined)).toBe('');
|
|
29
|
+
expect(convertQueryParamsToUrlString(true)).toBe('');
|
|
30
|
+
expect(convertQueryParamsToUrlString(123)).toBe('');
|
|
31
|
+
|
|
32
|
+
expect(convertQueryParamsToUrlString(null, true)).toBe('');
|
|
33
|
+
expect(convertQueryParamsToUrlString(undefined, true)).toBe('');
|
|
34
|
+
expect(convertQueryParamsToUrlString(true, true)).toBe('');
|
|
35
|
+
expect(convertQueryParamsToUrlString(123, true)).toBe('');
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe('encodeQueryParams', () => {
|
|
40
|
+
it('should be empty if no params', () => {
|
|
41
|
+
const query = {};
|
|
42
|
+
|
|
43
|
+
const result = encodeQueryParams(query);
|
|
44
|
+
|
|
45
|
+
expect(result).toBe('');
|
|
46
|
+
expect(qsStringify(query)).toBe(result);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('should convert query params to url string with many params', () => {
|
|
50
|
+
const query = {
|
|
51
|
+
id: '1',
|
|
52
|
+
commentId: '2',
|
|
53
|
+
commentId2: '3',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const result = encodeQueryParams(query);
|
|
57
|
+
|
|
58
|
+
expect(result).toBe(encodeURI('id=1&commentId=2&commentId2=3'));
|
|
59
|
+
|
|
60
|
+
expect(qsParse(result)).toEqual({
|
|
61
|
+
id: '1',
|
|
62
|
+
commentId: '2',
|
|
63
|
+
commentId2: '3',
|
|
64
|
+
});
|
|
65
|
+
expect(qsStringify(query)).toBe(result);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should explode arrays', () => {
|
|
69
|
+
const query = {
|
|
70
|
+
array: ['1', '2', '3'],
|
|
71
|
+
id: '1',
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const result = encodeQueryParams(query);
|
|
75
|
+
|
|
76
|
+
expect(result).toBe(encodeURI('array[0]=1&array[1]=2&array[2]=3&id=1'));
|
|
77
|
+
|
|
78
|
+
expect(qsParse(result)).toEqual({
|
|
79
|
+
array: ['1', '2', '3'],
|
|
80
|
+
id: '1',
|
|
81
|
+
});
|
|
82
|
+
expect(qsStringify(query)).toBe(result);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should explode nested query strings with arrays and other keys', () => {
|
|
86
|
+
const query = {
|
|
87
|
+
nested: {
|
|
88
|
+
array: ['1', '2', '3'],
|
|
89
|
+
id: '1',
|
|
90
|
+
nestedNested: {
|
|
91
|
+
id: '2',
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const result = encodeQueryParams(query);
|
|
97
|
+
|
|
98
|
+
expect(result).toBe(
|
|
99
|
+
encodeURI(
|
|
100
|
+
'nested[array][0]=1&nested[array][1]=2&nested[array][2]=3&nested[id]=1&nested[nestedNested][id]=2',
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
|
|
104
|
+
expect(qsParse(result)).toEqual({
|
|
105
|
+
nested: {
|
|
106
|
+
array: ['1', '2', '3'],
|
|
107
|
+
id: '1',
|
|
108
|
+
nestedNested: {
|
|
109
|
+
id: '2',
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
expect(qsStringify(query)).toBe(result);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('should work for null, undefined, and NaN', () => {
|
|
117
|
+
const query = {
|
|
118
|
+
null: null,
|
|
119
|
+
undefined: undefined,
|
|
120
|
+
nan: NaN,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const result = encodeQueryParams(query);
|
|
124
|
+
|
|
125
|
+
expect(result).toBe(encodeURI('null=&nan=NaN'));
|
|
126
|
+
|
|
127
|
+
// qs compatibility
|
|
128
|
+
expect(qsParse(result)).toEqual({
|
|
129
|
+
null: '',
|
|
130
|
+
nan: 'NaN',
|
|
131
|
+
});
|
|
132
|
+
expect(qsStringify(query)).toBe(result);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should format dates as ISO strings', () => {
|
|
136
|
+
const query = {
|
|
137
|
+
date: new Date('2020-01-01'),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const result = encodeQueryParams(query);
|
|
141
|
+
|
|
142
|
+
expect(result).toBe('date=2020-01-01T00%3A00%3A00.000Z');
|
|
143
|
+
expect(qsParse(result)).toEqual({
|
|
144
|
+
date: '2020-01-01T00:00:00.000Z',
|
|
145
|
+
});
|
|
146
|
+
expect(qsStringify(query)).toBe(result);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should parse booleans', () => {
|
|
150
|
+
const query = {
|
|
151
|
+
bool: true,
|
|
152
|
+
false: false,
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
const result = encodeQueryParams(query);
|
|
156
|
+
|
|
157
|
+
expect(result).toBe(encodeURI('bool=true&false=false'));
|
|
158
|
+
expect(qsParse(result)).toEqual({
|
|
159
|
+
bool: 'true',
|
|
160
|
+
false: 'false',
|
|
161
|
+
});
|
|
162
|
+
expect(qsStringify(query)).toBe(result);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
it('should parse numbers', () => {
|
|
166
|
+
const query = {
|
|
167
|
+
number: 1,
|
|
168
|
+
float: 1.1,
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const result = encodeQueryParams(query);
|
|
172
|
+
|
|
173
|
+
expect(result).toBe(encodeURI('number=1&float=1.1'));
|
|
174
|
+
expect(qsParse(result)).toEqual({
|
|
175
|
+
number: '1',
|
|
176
|
+
float: '1.1',
|
|
177
|
+
});
|
|
178
|
+
expect(qsStringify(query)).toBe(result);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it('should parse objects', () => {
|
|
182
|
+
const query = {
|
|
183
|
+
object: { id: '1' },
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const result = encodeQueryParams(query);
|
|
187
|
+
expect(result).toBe(encodeURI('object[id]=1'));
|
|
188
|
+
expect(qsParse(result)).toEqual(query);
|
|
189
|
+
expect(qsStringify(query)).toBe(result);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should parse with arrays of objects', () => {
|
|
193
|
+
const query = {
|
|
194
|
+
array: [{ id: '1' }, { id: '2' }],
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const result = encodeQueryParams(query);
|
|
198
|
+
|
|
199
|
+
expect(result).toBe(encodeURI('array[0][id]=1&array[1][id]=2'));
|
|
200
|
+
expect(qsParse(result)).toEqual(query);
|
|
201
|
+
expect(qsStringify(query)).toBe(result);
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it('should parse arrays in arrays', () => {
|
|
205
|
+
const query = {
|
|
206
|
+
array: [['1', '2']],
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const result = encodeQueryParams(query);
|
|
210
|
+
|
|
211
|
+
expect(result).toBe(encodeURI('array[0][0]=1&array[0][1]=2'));
|
|
212
|
+
expect(qsParse(result)).toEqual(query);
|
|
213
|
+
expect(qsStringify(query)).toBe(result);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should encode values with equals sign in key', () => {
|
|
217
|
+
const query = {
|
|
218
|
+
'foo=bar': 'baz',
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
const result = encodeQueryParams(query);
|
|
222
|
+
|
|
223
|
+
expect(result).toBe('foo%3Dbar=baz');
|
|
224
|
+
expect(qsParse(result)).toEqual(query);
|
|
225
|
+
expect(qsStringify(query)).toBe(result);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should encode values with equals sign in value', () => {
|
|
229
|
+
const query = {
|
|
230
|
+
foo: 'bar=baz',
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
const result = encodeQueryParams(query);
|
|
234
|
+
|
|
235
|
+
expect(result).toBe('foo=bar%3Dbaz');
|
|
236
|
+
expect(qsParse(result)).toEqual(query);
|
|
237
|
+
expect(qsStringify(query)).toBe(result);
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
describe('encodeQueryParamsJson', () => {
|
|
242
|
+
it('should be empty if no params', () => {
|
|
243
|
+
const query = {};
|
|
244
|
+
|
|
245
|
+
const result = encodeQueryParamsJson(query);
|
|
246
|
+
|
|
247
|
+
expect(result).toBe('');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should convert query params to url string with many params', () => {
|
|
251
|
+
const query = {
|
|
252
|
+
id: 123,
|
|
253
|
+
string: 'ABC',
|
|
254
|
+
trueString: 'true',
|
|
255
|
+
falseString: 'false',
|
|
256
|
+
nullString: 'null',
|
|
257
|
+
numberString: '123',
|
|
258
|
+
boolean: true,
|
|
259
|
+
null: null,
|
|
260
|
+
undefined: undefined,
|
|
261
|
+
sorting: {
|
|
262
|
+
by: 'date',
|
|
263
|
+
order: 'asc',
|
|
264
|
+
},
|
|
265
|
+
filter: {
|
|
266
|
+
date: {
|
|
267
|
+
gt: new Date('2020-01-01'),
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const result = encodeQueryParamsJson(query);
|
|
273
|
+
|
|
274
|
+
expect(result).toBe(
|
|
275
|
+
`id=123&string=ABC&trueString=%22true%22&falseString=%22false%22&nullString=%22null%22&numberString=%22123%22&boolean=true&null=null&sorting=${encodeURIComponent(
|
|
276
|
+
'{"by":"date","order":"asc"}',
|
|
277
|
+
)}&filter=${encodeURIComponent(
|
|
278
|
+
'{"date":{"gt":"2020-01-01T00:00:00.000Z"}}',
|
|
279
|
+
)}`,
|
|
280
|
+
);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
describe('parseJsonQueryObject', () => {
|
|
285
|
+
it('should be empty if no params', () => {
|
|
286
|
+
const query = {};
|
|
287
|
+
|
|
288
|
+
const result = parseJsonQueryObject(query);
|
|
289
|
+
|
|
290
|
+
expect(result).toEqual({});
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should convert json query object to regular object', () => {
|
|
294
|
+
const query = {
|
|
295
|
+
id: '123',
|
|
296
|
+
string: 'ABC',
|
|
297
|
+
trueString: '"true"',
|
|
298
|
+
falseString: '"false"',
|
|
299
|
+
nullString: '"null"',
|
|
300
|
+
numberString: '"123"',
|
|
301
|
+
boolean: 'true',
|
|
302
|
+
null: 'null',
|
|
303
|
+
sorting: '{"by":"date","order":"asc"}',
|
|
304
|
+
filter: '{"date":{"gt":"2020-01-01T00:00:00.000Z"}}',
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const result = parseJsonQueryObject(query);
|
|
308
|
+
|
|
309
|
+
expect(result).toEqual({
|
|
310
|
+
id: 123,
|
|
311
|
+
string: 'ABC',
|
|
312
|
+
trueString: 'true',
|
|
313
|
+
falseString: 'false',
|
|
314
|
+
nullString: 'null',
|
|
315
|
+
numberString: '123',
|
|
316
|
+
boolean: true,
|
|
317
|
+
null: null,
|
|
318
|
+
sorting: {
|
|
319
|
+
by: 'date',
|
|
320
|
+
order: 'asc',
|
|
321
|
+
},
|
|
322
|
+
filter: {
|
|
323
|
+
date: {
|
|
324
|
+
gt: '2020-01-01T00:00:00.000Z',
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
});
|
package/src/lib/query.ts
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
*
|
|
3
|
+
* @param query - Any JSON object
|
|
4
|
+
* @param json - Use JSON.stringify to encode the query values
|
|
5
|
+
* @returns - The query url segment, using explode array syntax, and deep object syntax
|
|
6
|
+
*/
|
|
7
|
+
export const convertQueryParamsToUrlString = (query: unknown, json = false) => {
|
|
8
|
+
const queryString = json
|
|
9
|
+
? encodeQueryParamsJson(query)
|
|
10
|
+
: encodeQueryParams(query);
|
|
11
|
+
return queryString?.length > 0 ? '?' + queryString : '';
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
export const encodeQueryParamsJson = (query: unknown) => {
|
|
15
|
+
if (!query) {
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return Object.entries(query)
|
|
20
|
+
.filter(([, value]) => value !== undefined)
|
|
21
|
+
.map(([key, value]) => {
|
|
22
|
+
let encodedValue;
|
|
23
|
+
|
|
24
|
+
// if value is a string and is not a reserved JSON value or a number, pass it without encoding
|
|
25
|
+
// this makes strings look nicer in the URL (e.g. ?name=John instead of ?name=%22John%22)
|
|
26
|
+
// this is also how OpenAPI will pass strings even if they are marked as application/json types
|
|
27
|
+
if (
|
|
28
|
+
typeof value === 'string' &&
|
|
29
|
+
!['true', 'false', 'null'].includes(value.trim()) &&
|
|
30
|
+
isNaN(Number(value))
|
|
31
|
+
) {
|
|
32
|
+
encodedValue = value;
|
|
33
|
+
} else {
|
|
34
|
+
encodedValue = JSON.stringify(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return `${encodeURIComponent(key)}=${encodeURIComponent(encodedValue)}`;
|
|
38
|
+
})
|
|
39
|
+
.join('&');
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const encodeQueryParams = (query: unknown) => {
|
|
43
|
+
if (!query) {
|
|
44
|
+
return '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
Object.keys(query)
|
|
49
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
50
|
+
// @ts-ignore
|
|
51
|
+
.flatMap((key) => tokeniseValue(key, query[key]))
|
|
52
|
+
.map(([key, value]) => {
|
|
53
|
+
return `${encodeURIComponent(key)}=${encodeURIComponent(value)}`;
|
|
54
|
+
})
|
|
55
|
+
.join('&')
|
|
56
|
+
);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A recursive function to convert an object/string/number/Date/whatever into an array of key=value pairs
|
|
61
|
+
*
|
|
62
|
+
* The output of this should be flatMap-able to a string of key=value pairs which can be
|
|
63
|
+
* joined with & to form a query string
|
|
64
|
+
*
|
|
65
|
+
* This should be fully compatible with the "qs" library, but without the need to add a dependency
|
|
66
|
+
*/
|
|
67
|
+
const tokeniseValue = (key: string, value: unknown): [string, string][] => {
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
return value.flatMap((v, idx) => tokeniseValue(`${key}[${idx}]`, v));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (value instanceof Date) {
|
|
73
|
+
return [[`${key}`, value.toISOString()]];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (value === null) {
|
|
77
|
+
return [[`${key}`, '']];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (value === undefined) {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (typeof value === 'object') {
|
|
85
|
+
return Object.keys(value).flatMap((k) =>
|
|
86
|
+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
87
|
+
// @ts-ignore
|
|
88
|
+
tokeniseValue(`${key}[${k}]`, value[k]),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return [[`${key}`, `${value}`]];
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
*
|
|
97
|
+
* @param query - A server-side query object where values have been encoded as JSON strings
|
|
98
|
+
* @returns - The same object with the JSON strings decoded. Objects that were encoded using toJSON such as Dates will remain as strings
|
|
99
|
+
*/
|
|
100
|
+
export const parseJsonQueryObject = (query: Record<string, string>) => {
|
|
101
|
+
return Object.fromEntries(
|
|
102
|
+
Object.entries(query).map(([key, value]) => {
|
|
103
|
+
let parsedValue: any;
|
|
104
|
+
// if json parse fails, treat the value as a string
|
|
105
|
+
// this allows us to pass strings without having to surround them with quotes
|
|
106
|
+
try {
|
|
107
|
+
parsedValue = JSON.parse(value);
|
|
108
|
+
} catch {
|
|
109
|
+
parsedValue = value;
|
|
110
|
+
}
|
|
111
|
+
return [key, parsedValue];
|
|
112
|
+
}),
|
|
113
|
+
);
|
|
114
|
+
};
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Equal, Expect } from './test-helpers';
|
|
2
|
+
import { initContract } from './dsl';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { TsRestResponseError } from './response-error';
|
|
5
|
+
import { HTTPStatusCode } from './status-codes';
|
|
6
|
+
|
|
7
|
+
const c = initContract();
|
|
8
|
+
|
|
9
|
+
const contract = c.router(
|
|
10
|
+
{
|
|
11
|
+
posts: {
|
|
12
|
+
getPost: {
|
|
13
|
+
method: 'GET',
|
|
14
|
+
path: '/posts/:id',
|
|
15
|
+
responses: {
|
|
16
|
+
200: z.object({
|
|
17
|
+
id: z.number(),
|
|
18
|
+
content: z.string(),
|
|
19
|
+
}),
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
users: {
|
|
24
|
+
getUser: {
|
|
25
|
+
method: 'GET',
|
|
26
|
+
path: '/users/:id',
|
|
27
|
+
responses: {
|
|
28
|
+
200: z.object({
|
|
29
|
+
id: z.number(),
|
|
30
|
+
name: z.string(),
|
|
31
|
+
}),
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
commonResponses: {
|
|
38
|
+
404: z.object({
|
|
39
|
+
message: z.string(),
|
|
40
|
+
}),
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
describe('TsRestResponseError', () => {
|
|
46
|
+
it('correctly sets response type for single endpoint', () => {
|
|
47
|
+
type ResponseType = Expect<
|
|
48
|
+
Equal<
|
|
49
|
+
ConstructorParameters<
|
|
50
|
+
typeof TsRestResponseError<typeof contract.posts.getPost>
|
|
51
|
+
>[1],
|
|
52
|
+
| { status: 200; body: { id: number; content: string } }
|
|
53
|
+
| { status: 404; body: { message: string } }
|
|
54
|
+
| { status: Exclude<HTTPStatusCode, 200 | 404>; body: unknown }
|
|
55
|
+
>
|
|
56
|
+
>;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('correctly sets response type for entire contract', () => {
|
|
60
|
+
type ResponseType = Expect<
|
|
61
|
+
Equal<
|
|
62
|
+
ConstructorParameters<typeof TsRestResponseError<typeof contract>>[1],
|
|
63
|
+
{ status: 404; body: { message: string } }
|
|
64
|
+
>
|
|
65
|
+
>;
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { AppRoute, AppRouter, AppRouteResponse } from './dsl';
|
|
2
|
+
import { ResolveResponseType, ServerInferResponses } from './infer-types';
|
|
3
|
+
import { HTTPStatusCode } from './status-codes';
|
|
4
|
+
import { CommonAndEqual, SchemaInputOrType } from './type-utils';
|
|
5
|
+
|
|
6
|
+
export class TsRestResponseError<T extends AppRoute | AppRouter> extends Error {
|
|
7
|
+
public statusCode: HTTPStatusCode;
|
|
8
|
+
public body: any;
|
|
9
|
+
|
|
10
|
+
constructor(
|
|
11
|
+
route: T,
|
|
12
|
+
response: T extends AppRouter
|
|
13
|
+
? ServerCommonResponses<T>
|
|
14
|
+
: ServerInferResponses<T>,
|
|
15
|
+
) {
|
|
16
|
+
super();
|
|
17
|
+
|
|
18
|
+
this.statusCode = response.status as HTTPStatusCode;
|
|
19
|
+
this.body = response.body;
|
|
20
|
+
this.name = this.constructor.name;
|
|
21
|
+
|
|
22
|
+
if (typeof response.body === 'string') {
|
|
23
|
+
this.message = response.body;
|
|
24
|
+
} else if (
|
|
25
|
+
typeof response.body === 'object' &&
|
|
26
|
+
response.body !== null &&
|
|
27
|
+
'message' in response.body &&
|
|
28
|
+
typeof response.body.message === 'string'
|
|
29
|
+
) {
|
|
30
|
+
this.message = response.body['message'];
|
|
31
|
+
} else {
|
|
32
|
+
this.message = 'Error';
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
type FlattenAppRouter<T extends AppRouter | AppRoute> = T extends AppRoute
|
|
38
|
+
? T
|
|
39
|
+
: {
|
|
40
|
+
[TKey in keyof T]: T[TKey] extends AppRoute
|
|
41
|
+
? T[TKey]
|
|
42
|
+
: T[TKey] extends AppRouter
|
|
43
|
+
? FlattenAppRouter<T[TKey]>
|
|
44
|
+
: never;
|
|
45
|
+
}[keyof T];
|
|
46
|
+
|
|
47
|
+
type AppRouterCommonResponses<T extends AppRouter> = CommonAndEqual<
|
|
48
|
+
FlattenAppRouter<T>['responses']
|
|
49
|
+
>;
|
|
50
|
+
|
|
51
|
+
type ServerCommonResponses<
|
|
52
|
+
T extends AppRouter,
|
|
53
|
+
TResponses = AppRouterCommonResponses<T>,
|
|
54
|
+
> = {
|
|
55
|
+
[K in keyof TResponses]: {
|
|
56
|
+
status: K;
|
|
57
|
+
body: TResponses[K] extends AppRouteResponse
|
|
58
|
+
? SchemaInputOrType<ResolveResponseType<TResponses[K]>>
|
|
59
|
+
: never;
|
|
60
|
+
};
|
|
61
|
+
}[keyof TResponses];
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { AppRoute } from './dsl';
|
|
2
|
+
import { StandardSchemaError } from './validation-error';
|
|
3
|
+
|
|
4
|
+
export class TsRestResponseValidationError extends Error {
|
|
5
|
+
constructor(
|
|
6
|
+
public appRoute: AppRoute,
|
|
7
|
+
public cause: StandardSchemaError,
|
|
8
|
+
) {
|
|
9
|
+
super(
|
|
10
|
+
`[ts-rest] Response validation failed for ${appRoute.method} ${appRoute.path}: ${cause.message}`,
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export class TsRestRequestValidationError extends Error {
|
|
16
|
+
constructor(
|
|
17
|
+
public pathParams: StandardSchemaError | null,
|
|
18
|
+
public headers: StandardSchemaError | null,
|
|
19
|
+
public query: StandardSchemaError | null,
|
|
20
|
+
public body: StandardSchemaError | null,
|
|
21
|
+
) {
|
|
22
|
+
super('[ts-rest] request validation failed');
|
|
23
|
+
}
|
|
24
|
+
}
|