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.
- package/README.md +83 -675
- package/example/cors-example.ts +49 -0
- package/example/index.html +5 -0
- package/example/index.ts +57 -0
- package/package.json +9 -15
- package/plugins/cors.ts +124 -98
- package/plugins/index.ts +2 -9
- package/plugins/openapi.ts +130 -0
- package/src/index.ts +646 -59
- package/tsconfig.json +3 -5
- package/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc +0 -111
- package/examples/serve-react/README.md +0 -15
- package/examples/serve-react/app.tsx +0 -8
- package/examples/serve-react/bun.lock +0 -42
- package/examples/serve-react/index.html +0 -9
- package/examples/serve-react/index.ts +0 -27
- package/examples/serve-react/package.json +0 -17
- package/examples/serve-react/tsconfig.json +0 -29
- package/index.ts +0 -5
- package/plugins/README.md +0 -160
- package/plugins/ratelimit.ts +0 -136
- package/src/core/bxo.ts +0 -458
- package/src/handlers/request-handler.ts +0 -230
- package/src/types/index.ts +0 -167
- package/src/utils/context-factory.ts +0 -158
- package/src/utils/helpers.ts +0 -40
- package/src/utils/index.ts +0 -448
- package/src/utils/response-handler.ts +0 -293
- package/src/utils/route-matcher.ts +0 -191
- package/tests/README.md +0 -359
- package/tests/integration/bxo.test.ts +0 -616
- package/tests/run-tests.ts +0 -44
- package/tests/unit/context-factory.test.ts +0 -386
- package/tests/unit/helpers.test.ts +0 -253
- package/tests/unit/response-handler.test.ts +0 -327
- package/tests/unit/route-matcher.test.ts +0 -181
- package/tests/unit/utils.test.ts +0 -475
package/src/utils/index.ts
DELETED
|
@@ -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
|
-
}
|