@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,163 @@
|
|
|
1
|
+
import {
|
|
2
|
+
initContract,
|
|
3
|
+
TsRestResponseValidationError,
|
|
4
|
+
validateResponse,
|
|
5
|
+
} from '..';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
const c = initContract();
|
|
9
|
+
const contract = c.router(
|
|
10
|
+
{
|
|
11
|
+
plainResponse: {
|
|
12
|
+
method: 'GET',
|
|
13
|
+
path: `/plain`,
|
|
14
|
+
responses: {
|
|
15
|
+
200: c.type<{ foo: string }>(),
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
zodResponse: {
|
|
19
|
+
method: 'GET',
|
|
20
|
+
path: '/zod',
|
|
21
|
+
responses: {
|
|
22
|
+
200: z.object({
|
|
23
|
+
foo: z.string().transform((val) => val.toUpperCase()),
|
|
24
|
+
}),
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
overrideCommonResponse: {
|
|
28
|
+
method: 'GET',
|
|
29
|
+
path: '/override-common',
|
|
30
|
+
responses: {
|
|
31
|
+
200: z.object({
|
|
32
|
+
foo: z.string(),
|
|
33
|
+
}),
|
|
34
|
+
400: z.object({
|
|
35
|
+
foo: z.string(),
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
commonResponses: {
|
|
42
|
+
400: z.object({
|
|
43
|
+
message: z.literal('Bad Request'),
|
|
44
|
+
}),
|
|
45
|
+
404: z.object({
|
|
46
|
+
message: z.literal('Not Found'),
|
|
47
|
+
}),
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
describe('server', () => {
|
|
53
|
+
it('succeeds validation on plain type', () => {
|
|
54
|
+
const validatedResponse = validateResponse({
|
|
55
|
+
appRoute: contract.plainResponse,
|
|
56
|
+
response: {
|
|
57
|
+
status: 200,
|
|
58
|
+
body: { anything: 'foo' },
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
expect(validatedResponse).toEqual({
|
|
63
|
+
status: 200,
|
|
64
|
+
body: { anything: 'foo' },
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('succeeds validation on non-existent status code', () => {
|
|
69
|
+
const validatedResponse = validateResponse({
|
|
70
|
+
appRoute: contract.plainResponse,
|
|
71
|
+
response: {
|
|
72
|
+
status: 500,
|
|
73
|
+
body: { anything: 'foo' },
|
|
74
|
+
},
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(validatedResponse).toEqual({
|
|
78
|
+
status: 500,
|
|
79
|
+
body: { anything: 'foo' },
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('succeeds validation on zod response', () => {
|
|
84
|
+
const validatedResponse = validateResponse({
|
|
85
|
+
appRoute: contract.zodResponse,
|
|
86
|
+
response: {
|
|
87
|
+
status: 200,
|
|
88
|
+
body: { foo: 'bar' },
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
expect(validatedResponse).toEqual({
|
|
93
|
+
status: 200,
|
|
94
|
+
body: { foo: 'BAR' },
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('fails validation on zod response', () => {
|
|
99
|
+
expect(() => {
|
|
100
|
+
validateResponse({
|
|
101
|
+
appRoute: contract.zodResponse,
|
|
102
|
+
response: {
|
|
103
|
+
status: 200,
|
|
104
|
+
body: { bar: 'foo' },
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}).toThrow(TsRestResponseValidationError);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('succeeds validation on zod common response', () => {
|
|
111
|
+
const validatedResponse = validateResponse({
|
|
112
|
+
appRoute: contract.plainResponse,
|
|
113
|
+
response: {
|
|
114
|
+
status: 400,
|
|
115
|
+
body: { message: 'Bad Request' },
|
|
116
|
+
},
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
expect(validatedResponse).toEqual({
|
|
120
|
+
status: 400,
|
|
121
|
+
body: { message: 'Bad Request' },
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('fails validation on zod common response', () => {
|
|
126
|
+
expect(() => {
|
|
127
|
+
validateResponse({
|
|
128
|
+
appRoute: contract.plainResponse,
|
|
129
|
+
response: {
|
|
130
|
+
status: 400,
|
|
131
|
+
body: { message: 'not bad request' },
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
}).toThrow(TsRestResponseValidationError);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it('succeeds validation on overridden zod common response', () => {
|
|
138
|
+
const validatedResponse = validateResponse({
|
|
139
|
+
appRoute: contract.overrideCommonResponse,
|
|
140
|
+
response: {
|
|
141
|
+
status: 400,
|
|
142
|
+
body: { foo: 'bar' },
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
expect(validatedResponse).toEqual({
|
|
147
|
+
status: 400,
|
|
148
|
+
body: { foo: 'bar' },
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it('fails validation on overridden zod common response', () => {
|
|
153
|
+
expect(() => {
|
|
154
|
+
validateResponse({
|
|
155
|
+
appRoute: contract.overrideCommonResponse,
|
|
156
|
+
response: {
|
|
157
|
+
status: 400,
|
|
158
|
+
body: { message: 'Bad Request' },
|
|
159
|
+
},
|
|
160
|
+
});
|
|
161
|
+
}).toThrow(TsRestResponseValidationError);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { HTTPStatusCode } from './status-codes';
|
|
2
|
+
import { TsRestResponseValidationError } from './response-validation-error';
|
|
3
|
+
import {
|
|
4
|
+
AppRoute,
|
|
5
|
+
ContractAnyType,
|
|
6
|
+
ContractNoBody,
|
|
7
|
+
ContractNoBodyType,
|
|
8
|
+
ContractOtherResponse,
|
|
9
|
+
} from './dsl';
|
|
10
|
+
import {
|
|
11
|
+
parseAsStandardSchema,
|
|
12
|
+
validateIfSchema,
|
|
13
|
+
} from './standard-schema-utils';
|
|
14
|
+
import { StandardSchemaError } from './validation-error';
|
|
15
|
+
|
|
16
|
+
export const isAppRouteResponse = (
|
|
17
|
+
value: unknown,
|
|
18
|
+
): value is { status: HTTPStatusCode; body?: any } => {
|
|
19
|
+
return (
|
|
20
|
+
value != null &&
|
|
21
|
+
typeof value === 'object' &&
|
|
22
|
+
'status' in value &&
|
|
23
|
+
typeof value.status === 'number'
|
|
24
|
+
);
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const isAppRouteOtherResponse = (
|
|
28
|
+
response:
|
|
29
|
+
| ContractAnyType
|
|
30
|
+
| ContractNoBodyType
|
|
31
|
+
| ContractOtherResponse<ContractAnyType>,
|
|
32
|
+
): response is ContractOtherResponse<ContractAnyType> => {
|
|
33
|
+
return (
|
|
34
|
+
response != null &&
|
|
35
|
+
typeof response === 'object' &&
|
|
36
|
+
'contentType' in response
|
|
37
|
+
);
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export const isAppRouteNoBody = (
|
|
41
|
+
response:
|
|
42
|
+
| ContractAnyType
|
|
43
|
+
| ContractNoBodyType
|
|
44
|
+
| ContractOtherResponse<ContractAnyType>,
|
|
45
|
+
): response is ContractNoBodyType => {
|
|
46
|
+
return response === ContractNoBody;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
export const validateResponse = ({
|
|
50
|
+
appRoute,
|
|
51
|
+
response,
|
|
52
|
+
}: {
|
|
53
|
+
appRoute: AppRoute;
|
|
54
|
+
response: { status: number; body?: unknown };
|
|
55
|
+
}): { status: number; body?: unknown } => {
|
|
56
|
+
if (isAppRouteResponse(response)) {
|
|
57
|
+
const responseType = appRoute.responses[response.status];
|
|
58
|
+
|
|
59
|
+
const responseSchema = isAppRouteOtherResponse(responseType)
|
|
60
|
+
? responseType.body
|
|
61
|
+
: responseType;
|
|
62
|
+
|
|
63
|
+
const responseStandardSchema = parseAsStandardSchema(responseSchema);
|
|
64
|
+
const responseValidation = validateIfSchema(
|
|
65
|
+
response.body,
|
|
66
|
+
responseStandardSchema,
|
|
67
|
+
);
|
|
68
|
+
|
|
69
|
+
if (responseValidation.error) {
|
|
70
|
+
throw new TsRestResponseValidationError(
|
|
71
|
+
appRoute,
|
|
72
|
+
responseValidation.error as StandardSchemaError,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
status: response.status,
|
|
78
|
+
body: responseValidation.value,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return response;
|
|
83
|
+
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { StandardSchemaV1 } from './standard-schema';
|
|
2
|
+
import {
|
|
3
|
+
isStandardSchema,
|
|
4
|
+
parseAsStandardSchema,
|
|
5
|
+
validateAgainstStandardSchema,
|
|
6
|
+
validateMultiSchemaObject,
|
|
7
|
+
validateIfSchema,
|
|
8
|
+
} from './standard-schema-utils';
|
|
9
|
+
import { z } from 'zod';
|
|
10
|
+
import * as v from 'valibot';
|
|
11
|
+
import { StandardSchemaError } from './validation-error';
|
|
12
|
+
import { initContract } from './dsl';
|
|
13
|
+
|
|
14
|
+
const c = initContract();
|
|
15
|
+
|
|
16
|
+
describe('standard schema utils', () => {
|
|
17
|
+
describe('validateAgainstStandardSchema', () => {
|
|
18
|
+
it('zod 3', () => {
|
|
19
|
+
const value = { foo: 'bar' };
|
|
20
|
+
|
|
21
|
+
const schema = parseAsStandardSchema(z.object({ foo: z.string() }))!;
|
|
22
|
+
const result = validateAgainstStandardSchema(value, schema);
|
|
23
|
+
|
|
24
|
+
expect(result).toEqual({ value: { foo: 'bar' } });
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it('valibot', () => {
|
|
28
|
+
const value = { foo: 'bar' };
|
|
29
|
+
const schema = parseAsStandardSchema(v.object({ foo: v.string() }))!;
|
|
30
|
+
const result = validateAgainstStandardSchema(value, schema);
|
|
31
|
+
|
|
32
|
+
expect(result).toEqual({ value: { foo: 'bar' } });
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('isStandardSchema', () => {
|
|
37
|
+
const diyStandardSchema = {
|
|
38
|
+
'~standard': {
|
|
39
|
+
version: 1,
|
|
40
|
+
vendor: 'ts-rest-test',
|
|
41
|
+
validate: () => ({ value: {} }),
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
it.each([
|
|
45
|
+
{
|
|
46
|
+
input: v.object({ foo: v.string() }),
|
|
47
|
+
expected: true,
|
|
48
|
+
description: 'valibot',
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
input: diyStandardSchema,
|
|
52
|
+
expected: true,
|
|
53
|
+
description: 'diy standard schema',
|
|
54
|
+
},
|
|
55
|
+
{ input: null, expected: false, description: 'null' },
|
|
56
|
+
{ input: undefined, expected: false, description: 'undefined' },
|
|
57
|
+
{ input: 1, expected: false, description: '1' },
|
|
58
|
+
{ input: true, expected: false, description: 'true' },
|
|
59
|
+
{ input: 'foo', expected: false, description: 'foo' },
|
|
60
|
+
{ input: false, expected: false, description: 'false' },
|
|
61
|
+
])('should return $expected for $description', ({ input, expected }) => {
|
|
62
|
+
expect(isStandardSchema(input)).toBe(expected);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('validateIfSchema', () => {
|
|
67
|
+
it('should validate the data if a schema is provided', () => {
|
|
68
|
+
const data = { foo: 'bar' };
|
|
69
|
+
const schema = z.object({ foo: z.string() });
|
|
70
|
+
|
|
71
|
+
const result = validateIfSchema(data, schema);
|
|
72
|
+
|
|
73
|
+
expect(result).toEqual({
|
|
74
|
+
value: { foo: 'bar' },
|
|
75
|
+
schemasUsed: [schema],
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should pass through the data if no schema is provided', () => {
|
|
80
|
+
const data = { foo: 'bar' };
|
|
81
|
+
|
|
82
|
+
const result = validateIfSchema(data, null);
|
|
83
|
+
|
|
84
|
+
expect(result).toEqual({ value: { foo: 'bar' }, schemasUsed: [] });
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('validateMultiSchemaObject', () => {
|
|
89
|
+
it('should error for missing headers', () => {
|
|
90
|
+
const headers = {
|
|
91
|
+
'x-foo': 'bar',
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const headersSchema = {
|
|
95
|
+
'x-foo': z.string(),
|
|
96
|
+
'x-bar': z.string(),
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const result = validateMultiSchemaObject(headers, headersSchema);
|
|
100
|
+
|
|
101
|
+
expect(result).toStrictEqual({
|
|
102
|
+
error: new StandardSchemaError([
|
|
103
|
+
{
|
|
104
|
+
expected: 'string',
|
|
105
|
+
code: 'invalid_type',
|
|
106
|
+
path: ['x-bar'],
|
|
107
|
+
message: 'Invalid input: expected string, received undefined',
|
|
108
|
+
} as StandardSchemaV1.Issue,
|
|
109
|
+
]),
|
|
110
|
+
schemasUsed: [headersSchema['x-foo'], headersSchema['x-bar']],
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('should not error for a missing schema', () => {
|
|
115
|
+
const headers = {
|
|
116
|
+
'x-foo': 'bar',
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const result = validateMultiSchemaObject(headers, undefined);
|
|
120
|
+
|
|
121
|
+
expect(result).toEqual({
|
|
122
|
+
value: {
|
|
123
|
+
'x-foo': 'bar',
|
|
124
|
+
},
|
|
125
|
+
schemasUsed: [],
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it('should work for a valibot object', () => {
|
|
130
|
+
const headers = {
|
|
131
|
+
'x-foo': 'bar',
|
|
132
|
+
'x-bar': 'baz',
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const headersSchema = {
|
|
136
|
+
'x-foo': v.string(),
|
|
137
|
+
'x-bar': v.string(),
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const result = validateMultiSchemaObject(headers, headersSchema);
|
|
141
|
+
|
|
142
|
+
expect(result).toEqual({
|
|
143
|
+
value: {
|
|
144
|
+
'x-foo': 'bar',
|
|
145
|
+
'x-bar': 'baz',
|
|
146
|
+
},
|
|
147
|
+
schemasUsed: [headersSchema['x-foo'], headersSchema['x-bar']],
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should error if missing a required header', () => {
|
|
152
|
+
const schema = { 'x-foo': v.string() };
|
|
153
|
+
const result = validateMultiSchemaObject({}, schema);
|
|
154
|
+
|
|
155
|
+
expect(result).toEqual({
|
|
156
|
+
error: new StandardSchemaError([
|
|
157
|
+
{
|
|
158
|
+
kind: 'schema',
|
|
159
|
+
type: 'string',
|
|
160
|
+
expected: 'string',
|
|
161
|
+
received: 'undefined',
|
|
162
|
+
message: 'Invalid type: Expected string but received undefined',
|
|
163
|
+
path: ['x-foo'],
|
|
164
|
+
} as StandardSchemaV1.Issue,
|
|
165
|
+
]),
|
|
166
|
+
schemasUsed: [schema['x-foo']],
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should error if the header is the wrong type', () => {
|
|
171
|
+
const schema = v.string();
|
|
172
|
+
const result = validateMultiSchemaObject(
|
|
173
|
+
{ 'x-foo': 1 },
|
|
174
|
+
{ 'x-foo': schema },
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
expect(result).toEqual({
|
|
178
|
+
error: new StandardSchemaError([
|
|
179
|
+
{
|
|
180
|
+
kind: 'schema',
|
|
181
|
+
type: 'string',
|
|
182
|
+
input: 1,
|
|
183
|
+
expected: 'string',
|
|
184
|
+
received: '1',
|
|
185
|
+
message: 'Invalid type: Expected string but received 1',
|
|
186
|
+
path: ['x-foo'],
|
|
187
|
+
} as StandardSchemaV1.Issue,
|
|
188
|
+
]),
|
|
189
|
+
schemasUsed: [schema],
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it('should gracefully deal with null, and with other helpers like c.type()', () => {
|
|
194
|
+
const headers = {
|
|
195
|
+
'x-foo': 'bar',
|
|
196
|
+
'x-bar': 1,
|
|
197
|
+
'x-baz': 1,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
const headersSchema = {
|
|
201
|
+
'x-foo': c.type<string>(),
|
|
202
|
+
'x-bar': null,
|
|
203
|
+
'x-baz': c.type<null>(),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const result = validateMultiSchemaObject(headers, headersSchema);
|
|
207
|
+
|
|
208
|
+
expect(result).toEqual({
|
|
209
|
+
value: {
|
|
210
|
+
'x-foo': 'bar',
|
|
211
|
+
'x-bar': 1,
|
|
212
|
+
'x-baz': 1,
|
|
213
|
+
},
|
|
214
|
+
schemasUsed: [],
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
});
|