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,448 +0,0 @@
1
- import { z } from 'zod';
2
- import type { ResponseConfig, InternalCookie, CookieOptions } from '../types';
3
-
4
- // Parse query string
5
- export function parseQuery(searchParams: URLSearchParams): Record<string, string | undefined> {
6
- const query: Record<string, string | undefined> = {};
7
- searchParams.forEach((value, key) => {
8
- query[key] = value;
9
- });
10
- return query;
11
- }
12
-
13
- // Parse headers
14
- export function parseHeaders(headers: Headers): Record<string, string> {
15
- const headerObj: Record<string, string> = {};
16
- headers.forEach((value, key) => {
17
- headerObj[key] = value;
18
- });
19
- return headerObj;
20
- }
21
-
22
- // Parse cookies from Cookie header
23
- export function parseCookies(cookieHeader: string | null): Record<string, string> {
24
- const cookies: Record<string, string> = {};
25
-
26
- if (!cookieHeader) return cookies;
27
-
28
- const cookiePairs = cookieHeader.split(';');
29
- for (const pair of cookiePairs) {
30
- const [name, value] = pair.trim().split('=');
31
- if (name && value) {
32
- cookies[decodeURIComponent(name)] = decodeURIComponent(value);
33
- }
34
- }
35
-
36
- return cookies;
37
- }
38
-
39
- // Validate data against Zod schema
40
- export function validateData<T>(schema: z.ZodSchema<T> | undefined, data: any): T {
41
- if (!schema) return data;
42
- return schema.parse(data);
43
- }
44
-
45
- // Validate response against response config (only supports Record<number, schema> format)
46
- export function validateResponse(
47
- responseConfig: ResponseConfig | undefined,
48
- data: any,
49
- status: number = 200
50
- ): any {
51
- if (!responseConfig) return data;
52
-
53
- // Get the schema for the specific status code
54
- const statusSchema = responseConfig[status];
55
- if (statusSchema) {
56
- return statusSchema.parse(data);
57
- }
58
-
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);
65
- }
66
- }
67
-
68
- // If no schema found for the status, return data as-is
69
- return data;
70
- }
71
-
72
- // Helper function to parse form keys with nested object and array notation (Axios-compatible)
73
- function parseFormKey(key: string): {
74
- baseKey: string;
75
- path: string[];
76
- isArray: boolean;
77
- isJson: boolean;
78
- hasIndexes: boolean;
79
- } {
80
- // Check for special endings like "{}" for JSON serialization FIRST
81
- if (key.endsWith('{}')) {
82
- const actualBaseKey = key.slice(0, -2);
83
- return { baseKey: actualBaseKey, path: [], isArray: false, isJson: true, hasIndexes: false };
84
- }
85
-
86
- const bracketMatch = key.match(/^([^\[]+)(\[.*\])*$/);
87
- if (!bracketMatch) {
88
- return { baseKey: key, path: [], isArray: false, isJson: false, hasIndexes: false };
89
- }
90
-
91
- const baseKey = bracketMatch[1];
92
- if (!baseKey) {
93
- return { baseKey: key, path: [], isArray: false, isJson: false, hasIndexes: false };
94
- }
95
-
96
- const bracketPart = key.slice(baseKey.length);
97
-
98
- if (!bracketPart) {
99
- return { baseKey, path: [], isArray: false, isJson: false, hasIndexes: false };
100
- }
101
-
102
- // Check for special endings like "{}" for JSON serialization
103
- if (baseKey.endsWith('{}')) {
104
- const actualBaseKey = baseKey.slice(0, -2);
105
- return { baseKey: actualBaseKey, path: [], isArray: false, isJson: true, hasIndexes: false };
106
- }
107
-
108
- // If no bracket part, return simple key
109
- if (!bracketPart) {
110
- return { baseKey, path: [], isArray: false, isJson: false, hasIndexes: false };
111
- }
112
-
113
- // Check if this is an array notation (e.g., "recordIds[]")
114
- if (bracketPart === '[]') {
115
- return { baseKey, path: [], isArray: true, isJson: false, hasIndexes: false };
116
- }
117
-
118
- // Extract all bracket contents
119
- const path: string[] = [];
120
- const bracketRegex = /\[([^\]]*)\]/g;
121
- let match;
122
- let hasIndexes = false;
123
-
124
- while ((match = bracketRegex.exec(bracketPart)) !== null) {
125
- if (match[1] !== undefined) {
126
- path.push(match[1]);
127
- // Check if this is a numeric index
128
- if (/^\d+$/.test(match[1])) {
129
- hasIndexes = true;
130
- }
131
- }
132
- }
133
-
134
- // Check if the last path element is empty (indicating array without indexes)
135
- const isArray = path.length > 0 && path[path.length - 1] === '';
136
-
137
- return { baseKey, path, isArray, isJson: false, hasIndexes };
138
- }
139
-
140
- // Helper function to set nested value in object (Axios-compatible)
141
- function setNestedValue(obj: any, baseKey: string, path: string[], value: any, isArray: boolean = false): void {
142
- if (!(baseKey in obj)) {
143
- obj[baseKey] = isArray ? [] : {};
144
- }
145
-
146
- let current = obj[baseKey];
147
-
148
- // Navigate to the parent of the target location
149
- for (let i = 0; i < path.length - 1; i++) {
150
- const key = path[i];
151
- if (key && (!(key in current) || typeof current[key] !== 'object')) {
152
- // Check if next key is numeric (array index)
153
- const nextKey = path[i + 1];
154
- const isNextKeyNumeric = nextKey && /^\d+$/.test(nextKey);
155
- current[key] = isNextKeyNumeric ? [] : {};
156
- }
157
- if (key) {
158
- current = current[key];
159
- }
160
- }
161
-
162
- // Set the final value
163
- const lastKey = path[path.length - 1];
164
- if (lastKey) {
165
- if (/^\d+$/.test(lastKey)) {
166
- // Numeric key - treat as array index
167
- const index = parseInt(lastKey, 10);
168
- if (Array.isArray(current)) {
169
- current[index] = value;
170
- } else {
171
- // Convert to array if needed
172
- const newArray = [];
173
- newArray[index] = value;
174
- current[lastKey] = newArray;
175
- }
176
- } else {
177
- current[lastKey] = value;
178
- }
179
- }
180
- }
181
-
182
- // Parse request body based on content type
183
- export async function parseRequestBody(request: Request): Promise<any> {
184
- const contentType = request.headers.get('content-type');
185
-
186
- if (contentType?.includes('application/json')) {
187
- try {
188
- return await request.json();
189
- } catch {
190
- return {};
191
- }
192
- } else if (contentType?.includes('multipart/form-data') || contentType?.includes('application/x-www-form-urlencoded')) {
193
- const formData = await request.formData();
194
- // Convert FormData to a structured object
195
- const formBody: Record<string, any> = {};
196
-
197
- for (const [key, value] of formData.entries()) {
198
- // Parse the key to handle nested objects and arrays (Axios-compatible)
199
- const parsedKey = parseFormKey(key);
200
-
201
- if (parsedKey.isJson) {
202
- // Handle JSON serialization (e.g., "obj{}")
203
- if (typeof value === 'string') {
204
- try {
205
- formBody[parsedKey.baseKey] = JSON.parse(value);
206
- } catch {
207
- formBody[parsedKey.baseKey] = value;
208
- }
209
- } else {
210
- formBody[parsedKey.baseKey] = value;
211
- }
212
- } else if (parsedKey.isArray) {
213
- // Handle array notation like "recordIds[]"
214
- if (parsedKey.baseKey in formBody) {
215
- if (Array.isArray(formBody[parsedKey.baseKey])) {
216
- formBody[parsedKey.baseKey].push(value);
217
- } else {
218
- formBody[parsedKey.baseKey] = [formBody[parsedKey.baseKey], value];
219
- }
220
- } else {
221
- formBody[parsedKey.baseKey] = [value];
222
- }
223
- } else if (parsedKey.path.length > 0) {
224
- // Handle nested object notation like "test[new]", "test[hi][hi]", "arr[0]", "users[0][name]"
225
- setNestedValue(formBody, parsedKey.baseKey, parsedKey.path, value, parsedKey.hasIndexes);
226
- } else {
227
- // Handle regular form fields - check if this key already exists
228
- if (key in formBody) {
229
- // If key already exists, convert to array or append to existing array
230
- if (Array.isArray(formBody[key])) {
231
- formBody[key].push(value);
232
- } else {
233
- formBody[key] = [formBody[key], value];
234
- }
235
- } else {
236
- // First occurrence of this key
237
- formBody[key] = value;
238
- }
239
- }
240
- }
241
-
242
- return formBody;
243
- } else {
244
- // Try to parse as JSON if it looks like JSON, otherwise treat as text
245
- const textBody = await request.text();
246
- try {
247
- // Check if the text looks like JSON
248
- if (textBody.trim().startsWith('{') || textBody.trim().startsWith('[')) {
249
- return JSON.parse(textBody);
250
- } else {
251
- return textBody;
252
- }
253
- } catch {
254
- return textBody;
255
- }
256
- }
257
- }
258
-
259
- // Convert internal cookies to Set-Cookie header strings
260
- export function cookiesToHeaders(cookies: InternalCookie[]): string[] {
261
- return cookies.map(cookie => {
262
- let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
263
- if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
264
- if (cookie.path) cookieString += `; Path=${cookie.path}`;
265
- if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
266
- if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
267
- if (cookie.secure) cookieString += `; Secure`;
268
- if (cookie.httpOnly) cookieString += `; HttpOnly`;
269
- if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
270
- return cookieString;
271
- });
272
- }
273
-
274
- // Special cases for HTTP headers that need specific casing
275
- const HEADER_CASING_SPECIAL_CASES: Record<string, string> = {
276
- 'www-authenticate': 'WWW-Authenticate',
277
- 'content-md5': 'Content-MD5',
278
- 'dnt': 'DNT',
279
- 'etag': 'ETag',
280
- 'te': 'TE',
281
- 'trailer': 'Trailer',
282
- 'transfer-encoding': 'Transfer-Encoding',
283
- 'upgrade': 'Upgrade',
284
- 'x-forwarded-for': 'X-Forwarded-For',
285
- 'x-forwarded-proto': 'X-Forwarded-Proto',
286
- 'x-forwarded-host': 'X-Forwarded-Host',
287
- 'x-real-ip': 'X-Real-IP',
288
- 'x-requested-with': 'X-Requested-With',
289
- 'x-csrf-token': 'X-CSRF-Token',
290
- 'x-frame-options': 'X-Frame-Options',
291
- 'x-content-type-options': 'X-Content-Type-Options',
292
- 'x-xss-protection': 'X-XSS-Protection',
293
- 'strict-transport-security': 'Strict-Transport-Security',
294
- 'content-security-policy': 'Content-Security-Policy',
295
- 'referrer-policy': 'Referrer-Policy',
296
- 'permissions-policy': 'Permissions-Policy'
297
- };
298
-
299
- // Helper function to normalize header casing while preserving special cases
300
- function normalizeHeaderCase(headerKey: string): string {
301
- const lowerKey = headerKey.toLowerCase();
302
- return HEADER_CASING_SPECIAL_CASES[lowerKey] || headerKey;
303
- }
304
-
305
- // Helper function to convert Headers object back to plain object while preserving casing
306
- export function headersToPlainObject(headers: Headers): Record<string, string> {
307
- const result: Record<string, string> = {};
308
- headers.forEach((value, key) => {
309
- // Preserve the original casing from the Headers object
310
- result[key] = value;
311
- });
312
- return result;
313
- }
314
-
315
- // Merge headers with cookies
316
- export function mergeHeadersWithCookies(
317
- headers: Record<string, string>,
318
- cookies: InternalCookie[]
319
- ): Headers {
320
- const newHeaders = new Headers();
321
-
322
- // Add regular headers with proper casing
323
- Object.entries(headers).forEach(([key, value]) => {
324
- const normalizedKey = normalizeHeaderCase(key);
325
- newHeaders.set(normalizedKey, value);
326
- });
327
-
328
- // Add Set-Cookie headers
329
- const cookieHeaders = cookiesToHeaders(cookies);
330
- cookieHeaders.forEach(cookieHeader => {
331
- newHeaders.append('Set-Cookie', cookieHeader);
332
- });
333
-
334
- return newHeaders;
335
- }
336
-
337
- // Alternative function that returns headers with preserved casing
338
- export function mergeHeadersWithCookiesPreserveCasing(
339
- headers: Record<string, string>,
340
- cookies: InternalCookie[]
341
- ): Record<string, string> {
342
- const result: Record<string, string> = { ...headers };
343
-
344
- // Apply special casing rules
345
- Object.keys(result).forEach(key => {
346
- const normalizedKey = normalizeHeaderCase(key);
347
- if (normalizedKey !== key) {
348
- result[normalizedKey] = result[key] || '';
349
- delete result[key];
350
- }
351
- });
352
-
353
- // Add Set-Cookie headers
354
- if (cookies.length > 0) {
355
- const cookieHeaders = cookiesToHeaders(cookies);
356
- // Set-Cookie headers should be separate entries, not joined
357
- cookieHeaders.forEach((cookieHeader, index) => {
358
- const key = index === 0 ? 'Set-Cookie' : `Set-Cookie-${index + 1}`;
359
- result[key] = cookieHeader;
360
- });
361
- }
362
-
363
- return result;
364
- }
365
-
366
- // Create a redirect response
367
- export function createRedirectResponse(
368
- location: string,
369
- status: number = 302,
370
- headers: Record<string, string> = {}
371
- ): Response {
372
- const responseHeaders = new Headers();
373
- responseHeaders.set('Location', location);
374
-
375
- // Add additional headers
376
- Object.entries(headers).forEach(([key, value]) => {
377
- responseHeaders.set(key, value);
378
- });
379
-
380
- return new Response(null, {
381
- status,
382
- headers: responseHeaders
383
- });
384
- }
385
-
386
- // Check if a value is a file upload
387
- export function isFileUpload(value: any): value is File {
388
- return value instanceof File;
389
- }
390
-
391
- // Extract File object from upload value
392
- export function getFileFromUpload(value: any): File | null {
393
- return isFileUpload(value) ? value : null;
394
- }
395
-
396
- // Get file metadata without the File object
397
- export function getFileInfo(value: any): { name: string; size: number; mimetype: string; lastModified: number } | null {
398
- if (isFileUpload(value)) {
399
- return {
400
- name: value.name,
401
- size: value.size,
402
- mimetype: value.type || 'application/octet-stream',
403
- lastModified: value.lastModified
404
- };
405
- }
406
- return null;
407
- }
408
-
409
- // Save uploaded file to disk
410
- export async function saveUploadedFile(
411
- uploadValue: any,
412
- destinationPath: string
413
- ): Promise<boolean> {
414
- const file = getFileFromUpload(uploadValue);
415
- if (!file) return false;
416
-
417
- try {
418
- const arrayBuffer = await file.arrayBuffer();
419
- const buffer = Buffer.from(arrayBuffer);
420
- await Bun.write(destinationPath, buffer);
421
- return true;
422
- } catch (error) {
423
- console.error('Error saving file:', error);
424
- return false;
425
- }
426
- }
427
-
428
- // Get all file uploads from form data
429
- export function getFileUploads(formData: Record<string, any>): Record<string, File> {
430
- const files: Record<string, File> = {};
431
- for (const [key, value] of Object.entries(formData)) {
432
- if (isFileUpload(value)) {
433
- files[key] = value;
434
- }
435
- }
436
- return files;
437
- }
438
-
439
- // Get all non-file fields from form data
440
- export function getFormFields(formData: Record<string, any>): Record<string, string> {
441
- const fields: Record<string, string> = {};
442
- for (const [key, value] of Object.entries(formData)) {
443
- if (!isFileUpload(value)) {
444
- fields[key] = String(value);
445
- }
446
- }
447
- return fields;
448
- }