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 CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bxo",
3
3
  "module": "index.ts",
4
- "version": "0.0.5-dev.60",
4
+ "version": "0.0.5-dev.62",
5
5
  "description": "A simple and lightweight web framework for Bun",
6
6
  "type": "module",
7
7
  "exports": {
@@ -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 = ResponseSchema | StatusResponseSchema;
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 ResponseSchema
13
- ? InferZodType<T>
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
- ) => Promise<InferResponseType<TConfig['response']> | any> | InferResponseType<TConfig['response']> | any;
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 {
@@ -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 both simple and status-based schemas)
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
- // If it's a simple schema (not status-based)
54
- if ('parse' in responseConfig && typeof responseConfig.parse === 'function') {
55
- return responseConfig.parse(data);
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 it's a status-based schema
59
- if (typeof responseConfig === 'object' && !('parse' in responseConfig)) {
60
- const statusSchema = responseConfig[status];
61
- if (statusSchema) {
62
- return statusSchema.parse(data);
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,
@@ -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(schema, data);
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'