bxo 0.0.5-dev.65 → 0.0.5-dev.67

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.
Files changed (37) hide show
  1. package/README.md +83 -675
  2. package/example/cors-example.ts +49 -0
  3. package/example/index.html +5 -0
  4. package/example/index.ts +57 -0
  5. package/package.json +9 -15
  6. package/plugins/cors.ts +124 -98
  7. package/plugins/index.ts +2 -9
  8. package/plugins/openapi.ts +130 -0
  9. package/src/index.ts +646 -59
  10. package/tsconfig.json +3 -5
  11. package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +0 -111
  12. package/examples/serve-react/README.md +0 -15
  13. package/examples/serve-react/app.tsx +0 -8
  14. package/examples/serve-react/bun.lock +0 -42
  15. package/examples/serve-react/index.html +0 -9
  16. package/examples/serve-react/index.ts +0 -27
  17. package/examples/serve-react/package.json +0 -17
  18. package/examples/serve-react/tsconfig.json +0 -29
  19. package/index.ts +0 -5
  20. package/plugins/README.md +0 -160
  21. package/plugins/ratelimit.ts +0 -136
  22. package/src/core/bxo.ts +0 -458
  23. package/src/handlers/request-handler.ts +0 -230
  24. package/src/types/index.ts +0 -167
  25. package/src/utils/context-factory.ts +0 -158
  26. package/src/utils/helpers.ts +0 -40
  27. package/src/utils/index.ts +0 -448
  28. package/src/utils/response-handler.ts +0 -293
  29. package/src/utils/route-matcher.ts +0 -191
  30. package/tests/README.md +0 -359
  31. package/tests/integration/bxo.test.ts +0 -616
  32. package/tests/run-tests.ts +0 -44
  33. package/tests/unit/context-factory.test.ts +0 -386
  34. package/tests/unit/helpers.test.ts +0 -253
  35. package/tests/unit/response-handler.test.ts +0 -327
  36. package/tests/unit/route-matcher.test.ts +0 -181
  37. package/tests/unit/utils.test.ts +0 -475
