bxo 0.0.5-dev.60 → 0.0.5-dev.61
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 +3 -9
- package/src/utils/index.ts +12 -25
- package/tests/unit/utils.test.ts +39 -37
package/package.json
CHANGED
package/src/types/index.ts
CHANGED
|
@@ -3,15 +3,13 @@ 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
|
-
? InferZodType<T>
|
|
14
|
-
: T extends StatusResponseSchema
|
|
12
|
+
export type InferResponseType<T> = T extends StatusResponseSchema
|
|
15
13
|
? { [K in keyof T]: InferZodType<T[K]> }[number]
|
|
16
14
|
: never;
|
|
17
15
|
|
|
@@ -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;
|
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
|
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'
|