bxo 0.0.5-dev.6 โ†’ 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.
@@ -0,0 +1,433 @@
1
+ import { describe, it, expect } from 'bun:test';
2
+ import {
3
+ parseQuery,
4
+ parseHeaders,
5
+ parseCookies,
6
+ validateData,
7
+ validateResponse,
8
+ parseRequestBody,
9
+ cookiesToHeaders,
10
+ mergeHeadersWithCookies,
11
+ createRedirectResponse,
12
+ isFileUpload,
13
+ getFileFromUpload,
14
+ getFileInfo,
15
+ getFileUploads,
16
+ getFormFields
17
+ } from '../../src/utils';
18
+ import { z } from 'zod';
19
+
20
+ describe('Utility Functions', () => {
21
+ describe('parseQuery', () => {
22
+ it('should parse URLSearchParams correctly', () => {
23
+ const searchParams = new URLSearchParams('name=john&age=25&city=new%20york');
24
+ const result = parseQuery(searchParams);
25
+
26
+ expect(result).toEqual({
27
+ name: 'john',
28
+ age: '25',
29
+ city: 'new york'
30
+ });
31
+ });
32
+
33
+ it('should handle empty search params', () => {
34
+ const searchParams = new URLSearchParams('');
35
+ const result = parseQuery(searchParams);
36
+
37
+ expect(result).toEqual({});
38
+ });
39
+ });
40
+
41
+ describe('parseHeaders', () => {
42
+ it('should parse Headers object correctly', () => {
43
+ const headers = new Headers();
44
+ headers.set('content-type', 'application/json');
45
+ headers.set('authorization', 'Bearer token123');
46
+
47
+ const result = parseHeaders(headers);
48
+
49
+ expect(result).toEqual({
50
+ 'content-type': 'application/json',
51
+ 'authorization': 'Bearer token123'
52
+ });
53
+ });
54
+ });
55
+
56
+ describe('parseCookies', () => {
57
+ it('should parse cookie header correctly', () => {
58
+ const cookieHeader = 'name=john; age=25; city=new%20york';
59
+ const result = parseCookies(cookieHeader);
60
+
61
+ expect(result).toEqual({
62
+ name: 'john',
63
+ age: '25',
64
+ city: 'new york'
65
+ });
66
+ });
67
+
68
+ it('should handle null cookie header', () => {
69
+ const result = parseCookies(null);
70
+ expect(result).toEqual({});
71
+ });
72
+
73
+ it('should handle empty cookie header', () => {
74
+ const result = parseCookies('');
75
+ expect(result).toEqual({});
76
+ });
77
+ });
78
+
79
+ describe('validateData', () => {
80
+ it('should validate data with schema', () => {
81
+ const schema = z.object({
82
+ name: z.string(),
83
+ age: z.number()
84
+ });
85
+
86
+ const data = { name: 'john', age: 25 };
87
+ const result = validateData(schema, data);
88
+
89
+ expect(result).toEqual(data);
90
+ });
91
+
92
+ it('should return data without validation when no schema', () => {
93
+ const data = { name: 'john', age: 25 };
94
+ const result = validateData(undefined, data);
95
+
96
+ expect(result).toEqual(data);
97
+ });
98
+
99
+ it('should throw error for invalid data', () => {
100
+ const schema = z.object({
101
+ name: z.string(),
102
+ age: z.number()
103
+ });
104
+
105
+ const data = { name: 'john', age: 'invalid' };
106
+
107
+ expect(() => validateData(schema, data)).toThrow();
108
+ });
109
+ });
110
+
111
+ describe('validateResponse', () => {
112
+ it('should validate response with simple schema', () => {
113
+ const schema = z.object({
114
+ message: z.string()
115
+ });
116
+
117
+ const data = { message: 'success' };
118
+ const result = validateResponse({
119
+ 200: schema
120
+ }, data, 200);
121
+
122
+ expect(result).toEqual(data);
123
+ });
124
+
125
+ it('should validate response with status-based schema', () => {
126
+ const schema = {
127
+ 200: z.object({ message: z.string() }),
128
+ 400: z.object({ error: z.string() })
129
+ };
130
+
131
+ const data = { message: 'success' };
132
+ const result = validateResponse(schema, data, 200);
133
+
134
+ expect(result).toEqual(data);
135
+ });
136
+
137
+ it('should return data without validation when no schema', () => {
138
+ const data = { message: 'success' };
139
+ const result = validateResponse(undefined, data);
140
+
141
+ expect(result).toEqual(data);
142
+ });
143
+ });
144
+
145
+ describe('cookiesToHeaders', () => {
146
+ it('should convert internal cookies to header strings', () => {
147
+ const cookies = [
148
+ {
149
+ name: 'session',
150
+ value: 'abc123',
151
+ domain: 'example.com',
152
+ path: '/',
153
+ secure: true,
154
+ httpOnly: true
155
+ }
156
+ ];
157
+
158
+ const result = cookiesToHeaders(cookies);
159
+
160
+ expect(result).toHaveLength(1);
161
+ expect(result[0]).toContain('session=abc123');
162
+ expect(result[0]).toContain('Domain=example.com');
163
+ expect(result[0]).toContain('Path=/');
164
+ expect(result[0]).toContain('Secure');
165
+ expect(result[0]).toContain('HttpOnly');
166
+ });
167
+ });
168
+
169
+ describe('mergeHeadersWithCookies', () => {
170
+ it('should merge headers with cookies', () => {
171
+ const headers = { 'content-type': 'application/json' };
172
+ const cookies = [
173
+ { name: 'session', value: 'abc123' }
174
+ ];
175
+
176
+ const result = mergeHeadersWithCookies(headers, cookies);
177
+
178
+ expect(result.get('content-type')).toBe('application/json');
179
+ expect(result.get('set-cookie')).toBe('session=abc123');
180
+ });
181
+ });
182
+
183
+ describe('createRedirectResponse', () => {
184
+ it('should create redirect response with default status', () => {
185
+ const result = createRedirectResponse('/new-location');
186
+
187
+ expect(result.status).toBe(302);
188
+ expect(result.headers.get('location')).toBe('/new-location');
189
+ });
190
+
191
+ it('should create redirect response with custom status', () => {
192
+ const result = createRedirectResponse('/new-location', 301);
193
+
194
+ expect(result.status).toBe(301);
195
+ expect(result.headers.get('location')).toBe('/new-location');
196
+ });
197
+
198
+ it('should include additional headers', () => {
199
+ const headers = { 'x-custom': 'value' };
200
+ const result = createRedirectResponse('/new-location', 302, headers);
201
+
202
+ expect(result.headers.get('x-custom')).toBe('value');
203
+ });
204
+ });
205
+
206
+ describe('parseRequestBody', () => {
207
+ it('should parse JSON body correctly', async () => {
208
+ const jsonData = { name: 'john', age: 25 };
209
+ const request = new Request('http://localhost/test', {
210
+ method: 'POST',
211
+ headers: { 'Content-Type': 'application/json' },
212
+ body: JSON.stringify(jsonData)
213
+ });
214
+
215
+ const result = await parseRequestBody(request);
216
+ expect(result).toEqual(jsonData);
217
+ });
218
+
219
+ it('should parse form data with arrays correctly', async () => {
220
+ const formData = new FormData();
221
+ formData.append('app', 'zodula');
222
+ formData.append('model', 'zodula_User');
223
+ formData.append('recordIds[]', 'asdfasdfdsa');
224
+ formData.append('recordIds[]', 'jarupak.sri@gmail.com');
225
+ formData.append('fields', '*');
226
+
227
+ const request = new Request('http://localhost/test', {
228
+ method: 'POST',
229
+ body: formData
230
+ });
231
+
232
+ const result = await parseRequestBody(request);
233
+
234
+ expect(result.app).toBe('zodula');
235
+ expect(result.model).toBe('zodula_User');
236
+ expect(Array.isArray(result.recordIds)).toBe(true);
237
+ expect(result.recordIds).toEqual(['asdfasdfdsa', 'jarupak.sri@gmail.com']);
238
+ expect(result.fields).toBe('*');
239
+ });
240
+
241
+ it('should handle single form field correctly', async () => {
242
+ const formData = new FormData();
243
+ formData.append('name', 'john');
244
+ formData.append('email', 'john@example.com');
245
+
246
+ const request = new Request('http://localhost/test', {
247
+ method: 'POST',
248
+ body: formData
249
+ });
250
+
251
+ const result = await parseRequestBody(request);
252
+
253
+ expect(result.name).toBe('john');
254
+ expect(result.email).toBe('john@example.com');
255
+ });
256
+
257
+ it('should handle file uploads in form data', async () => {
258
+ const file = new File(['test content'], 'test.txt', { type: 'text/plain' });
259
+ const formData = new FormData();
260
+ formData.append('name', 'john');
261
+ formData.append('file', file);
262
+
263
+ const request = new Request('http://localhost/test', {
264
+ method: 'POST',
265
+ body: formData
266
+ });
267
+
268
+ const result = await parseRequestBody(request);
269
+
270
+ expect(result.name).toBe('john');
271
+ expect(result.file).toBeInstanceOf(File);
272
+ expect(result.file.name).toBe('test.txt');
273
+ });
274
+
275
+ it('should parse nested object form data correctly', async () => {
276
+ const formData = new FormData();
277
+ formData.append('test[test]', 'test');
278
+ formData.append('test[new]', 'new');
279
+ formData.append('test[hi][hi]', 'hi');
280
+
281
+ const request = new Request('http://localhost/test', {
282
+ method: 'POST',
283
+ body: formData
284
+ });
285
+
286
+ const result = await parseRequestBody(request);
287
+
288
+ expect(result.test).toEqual({
289
+ test: 'test',
290
+ new: 'new',
291
+ hi: {
292
+ hi: 'hi'
293
+ }
294
+ });
295
+ });
296
+
297
+ it('should parse Axios-compatible multipart data with arrays and objects', async () => {
298
+ const formData = new FormData();
299
+ formData.append('x', '1');
300
+ formData.append('arr[]', '1');
301
+ formData.append('arr[]', '2');
302
+ formData.append('arr[]', '3');
303
+ formData.append('arr2[0]', '1');
304
+ formData.append('arr2[1][0]', '2');
305
+ formData.append('arr2[2]', '3');
306
+ formData.append('users[0][name]', 'Peter');
307
+ formData.append('users[0][surname]', 'Griffin');
308
+ formData.append('users[1][name]', 'Thomas');
309
+ formData.append('users[1][surname]', 'Anderson');
310
+ formData.append('obj2{}', '[{"x":1}]');
311
+
312
+ const request = new Request('http://localhost/test', {
313
+ method: 'POST',
314
+ body: formData
315
+ });
316
+
317
+ const result = await parseRequestBody(request);
318
+
319
+ expect(result.x).toBe('1');
320
+ expect(result.arr).toEqual(['1', '2', '3']);
321
+ expect(result.arr2).toEqual(['1', ['2'], '3']);
322
+ expect(result.users).toEqual([
323
+ { name: 'Peter', surname: 'Griffin' },
324
+ { name: 'Thomas', surname: 'Anderson' }
325
+ ]);
326
+ expect(result.obj2).toEqual([{ x: 1 }]);
327
+ });
328
+
329
+ it('should handle JSON serialization with special endings', async () => {
330
+ const formData = new FormData();
331
+ formData.append('myObj{}', '{"x": 1, "s": "foo"}');
332
+ formData.append('settings{}', '{"theme": "dark", "notifications": true}');
333
+
334
+ const request = new Request('http://localhost/test', {
335
+ method: 'POST',
336
+ body: formData
337
+ });
338
+
339
+ const result = await parseRequestBody(request);
340
+
341
+ expect(result.myObj).toEqual({ x: 1, s: 'foo' });
342
+ expect(result.settings).toEqual({ theme: 'dark', notifications: true });
343
+ });
344
+
345
+ it('should handle mixed array indexing styles', async () => {
346
+ const formData = new FormData();
347
+ formData.append('tags[]', 'javascript');
348
+ formData.append('tags[]', 'typescript');
349
+ formData.append('scores[0]', '100');
350
+ formData.append('scores[1]', '95');
351
+ formData.append('scores[2]', '88');
352
+
353
+ const request = new Request('http://localhost/test', {
354
+ method: 'POST',
355
+ body: formData
356
+ });
357
+
358
+ const result = await parseRequestBody(request);
359
+
360
+ expect(result.tags).toEqual(['javascript', 'typescript']);
361
+ expect(result.scores).toEqual(['100', '95', '88']);
362
+ });
363
+ });
364
+
365
+ describe('File Upload Utilities', () => {
366
+ it('should identify file uploads correctly', () => {
367
+ const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
368
+
369
+ expect(isFileUpload(file)).toBe(true);
370
+ expect(isFileUpload('not a file')).toBe(false);
371
+ expect(isFileUpload({ type: 'text' })).toBe(false);
372
+ });
373
+
374
+ it('should extract file from upload', () => {
375
+ const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
376
+
377
+ const result = getFileFromUpload(file);
378
+ expect(result).toBe(file);
379
+ });
380
+
381
+ it('should return null for non-file uploads', () => {
382
+ const result = getFileFromUpload('not a file');
383
+ expect(result).toBeNull();
384
+ });
385
+
386
+ it('should get file info', () => {
387
+ const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
388
+
389
+ const result = getFileInfo(file);
390
+ expect(result).toEqual({
391
+ name: 'test.jpg',
392
+ size: 4,
393
+ mimetype: 'image/jpeg',
394
+ lastModified: expect.any(Number)
395
+ });
396
+ });
397
+
398
+ it('should return null for non-file uploads when getting info', () => {
399
+ const result = getFileInfo('not a file');
400
+ expect(result).toBeNull();
401
+ });
402
+
403
+ it('should get all file uploads from form data', () => {
404
+ const file = new File(['test'], 'avatar.jpg', { type: 'image/jpeg' });
405
+ const formData = {
406
+ name: 'john',
407
+ avatar: file,
408
+ email: 'john@example.com'
409
+ };
410
+
411
+ const result = getFileUploads(formData);
412
+
413
+ expect(Object.keys(result)).toHaveLength(1);
414
+ expect(result.avatar).toBeInstanceOf(File);
415
+ });
416
+
417
+ it('should get all non-file fields from form data', () => {
418
+ const file = new File(['test'], 'avatar.jpg', { type: 'image/jpeg' });
419
+ const formData = {
420
+ name: 'john',
421
+ avatar: file,
422
+ email: 'john@example.com'
423
+ };
424
+
425
+ const result = getFormFields(formData);
426
+
427
+ expect(result).toEqual({
428
+ name: 'john',
429
+ email: 'john@example.com'
430
+ });
431
+ });
432
+ });
433
+ });
package/example.ts DELETED
@@ -1,183 +0,0 @@
1
- import BXO, { z } from './index';
2
- import { cors, logger, auth, rateLimit, createJWT } from './plugins';
3
-
4
- // Create the app instance
5
- const app = new BXO();
6
-
7
- // Enable hot reload
8
- app.enableHotReload(['./']); // Watch current directory
9
-
10
- // Add plugins
11
- app
12
- .use(logger({ format: 'simple' }))
13
- .use(cors({
14
- origin: ['http://localhost:3000', 'https://example.com'],
15
- credentials: true
16
- }))
17
- .use(rateLimit({
18
- max: 100,
19
- window: 60, // 1 minute
20
- exclude: ['/health']
21
- }))
22
- .use(auth({
23
- type: 'jwt',
24
- secret: 'your-secret-key',
25
- exclude: ['/', '/login', '/health']
26
- }));
27
-
28
- // Add simplified lifecycle hooks
29
- app
30
- .onBeforeStart(() => {
31
- console.log('๐Ÿ”ง Preparing to start server...');
32
- })
33
- .onAfterStart(() => {
34
- console.log('โœ… Server fully started and ready!');
35
- })
36
- .onBeforeStop(() => {
37
- console.log('๐Ÿ”ง Preparing to stop server...');
38
- })
39
- .onAfterStop(() => {
40
- console.log('โœ… Server fully stopped!');
41
- })
42
- .onBeforeRestart(() => {
43
- console.log('๐Ÿ”ง Preparing to restart server...');
44
- })
45
- .onAfterRestart(() => {
46
- console.log('โœ… Server restart completed!');
47
- })
48
- .onRequest((ctx) => {
49
- console.log(`๐Ÿ“จ Processing ${ctx.request.method} ${ctx.request.url}`);
50
- })
51
- .onResponse((ctx, response) => {
52
- console.log(`๐Ÿ“ค Response sent for ${ctx.request.method} ${ctx.request.url}`);
53
- return response;
54
- })
55
- .onError((ctx, error) => {
56
- console.error(`๐Ÿ’ฅ Error in ${ctx.request.method} ${ctx.request.url}:`, error.message);
57
- return { error: 'Something went wrong', timestamp: new Date().toISOString() };
58
- });
59
-
60
- // Routes exactly like your example
61
- app
62
- // Two arguments: path, handler
63
- .get('/simple', async (ctx) => {
64
- return { message: 'Hello World' };
65
- })
66
-
67
- // Three arguments: path, handler, config
68
- .get('/users/:id', async (ctx) => {
69
- // ctx.params.id is fully typed as string (UUID)
70
- // ctx.query.include is typed as string | undefined
71
- return { user: { id: ctx.params.id, include: ctx.query.include } };
72
- }, {
73
- params: z.object({ id: z.string().uuid() }),
74
- query: z.object({ include: z.string().optional() })
75
- })
76
-
77
- .post('/users', async (ctx) => {
78
- // ctx.body is fully typed with name: string, email: string
79
- return { created: ctx.body };
80
- }, {
81
- body: z.object({
82
- name: z.string(),
83
- email: z.string().email()
84
- })
85
- })
86
-
87
- // Additional examples
88
- .get('/health', async (ctx) => {
89
- return {
90
- status: 'ok',
91
- timestamp: new Date().toISOString(),
92
- server: app.getServerInfo()
93
- };
94
- })
95
-
96
- .post('/login', async (ctx) => {
97
- const { username, password } = ctx.body;
98
-
99
- // Simple auth check (in production, verify against database)
100
- if (username === 'admin' && password === 'password') {
101
- const token = createJWT({ username, role: 'admin' }, 'your-secret-key', 3600);
102
- return { token, user: { username, role: 'admin' } };
103
- }
104
-
105
- ctx.set.status = 401;
106
- return { error: 'Invalid credentials' };
107
- }, {
108
- body: z.object({
109
- username: z.string(),
110
- password: z.string()
111
- })
112
- })
113
-
114
- .get('/protected', async (ctx) => {
115
- // ctx.user is available here because of auth plugin
116
- return { message: 'This is protected', user: ctx.user };
117
- })
118
-
119
- // Server control endpoints
120
- .post('/restart', async (ctx) => {
121
- // Restart the server
122
- setTimeout(() => app.restart(3000), 100);
123
- return { message: 'Server restart initiated' };
124
- })
125
-
126
- .get('/status', async (ctx) => {
127
- return {
128
- ...app.getServerInfo(),
129
- uptime: process.uptime(),
130
- memory: process.memoryUsage()
131
- };
132
- })
133
-
134
- .put('/users/:id', async (ctx) => {
135
- return {
136
- updated: ctx.body,
137
- id: ctx.params.id,
138
- version: ctx.headers['if-match']
139
- };
140
- }, {
141
- params: z.object({ id: z.string().uuid() }),
142
- body: z.object({
143
- name: z.string().optional(),
144
- email: z.string().email().optional()
145
- }),
146
- headers: z.object({
147
- 'if-match': z.string()
148
- })
149
- })
150
-
151
- .delete('/users/:id', async (ctx) => {
152
- ctx.set.status = 204;
153
- return null;
154
- }, {
155
- params: z.object({ id: z.string().uuid() })
156
- });
157
-
158
- // Start the server (with hot reload enabled)
159
- app.start(3000, 'localhost');
160
-
161
- console.log(`
162
- ๐ŸฆŠ BXO Framework with Hot Reload
163
-
164
- โœจ Features Enabled:
165
- - ๐Ÿ”„ Hot reload (edit any .ts/.js file to restart)
166
- - ๐ŸŽฃ Full lifecycle hooks (before/after pattern)
167
- - ๐Ÿ”’ JWT authentication
168
- - ๐Ÿ“Š Rate limiting
169
- - ๐ŸŒ CORS support
170
- - ๐Ÿ“ Request logging
171
-
172
- ๐Ÿงช Try these endpoints:
173
- - GET /simple
174
- - GET /users/123e4567-e89b-12d3-a456-426614174000?include=profile
175
- - POST /users (with JSON body: {"name": "John", "email": "john@example.com"})
176
- - GET /health (shows server info)
177
- - POST /login (with JSON body: {"username": "admin", "password": "password"})
178
- - GET /protected (requires Bearer token from /login)
179
- - GET /status (server statistics)
180
- - POST /restart (restart server programmatically)
181
-
182
- ๐Ÿ’ก Edit this file and save to see hot reload in action!
183
- `);