@@ -1,475 +0,0 @@
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
- headersToPlainObject,
12
- createRedirectResponse,
13
- isFileUpload,
14
- getFileFromUpload,
15
- getFileInfo,
16
- getFileUploads,
17
- getFormFields
18
- } from '../../src/utils';
19
- import { z } from 'zod';
20
-
21
- describe('Utility Functions', () => {
22
- describe('parseQuery', () => {
23
- it('should parse URLSearchParams correctly', () => {
24
- const searchParams = new URLSearchParams('name=john&age=25&city=new%20york');
25
- const result = parseQuery(searchParams);
26
-
27
- expect(result).toEqual({
28
- name: 'john',
29
- age: '25',
30
- city: 'new york'
31
- });
32
- });
33
-
34
- it('should handle empty search params', () => {
35
- const searchParams = new URLSearchParams('');
36
- const result = parseQuery(searchParams);
37
-
38
- expect(result).toEqual({});
39
- });
40
- });
41
-
42
- describe('parseHeaders', () => {
43
- it('should parse Headers object correctly', () => {
44
- const headers = new Headers();
45
- headers.set('content-type', 'application/json');
46
- headers.set('authorization', 'Bearer token123');
47
-
48
- const result = parseHeaders(headers);
49
-
50
- expect(result).toEqual({
51
- 'content-type': 'application/json',
52
- 'authorization': 'Bearer token123'
53
- });
54
- });
55
- });
56
-
57
- describe('parseCookies', () => {
58
- it('should parse cookie header correctly', () => {
59
- const cookieHeader = 'name=john; age=25; city=new%20york';
60
- const result = parseCookies(cookieHeader);
61
-
62
- expect(result).toEqual({
63
- name: 'john',
64
- age: '25',
65
- city: 'new york'
66
- });
67
- });
68
-
69
- it('should handle null cookie header', () => {
70
- const result = parseCookies(null);
71
- expect(result).toEqual({});
72
- });
73
-
74
- it('should handle empty cookie header', () => {
75
- const result = parseCookies('');
76
- expect(result).toEqual({});
77
- });
78
- });
79
-
80
- describe('validateData', () => {
81
- it('should validate data with schema', () => {
82
- const schema = z.object({
83
- name: z.string(),
84
- age: z.number()
85
- });
86
-
87
- const data = { name: 'john', age: 25 };
88
- const result = validateData(schema, data);
89
-
90
- expect(result).toEqual(data);
91
- });
92
-
93
- it('should return data without validation when no schema', () => {
94
- const data = { name: 'john', age: 25 };
95
- const result = validateData(undefined, data);
96
-
97
- expect(result).toEqual(data);
98
- });
99
-
100
- it('should throw error for invalid data', () => {
101
- const schema = z.object({
102
- name: z.string(),
103
- age: z.number()
104
- });
105
-
106
- const data = { name: 'john', age: 'invalid' };
107
-
108
- expect(() => validateData(schema, data)).toThrow();
109
- });
110
- });
111
-
112
- describe('validateResponse', () => {
113
- it('should validate response with simple schema', () => {
114
- const schema = z.object({
115
- message: z.string()
116
- });
117
-
118
- const data = { message: 'success' };
119
- const result = validateResponse({
120
- 200: schema
121
- }, data, 200);
122
-
123
- expect(result).toEqual(data);
124
- });
125
-
126
- it('should validate response with status-based schema', () => {
127
- const schema = {
128
- 200: z.object({ message: z.string() }),
129
- 400: z.object({ error: z.string() })
130
- };
131
-
132
- const data = { message: 'success' };
133
- const result = validateResponse(schema, data, 200);
134
-
135
- expect(result).toEqual(data);
136
- });
137
-
138
- it('should return data without validation when no schema', () => {
139
- const data = { message: 'success' };
140
- const result = validateResponse(undefined, data);
141
-
142
- expect(result).toEqual(data);
143
- });
144
- });
145
-
146
- describe('cookiesToHeaders', () => {
147
- it('should convert internal cookies to header strings', () => {
148
- const cookies = [
149
- {
150
- name: 'session',
151
- value: 'abc123',
152
- domain: 'example.com',
153
- path: '/',
154
- secure: true,
155
- httpOnly: true
156
- }
157
- ];
158
-
159
- const result = cookiesToHeaders(cookies);
160
-
161
- expect(result).toHaveLength(1);
162
- expect(result[0]).toContain('session=abc123');
163
- expect(result[0]).toContain('Domain=example.com');
164
- expect(result[0]).toContain('Path=/');
165
- expect(result[0]).toContain('Secure');
166
- expect(result[0]).toContain('HttpOnly');
167
- });
168
- });
169
-
170
- describe('mergeHeadersWithCookies', () => {
171
- it('should merge headers with cookies', () => {
172
- const headers = { 'content-type': 'application/json' };
173
- const cookies = [
174
- { name: 'session', value: 'abc123' }
175
- ];
176
-
177
- const result = mergeHeadersWithCookies(headers, cookies);
178
-
179
- expect(result.get('content-type')).toBe('application/json');
180
- expect(result.get('set-cookie')).toBe('session=abc123');
181
- });
182
-
183
- it('should preserve special header casing', () => {
184
- const headers = {
185
- 'WWW-Authenticate': 'Basic realm="example"',
186
- 'x-frame-options': 'DENY',
187
- 'content-type': 'application/json'
188
- };
189
-
190
- const result = mergeHeadersWithCookies(headers, []);
191
-
192
- // Check that special headers maintain their casing
193
- expect(result.get('WWW-Authenticate')).toBe('Basic realm="example"');
194
- expect(result.get('X-Frame-Options')).toBe('DENY');
195
- // Regular headers should keep their original casing
196
- expect(result.get('content-type')).toBe('application/json');
197
- });
198
- });
199
-
200
- describe('headersToPlainObject', () => {
201
- it('should convert Headers object to plain object', () => {
202
- const headers = new Headers();
203
- headers.set('www-authenticate', 'Basic realm="example"');
204
- headers.set('x-frame-options', 'DENY');
205
- headers.set('content-type', 'application/json');
206
-
207
- const result = headersToPlainObject(headers);
208
-
209
- // Note: Headers object normalizes keys to lowercase
210
- expect(result).toEqual({
211
- 'www-authenticate': 'Basic realm="example"',
212
- 'x-frame-options': 'DENY',
213
- 'content-type': 'application/json'
214
- });
215
- });
216
-
217
- it('should handle empty Headers object', () => {
218
- const headers = new Headers();
219
- const result = headersToPlainObject(headers);
220
-
221
- expect(result).toEqual({});
222
- });
223
- });
224
-
225
- describe('createRedirectResponse', () => {
226
- it('should create redirect response with default status', () => {
227
- const result = createRedirectResponse('/new-location');
228
-
229
- expect(result.status).toBe(302);
230
- expect(result.headers.get('location')).toBe('/new-location');
231
- });
232
-
233
- it('should create redirect response with custom status', () => {
234
- const result = createRedirectResponse('/new-location', 301);
235
-
236
- expect(result.status).toBe(301);
237
- expect(result.headers.get('location')).toBe('/new-location');
238
- });
239
-
240
- it('should include additional headers', () => {
241
- const headers = { 'x-custom': 'value' };
242
- const result = createRedirectResponse('/new-location', 302, headers);
243
-
244
- expect(result.headers.get('x-custom')).toBe('value');
245
- });
246
- });
247
-
248
- describe('parseRequestBody', () => {
249
- it('should parse JSON body correctly', async () => {
250
- const jsonData = { name: 'john', age: 25 };
251
- const request = new Request('http://localhost/test', {
252
- method: 'POST',
253
- headers: { 'Content-Type': 'application/json' },
254
- body: JSON.stringify(jsonData)
255
- });
256
-
257
- const result = await parseRequestBody(request);
258
- expect(result).toEqual(jsonData);
259
- });
260
-
261
- it('should parse form data with arrays correctly', async () => {
262
- const formData = new FormData();
263
- formData.append('app', 'zodula');
264
- formData.append('model', 'zodula_User');
265
- formData.append('recordIds[]', 'asdfasdfdsa');
266
- formData.append('recordIds[]', 'jarupak.sri@gmail.com');
267
- formData.append('fields', '*');
268
-
269
- const request = new Request('http://localhost/test', {
270
- method: 'POST',
271
- body: formData
272
- });
273
-
274
- const result = await parseRequestBody(request);
275
-
276
- expect(result.app).toBe('zodula');
277
- expect(result.model).toBe('zodula_User');
278
- expect(Array.isArray(result.recordIds)).toBe(true);
279
- expect(result.recordIds).toEqual(['asdfasdfdsa', 'jarupak.sri@gmail.com']);
280
- expect(result.fields).toBe('*');
281
- });
282
-
283
- it('should handle single form field correctly', async () => {
284
- const formData = new FormData();
285
- formData.append('name', 'john');
286
- formData.append('email', 'john@example.com');
287
-
288
- const request = new Request('http://localhost/test', {
289
- method: 'POST',
290
- body: formData
291
- });
292
-
293
- const result = await parseRequestBody(request);
294
-
295
- expect(result.name).toBe('john');
296
- expect(result.email).toBe('john@example.com');
297
- });
298
-
299
- it('should handle file uploads in form data', async () => {
300
- const file = new File(['test content'], 'test.txt', { type: 'text/plain' });
301
- const formData = new FormData();
302
- formData.append('name', 'john');
303
- formData.append('file', file);
304
-
305
- const request = new Request('http://localhost/test', {
306
- method: 'POST',
307
- body: formData
308
- });
309
-
310
- const result = await parseRequestBody(request);
311
-
312
- expect(result.name).toBe('john');
313
- expect(result.file).toBeInstanceOf(File);
314
- expect(result.file.name).toBe('test.txt');
315
- });
316
-
317
- it('should parse nested object form data correctly', async () => {
318
- const formData = new FormData();
319
- formData.append('test[test]', 'test');
320
- formData.append('test[new]', 'new');
321
- formData.append('test[hi][hi]', 'hi');
322
-
323
- const request = new Request('http://localhost/test', {
324
- method: 'POST',
325
- body: formData
326
- });
327
-
328
- const result = await parseRequestBody(request);
329
-
330
- expect(result.test).toEqual({
331
- test: 'test',
332
- new: 'new',
333
- hi: {
334
- hi: 'hi'
335
- }
336
- });
337
- });
338
-
339
- it('should parse Axios-compatible multipart data with arrays and objects', async () => {
340
- const formData = new FormData();
341
- formData.append('x', '1');
342
- formData.append('arr[]', '1');
343
- formData.append('arr[]', '2');
344
- formData.append('arr[]', '3');
345
- formData.append('arr2[0]', '1');
346
- formData.append('arr2[1][0]', '2');
347
- formData.append('arr2[2]', '3');
348
- formData.append('users[0][name]', 'Peter');
349
- formData.append('users[0][surname]', 'Griffin');
350
- formData.append('users[1][name]', 'Thomas');
351
- formData.append('users[1][surname]', 'Anderson');
352
- formData.append('obj2{}', '[{"x":1}]');
353
-
354
- const request = new Request('http://localhost/test', {
355
- method: 'POST',
356
- body: formData
357
- });
358
-
359
- const result = await parseRequestBody(request);
360
-
361
- expect(result.x).toBe('1');
362
- expect(result.arr).toEqual(['1', '2', '3']);
363
- expect(result.arr2).toEqual(['1', ['2'], '3']);
364
- expect(result.users).toEqual([
365
- { name: 'Peter', surname: 'Griffin' },
366
- { name: 'Thomas', surname: 'Anderson' }
367
- ]);
368
- expect(result.obj2).toEqual([{ x: 1 }]);
369
- });
370
-
371
- it('should handle JSON serialization with special endings', async () => {
372
- const formData = new FormData();
373
- formData.append('myObj{}', '{"x": 1, "s": "foo"}');
374
- formData.append('settings{}', '{"theme": "dark", "notifications": true}');
375
-
376
- const request = new Request('http://localhost/test', {
377
- method: 'POST',
378
- body: formData
379
- });
380
-
381
- const result = await parseRequestBody(request);
382
-
383
- expect(result.myObj).toEqual({ x: 1, s: 'foo' });
384
- expect(result.settings).toEqual({ theme: 'dark', notifications: true });
385
- });
386
-
387
- it('should handle mixed array indexing styles', async () => {
388
- const formData = new FormData();
389
- formData.append('tags[]', 'javascript');
390
- formData.append('tags[]', 'typescript');
391
- formData.append('scores[0]', '100');
392
- formData.append('scores[1]', '95');
393
- formData.append('scores[2]', '88');
394
-
395
- const request = new Request('http://localhost/test', {
396
- method: 'POST',
397
- body: formData
398
- });
399
-
400
- const result = await parseRequestBody(request);
401
-
402
- expect(result.tags).toEqual(['javascript', 'typescript']);
403
- expect(result.scores).toEqual(['100', '95', '88']);
404
- });
405
- });
406
-
407
- describe('File Upload Utilities', () => {
408
- it('should identify file uploads correctly', () => {
409
- const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
410
-
411
- expect(isFileUpload(file)).toBe(true);
412
- expect(isFileUpload('not a file')).toBe(false);
413
- expect(isFileUpload({ type: 'text' })).toBe(false);
414
- });
415
-
416
- it('should extract file from upload', () => {
417
- const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
418
-
419
- const result = getFileFromUpload(file);
420
- expect(result).toBe(file);
421
- });
422
-
423
- it('should return null for non-file uploads', () => {
424
- const result = getFileFromUpload('not a file');
425
- expect(result).toBeNull();
426
- });
427
-
428
- it('should get file info', () => {
429
- const file = new File(['test'], 'test.jpg', { type: 'image/jpeg' });
430
-
431
- const result = getFileInfo(file);
432
- expect(result).toEqual({
433
- name: 'test.jpg',
434
- size: 4,
435
- mimetype: 'image/jpeg',
436
- lastModified: expect.any(Number)
437
- });
438
- });
439
-
440
- it('should return null for non-file uploads when getting info', () => {
441
- const result = getFileInfo('not a file');
442
- expect(result).toBeNull();
443
- });
444
-
445
- it('should get all file uploads from form data', () => {
446
- const file = new File(['test'], 'avatar.jpg', { type: 'image/jpeg' });
447
- const formData = {
448
- name: 'john',
449
- avatar: file,
450
- email: 'john@example.com'
451
- };
452
-
453
- const result = getFileUploads(formData);
454
-
455
- expect(Object.keys(result)).toHaveLength(1);
456
- expect(result.avatar).toBeInstanceOf(File);
457
- });
458
-
459
- it('should get all non-file fields from form data', () => {
460
- const file = new File(['test'], 'avatar.jpg', { type: 'image/jpeg' });
461
- const formData = {
462
- name: 'john',
463
- avatar: file,
464
- email: 'john@example.com'
465
- };
466
-
467
- const result = getFormFields(formData);
468
-
469
- expect(result).toEqual({
470
- name: 'john',
471
- email: 'john@example.com'
472
- });
473
- });
474
- });
475
- });