ts-typed-api 0.1.13 → 0.1.14
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/dist/openapi.d.ts +1 -1
- package/dist/openapi.js +59 -53
- package/package.json +1 -1
- package/src/openapi.ts +67 -61
- package/tests/strict-validation.test.ts +0 -269
package/dist/openapi.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ApiDefinitionSchema } from './definition';
|
|
2
|
-
export declare function generateOpenApiSpec(
|
|
2
|
+
export declare function generateOpenApiSpec(definitions: ApiDefinitionSchema | ApiDefinitionSchema[], options?: {
|
|
3
3
|
info?: {
|
|
4
4
|
title?: string;
|
|
5
5
|
version?: string;
|
package/dist/openapi.js
CHANGED
|
@@ -5,12 +5,16 @@ const zod_to_openapi_1 = require("@asteasolutions/zod-to-openapi");
|
|
|
5
5
|
const zod_1 = require("zod");
|
|
6
6
|
// Extend Zod with OpenAPI capabilities
|
|
7
7
|
(0, zod_to_openapi_1.extendZodWithOpenApi)(zod_1.z);
|
|
8
|
-
function generateOpenApiSpec(
|
|
8
|
+
function generateOpenApiSpec(definitions, options = {}) {
|
|
9
|
+
// Normalize input to always be an array
|
|
10
|
+
const definitionArray = Array.isArray(definitions) ? definitions : [definitions];
|
|
9
11
|
const registry = new zod_to_openapi_1.OpenAPIRegistry();
|
|
10
12
|
// Helper to convert Zod schema to OpenAPI schema component
|
|
11
13
|
function registerSchema(name, schema) {
|
|
12
14
|
try {
|
|
13
|
-
|
|
15
|
+
// Add a unique identifier to ensure no schema name conflicts across multiple definitions
|
|
16
|
+
const uniqueName = `${name}_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
17
|
+
return registry.register(uniqueName, schema); // Cast to any to handle complex Zod types
|
|
14
18
|
}
|
|
15
19
|
catch (error) {
|
|
16
20
|
console.warn(`Could not register schema ${name}: ${error.message}`);
|
|
@@ -27,7 +31,7 @@ function generateOpenApiSpec(definition, options = {}) {
|
|
|
27
31
|
name: key,
|
|
28
32
|
in: inType,
|
|
29
33
|
required: !val.isOptional(),
|
|
30
|
-
schema: registerSchema(`${inType}_${key}
|
|
34
|
+
schema: registerSchema(`${inType}_${key}`, val), // Unique name for registration
|
|
31
35
|
description: val.description,
|
|
32
36
|
}));
|
|
33
37
|
}
|
|
@@ -38,67 +42,69 @@ function generateOpenApiSpec(definition, options = {}) {
|
|
|
38
42
|
required: true, // Assuming body is required if schema is provided
|
|
39
43
|
content: {
|
|
40
44
|
'application/json': {
|
|
41
|
-
schema: registerSchema(
|
|
45
|
+
schema: registerSchema('RequestBody', schema), // Unique name
|
|
42
46
|
},
|
|
43
47
|
},
|
|
44
48
|
};
|
|
45
49
|
}
|
|
46
|
-
// Iterate over
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
50
|
+
// Iterate over multiple API definitions to register routes
|
|
51
|
+
definitionArray.forEach((definition) => {
|
|
52
|
+
Object.keys(definition.endpoints).forEach(domainNameKey => {
|
|
53
|
+
// domainNameKey is a string, representing the domain like 'users', 'products'
|
|
54
|
+
const domain = definition.endpoints[domainNameKey];
|
|
55
|
+
Object.keys(domain).forEach(routeNameKey => {
|
|
56
|
+
// routeNameKey is a string, representing the route name like 'getUser', 'createProduct'
|
|
57
|
+
const route = domain[routeNameKey];
|
|
58
|
+
const parameters = [];
|
|
59
|
+
if (route.params) {
|
|
60
|
+
parameters.push(...zodSchemaToOpenApiParameter(route.params, 'path'));
|
|
61
|
+
}
|
|
62
|
+
if (route.query) {
|
|
63
|
+
parameters.push(...zodSchemaToOpenApiParameter(route.query, 'query'));
|
|
64
|
+
}
|
|
65
|
+
const requestBody = zodSchemaToOpenApiRequestBody(route.body);
|
|
66
|
+
const responses = {};
|
|
67
|
+
for (const statusCode in route.responses) {
|
|
68
|
+
const responseSchema = route.responses[parseInt(statusCode)];
|
|
69
|
+
if (responseSchema) {
|
|
70
|
+
responses[statusCode] = {
|
|
71
|
+
description: `Response for status code ${statusCode}`,
|
|
72
|
+
content: {
|
|
73
|
+
'application/json': {
|
|
74
|
+
schema: registerSchema(`Response_${statusCode}_${routeNameKey}_${domainNameKey}`, responseSchema),
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Add 422 response if not already defined, as it's a default in createResponses
|
|
81
|
+
// Assuming route.responses[422] would exist if it's a standard part of the definition
|
|
82
|
+
if (!responses['422'] && route.responses && route.responses[422]) {
|
|
83
|
+
responses['422'] = {
|
|
84
|
+
description: 'Validation Error',
|
|
67
85
|
content: {
|
|
68
86
|
'application/json': {
|
|
69
|
-
schema: registerSchema(`
|
|
87
|
+
schema: registerSchema(`Response_422_${routeNameKey}_${domainNameKey}`, route.responses[422]),
|
|
70
88
|
},
|
|
71
89
|
},
|
|
72
90
|
};
|
|
73
91
|
}
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
content: {
|
|
81
|
-
'application/json': {
|
|
82
|
-
schema: registerSchema(`Response_422_${routeNameKey}_${domainNameKey}`, route.responses[422]),
|
|
83
|
-
},
|
|
84
|
-
},
|
|
92
|
+
const operation = {
|
|
93
|
+
summary: `${domainNameKey} - ${routeNameKey}`, // Use keys directly for summary
|
|
94
|
+
tags: [domainNameKey], // Use domainNameKey for tags
|
|
95
|
+
parameters: parameters.length > 0 ? parameters : undefined,
|
|
96
|
+
requestBody: requestBody,
|
|
97
|
+
responses: responses,
|
|
85
98
|
};
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// The path needs to be transformed from Express-style (:param) to OpenAPI-style ({param})
|
|
96
|
-
const openApiPath = route.path.replace(/:(\w+)/g, '{$1}');
|
|
97
|
-
registry.registerPath({
|
|
98
|
-
method: route.method.toLowerCase(), // Ensure method is lowercase
|
|
99
|
-
path: openApiPath,
|
|
100
|
-
...operation,
|
|
101
|
-
// Add description or other OpenAPI fields if available in RouteSchema
|
|
99
|
+
// Register the route with the registry
|
|
100
|
+
// The path needs to be transformed from Express-style (:param) to OpenAPI-style ({param})
|
|
101
|
+
const openApiPath = route.path.replace(/:(\w+)/g, '{$1}');
|
|
102
|
+
registry.registerPath({
|
|
103
|
+
method: route.method.toLowerCase(), // Ensure method is lowercase
|
|
104
|
+
path: openApiPath,
|
|
105
|
+
...operation,
|
|
106
|
+
// Add description or other OpenAPI fields if available in RouteSchema
|
|
107
|
+
});
|
|
102
108
|
});
|
|
103
109
|
});
|
|
104
110
|
});
|
package/package.json
CHANGED
package/src/openapi.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { z, ZodTypeAny } from 'zod';
|
|
|
6
6
|
extendZodWithOpenApi(z);
|
|
7
7
|
|
|
8
8
|
export function generateOpenApiSpec(
|
|
9
|
-
|
|
9
|
+
definitions: ApiDefinitionSchema | ApiDefinitionSchema[],
|
|
10
10
|
options: {
|
|
11
11
|
info?: {
|
|
12
12
|
title?: string;
|
|
@@ -16,12 +16,17 @@ export function generateOpenApiSpec(
|
|
|
16
16
|
servers?: { url: string, description?: string }[];
|
|
17
17
|
} = {}
|
|
18
18
|
) {
|
|
19
|
+
// Normalize input to always be an array
|
|
20
|
+
const definitionArray = Array.isArray(definitions) ? definitions : [definitions];
|
|
21
|
+
|
|
19
22
|
const registry = new OpenAPIRegistry();
|
|
20
23
|
|
|
21
24
|
// Helper to convert Zod schema to OpenAPI schema component
|
|
22
25
|
function registerSchema(name: string, schema: ZodTypeAny) {
|
|
23
26
|
try {
|
|
24
|
-
|
|
27
|
+
// Add a unique identifier to ensure no schema name conflicts across multiple definitions
|
|
28
|
+
const uniqueName = `${name}_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
29
|
+
return registry.register(uniqueName, schema as any); // Cast to any to handle complex Zod types
|
|
25
30
|
} catch (error) {
|
|
26
31
|
console.warn(`Could not register schema ${name}: ${(error as Error).message}`);
|
|
27
32
|
// Fallback or simplified schema if registration fails
|
|
@@ -37,7 +42,7 @@ export function generateOpenApiSpec(
|
|
|
37
42
|
name: key,
|
|
38
43
|
in: inType,
|
|
39
44
|
required: !val.isOptional(),
|
|
40
|
-
schema: registerSchema(`${inType}_${key}
|
|
45
|
+
schema: registerSchema(`${inType}_${key}`, val), // Unique name for registration
|
|
41
46
|
description: val.description,
|
|
42
47
|
}));
|
|
43
48
|
}
|
|
@@ -48,76 +53,77 @@ export function generateOpenApiSpec(
|
|
|
48
53
|
required: true, // Assuming body is required if schema is provided
|
|
49
54
|
content: {
|
|
50
55
|
'application/json': {
|
|
51
|
-
schema: registerSchema(
|
|
56
|
+
schema: registerSchema('RequestBody', schema), // Unique name
|
|
52
57
|
},
|
|
53
58
|
},
|
|
54
59
|
};
|
|
55
60
|
}
|
|
56
61
|
|
|
57
|
-
// Iterate over
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
62
|
+
// Iterate over multiple API definitions to register routes
|
|
63
|
+
definitionArray.forEach((definition) => {
|
|
64
|
+
Object.keys(definition.endpoints).forEach(domainNameKey => {
|
|
65
|
+
// domainNameKey is a string, representing the domain like 'users', 'products'
|
|
66
|
+
const domain = definition.endpoints[domainNameKey];
|
|
67
|
+
Object.keys(domain).forEach(routeNameKey => {
|
|
68
|
+
// routeNameKey is a string, representing the route name like 'getUser', 'createProduct'
|
|
69
|
+
const route: RouteSchema = domain[routeNameKey];
|
|
70
|
+
|
|
71
|
+
const parameters: any[] = [];
|
|
72
|
+
if (route.params) {
|
|
73
|
+
parameters.push(...zodSchemaToOpenApiParameter(route.params, 'path'));
|
|
74
|
+
}
|
|
75
|
+
if (route.query) {
|
|
76
|
+
parameters.push(...zodSchemaToOpenApiParameter(route.query, 'query'));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const requestBody = zodSchemaToOpenApiRequestBody(route.body);
|
|
80
|
+
|
|
81
|
+
const responses: any = {};
|
|
82
|
+
for (const statusCode in route.responses) {
|
|
83
|
+
const responseSchema = route.responses[parseInt(statusCode)];
|
|
84
|
+
if (responseSchema) {
|
|
85
|
+
responses[statusCode] = {
|
|
86
|
+
description: `Response for status code ${statusCode}`,
|
|
87
|
+
content: {
|
|
88
|
+
'application/json': {
|
|
89
|
+
schema: registerSchema(`Response_${statusCode}_${routeNameKey}_${domainNameKey}`, responseSchema),
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Add 422 response if not already defined, as it's a default in createResponses
|
|
97
|
+
// Assuming route.responses[422] would exist if it's a standard part of the definition
|
|
98
|
+
if (!responses['422'] && route.responses && route.responses[422]) {
|
|
99
|
+
responses['422'] = {
|
|
100
|
+
description: 'Validation Error',
|
|
81
101
|
content: {
|
|
82
102
|
'application/json': {
|
|
83
|
-
schema: registerSchema(`
|
|
103
|
+
schema: registerSchema(`Response_422_${routeNameKey}_${domainNameKey}`, route.responses[422]),
|
|
84
104
|
},
|
|
85
105
|
},
|
|
86
106
|
};
|
|
87
107
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
content: {
|
|
96
|
-
'application/json': {
|
|
97
|
-
schema: registerSchema(`Response_422_${routeNameKey}_${domainNameKey}`, route.responses[422]),
|
|
98
|
-
},
|
|
99
|
-
},
|
|
108
|
+
|
|
109
|
+
const operation = {
|
|
110
|
+
summary: `${domainNameKey} - ${routeNameKey}`, // Use keys directly for summary
|
|
111
|
+
tags: [domainNameKey], // Use domainNameKey for tags
|
|
112
|
+
parameters: parameters.length > 0 ? parameters : undefined,
|
|
113
|
+
requestBody: requestBody,
|
|
114
|
+
responses: responses,
|
|
100
115
|
};
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
// Register the route with the registry
|
|
113
|
-
// The path needs to be transformed from Express-style (:param) to OpenAPI-style ({param})
|
|
114
|
-
const openApiPath = route.path.replace(/:(\w+)/g, '{$1}');
|
|
115
|
-
|
|
116
|
-
registry.registerPath({
|
|
117
|
-
method: route.method.toLowerCase() as any, // Ensure method is lowercase
|
|
118
|
-
path: openApiPath,
|
|
119
|
-
...operation,
|
|
120
|
-
// Add description or other OpenAPI fields if available in RouteSchema
|
|
116
|
+
|
|
117
|
+
// Register the route with the registry
|
|
118
|
+
// The path needs to be transformed from Express-style (:param) to OpenAPI-style ({param})
|
|
119
|
+
const openApiPath = route.path.replace(/:(\w+)/g, '{$1}');
|
|
120
|
+
|
|
121
|
+
registry.registerPath({
|
|
122
|
+
method: route.method.toLowerCase() as any, // Ensure method is lowercase
|
|
123
|
+
path: openApiPath,
|
|
124
|
+
...operation,
|
|
125
|
+
// Add description or other OpenAPI fields if available in RouteSchema
|
|
126
|
+
});
|
|
121
127
|
});
|
|
122
128
|
});
|
|
123
129
|
});
|
|
@@ -1,269 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from '@jest/globals';
|
|
2
|
-
import fetch from 'node-fetch';
|
|
3
|
-
import { z } from 'zod';
|
|
4
|
-
import { CreateApiDefinition, CreateResponses, RegisterHandlers } from '../src';
|
|
5
|
-
import express from 'express';
|
|
6
|
-
import { Server } from 'http';
|
|
7
|
-
|
|
8
|
-
describe('Strict Validation Tests', () => {
|
|
9
|
-
let server: Server;
|
|
10
|
-
const port = 3004;
|
|
11
|
-
const baseUrl = `http://localhost:${port}`;
|
|
12
|
-
|
|
13
|
-
beforeAll(async () => {
|
|
14
|
-
await startTestServer();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
afterAll(async () => {
|
|
18
|
-
if (server) {
|
|
19
|
-
server.close();
|
|
20
|
-
}
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
async function startTestServer(): Promise<void> {
|
|
24
|
-
return new Promise((resolve) => {
|
|
25
|
-
// Create API definition with strict schemas
|
|
26
|
-
const StrictApiDefinition = CreateApiDefinition({
|
|
27
|
-
prefix: '/api',
|
|
28
|
-
endpoints: {
|
|
29
|
-
test: {
|
|
30
|
-
strictResponse: {
|
|
31
|
-
path: '/strict-response',
|
|
32
|
-
method: 'GET',
|
|
33
|
-
responses: CreateResponses({
|
|
34
|
-
200: z.object({
|
|
35
|
-
name: z.string(),
|
|
36
|
-
age: z.number()
|
|
37
|
-
})
|
|
38
|
-
})
|
|
39
|
-
},
|
|
40
|
-
strictBody: {
|
|
41
|
-
path: '/strict-body',
|
|
42
|
-
method: 'POST',
|
|
43
|
-
body: z.object({
|
|
44
|
-
title: z.string(),
|
|
45
|
-
count: z.number()
|
|
46
|
-
}),
|
|
47
|
-
responses: CreateResponses({
|
|
48
|
-
200: z.object({
|
|
49
|
-
success: z.boolean()
|
|
50
|
-
})
|
|
51
|
-
})
|
|
52
|
-
},
|
|
53
|
-
strictQuery: {
|
|
54
|
-
path: '/strict-query',
|
|
55
|
-
method: 'GET',
|
|
56
|
-
query: z.object({
|
|
57
|
-
filter: z.string(),
|
|
58
|
-
limit: z.number()
|
|
59
|
-
}),
|
|
60
|
-
responses: CreateResponses({
|
|
61
|
-
200: z.object({
|
|
62
|
-
results: z.array(z.string())
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const app = express();
|
|
71
|
-
app.use(express.json());
|
|
72
|
-
|
|
73
|
-
RegisterHandlers(app, StrictApiDefinition, {
|
|
74
|
-
test: {
|
|
75
|
-
strictResponse: async (req, res) => {
|
|
76
|
-
// Try to send response with extra properties
|
|
77
|
-
// This should fail due to strict validation
|
|
78
|
-
const responseData = {
|
|
79
|
-
name: 'John',
|
|
80
|
-
age: 30,
|
|
81
|
-
// These extra properties should cause validation to fail
|
|
82
|
-
extraProperty: 'should not be allowed',
|
|
83
|
-
anotherExtra: 123
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
res.respond(200, responseData);
|
|
87
|
-
},
|
|
88
|
-
strictBody: async (req, res) => {
|
|
89
|
-
// The request body should be strictly validated
|
|
90
|
-
// Extra properties in the request should cause validation errors
|
|
91
|
-
res.respond(200, { success: true });
|
|
92
|
-
},
|
|
93
|
-
strictQuery: async (req, res) => {
|
|
94
|
-
// Query parameters should be strictly validated
|
|
95
|
-
res.respond(200, { results: ['item1', 'item2'] });
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
server = app.listen(port, () => {
|
|
101
|
-
resolve();
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
test('should fail when response contains extra properties', async () => {
|
|
107
|
-
// This should return a 500 error because the response contains extra properties
|
|
108
|
-
const response = await fetch(`${baseUrl}/api/strict-response`);
|
|
109
|
-
|
|
110
|
-
expect(response.status).toBe(500);
|
|
111
|
-
|
|
112
|
-
const data = await response.json();
|
|
113
|
-
expect(data).toHaveProperty('error');
|
|
114
|
-
expect(Array.isArray(data.error)).toBe(true);
|
|
115
|
-
expect(data.error[0].message).toContain('Internal server error');
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
test('should fail when request body contains extra properties', async () => {
|
|
119
|
-
const requestBody = {
|
|
120
|
-
title: 'Test Title',
|
|
121
|
-
count: 5,
|
|
122
|
-
// Extra properties that should cause validation to fail
|
|
123
|
-
extraField: 'not allowed',
|
|
124
|
-
anotherField: true
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
const response = await fetch(`${baseUrl}/api/strict-body`, {
|
|
128
|
-
method: 'POST',
|
|
129
|
-
headers: {
|
|
130
|
-
'Content-Type': 'application/json'
|
|
131
|
-
},
|
|
132
|
-
body: JSON.stringify(requestBody)
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
expect(response.status).toBe(422);
|
|
136
|
-
|
|
137
|
-
const data = await response.json();
|
|
138
|
-
expect(data).toHaveProperty('error');
|
|
139
|
-
expect(Array.isArray(data.error)).toBe(true);
|
|
140
|
-
|
|
141
|
-
// Should contain validation errors for the extra properties
|
|
142
|
-
const errorMessages = data.error.map((err: any) => err.message);
|
|
143
|
-
expect(errorMessages.some((msg: string) => msg.includes('Unrecognized key'))).toBe(true);
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
test('should fail when query parameters contain extra properties', async () => {
|
|
147
|
-
// Add extra query parameters that aren't in the schema
|
|
148
|
-
const queryParams = new URLSearchParams({
|
|
149
|
-
filter: 'test',
|
|
150
|
-
limit: '10',
|
|
151
|
-
// Extra parameters that should cause validation to fail
|
|
152
|
-
extraParam: 'not allowed',
|
|
153
|
-
anotherParam: 'also not allowed'
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
const response = await fetch(`${baseUrl}/api/strict-query?${queryParams}`);
|
|
157
|
-
|
|
158
|
-
expect(response.status).toBe(422);
|
|
159
|
-
|
|
160
|
-
const data = await response.json();
|
|
161
|
-
expect(data).toHaveProperty('error');
|
|
162
|
-
expect(Array.isArray(data.error)).toBe(true);
|
|
163
|
-
|
|
164
|
-
// Should contain validation errors for the extra query parameters
|
|
165
|
-
const errorMessages = data.error.map((err: any) => err.message);
|
|
166
|
-
expect(errorMessages.some((msg: string) => msg.includes('Unrecognized key'))).toBe(true);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test('should succeed when request matches schema exactly', async () => {
|
|
170
|
-
const requestBody = {
|
|
171
|
-
title: 'Valid Title',
|
|
172
|
-
count: 42
|
|
173
|
-
// No extra properties
|
|
174
|
-
};
|
|
175
|
-
|
|
176
|
-
const response = await fetch(`${baseUrl}/api/strict-body`, {
|
|
177
|
-
method: 'POST',
|
|
178
|
-
headers: {
|
|
179
|
-
'Content-Type': 'application/json'
|
|
180
|
-
},
|
|
181
|
-
body: JSON.stringify(requestBody)
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
expect(response.status).toBe(200);
|
|
185
|
-
|
|
186
|
-
const data = await response.json();
|
|
187
|
-
expect(data).toHaveProperty('data');
|
|
188
|
-
expect(data.data.success).toBe(true);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test('should succeed when query parameters match schema exactly', async () => {
|
|
192
|
-
const queryParams = new URLSearchParams({
|
|
193
|
-
filter: 'test-filter',
|
|
194
|
-
limit: '5'
|
|
195
|
-
// No extra parameters
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
const response = await fetch(`${baseUrl}/api/strict-query?${queryParams}`);
|
|
199
|
-
|
|
200
|
-
expect(response.status).toBe(200);
|
|
201
|
-
|
|
202
|
-
const data = await response.json();
|
|
203
|
-
expect(data).toHaveProperty('data');
|
|
204
|
-
expect(data.data.results).toEqual(['item1', 'item2']);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
describe('Schema Definition Strictness', () => {
|
|
208
|
-
test('CreateResponses should make schemas strict', () => {
|
|
209
|
-
const responses = CreateResponses({
|
|
210
|
-
200: z.object({
|
|
211
|
-
name: z.string(),
|
|
212
|
-
age: z.number()
|
|
213
|
-
})
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
const testData = {
|
|
217
|
-
data: {
|
|
218
|
-
name: 'John',
|
|
219
|
-
age: 30,
|
|
220
|
-
extraProperty: 'should fail'
|
|
221
|
-
},
|
|
222
|
-
error: null
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
// This should fail validation due to strict mode
|
|
226
|
-
const result = responses[200].safeParse(testData);
|
|
227
|
-
expect(result.success).toBe(false);
|
|
228
|
-
|
|
229
|
-
if (!result.success) {
|
|
230
|
-
const errorMessages = result.error.issues.map((err: { message: string }) => err.message);
|
|
231
|
-
expect(errorMessages.some(msg => msg.includes('Unrecognized key'))).toBe(true);
|
|
232
|
-
}
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
test('CreateApiDefinition should make all schemas strict', () => {
|
|
236
|
-
const apiDef = CreateApiDefinition({
|
|
237
|
-
endpoints: {
|
|
238
|
-
test: {
|
|
239
|
-
endpoint: {
|
|
240
|
-
path: '/test',
|
|
241
|
-
method: 'POST',
|
|
242
|
-
body: z.object({
|
|
243
|
-
name: z.string()
|
|
244
|
-
}),
|
|
245
|
-
responses: CreateResponses({
|
|
246
|
-
200: z.object({
|
|
247
|
-
success: z.boolean()
|
|
248
|
-
})
|
|
249
|
-
})
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
});
|
|
254
|
-
|
|
255
|
-
// Test that body schema is strict
|
|
256
|
-
const bodySchema = apiDef.endpoints.test.endpoint.body;
|
|
257
|
-
const bodyResult = bodySchema?.safeParse({
|
|
258
|
-
name: 'test',
|
|
259
|
-
extraField: 'should fail'
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
expect(bodyResult?.success).toBe(false);
|
|
263
|
-
if (bodyResult && !bodyResult.success) {
|
|
264
|
-
const errorMessages = bodyResult.error.issues.map((err: { message: string }) => err.message);
|
|
265
|
-
expect(errorMessages.some(msg => msg.includes('Unrecognized key'))).toBe(true);
|
|
266
|
-
}
|
|
267
|
-
});
|
|
268
|
-
});
|
|
269
|
-
});
|