cushin-monorepo 3.0.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/.changeset/README.md +8 -0
- package/.changeset/config.json +14 -0
- package/.claude/settings.local.json +44 -0
- package/CHANGELOG.md +93 -0
- package/LICENSE +0 -0
- package/README.md +482 -0
- package/biome.json +34 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1552 -0
- package/dist/cli.js.map +1 -0
- package/dist/config/index.d.ts +84 -0
- package/dist/config/index.js +69 -0
- package/dist/config/index.js.map +1 -0
- package/dist/config/schema.d.ts +43 -0
- package/dist/config/schema.js +14 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/index.d.ts +27 -0
- package/dist/index.js +1666 -0
- package/dist/index.js.map +1 -0
- package/dist/runtime/client.d.ts +40 -0
- package/dist/runtime/client.js +260 -0
- package/dist/runtime/client.js.map +1 -0
- package/package.json +41 -0
- package/packages/api-codegen/CHANGELOG.md +86 -0
- package/packages/api-codegen/biome.json +34 -0
- package/packages/api-codegen/dist/cli.js +1038 -0
- package/packages/api-codegen/dist/cli.js.map +1 -0
- package/packages/api-codegen/dist/index.d.ts +103 -0
- package/packages/api-codegen/dist/index.js +1026 -0
- package/packages/api-codegen/dist/index.js.map +1 -0
- package/packages/api-codegen/node_modules/.bin/acorn +21 -0
- package/packages/api-codegen/node_modules/.bin/conventional-changelog +21 -0
- package/packages/api-codegen/node_modules/.bin/conventional-commits-parser +21 -0
- package/packages/api-codegen/node_modules/.bin/esbuild +21 -0
- package/packages/api-codegen/node_modules/.bin/eslint +21 -0
- package/packages/api-codegen/node_modules/.bin/jiti +21 -0
- package/packages/api-codegen/node_modules/.bin/next +21 -0
- package/packages/api-codegen/node_modules/.bin/tsc +21 -0
- package/packages/api-codegen/node_modules/.bin/tsserver +21 -0
- package/packages/api-codegen/node_modules/.bin/tsup +21 -0
- package/packages/api-codegen/node_modules/.bin/tsup-node +21 -0
- package/packages/api-codegen/node_modules/.bin/vitest +21 -0
- package/packages/api-codegen/package.json +88 -0
- package/packages/api-runtime/CHANGELOG.md +46 -0
- package/packages/api-runtime/README.md +95 -0
- package/packages/api-runtime/dist/chunk-3FFXWCVP.js +17 -0
- package/packages/api-runtime/dist/chunk-3FFXWCVP.js.map +1 -0
- package/packages/api-runtime/dist/chunk-EZ5P7OPH.js +267 -0
- package/packages/api-runtime/dist/chunk-EZ5P7OPH.js.map +1 -0
- package/packages/api-runtime/dist/client.d.ts +40 -0
- package/packages/api-runtime/dist/client.js +13 -0
- package/packages/api-runtime/dist/client.js.map +1 -0
- package/packages/api-runtime/dist/index.d.ts +3 -0
- package/packages/api-runtime/dist/index.js +21 -0
- package/packages/api-runtime/dist/index.js.map +1 -0
- package/packages/api-runtime/dist/schema.d.ts +45 -0
- package/packages/api-runtime/dist/schema.js +11 -0
- package/packages/api-runtime/dist/schema.js.map +1 -0
- package/packages/api-runtime/node_modules/.bin/esbuild +21 -0
- package/packages/api-runtime/node_modules/.bin/jiti +21 -0
- package/packages/api-runtime/node_modules/.bin/tsc +21 -0
- package/packages/api-runtime/node_modules/.bin/tsserver +21 -0
- package/packages/api-runtime/node_modules/.bin/tsup +21 -0
- package/packages/api-runtime/node_modules/.bin/tsup-node +21 -0
- package/packages/api-runtime/package.json +54 -0
- package/packages/cli/CHANGELOG.md +34 -0
- package/packages/cli/biome.json +34 -0
- package/packages/cli/dist/index.d.ts +27 -0
- package/packages/cli/dist/index.js +183 -0
- package/packages/cli/dist/index.js.map +1 -0
- package/packages/cli/node_modules/.bin/esbuild +21 -0
- package/packages/cli/node_modules/.bin/jiti +21 -0
- package/packages/cli/node_modules/.bin/tsc +21 -0
- package/packages/cli/node_modules/.bin/tsserver +21 -0
- package/packages/cli/node_modules/.bin/tsup +21 -0
- package/packages/cli/node_modules/.bin/tsup-node +21 -0
- package/packages/cli/package.json +47 -0
- package/pnpm-workspace.yaml +2 -0
- package/test-config.js +9 -0
- package/test-content-type-handling.mjs +100 -0
- package/test-endpoints-config.mjs +144 -0
- package/test-formdata-content-type-protection.mjs +127 -0
- package/test-formdata-runtime.mjs +127 -0
- package/test-full-integration.mjs +90 -0
- package/test-headers-formdata.mjs +97 -0
- package/test-headers-runtime.mjs +106 -0
- package/test-headers.mjs +79 -0
- package/test-internal-calls.mjs +57 -0
- package/test-ky-formdata.mjs +81 -0
- package/test-output/actions.ts +134 -0
- package/test-output/client.ts +81 -0
- package/test-output/hooks.ts +182 -0
- package/test-output/index.ts +9 -0
- package/test-output/prefetchs.ts +25 -0
- package/test-output/queries.ts +78 -0
- package/test-output/query-keys.ts +16 -0
- package/test-output/query-options.ts +38 -0
- package/test-output/server-client.ts +32 -0
- package/test-output/types.ts +61 -0
- package/test-real-endpoints.mjs +132 -0
- package/test-runtime-params.mjs +160 -0
- package/test-simple-config.mjs +71 -0
- package/tsconfig.base.json +29 -0
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive test for Content-Type handling with JSON and FormData
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createAPIClient, defineEndpoint } from './packages/api-runtime/dist/index.js';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
console.log('\n🧪 Testing Content-Type Handling\n');
|
|
9
|
+
|
|
10
|
+
// Define endpoints
|
|
11
|
+
const jsonEndpoint = defineEndpoint({
|
|
12
|
+
path: 'post',
|
|
13
|
+
method: 'POST',
|
|
14
|
+
body: z.object({ message: z.string() }),
|
|
15
|
+
response: z.any(),
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const formDataEndpoint = defineEndpoint({
|
|
19
|
+
path: 'post',
|
|
20
|
+
method: 'POST',
|
|
21
|
+
response: z.any(),
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const getEndpoint = defineEndpoint({
|
|
25
|
+
path: 'get',
|
|
26
|
+
method: 'GET',
|
|
27
|
+
response: z.any(),
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Create API client
|
|
31
|
+
const apiClient = createAPIClient({
|
|
32
|
+
baseUrl: 'https://httpbin.org',
|
|
33
|
+
endpoints: {
|
|
34
|
+
jsonRequest: jsonEndpoint,
|
|
35
|
+
formDataRequest: formDataEndpoint,
|
|
36
|
+
getRequest: getEndpoint,
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Track requests
|
|
41
|
+
const requests = [];
|
|
42
|
+
const originalFetch = globalThis.fetch;
|
|
43
|
+
|
|
44
|
+
globalThis.fetch = async (input, init) => {
|
|
45
|
+
if (input instanceof Request) {
|
|
46
|
+
const contentType = input.headers.get('content-type') || input.headers.get('Content-Type');
|
|
47
|
+
requests.push({
|
|
48
|
+
url: input.url,
|
|
49
|
+
method: input.method,
|
|
50
|
+
contentType,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
54
|
+
status: 200,
|
|
55
|
+
headers: { 'content-type': 'application/json' },
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return originalFetch(input, init);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
console.log('Test 1: JSON body with Zod schema');
|
|
64
|
+
await apiClient.jsonRequest({ message: 'Hello, World!' });
|
|
65
|
+
console.log(' Method:', requests[0].method);
|
|
66
|
+
console.log(' Content-Type:', requests[0].contentType);
|
|
67
|
+
if (requests[0].contentType === 'application/json') {
|
|
68
|
+
console.log(' ✅ PASS: Content-Type is application/json\n');
|
|
69
|
+
} else {
|
|
70
|
+
console.log(' ❌ FAIL: Expected application/json, got', requests[0].contentType, '\n');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
console.log('Test 2: FormData body');
|
|
74
|
+
const formData = new FormData();
|
|
75
|
+
formData.append('file', new Blob(['test'], { type: 'text/plain' }), 'test.txt');
|
|
76
|
+
await apiClient.formDataRequest(formData);
|
|
77
|
+
console.log(' Method:', requests[1].method);
|
|
78
|
+
console.log(' Content-Type:', requests[1].contentType);
|
|
79
|
+
if (requests[1].contentType && requests[1].contentType.startsWith('multipart/form-data')) {
|
|
80
|
+
console.log(' ✅ PASS: Content-Type is multipart/form-data with boundary\n');
|
|
81
|
+
} else {
|
|
82
|
+
console.log(' ❌ FAIL: Expected multipart/form-data, got', requests[1].contentType, '\n');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
console.log('Test 3: GET request (no body)');
|
|
86
|
+
await apiClient.getRequest();
|
|
87
|
+
console.log(' Method:', requests[2].method);
|
|
88
|
+
console.log(' Content-Type:', requests[2].contentType);
|
|
89
|
+
if (requests[2].contentType === 'application/json') {
|
|
90
|
+
console.log(' ✅ PASS: Content-Type is application/json for GET\n');
|
|
91
|
+
} else {
|
|
92
|
+
console.log(' ❌ FAIL: Expected application/json for GET, got', requests[2].contentType, '\n');
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log('✅ All tests passed!\n');
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('❌ Error:', error);
|
|
98
|
+
} finally {
|
|
99
|
+
globalThis.fetch = originalFetch;
|
|
100
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Test configuration với các endpoint scenarios khác nhau
|
|
2
|
+
// Mock z.object để không cần import zod
|
|
3
|
+
const z = {
|
|
4
|
+
object: (shape) => ({ _type: "object", shape }),
|
|
5
|
+
array: (item) => ({ _type: "array", item }),
|
|
6
|
+
string: () => ({ _type: "string" }),
|
|
7
|
+
number: () => ({ _type: "number" }),
|
|
8
|
+
boolean: () => ({ _type: "boolean" }),
|
|
9
|
+
enum: (...values) => ({ _type: "enum", values }),
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
z.string.prototype.email = function() { return this; };
|
|
13
|
+
z.number.prototype.optional = function() { return this; };
|
|
14
|
+
z.string.prototype.optional = function() { return this; };
|
|
15
|
+
|
|
16
|
+
export const testEndpointsConfig = {
|
|
17
|
+
baseUrl: "https://api.example.com",
|
|
18
|
+
endpoints: {
|
|
19
|
+
// Case 1: GET - No params, no query
|
|
20
|
+
listUsers: {
|
|
21
|
+
path: "/users",
|
|
22
|
+
method: "GET",
|
|
23
|
+
response: z.object({
|
|
24
|
+
users: z.array(z.object({ id: z.string(), name: z.string() })),
|
|
25
|
+
}),
|
|
26
|
+
description: "List all users",
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// Case 2: GET - Has params only
|
|
30
|
+
getUser: {
|
|
31
|
+
path: "/users/:id",
|
|
32
|
+
method: "GET",
|
|
33
|
+
params: z.object({ id: z.string() }),
|
|
34
|
+
response: z.object({
|
|
35
|
+
id: z.string(),
|
|
36
|
+
name: z.string(),
|
|
37
|
+
email: z.string(),
|
|
38
|
+
}),
|
|
39
|
+
description: "Get user by ID",
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
// Case 3: GET - Has query only
|
|
43
|
+
searchUsers: {
|
|
44
|
+
path: "/users/search",
|
|
45
|
+
method: "GET",
|
|
46
|
+
query: z.object({
|
|
47
|
+
q: z.string(),
|
|
48
|
+
limit: z.number().optional(),
|
|
49
|
+
}),
|
|
50
|
+
response: z.object({
|
|
51
|
+
results: z.array(z.object({ id: z.string(), name: z.string() })),
|
|
52
|
+
}),
|
|
53
|
+
description: "Search users",
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
// Case 4: GET - Has params and query
|
|
57
|
+
getUserPosts: {
|
|
58
|
+
path: "/users/:id/posts",
|
|
59
|
+
method: "GET",
|
|
60
|
+
params: z.object({ id: z.string() }),
|
|
61
|
+
query: z.object({
|
|
62
|
+
status: z.enum(["draft", "published"]).optional(),
|
|
63
|
+
limit: z.number().optional(),
|
|
64
|
+
}),
|
|
65
|
+
response: z.object({
|
|
66
|
+
posts: z.array(z.object({ id: z.string(), title: z.string() })),
|
|
67
|
+
}),
|
|
68
|
+
description: "Get user posts",
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
// Case 5: POST - No params, no body (tạo session)
|
|
72
|
+
createSession: {
|
|
73
|
+
path: "/sessions",
|
|
74
|
+
method: "POST",
|
|
75
|
+
response: z.object({
|
|
76
|
+
sessionId: z.string(),
|
|
77
|
+
expiresAt: z.string(),
|
|
78
|
+
}),
|
|
79
|
+
tags: ["auth"],
|
|
80
|
+
description: "Create new session",
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// Case 6: POST - Has body only
|
|
84
|
+
createUser: {
|
|
85
|
+
path: "/users",
|
|
86
|
+
method: "POST",
|
|
87
|
+
body: z.object({
|
|
88
|
+
name: z.string(),
|
|
89
|
+
email: z.string().email(),
|
|
90
|
+
}),
|
|
91
|
+
response: z.object({
|
|
92
|
+
id: z.string(),
|
|
93
|
+
name: z.string(),
|
|
94
|
+
email: z.string(),
|
|
95
|
+
}),
|
|
96
|
+
tags: ["users"],
|
|
97
|
+
description: "Create new user",
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// Case 7: POST - Has params and body
|
|
101
|
+
updateUser: {
|
|
102
|
+
path: "/users/:id",
|
|
103
|
+
method: "POST",
|
|
104
|
+
params: z.object({ id: z.string() }),
|
|
105
|
+
body: z.object({
|
|
106
|
+
name: z.string().optional(),
|
|
107
|
+
email: z.string().email().optional(),
|
|
108
|
+
}),
|
|
109
|
+
response: z.object({
|
|
110
|
+
id: z.string(),
|
|
111
|
+
name: z.string(),
|
|
112
|
+
email: z.string(),
|
|
113
|
+
}),
|
|
114
|
+
tags: ["users"],
|
|
115
|
+
description: "Update user",
|
|
116
|
+
},
|
|
117
|
+
|
|
118
|
+
// Case 8: DELETE - Has params only
|
|
119
|
+
deleteUser: {
|
|
120
|
+
path: "/users/:id",
|
|
121
|
+
method: "DELETE",
|
|
122
|
+
params: z.object({ id: z.string() }),
|
|
123
|
+
response: z.object({
|
|
124
|
+
success: z.boolean(),
|
|
125
|
+
}),
|
|
126
|
+
tags: ["users"],
|
|
127
|
+
description: "Delete user",
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
// Case 9: PATCH - No params, no body (refresh token)
|
|
131
|
+
refreshToken: {
|
|
132
|
+
path: "/auth/refresh",
|
|
133
|
+
method: "PATCH",
|
|
134
|
+
response: z.object({
|
|
135
|
+
accessToken: z.string(),
|
|
136
|
+
refreshToken: z.string(),
|
|
137
|
+
}),
|
|
138
|
+
tags: ["auth"],
|
|
139
|
+
description: "Refresh access token",
|
|
140
|
+
},
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export default testEndpointsConfig;
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test that Content-Type is NEVER manually set for FormData,
|
|
3
|
+
* even if endpoint has custom Content-Type header
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { createAPIClient, defineEndpoint } from './packages/api-runtime/dist/index.js';
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
console.log('\n🧪 Testing FormData Content-Type Protection\n');
|
|
10
|
+
|
|
11
|
+
// Define endpoint WITH custom Content-Type header (this should be IGNORED for FormData)
|
|
12
|
+
const uploadWithCustomHeader = defineEndpoint({
|
|
13
|
+
path: 'post',
|
|
14
|
+
method: 'POST',
|
|
15
|
+
headers: {
|
|
16
|
+
'Content-Type': 'application/json', // This should be IGNORED for FormData
|
|
17
|
+
'X-Custom-Header': 'test-value', // This should be preserved
|
|
18
|
+
},
|
|
19
|
+
response: z.any(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
// Define endpoint WITHOUT custom headers
|
|
23
|
+
const uploadNoHeaders = defineEndpoint({
|
|
24
|
+
path: 'post',
|
|
25
|
+
method: 'POST',
|
|
26
|
+
response: z.any(),
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
// Create API client
|
|
30
|
+
const apiClient = createAPIClient({
|
|
31
|
+
baseUrl: 'https://httpbin.org',
|
|
32
|
+
endpoints: {
|
|
33
|
+
uploadWithHeader: uploadWithCustomHeader,
|
|
34
|
+
uploadNoHeader: uploadNoHeaders,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Mock fetch to capture requests
|
|
39
|
+
const originalFetch = globalThis.fetch;
|
|
40
|
+
const requests = [];
|
|
41
|
+
|
|
42
|
+
globalThis.fetch = async (input) => {
|
|
43
|
+
if (input instanceof Request) {
|
|
44
|
+
const headers = {};
|
|
45
|
+
for (const [key, value] of input.headers.entries()) {
|
|
46
|
+
headers[key] = value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
requests.push({
|
|
50
|
+
url: input.url,
|
|
51
|
+
method: input.method,
|
|
52
|
+
headers,
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
return new Response(JSON.stringify({ success: true }), {
|
|
56
|
+
status: 200,
|
|
57
|
+
headers: { 'content-type': 'application/json' },
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
return originalFetch(input);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
try {
|
|
64
|
+
console.log('Test 1: FormData with endpoint that has Content-Type header');
|
|
65
|
+
console.log(' Expected: Content-Type should be multipart/form-data (NOT application/json)');
|
|
66
|
+
console.log(' Expected: Custom headers should still be applied\n');
|
|
67
|
+
|
|
68
|
+
const formData1 = new FormData();
|
|
69
|
+
formData1.append('file', new Blob(['test'], { type: 'text/plain' }), 'test.txt');
|
|
70
|
+
formData1.append('name', 'Test File');
|
|
71
|
+
|
|
72
|
+
await apiClient.uploadWithHeader(formData1);
|
|
73
|
+
|
|
74
|
+
const req1 = requests[0];
|
|
75
|
+
console.log(' Headers sent:');
|
|
76
|
+
Object.entries(req1.headers).forEach(([key, value]) => {
|
|
77
|
+
console.log(` ${key}: ${value}`);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const contentType1 = req1.headers['content-type'];
|
|
81
|
+
const customHeader1 = req1.headers['x-custom-header'];
|
|
82
|
+
|
|
83
|
+
if (contentType1 && contentType1.startsWith('multipart/form-data')) {
|
|
84
|
+
console.log(' ✅ PASS: Content-Type is multipart/form-data with boundary');
|
|
85
|
+
} else {
|
|
86
|
+
console.log(` ❌ FAIL: Content-Type is ${contentType1} (expected multipart/form-data)`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
if (customHeader1 === 'test-value') {
|
|
91
|
+
console.log(' ✅ PASS: Custom header X-Custom-Header is preserved\n');
|
|
92
|
+
} else {
|
|
93
|
+
console.log(` ❌ FAIL: Custom header X-Custom-Header is ${customHeader1} (expected test-value)\n`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
console.log('Test 2: FormData with endpoint without custom headers');
|
|
98
|
+
console.log(' Expected: Content-Type should be multipart/form-data\n');
|
|
99
|
+
|
|
100
|
+
const formData2 = new FormData();
|
|
101
|
+
formData2.append('file', new Blob(['test2'], { type: 'text/plain' }), 'test2.txt');
|
|
102
|
+
|
|
103
|
+
await apiClient.uploadNoHeader(formData2);
|
|
104
|
+
|
|
105
|
+
const req2 = requests[1];
|
|
106
|
+
console.log(' Headers sent:');
|
|
107
|
+
Object.entries(req2.headers).forEach(([key, value]) => {
|
|
108
|
+
console.log(` ${key}: ${value}`);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
const contentType2 = req2.headers['content-type'];
|
|
112
|
+
|
|
113
|
+
if (contentType2 && contentType2.startsWith('multipart/form-data')) {
|
|
114
|
+
console.log(' ✅ PASS: Content-Type is multipart/form-data with boundary\n');
|
|
115
|
+
} else {
|
|
116
|
+
console.log(` ❌ FAIL: Content-Type is ${contentType2} (expected multipart/form-data)\n`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
console.log('✅ All tests passed!');
|
|
121
|
+
console.log('✅ FormData Content-Type is protected from being overridden\n');
|
|
122
|
+
} catch (error) {
|
|
123
|
+
console.error('❌ Error:', error);
|
|
124
|
+
process.exit(1);
|
|
125
|
+
} finally {
|
|
126
|
+
globalThis.fetch = originalFetch;
|
|
127
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test script to verify FormData runtime behavior with Content-Type handling
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { createAPIClient, defineEndpoint } from './packages/api-runtime/dist/index.js';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
console.log('\n🧪 Testing FormData Runtime Behavior\n');
|
|
9
|
+
|
|
10
|
+
// Define upload endpoint
|
|
11
|
+
const uploadEndpoint = defineEndpoint({
|
|
12
|
+
path: 'post', // httpbin.org/post endpoint
|
|
13
|
+
method: 'POST',
|
|
14
|
+
response: z.any(), // httpbin.org returns various fields, so we use z.any() for simplicity
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
// Create API client
|
|
18
|
+
const apiClient = createAPIClient({
|
|
19
|
+
baseUrl: 'https://httpbin.org',
|
|
20
|
+
endpoints: {
|
|
21
|
+
upload: uploadEndpoint,
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Test 1: Verify FormData detection
|
|
26
|
+
console.log('✅ Test 1: Create FormData and verify detection');
|
|
27
|
+
const formData = new FormData();
|
|
28
|
+
// Use Blob for file content (Node.js FormData requirement)
|
|
29
|
+
const fileBlob = new Blob(['test content'], { type: 'text/plain' });
|
|
30
|
+
formData.append('file', fileBlob, 'test.txt');
|
|
31
|
+
formData.append('metadata', JSON.stringify({ name: 'Test File' }));
|
|
32
|
+
|
|
33
|
+
console.log(' FormData created:', formData instanceof FormData);
|
|
34
|
+
console.log(' FormData entries:', Array.from(formData.entries()).map(([k]) => k));
|
|
35
|
+
|
|
36
|
+
// Test 2: Mock request to see headers
|
|
37
|
+
console.log('\n✅ Test 2: Inspect request headers (using ky internals)');
|
|
38
|
+
|
|
39
|
+
// We'll use a mock to intercept the request
|
|
40
|
+
const originalFetch = globalThis.fetch;
|
|
41
|
+
let capturedRequest = null;
|
|
42
|
+
|
|
43
|
+
globalThis.fetch = async (input, init) => {
|
|
44
|
+
console.log(' 📦 Request intercepted!');
|
|
45
|
+
|
|
46
|
+
// Ky uses Request objects, not (url, init) pairs
|
|
47
|
+
if (input instanceof Request) {
|
|
48
|
+
console.log(' Input type: Request object');
|
|
49
|
+
console.log(' URL:', input.url);
|
|
50
|
+
console.log(' Method:', input.method);
|
|
51
|
+
console.log(' Body type:', input.body?.constructor?.name || 'none');
|
|
52
|
+
|
|
53
|
+
console.log(' Headers:');
|
|
54
|
+
for (const [key, value] of input.headers.entries()) {
|
|
55
|
+
console.log(` ${key}: ${value}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
capturedRequest = { request: input };
|
|
59
|
+
} else {
|
|
60
|
+
console.log(' Input type:', typeof input);
|
|
61
|
+
console.log(' URL:', typeof input === 'string' ? input : input.url);
|
|
62
|
+
console.log(' Method:', init?.method || 'GET');
|
|
63
|
+
console.log(' Body:', init?.body);
|
|
64
|
+
console.log(' Body type:', init?.body?.constructor?.name || 'undefined');
|
|
65
|
+
|
|
66
|
+
const headers = init?.headers;
|
|
67
|
+
if (headers) {
|
|
68
|
+
console.log(' Headers:');
|
|
69
|
+
if (headers instanceof Headers) {
|
|
70
|
+
for (const [key, value] of headers.entries()) {
|
|
71
|
+
console.log(` ${key}: ${value}`);
|
|
72
|
+
}
|
|
73
|
+
} else if (typeof headers === 'object') {
|
|
74
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
75
|
+
console.log(` ${key}: ${value === undefined ? 'undefined (will be removed)' : value}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} else {
|
|
79
|
+
console.log(' Headers: (none)');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
capturedRequest = { input, init };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Return mock response (mock httpbin response)
|
|
86
|
+
return new Response(JSON.stringify({
|
|
87
|
+
args: {},
|
|
88
|
+
files: {},
|
|
89
|
+
form: {},
|
|
90
|
+
headers: {},
|
|
91
|
+
json: null,
|
|
92
|
+
url: input instanceof Request ? input.url : (typeof input === 'string' ? input : input.url)
|
|
93
|
+
}), {
|
|
94
|
+
status: 200,
|
|
95
|
+
headers: { 'content-type': 'application/json' },
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
try {
|
|
100
|
+
// Make the request - note: upload endpoint is POST method, so we pass body as first parameter
|
|
101
|
+
console.log('\n📤 Making upload request with FormData...\n');
|
|
102
|
+
console.log(' Endpoint method:', uploadEndpoint.method);
|
|
103
|
+
console.log(' Calling apiClient.upload(formData)...');
|
|
104
|
+
|
|
105
|
+
await apiClient.upload(formData);
|
|
106
|
+
|
|
107
|
+
console.log('\n✅ Test 2 Result:');
|
|
108
|
+
if (capturedRequest?.request) {
|
|
109
|
+
const contentType = capturedRequest.request.headers.get('Content-Type') || capturedRequest.request.headers.get('content-type');
|
|
110
|
+
console.log(' Content-Type in request:', contentType);
|
|
111
|
+
|
|
112
|
+
if (contentType && contentType.startsWith('multipart/form-data')) {
|
|
113
|
+
console.log(' ✅ SUCCESS: Content-Type is correctly set to multipart/form-data with boundary!');
|
|
114
|
+
} else {
|
|
115
|
+
console.log(' ❌ FAIL: Content-Type is not multipart/form-data:', contentType);
|
|
116
|
+
}
|
|
117
|
+
} else {
|
|
118
|
+
console.log(' ❌ No request captured');
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(' ❌ Error:', error);
|
|
122
|
+
} finally {
|
|
123
|
+
// Restore original fetch
|
|
124
|
+
globalThis.fetch = originalFetch;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
console.log('\n🎉 Runtime test complete!\n');
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { createAPIClient } from "./packages/api-runtime/dist/client.js";
|
|
2
|
+
import testConfig from "./test-simple-config.mjs";
|
|
3
|
+
|
|
4
|
+
console.log("=== FULL INTEGRATION TEST ===\n");
|
|
5
|
+
console.log("Testing createAPIClient với các endpoint scenarios khác nhau\n");
|
|
6
|
+
|
|
7
|
+
const client = createAPIClient(testConfig);
|
|
8
|
+
|
|
9
|
+
// Helper để test method signatures
|
|
10
|
+
function testMethodSignature(methodName, method) {
|
|
11
|
+
console.log(`${methodName}:`);
|
|
12
|
+
console.log(` - Function length (parameters): ${method.length}`);
|
|
13
|
+
|
|
14
|
+
// Extract parameter names bằng cách parse function string
|
|
15
|
+
const funcStr = method.toString();
|
|
16
|
+
const paramsMatch = funcStr.match(/\(([^)]*)\)/);
|
|
17
|
+
const params = paramsMatch ? paramsMatch[1].trim() : "no params";
|
|
18
|
+
console.log(` - Parameters: ${params || "() - no parameters"}`);
|
|
19
|
+
console.log("");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// Test từng endpoint type
|
|
23
|
+
console.log("--- Query Endpoints (GET) ---\n");
|
|
24
|
+
|
|
25
|
+
testMethodSignature("listUsers (no params, no query)", client.listUsers);
|
|
26
|
+
testMethodSignature("getUser (params only)", client.getUser);
|
|
27
|
+
testMethodSignature("searchUsers (query only)", client.searchUsers);
|
|
28
|
+
testMethodSignature("getUserPosts (params + query)", client.getUserPosts);
|
|
29
|
+
|
|
30
|
+
console.log("\n--- Mutation Endpoints (POST/DELETE/PATCH) ---\n");
|
|
31
|
+
|
|
32
|
+
testMethodSignature("createSession (POST, no params, no body)", client.createSession);
|
|
33
|
+
testMethodSignature("createUser (POST, body only)", client.createUser);
|
|
34
|
+
testMethodSignature("updateUser (POST, params + body)", client.updateUser);
|
|
35
|
+
testMethodSignature("deleteUser (DELETE, params only)", client.deleteUser);
|
|
36
|
+
testMethodSignature("refreshToken (PATCH, no params, no body)", client.refreshToken);
|
|
37
|
+
|
|
38
|
+
// Kiểm tra generated method có đúng không
|
|
39
|
+
console.log("\n--- Verification ---\n");
|
|
40
|
+
|
|
41
|
+
const expectedSignatures = {
|
|
42
|
+
listUsers: { length: 0, desc: "no parameters" },
|
|
43
|
+
getUser: { length: 1, desc: "params" },
|
|
44
|
+
searchUsers: { length: 1, desc: "query (optional)" },
|
|
45
|
+
getUserPosts: { length: 2, desc: "params, query (optional)" },
|
|
46
|
+
createSession: { length: 0, desc: "no parameters" },
|
|
47
|
+
createUser: { length: 1, desc: "body" },
|
|
48
|
+
updateUser: { length: 2, desc: "params, body" },
|
|
49
|
+
deleteUser: { length: 1, desc: "params" },
|
|
50
|
+
refreshToken: { length: 0, desc: "no parameters" },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
let allPass = true;
|
|
54
|
+
for (const [name, expected] of Object.entries(expectedSignatures)) {
|
|
55
|
+
const actual = client[name].length;
|
|
56
|
+
const pass = actual === expected.length;
|
|
57
|
+
allPass = allPass && pass;
|
|
58
|
+
|
|
59
|
+
console.log(
|
|
60
|
+
`${pass ? "✅" : "❌"} ${name}: expected ${expected.length} params (${expected.desc}), got ${actual}`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
console.log("");
|
|
65
|
+
console.log(allPass ? "✅ ALL TESTS PASSED!" : "❌ SOME TESTS FAILED");
|
|
66
|
+
console.log("");
|
|
67
|
+
|
|
68
|
+
// Demonstrate usage examples
|
|
69
|
+
console.log("\n--- Usage Examples (no actual network calls) ---\n");
|
|
70
|
+
|
|
71
|
+
console.log("// Query endpoints:");
|
|
72
|
+
console.log("await client.listUsers() // ✅ No undefined needed");
|
|
73
|
+
console.log("await client.getUser({ id: '123' }) // ✅ Pass params");
|
|
74
|
+
console.log("await client.searchUsers({ q: 'john' }) // ✅ Pass query");
|
|
75
|
+
console.log("await client.getUserPosts({ id: '1' }, { status: 'published' })");
|
|
76
|
+
console.log("");
|
|
77
|
+
|
|
78
|
+
console.log("// Mutation endpoints:");
|
|
79
|
+
console.log("await client.createSession() // ✅ No undefined needed");
|
|
80
|
+
console.log("await client.createUser({ name: 'John', email: 'john@example.com' })");
|
|
81
|
+
console.log("await client.updateUser({ id: '1' }, { name: 'Jane' })");
|
|
82
|
+
console.log("await client.deleteUser({ id: '1' })");
|
|
83
|
+
console.log("await client.refreshToken() // ✅ No undefined needed");
|
|
84
|
+
console.log("");
|
|
85
|
+
|
|
86
|
+
console.log("--- Summary ---\n");
|
|
87
|
+
console.log("✅ Runtime client xử lý tất cả cases chính xác");
|
|
88
|
+
console.log("✅ Không cần pass undefined cho endpoints không có params/body");
|
|
89
|
+
console.log("✅ Method signatures match với endpoint definitions");
|
|
90
|
+
console.log("");
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Test script to demonstrate custom headers and FormData support
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { defineEndpoint } from './packages/api-runtime/dist/index.js';
|
|
6
|
+
import { z } from 'zod';
|
|
7
|
+
|
|
8
|
+
console.log('\n🧪 Testing Custom Headers and FormData Support\n');
|
|
9
|
+
|
|
10
|
+
// Test 1: Endpoint with custom headers
|
|
11
|
+
console.log('✅ Test 1: Define endpoint with custom headers');
|
|
12
|
+
const apiKeyEndpoint = defineEndpoint({
|
|
13
|
+
path: '/api/data',
|
|
14
|
+
method: 'POST',
|
|
15
|
+
body: z.object({
|
|
16
|
+
name: z.string(),
|
|
17
|
+
}),
|
|
18
|
+
response: z.object({
|
|
19
|
+
id: z.string(),
|
|
20
|
+
}),
|
|
21
|
+
headers: {
|
|
22
|
+
'X-API-Key': 'test-api-key-123',
|
|
23
|
+
'X-Custom-Header': 'custom-value',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
console.log(' Endpoint path:', apiKeyEndpoint.path);
|
|
28
|
+
console.log(' Custom headers:', apiKeyEndpoint.headers);
|
|
29
|
+
console.log(' ✓ Custom headers defined successfully\n');
|
|
30
|
+
|
|
31
|
+
// Test 2: FormData upload endpoint
|
|
32
|
+
console.log('✅ Test 2: Define FormData upload endpoint');
|
|
33
|
+
const uploadEndpoint = defineEndpoint({
|
|
34
|
+
path: '/users/:userId/avatar',
|
|
35
|
+
method: 'POST',
|
|
36
|
+
params: z.object({
|
|
37
|
+
userId: z.string(),
|
|
38
|
+
}),
|
|
39
|
+
response: z.object({
|
|
40
|
+
avatarUrl: z.string(),
|
|
41
|
+
}),
|
|
42
|
+
headers: {
|
|
43
|
+
'X-Upload-Source': 'test-app',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
console.log(' Endpoint path:', uploadEndpoint.path);
|
|
48
|
+
console.log(' Method:', uploadEndpoint.method);
|
|
49
|
+
console.log(' Custom headers:', uploadEndpoint.headers);
|
|
50
|
+
console.log(' ✓ FormData endpoint defined successfully\n');
|
|
51
|
+
|
|
52
|
+
// Test 3: Type checking
|
|
53
|
+
console.log('✅ Test 3: Type safety validation');
|
|
54
|
+
try {
|
|
55
|
+
// This should work - all required fields present
|
|
56
|
+
const validEndpoint = defineEndpoint({
|
|
57
|
+
path: '/test',
|
|
58
|
+
method: 'GET',
|
|
59
|
+
response: z.object({ data: z.string() }),
|
|
60
|
+
headers: {
|
|
61
|
+
'Authorization': 'Bearer token',
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
console.log(' ✓ Valid endpoint created with headers\n');
|
|
65
|
+
} catch (error) {
|
|
66
|
+
console.log(' ✗ Unexpected error:', error.message);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Test 4: Combined features
|
|
70
|
+
console.log('✅ Test 4: Combined custom headers + FormData + baseUrl override');
|
|
71
|
+
const advancedEndpoint = defineEndpoint({
|
|
72
|
+
path: '/upload/documents',
|
|
73
|
+
method: 'POST',
|
|
74
|
+
baseUrl: 'https://upload.example.com',
|
|
75
|
+
response: z.object({
|
|
76
|
+
documentIds: z.array(z.string()),
|
|
77
|
+
}),
|
|
78
|
+
headers: {
|
|
79
|
+
'X-Upload-Type': 'bulk',
|
|
80
|
+
'X-Client-Version': '1.0.0',
|
|
81
|
+
},
|
|
82
|
+
tags: ['uploads', 'documents'],
|
|
83
|
+
description: 'Bulk document upload',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
console.log(' Endpoint path:', advancedEndpoint.path);
|
|
87
|
+
console.log(' Base URL:', advancedEndpoint.baseUrl);
|
|
88
|
+
console.log(' Headers:', advancedEndpoint.headers);
|
|
89
|
+
console.log(' Tags:', advancedEndpoint.tags);
|
|
90
|
+
console.log(' ✓ Advanced endpoint with all features\n');
|
|
91
|
+
|
|
92
|
+
console.log('🎉 All tests passed!\n');
|
|
93
|
+
console.log('📝 Summary:');
|
|
94
|
+
console.log(' - Custom headers can be added to any endpoint');
|
|
95
|
+
console.log(' - FormData is automatically detected at runtime');
|
|
96
|
+
console.log(' - Headers work with all other endpoint features');
|
|
97
|
+
console.log(' - Type safety is maintained throughout\n');
|