bxo 0.0.5-dev.60 → 0.0.5-dev.62
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/package.json +1 -1
- package/src/types/index.ts +7 -11
- package/src/utils/index.ts +12 -25
- package/tests/integration/bxo.test.ts +18 -18
- package/tests/unit/utils.test.ts +39 -37
package/package.json
CHANGED
package/src/types/index.ts
CHANGED
|
@@ -3,16 +3,14 @@ import { z } from 'zod';
|
|
|
3
3
|
// Type utilities for extracting types from Zod schemas
|
|
4
4
|
export type InferZodType<T> = T extends z.ZodType<infer U> ? U : never;
|
|
5
5
|
|
|
6
|
-
// Response configuration types
|
|
6
|
+
// Response configuration types - only allow Record<number, schema> for better maintainability
|
|
7
7
|
export type ResponseSchema = z.ZodSchema<any>;
|
|
8
8
|
export type StatusResponseSchema = Record<number, ResponseSchema>;
|
|
9
|
-
export type ResponseConfig =
|
|
9
|
+
export type ResponseConfig = StatusResponseSchema;
|
|
10
10
|
|
|
11
11
|
// Type utility to extract response type from response config
|
|
12
|
-
export type InferResponseType<T> = T extends
|
|
13
|
-
?
|
|
14
|
-
: T extends StatusResponseSchema
|
|
15
|
-
? { [K in keyof T]: InferZodType<T[K]> }[number]
|
|
12
|
+
export type InferResponseType<T> = T extends StatusResponseSchema
|
|
13
|
+
? T[keyof T] extends z.ZodType<infer U> ? U : never
|
|
16
14
|
: never;
|
|
17
15
|
|
|
18
16
|
// Cookie options interface for setting cookies
|
|
@@ -75,15 +73,11 @@ export type Context<TConfig extends RouteConfig = {}> = {
|
|
|
75
73
|
? T extends keyof TConfig['response']
|
|
76
74
|
? InferZodType<TConfig['response'][T]>
|
|
77
75
|
: any
|
|
78
|
-
: TConfig['response'] extends ResponseSchema
|
|
79
|
-
? InferZodType<TConfig['response']>
|
|
80
76
|
: any
|
|
81
77
|
) => TConfig['response'] extends StatusResponseSchema
|
|
82
78
|
? T extends keyof TConfig['response']
|
|
83
79
|
? InferZodType<TConfig['response'][T]>
|
|
84
80
|
: any
|
|
85
|
-
: TConfig['response'] extends ResponseSchema
|
|
86
|
-
? InferZodType<TConfig['response']>
|
|
87
81
|
: any;
|
|
88
82
|
redirect: (location: string, status?: number) => Response;
|
|
89
83
|
clearRedirect: () => void;
|
|
@@ -106,7 +100,9 @@ export interface InternalCookie {
|
|
|
106
100
|
// Handler function type with proper response typing
|
|
107
101
|
export type Handler<TConfig extends RouteConfig = {}, EC = {}> = (
|
|
108
102
|
ctx: Context<TConfig> & EC
|
|
109
|
-
) =>
|
|
103
|
+
) => TConfig['response'] extends StatusResponseSchema
|
|
104
|
+
? Promise<InferResponseType<TConfig['response']>> | InferResponseType<TConfig['response']>
|
|
105
|
+
: Promise<any> | any;
|
|
110
106
|
|
|
111
107
|
// Route definition
|
|
112
108
|
export interface Route {
|
package/src/utils/index.ts
CHANGED
|
@@ -42,7 +42,7 @@ export function validateData<T>(schema: z.ZodSchema<T> | undefined, data: any):
|
|
|
42
42
|
return schema.parse(data);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
// Validate response against response config (supports
|
|
45
|
+
// Validate response against response config (only supports Record<number, schema> format)
|
|
46
46
|
export function validateResponse(
|
|
47
47
|
responseConfig: ResponseConfig | undefined,
|
|
48
48
|
data: any,
|
|
@@ -50,31 +50,22 @@ export function validateResponse(
|
|
|
50
50
|
): any {
|
|
51
51
|
if (!responseConfig) return data;
|
|
52
52
|
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
53
|
+
// Get the schema for the specific status code
|
|
54
|
+
const statusSchema = responseConfig[status];
|
|
55
|
+
if (statusSchema) {
|
|
56
|
+
return statusSchema.parse(data);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
// If
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
// If no specific status schema found, try to find a fallback
|
|
60
|
+
// Common fallback statuses: 200, 201, 400, 500
|
|
61
|
+
const fallbackStatuses = [200, 201, 400, 500];
|
|
62
|
+
for (const fallbackStatus of fallbackStatuses) {
|
|
63
|
+
if (responseConfig[fallbackStatus]) {
|
|
64
|
+
return responseConfig[fallbackStatus]?.parse(data);
|
|
63
65
|
}
|
|
64
|
-
|
|
65
|
-
// If no specific status schema found, try to find a fallback
|
|
66
|
-
// Common fallback statuses: 200, 201, 400, 500
|
|
67
|
-
const fallbackStatuses = [200, 201, 400, 500];
|
|
68
|
-
for (const fallbackStatus of fallbackStatuses) {
|
|
69
|
-
if (responseConfig[fallbackStatus]) {
|
|
70
|
-
return responseConfig[fallbackStatus]?.parse(data);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
// If no schema found for the status, return data as-is
|
|
75
|
-
return data;
|
|
76
66
|
}
|
|
77
67
|
|
|
68
|
+
// If no schema found for the status, return data as-is
|
|
78
69
|
return data;
|
|
79
70
|
}
|
|
80
71
|
|
|
@@ -175,10 +166,6 @@ function setNestedValue(obj: any, baseKey: string, path: string[], value: any, i
|
|
|
175
166
|
// Numeric key - treat as array index
|
|
176
167
|
const index = parseInt(lastKey, 10);
|
|
177
168
|
if (Array.isArray(current)) {
|
|
178
|
-
// Ensure array is large enough
|
|
179
|
-
while (current.length <= index) {
|
|
180
|
-
current.push(undefined);
|
|
181
|
-
}
|
|
182
169
|
current[index] = value;
|
|
183
170
|
} else {
|
|
184
171
|
// Convert to array if needed
|
|
@@ -214,14 +214,14 @@ describe('BXO Framework Integration', () => {
|
|
|
214
214
|
body: formData
|
|
215
215
|
});
|
|
216
216
|
|
|
217
|
-
const data = await response.json() as {
|
|
218
|
-
formData: {
|
|
219
|
-
app: string,
|
|
220
|
-
model: string,
|
|
221
|
-
recordIds: string[],
|
|
222
|
-
fields: string
|
|
223
|
-
},
|
|
224
|
-
message: string
|
|
217
|
+
const data = await response.json() as {
|
|
218
|
+
formData: {
|
|
219
|
+
app: string,
|
|
220
|
+
model: string,
|
|
221
|
+
recordIds: string[],
|
|
222
|
+
fields: string
|
|
223
|
+
},
|
|
224
|
+
message: string
|
|
225
225
|
};
|
|
226
226
|
|
|
227
227
|
expect(response.status).toBe(200);
|
|
@@ -248,8 +248,8 @@ describe('BXO Framework Integration', () => {
|
|
|
248
248
|
body: formData
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
-
const data = await response.json() as {
|
|
252
|
-
formData: {
|
|
251
|
+
const data = await response.json() as {
|
|
252
|
+
formData: {
|
|
253
253
|
test: {
|
|
254
254
|
test: string,
|
|
255
255
|
new: string,
|
|
@@ -257,8 +257,8 @@ describe('BXO Framework Integration', () => {
|
|
|
257
257
|
hi: string
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
|
-
},
|
|
261
|
-
message: string
|
|
260
|
+
},
|
|
261
|
+
message: string
|
|
262
262
|
};
|
|
263
263
|
|
|
264
264
|
expect(response.status).toBe(200);
|
|
@@ -288,8 +288,8 @@ describe('BXO Framework Integration', () => {
|
|
|
288
288
|
body: formData
|
|
289
289
|
});
|
|
290
290
|
|
|
291
|
-
const data = await response.json() as {
|
|
292
|
-
formData: {
|
|
291
|
+
const data = await response.json() as {
|
|
292
|
+
formData: {
|
|
293
293
|
records: Array<{
|
|
294
294
|
qrPayment: {
|
|
295
295
|
type: string;
|
|
@@ -299,8 +299,8 @@ describe('BXO Framework Integration', () => {
|
|
|
299
299
|
},
|
|
300
300
|
name: string
|
|
301
301
|
}>
|
|
302
|
-
},
|
|
303
|
-
message: string
|
|
302
|
+
},
|
|
303
|
+
message: string
|
|
304
304
|
};
|
|
305
305
|
|
|
306
306
|
expect(response.status).toBe(200);
|
|
@@ -339,8 +339,8 @@ describe('BXO Framework Integration', () => {
|
|
|
339
339
|
console.log('File name:', records[0]?.qrPayment.name);
|
|
340
340
|
console.log('File size:', records[0]?.qrPayment.size);
|
|
341
341
|
console.log('File type:', records[0]?.qrPayment.type);
|
|
342
|
-
|
|
343
|
-
return {
|
|
342
|
+
|
|
343
|
+
return {
|
|
344
344
|
message: 'Files received',
|
|
345
345
|
fileInfo: {
|
|
346
346
|
isFile0: records[0]?.qrPayment instanceof File,
|
package/tests/unit/utils.test.ts
CHANGED
|
@@ -22,7 +22,7 @@ describe('Utility Functions', () => {
|
|
|
22
22
|
it('should parse URLSearchParams correctly', () => {
|
|
23
23
|
const searchParams = new URLSearchParams('name=john&age=25&city=new%20york');
|
|
24
24
|
const result = parseQuery(searchParams);
|
|
25
|
-
|
|
25
|
+
|
|
26
26
|
expect(result).toEqual({
|
|
27
27
|
name: 'john',
|
|
28
28
|
age: '25',
|
|
@@ -33,7 +33,7 @@ describe('Utility Functions', () => {
|
|
|
33
33
|
it('should handle empty search params', () => {
|
|
34
34
|
const searchParams = new URLSearchParams('');
|
|
35
35
|
const result = parseQuery(searchParams);
|
|
36
|
-
|
|
36
|
+
|
|
37
37
|
expect(result).toEqual({});
|
|
38
38
|
});
|
|
39
39
|
});
|
|
@@ -43,9 +43,9 @@ describe('Utility Functions', () => {
|
|
|
43
43
|
const headers = new Headers();
|
|
44
44
|
headers.set('content-type', 'application/json');
|
|
45
45
|
headers.set('authorization', 'Bearer token123');
|
|
46
|
-
|
|
46
|
+
|
|
47
47
|
const result = parseHeaders(headers);
|
|
48
|
-
|
|
48
|
+
|
|
49
49
|
expect(result).toEqual({
|
|
50
50
|
'content-type': 'application/json',
|
|
51
51
|
'authorization': 'Bearer token123'
|
|
@@ -57,7 +57,7 @@ describe('Utility Functions', () => {
|
|
|
57
57
|
it('should parse cookie header correctly', () => {
|
|
58
58
|
const cookieHeader = 'name=john; age=25; city=new%20york';
|
|
59
59
|
const result = parseCookies(cookieHeader);
|
|
60
|
-
|
|
60
|
+
|
|
61
61
|
expect(result).toEqual({
|
|
62
62
|
name: 'john',
|
|
63
63
|
age: '25',
|
|
@@ -82,17 +82,17 @@ describe('Utility Functions', () => {
|
|
|
82
82
|
name: z.string(),
|
|
83
83
|
age: z.number()
|
|
84
84
|
});
|
|
85
|
-
|
|
85
|
+
|
|
86
86
|
const data = { name: 'john', age: 25 };
|
|
87
87
|
const result = validateData(schema, data);
|
|
88
|
-
|
|
88
|
+
|
|
89
89
|
expect(result).toEqual(data);
|
|
90
90
|
});
|
|
91
91
|
|
|
92
92
|
it('should return data without validation when no schema', () => {
|
|
93
93
|
const data = { name: 'john', age: 25 };
|
|
94
94
|
const result = validateData(undefined, data);
|
|
95
|
-
|
|
95
|
+
|
|
96
96
|
expect(result).toEqual(data);
|
|
97
97
|
});
|
|
98
98
|
|
|
@@ -101,9 +101,9 @@ describe('Utility Functions', () => {
|
|
|
101
101
|
name: z.string(),
|
|
102
102
|
age: z.number()
|
|
103
103
|
});
|
|
104
|
-
|
|
104
|
+
|
|
105
105
|
const data = { name: 'john', age: 'invalid' };
|
|
106
|
-
|
|
106
|
+
|
|
107
107
|
expect(() => validateData(schema, data)).toThrow();
|
|
108
108
|
});
|
|
109
109
|
});
|
|
@@ -113,10 +113,12 @@ describe('Utility Functions', () => {
|
|
|
113
113
|
const schema = z.object({
|
|
114
114
|
message: z.string()
|
|
115
115
|
});
|
|
116
|
-
|
|
116
|
+
|
|
117
117
|
const data = { message: 'success' };
|
|
118
|
-
const result = validateResponse(
|
|
119
|
-
|
|
118
|
+
const result = validateResponse({
|
|
119
|
+
200: schema
|
|
120
|
+
}, data, 200);
|
|
121
|
+
|
|
120
122
|
expect(result).toEqual(data);
|
|
121
123
|
});
|
|
122
124
|
|
|
@@ -125,17 +127,17 @@ describe('Utility Functions', () => {
|
|
|
125
127
|
200: z.object({ message: z.string() }),
|
|
126
128
|
400: z.object({ error: z.string() })
|
|
127
129
|
};
|
|
128
|
-
|
|
130
|
+
|
|
129
131
|
const data = { message: 'success' };
|
|
130
132
|
const result = validateResponse(schema, data, 200);
|
|
131
|
-
|
|
133
|
+
|
|
132
134
|
expect(result).toEqual(data);
|
|
133
135
|
});
|
|
134
136
|
|
|
135
137
|
it('should return data without validation when no schema', () => {
|
|
136
138
|
const data = { message: 'success' };
|
|
137
139
|
const result = validateResponse(undefined, data);
|
|
138
|
-
|
|
140
|
+
|
|
139
141
|
expect(result).toEqual(data);
|
|
140
142
|
});
|
|
141
143
|
});
|
|
@@ -152,9 +154,9 @@ describe('Utility Functions', () => {
|
|
|
152
154
|
httpOnly: true
|
|
153
155
|
}
|
|
154
156
|
];
|
|
155
|
-
|
|
157
|
+
|
|
156
158
|
const result = cookiesToHeaders(cookies);
|
|
157
|
-
|
|
159
|
+
|
|
158
160
|
expect(result).toHaveLength(1);
|
|
159
161
|
expect(result[0]).toContain('session=abc123');
|
|
160
162
|
expect(result[0]).toContain('Domain=example.com');
|
|
@@ -170,9 +172,9 @@ describe('Utility Functions', () => {
|
|
|
170
172
|
const cookies = [
|
|
171
173
|
{ name: 'session', value: 'abc123' }
|
|
172
174
|
];
|
|
173
|
-
|
|
175
|
+
|
|
174
176
|
const result = mergeHeadersWithCookies(headers, cookies);
|
|
175
|
-
|
|
177
|
+
|
|
176
178
|
expect(result.get('content-type')).toBe('application/json');
|
|
177
179
|
expect(result.get('set-cookie')).toBe('session=abc123');
|
|
178
180
|
});
|
|
@@ -181,14 +183,14 @@ describe('Utility Functions', () => {
|
|
|
181
183
|
describe('createRedirectResponse', () => {
|
|
182
184
|
it('should create redirect response with default status', () => {
|
|
183
185
|
const result = createRedirectResponse('/new-location');
|
|
184
|
-
|
|
186
|
+
|
|
185
187
|
expect(result.status).toBe(302);
|
|
186
188
|
expect(result.headers.get('location')).toBe('/new-location');
|
|
187
189
|
});
|
|
188
190
|
|
|
189
191
|
it('should create redirect response with custom status', () => {
|
|
190
192
|
const result = createRedirectResponse('/new-location', 301);
|
|
191
|
-
|
|
193
|
+
|
|
192
194
|
expect(result.status).toBe(301);
|
|
193
195
|
expect(result.headers.get('location')).toBe('/new-location');
|
|
194
196
|
});
|
|
@@ -196,7 +198,7 @@ describe('Utility Functions', () => {
|
|
|
196
198
|
it('should include additional headers', () => {
|
|
197
199
|
const headers = { 'x-custom': 'value' };
|
|
198
200
|
const result = createRedirectResponse('/new-location', 302, headers);
|
|
199
|
-
|
|
201
|
+
|
|
200
202
|
expect(result.headers.get('x-custom')).toBe('value');
|
|
201
203
|
});
|
|
202
204
|
});
|
|
@@ -228,7 +230,7 @@ describe('Utility Functions', () => {
|
|
|
228
230
|
});
|
|
229
231
|
|
|
230
232
|
const result = await parseRequestBody(request);
|
|
231
|
-
|
|
233
|
+
|
|
232
234
|
expect(result.app).toBe('zodula');
|
|
233
235
|
expect(result.model).toBe('zodula_User');
|
|
234
236
|
expect(Array.isArray(result.recordIds)).toBe(true);
|
|
@@ -247,7 +249,7 @@ describe('Utility Functions', () => {
|
|
|
247
249
|
});
|
|
248
250
|
|
|
249
251
|
const result = await parseRequestBody(request);
|
|
250
|
-
|
|
252
|
+
|
|
251
253
|
expect(result.name).toBe('john');
|
|
252
254
|
expect(result.email).toBe('john@example.com');
|
|
253
255
|
});
|
|
@@ -264,7 +266,7 @@ describe('Utility Functions', () => {
|
|
|
264
266
|
});
|
|
265
267
|
|
|
266
268
|
const result = await parseRequestBody(request);
|
|
267
|
-
|
|
269
|
+
|
|
268
270
|
expect(result.name).toBe('john');
|
|
269
271
|
expect(result.file).toBeInstanceOf(File);
|
|
270
272
|
expect(result.file.name).toBe('test.txt');
|
|
@@ -282,7 +284,7 @@ describe('Utility Functions', () => {
|
|
|
282
284
|
});
|
|
283
285
|
|
|
284
286
|
const result = await parseRequestBody(request);
|
|
285
|
-
|
|
287
|
+
|
|
286
288
|
expect(result.test).toEqual({
|
|
287
289
|
test: 'test',
|
|
288
290
|
new: 'new',
|
|
@@ -313,7 +315,7 @@ describe('Utility Functions', () => {
|
|
|
313
315
|
});
|
|
314
316
|
|
|
315
317
|
const result = await parseRequestBody(request);
|
|
316
|
-
|
|
318
|
+
|
|
317
319
|
expect(result.x).toBe('1');
|
|
318
320
|
expect(result.arr).toEqual(['1', '2', '3']);
|
|
319
321
|
expect(result.arr2).toEqual(['1', ['2'], '3']);
|
|
@@ -335,7 +337,7 @@ describe('Utility Functions', () => {
|
|
|
335
337
|
});
|
|
336
338
|
|
|
337
339
|
const result = await parseRequestBody(request);
|
|
338
|
-
|
|
340
|
+
|
|
339
341
|
expect(result.myObj).toEqual({ x: 1, s: 'foo' });
|
|
340
342
|
expect(result.settings).toEqual({ theme: 'dark', notifications: true });
|
|
341
343
|
});
|
|
@@ -354,7 +356,7 @@ describe('Utility Functions', () => {
|
|
|
354
356
|
});
|
|
355
357
|
|
|
356
358
|
const result = await parseRequestBody(request);
|
|
357
|
-
|
|
359
|
+
|
|
358
360
|
expect(result.tags).toEqual(['javascript', 'typescript']);
|
|
359
361
|
expect(result.scores).toEqual(['100', '95', '88']);
|
|
360
362
|
});
|
|
@@ -363,7 +365,7 @@ describe('Utility Functions', () => {
|
|
|
363
365
|
describe('File Upload Utilities', () => {
|
|
364
366
|
it('should identify file uploads correctly', () => {
|
|
365
367
|
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
|
|
366
|
-
|
|
368
|
+
|
|
367
369
|
expect(isFileUpload(file)).toBe(true);
|
|
368
370
|
expect(isFileUpload('not a file')).toBe(false);
|
|
369
371
|
expect(isFileUpload({ type: 'text' })).toBe(false);
|
|
@@ -371,7 +373,7 @@ describe('Utility Functions', () => {
|
|
|
371
373
|
|
|
372
374
|
it('should extract file from upload', () => {
|
|
373
375
|
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
|
|
374
|
-
|
|
376
|
+
|
|
375
377
|
const result = getFileFromUpload(file);
|
|
376
378
|
expect(result).toBe(file);
|
|
377
379
|
});
|
|
@@ -383,7 +385,7 @@ describe('Utility Functions', () => {
|
|
|
383
385
|
|
|
384
386
|
it('should get file info', () => {
|
|
385
387
|
const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
|
|
386
|
-
|
|
388
|
+
|
|
387
389
|
const result = getFileInfo(file);
|
|
388
390
|
expect(result).toEqual({
|
|
389
391
|
name: 'test.jpg',
|
|
@@ -405,9 +407,9 @@ describe('Utility Functions', () => {
|
|
|
405
407
|
avatar: file,
|
|
406
408
|
email: 'john@example.com'
|
|
407
409
|
};
|
|
408
|
-
|
|
410
|
+
|
|
409
411
|
const result = getFileUploads(formData);
|
|
410
|
-
|
|
412
|
+
|
|
411
413
|
expect(Object.keys(result)).toHaveLength(1);
|
|
412
414
|
expect(result.avatar).toBeInstanceOf(File);
|
|
413
415
|
});
|
|
@@ -419,9 +421,9 @@ describe('Utility Functions', () => {
|
|
|
419
421
|
avatar: file,
|
|
420
422
|
email: 'john@example.com'
|
|
421
423
|
};
|
|
422
|
-
|
|
424
|
+
|
|
423
425
|
const result = getFormFields(formData);
|
|
424
|
-
|
|
426
|
+
|
|
425
427
|
expect(result).toEqual({
|
|
426
428
|
name: 'john',
|
|
427
429
|
email: 'john@example.com'
|