bxo 0.0.5-dev.50 → 0.0.5-dev.52

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/src/index.ts ADDED
@@ -0,0 +1,54 @@
1
+ // Main BXO class
2
+ export { default as BXO } from './core/bxo';
3
+
4
+ // Export Zod for convenience
5
+ export { z } from 'zod';
6
+
7
+ // Export helper functions
8
+ export { error, file, redirect, createCookieOptions } from './utils/helpers';
9
+
10
+ // Export utility functions
11
+ export {
12
+ parseQuery,
13
+ parseHeaders,
14
+ parseCookies,
15
+ validateData,
16
+ validateResponse,
17
+ parseRequestBody,
18
+ cookiesToHeaders,
19
+ mergeHeadersWithCookies,
20
+ createRedirectResponse,
21
+ isFileUpload,
22
+ getFileFromUpload,
23
+ getFileInfo,
24
+ saveUploadedFile,
25
+ getFileUploads,
26
+ getFormFields
27
+ } from './utils';
28
+
29
+ // Export route matching utilities
30
+ export { matchRoute, matchWSRoute } from './utils/route-matcher';
31
+
32
+ // Export context factory utilities
33
+ export { createContext, createOptionsContext, getInternalCookies } from './utils/context-factory';
34
+
35
+ // Export response handler utilities
36
+ export { processResponse, createErrorResponse, createValidationErrorResponse } from './utils/response-handler';
37
+
38
+ // Export types for external use
39
+ export type {
40
+ RouteConfig,
41
+ RouteDetail,
42
+ Handler,
43
+ WebSocketHandler,
44
+ WSRoute,
45
+ CookieOptions,
46
+ BXOOptions,
47
+ Plugin,
48
+ Context,
49
+ FileUpload,
50
+ FormData
51
+ } from './types';
52
+
53
+ // Re-export everything from the main BXO class for backward compatibility
54
+ export { default } from './core/bxo';
@@ -0,0 +1,170 @@
1
+ import { z } from 'zod';
2
+
3
+ // Type utilities for extracting types from Zod schemas
4
+ export type InferZodType<T> = T extends z.ZodType<infer U> ? U : never;
5
+
6
+ // Response configuration types
7
+ export type ResponseSchema = z.ZodSchema<any>;
8
+ export type StatusResponseSchema = Record<number, ResponseSchema>;
9
+ export type ResponseConfig = ResponseSchema | StatusResponseSchema;
10
+
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]
16
+ : never;
17
+
18
+ // Cookie options interface for setting cookies
19
+ export interface CookieOptions {
20
+ domain?: string;
21
+ path?: string;
22
+ expires?: Date;
23
+ maxAge?: number;
24
+ secure?: boolean;
25
+ httpOnly?: boolean;
26
+ sameSite?: 'Strict' | 'Lax' | 'None';
27
+ }
28
+
29
+ // OpenAPI detail information
30
+ export interface RouteDetail {
31
+ summary?: string;
32
+ description?: string;
33
+ tags?: string[];
34
+ operationId?: string;
35
+ deprecated?: boolean;
36
+ produces?: string[];
37
+ consumes?: string[];
38
+ [key: string]: any; // Allow additional OpenAPI properties
39
+ }
40
+
41
+ // Configuration interface for route handlers
42
+ export interface RouteConfig {
43
+ params?: z.ZodSchema<any>;
44
+ query?: z.ZodSchema<any>;
45
+ body?: z.ZodSchema<any>;
46
+ headers?: z.ZodSchema<any>;
47
+ cookies?: z.ZodSchema<any>;
48
+ response?: ResponseConfig;
49
+ detail?: RouteDetail;
50
+ }
51
+
52
+ // Helper type to extract status codes from response config
53
+ export type StatusCodes<T> = T extends Record<number, any> ? keyof T : never;
54
+
55
+ // Context type that's fully typed based on the route configuration
56
+ export type Context<TConfig extends RouteConfig = {}> = {
57
+ params: TConfig['params'] extends z.ZodSchema<any> ? InferZodType<TConfig['params']> : Record<string, string>;
58
+ query: TConfig['query'] extends z.ZodSchema<any> ? InferZodType<TConfig['query']> : Record<string, string | undefined>;
59
+ body: TConfig['body'] extends z.ZodSchema<any> ? InferZodType<TConfig['body']> : unknown;
60
+ headers: TConfig['headers'] extends z.ZodSchema<any> ? InferZodType<TConfig['headers']> : Record<string, string>;
61
+ cookies: TConfig['cookies'] extends z.ZodSchema<any> ? InferZodType<TConfig['cookies']> : Record<string, string>;
62
+ path: string;
63
+ request: Request;
64
+ set: {
65
+ status: number;
66
+ headers: Record<string, string>;
67
+ cookies: (name: string, value: string, options?: CookieOptions) => void;
68
+ redirect?: { location: string; status?: number };
69
+ };
70
+ status: <T extends number>(
71
+ code: TConfig['response'] extends StatusResponseSchema
72
+ ? StatusCodes<TConfig['response']> | number
73
+ : T,
74
+ data?: TConfig['response'] extends StatusResponseSchema
75
+ ? T extends keyof TConfig['response']
76
+ ? InferZodType<TConfig['response'][T]>
77
+ : any
78
+ : TConfig['response'] extends ResponseSchema
79
+ ? InferZodType<TConfig['response']>
80
+ : any
81
+ ) => TConfig['response'] extends StatusResponseSchema
82
+ ? T extends keyof TConfig['response']
83
+ ? InferZodType<TConfig['response'][T]>
84
+ : any
85
+ : TConfig['response'] extends ResponseSchema
86
+ ? InferZodType<TConfig['response']>
87
+ : any;
88
+ redirect: (location: string, status?: number) => Response;
89
+ clearRedirect: () => void;
90
+ [key: string]: any;
91
+ };
92
+
93
+ // Internal cookie storage interface
94
+ export interface InternalCookie {
95
+ name: string;
96
+ value: string;
97
+ domain?: string;
98
+ path?: string;
99
+ expires?: Date;
100
+ maxAge?: number;
101
+ secure?: boolean;
102
+ httpOnly?: boolean;
103
+ sameSite?: 'Strict' | 'Lax' | 'None';
104
+ }
105
+
106
+ // Handler function type with proper response typing
107
+ export type Handler<TConfig extends RouteConfig = {}, EC = {}> = (
108
+ ctx: Context<TConfig> & EC
109
+ ) => Promise<InferResponseType<TConfig['response']> | any> | InferResponseType<TConfig['response']> | any;
110
+
111
+ // Route definition
112
+ export interface Route {
113
+ method: string;
114
+ path: string;
115
+ handler: Handler<any>;
116
+ config?: RouteConfig;
117
+ }
118
+
119
+ // WebSocket handler interface
120
+ export interface WebSocketHandler {
121
+ onOpen?: (ws: any) => void;
122
+ onMessage?: (ws: any, message: string | Buffer) => void;
123
+ onClose?: (ws: any, code?: number, reason?: string) => void;
124
+ onError?: (ws: any, error: Error) => void;
125
+ }
126
+
127
+ // WebSocket route definition
128
+ export interface WSRoute {
129
+ path: string;
130
+ handler: WebSocketHandler;
131
+ }
132
+
133
+ // Lifecycle hooks
134
+ export interface LifecycleHooks {
135
+ onBeforeStart?: (instance: any) => Promise<void> | void;
136
+ onAfterStart?: (instance: any) => Promise<void> | void;
137
+ onBeforeStop?: (instance: any) => Promise<void> | void;
138
+ onAfterStop?: (instance: any) => Promise<void> | void;
139
+ onRequest?: (ctx: Context, instance: any) => Promise<void> | void;
140
+ onResponse?: (ctx: Context, response: any, instance: any) => Promise<any> | any;
141
+ onError?: (ctx: Context, error: Error, instance: any) => Promise<any> | any;
142
+ }
143
+
144
+ // BXO options interface
145
+ export interface BXOOptions {
146
+ enableValidation?: boolean;
147
+ }
148
+
149
+ // Plugin interface for middleware-style plugins
150
+ export interface Plugin {
151
+ name?: string;
152
+ onRequest?: (ctx: Context) => Promise<Response | void> | Response | void;
153
+ onResponse?: (ctx: Context, response: any) => Promise<any> | any;
154
+ onError?: (ctx: Context, error: Error) => Promise<any> | any;
155
+ }
156
+
157
+ // File upload types
158
+ export interface FileUpload {
159
+ type: 'file';
160
+ name: string;
161
+ size: number;
162
+ lastModified: number;
163
+ file: File;
164
+ filename: string;
165
+ mimetype: string;
166
+ }
167
+
168
+ export interface FormData {
169
+ [key: string]: string | FileUpload;
170
+ }
@@ -0,0 +1,158 @@
1
+ import type { Context, RouteConfig, InternalCookie, CookieOptions } from '../types';
2
+ import { validateData } from './index';
3
+
4
+ // Create a context object with validation
5
+ export function createContext<TConfig extends RouteConfig = {}>(
6
+ params: Record<string, string>,
7
+ query: Record<string, string | undefined>,
8
+ body: any,
9
+ headers: Record<string, string>,
10
+ cookies: Record<string, string>,
11
+ pathname: string,
12
+ request: Request,
13
+ config: RouteConfig | undefined,
14
+ enableValidation: boolean
15
+ ): Context<TConfig> {
16
+ // Create internal cookie storage
17
+ const internalCookies: InternalCookie[] = [];
18
+
19
+ // Create context with validation
20
+ const ctx: Context<TConfig> = {
21
+ params: enableValidation && config?.params ? validateData(config.params, params) : params,
22
+ query: enableValidation && config?.query ? validateData(config.query, query) : query,
23
+ body: enableValidation && config?.body ? validateData(config.body, body) : body,
24
+ headers: enableValidation && config?.headers ? validateData(config.headers, headers) : headers,
25
+ cookies: enableValidation && config?.cookies ? validateData(config.cookies, cookies) : cookies,
26
+ path: pathname,
27
+ request,
28
+ set: {
29
+ status: 200,
30
+ headers: {},
31
+ cookies: (name: string, value: string, options?: CookieOptions) => {
32
+ internalCookies.push({
33
+ name,
34
+ value,
35
+ domain: options?.domain,
36
+ path: options?.path,
37
+ expires: options?.expires,
38
+ maxAge: options?.maxAge,
39
+ secure: options?.secure,
40
+ httpOnly: options?.httpOnly,
41
+ sameSite: options?.sameSite
42
+ });
43
+ }
44
+ },
45
+ status: ((code: number, data?: any) => {
46
+ ctx.set.status = code;
47
+ return data;
48
+ }) as any,
49
+ redirect: ((location: string, status: number = 302) => {
50
+ // Record redirect intent only; avoid mutating generic status/headers so it can be canceled later
51
+ ctx.set.redirect = { location, status };
52
+
53
+ // Prepare headers for immediate Response return without persisting to ctx.set.headers
54
+ const responseHeaders = new Headers();
55
+ responseHeaders.set('Location', location);
56
+
57
+ // Add any additional headers from ctx.set.headers
58
+ if (ctx.set.headers) {
59
+ Object.entries(ctx.set.headers).forEach(([key, value]) => {
60
+ responseHeaders.set(key, value);
61
+ });
62
+ }
63
+
64
+ // Handle cookies if any are set on context
65
+ if (internalCookies.length > 0) {
66
+ const cookieHeaders = internalCookies.map(cookie => {
67
+ let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
68
+ if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
69
+ if (cookie.path) cookieString += `; Path=${cookie.path}`;
70
+ if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
71
+ if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
72
+ if (cookie.secure) cookieString += `; Secure`;
73
+ if (cookie.httpOnly) cookieString += `; HttpOnly`;
74
+ if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
75
+ return cookieString;
76
+ });
77
+ // Set multiple Set-Cookie headers properly
78
+ cookieHeaders.forEach(cookieHeader => {
79
+ responseHeaders.append('Set-Cookie', cookieHeader);
80
+ });
81
+ }
82
+
83
+ return new Response(null, {
84
+ status,
85
+ headers: responseHeaders
86
+ });
87
+ }) as any,
88
+ clearRedirect: (() => {
89
+ // Clear explicit redirect intent
90
+ delete ctx.set.redirect;
91
+ // Remove any Location header if present
92
+ if (ctx.set.headers) {
93
+ for (const key of Object.keys(ctx.set.headers)) {
94
+ if (key.toLowerCase() === 'location') {
95
+ delete ctx.set.headers[key];
96
+ }
97
+ }
98
+ }
99
+ // Reset status if it is a redirect
100
+ if (typeof ctx.set.status === 'number' && ctx.set.status >= 300 && ctx.set.status < 400) {
101
+ ctx.set.status = 200;
102
+ }
103
+ }) as any
104
+ };
105
+
106
+ // Store internal cookies for later use
107
+ (ctx as any)._internalCookies = internalCookies;
108
+
109
+ return ctx;
110
+ }
111
+
112
+ // Get internal cookies from context
113
+ export function getInternalCookies(ctx: Context): InternalCookie[] {
114
+ return (ctx as any)._internalCookies || [];
115
+ }
116
+
117
+ // Create a minimal context for OPTIONS requests
118
+ export function createOptionsContext(
119
+ pathname: string,
120
+ request: Request,
121
+ headers: Record<string, string>
122
+ ): Context {
123
+ return {
124
+ params: {},
125
+ query: {},
126
+ body: {},
127
+ headers,
128
+ cookies: {},
129
+ path: pathname,
130
+ request,
131
+ set: {
132
+ status: 200,
133
+ headers: {},
134
+ cookies: (name: string, value: string, options?: CookieOptions) => {
135
+ // This is a placeholder for setting cookies.
136
+ // In a real Bun.serve context, you'd use Bun.serve's cookie handling.
137
+ // For now, we'll just log it or throw an error if not Bun.serve.
138
+ console.warn(`Setting cookie '${name}' with value '${value}' via ctx.set.cookies is not directly supported by Bun.serve. Use Bun.serve's cookie handling.`);
139
+ },
140
+ redirect: undefined
141
+ },
142
+ status: ((code: number, data?: any) => {
143
+ return data;
144
+ }) as any,
145
+ redirect: ((location: string, status: number = 302) => {
146
+ const responseHeaders: Record<string, string> = {
147
+ Location: location
148
+ };
149
+ return new Response(null, {
150
+ status,
151
+ headers: responseHeaders
152
+ });
153
+ }) as any,
154
+ clearRedirect: (() => {
155
+ // No-op for options context
156
+ }) as any
157
+ };
158
+ }
@@ -0,0 +1,40 @@
1
+ import type { CookieOptions } from '../types';
2
+
3
+ // Error helper function
4
+ export const error = (error: Error | string, status: number = 500) => {
5
+ return new Response(JSON.stringify({ error: error instanceof Error ? error.message : error }), {
6
+ status,
7
+ headers: { 'Content-Type': 'application/json' }
8
+ });
9
+ };
10
+
11
+ // File helper function (like Elysia)
12
+ export const file = (path: string, options?: { type?: string; headers?: Record<string, string> }) => {
13
+ const bunFile = Bun.file(path);
14
+
15
+ if (options?.type || options?.headers) {
16
+ // Create a wrapper to override the MIME type and/or headers
17
+ return {
18
+ ...bunFile,
19
+ type: options.type || bunFile.type,
20
+ headers: options.headers
21
+ };
22
+ }
23
+
24
+ return bunFile;
25
+ };
26
+
27
+ // Redirect helper function (like Elysia)
28
+ export const redirect = (location: string, status: number = 302) => {
29
+ return new Response(null, {
30
+ status,
31
+ headers: { Location: location }
32
+ });
33
+ };
34
+
35
+ // Helper function to create cookie options
36
+ export const createCookieOptions = (
37
+ options: CookieOptions = {}
38
+ ): CookieOptions => ({
39
+ ...options
40
+ });
@@ -0,0 +1,258 @@
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 (supports both simple and status-based schemas)
46
+ export function validateResponse(
47
+ responseConfig: ResponseConfig | undefined,
48
+ data: any,
49
+ status: number = 200
50
+ ): any {
51
+ if (!responseConfig) return data;
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);
56
+ }
57
+
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);
63
+ }
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
+ }
77
+
78
+ return data;
79
+ }
80
+
81
+ // Parse request body based on content type
82
+ export async function parseRequestBody(request: Request): Promise<any> {
83
+ const contentType = request.headers.get('content-type');
84
+
85
+ if (contentType?.includes('application/json')) {
86
+ try {
87
+ return await request.json();
88
+ } catch {
89
+ return {};
90
+ }
91
+ } else if (contentType?.includes('multipart/form-data') || contentType?.includes('application/x-www-form-urlencoded')) {
92
+ const formData = await request.formData();
93
+ // Convert FormData to a structured object that preserves file information
94
+ const formBody: Record<string, any> = {};
95
+
96
+ for (const [key, value] of formData.entries()) {
97
+ if (value instanceof File) {
98
+ // Handle file uploads
99
+ formBody[key] = {
100
+ type: 'file',
101
+ name: value.name,
102
+ size: value.size,
103
+ lastModified: value.lastModified,
104
+ file: value, // Keep the actual File object for access
105
+ // Add convenience properties
106
+ filename: value.name,
107
+ mimetype: value.type || 'application/octet-stream'
108
+ };
109
+ } else {
110
+ // Handle regular form fields
111
+ formBody[key] = value;
112
+ }
113
+ }
114
+
115
+ return formBody;
116
+ } else {
117
+ // Try to parse as JSON if it looks like JSON, otherwise treat as text
118
+ const textBody = await request.text();
119
+ try {
120
+ // Check if the text looks like JSON
121
+ if (textBody.trim().startsWith('{') || textBody.trim().startsWith('[')) {
122
+ return JSON.parse(textBody);
123
+ } else {
124
+ return textBody;
125
+ }
126
+ } catch {
127
+ return textBody;
128
+ }
129
+ }
130
+ }
131
+
132
+ // Convert internal cookies to Set-Cookie header strings
133
+ export function cookiesToHeaders(cookies: InternalCookie[]): string[] {
134
+ return cookies.map(cookie => {
135
+ let cookieString = `${encodeURIComponent(cookie.name)}=${encodeURIComponent(cookie.value)}`;
136
+ if (cookie.domain) cookieString += `; Domain=${cookie.domain}`;
137
+ if (cookie.path) cookieString += `; Path=${cookie.path}`;
138
+ if (cookie.expires) cookieString += `; Expires=${cookie.expires.toUTCString()}`;
139
+ if (cookie.maxAge) cookieString += `; Max-Age=${cookie.maxAge}`;
140
+ if (cookie.secure) cookieString += `; Secure`;
141
+ if (cookie.httpOnly) cookieString += `; HttpOnly`;
142
+ if (cookie.sameSite) cookieString += `; SameSite=${cookie.sameSite}`;
143
+ return cookieString;
144
+ });
145
+ }
146
+
147
+ // Merge headers with cookies
148
+ export function mergeHeadersWithCookies(
149
+ headers: Record<string, string>,
150
+ cookies: InternalCookie[]
151
+ ): Headers {
152
+ const newHeaders = new Headers();
153
+
154
+ // Add regular headers
155
+ Object.entries(headers).forEach(([key, value]) => {
156
+ newHeaders.set(key, value);
157
+ });
158
+
159
+ // Add Set-Cookie headers
160
+ const cookieHeaders = cookiesToHeaders(cookies);
161
+ cookieHeaders.forEach(cookieHeader => {
162
+ newHeaders.append('Set-Cookie', cookieHeader);
163
+ });
164
+
165
+ return newHeaders;
166
+ }
167
+
168
+ // Create a redirect response
169
+ export function createRedirectResponse(
170
+ location: string,
171
+ status: number = 302,
172
+ headers: Record<string, string> = {}
173
+ ): Response {
174
+ const responseHeaders = new Headers();
175
+ responseHeaders.set('Location', location);
176
+
177
+ // Add additional headers
178
+ Object.entries(headers).forEach(([key, value]) => {
179
+ responseHeaders.set(key, value);
180
+ });
181
+
182
+ return new Response(null, {
183
+ status,
184
+ headers: responseHeaders
185
+ });
186
+ }
187
+
188
+ // Check if a value is a file upload
189
+ export function isFileUpload(value: any): value is {
190
+ type: 'file';
191
+ file: File;
192
+ name: string;
193
+ size: number;
194
+ lastModified: number;
195
+ filename: string;
196
+ mimetype: string;
197
+ } {
198
+ return value && typeof value === 'object' && value.type === 'file' && value.file instanceof File;
199
+ }
200
+
201
+ // Extract File object from upload value
202
+ export function getFileFromUpload(value: any): File | null {
203
+ return isFileUpload(value) ? value.file : null;
204
+ }
205
+
206
+ // Get file metadata without the File object
207
+ export function getFileInfo(value: any): { name: string; size: number; mimetype: string; lastModified: number } | null {
208
+ if (isFileUpload(value)) {
209
+ return {
210
+ name: value.name,
211
+ size: value.size,
212
+ mimetype: value.mimetype,
213
+ lastModified: value.lastModified
214
+ };
215
+ }
216
+ return null;
217
+ }
218
+
219
+ // Save uploaded file to disk
220
+ export async function saveUploadedFile(
221
+ uploadValue: any,
222
+ destinationPath: string
223
+ ): Promise<boolean> {
224
+ const file = getFileFromUpload(uploadValue);
225
+ if (!file) return false;
226
+
227
+ try {
228
+ const arrayBuffer = await file.arrayBuffer();
229
+ const buffer = Buffer.from(arrayBuffer);
230
+ await Bun.write(destinationPath, buffer);
231
+ return true;
232
+ } catch (error) {
233
+ console.error('Error saving file:', error);
234
+ return false;
235
+ }
236
+ }
237
+
238
+ // Get all file uploads from form data
239
+ export function getFileUploads(formData: Record<string, any>): Record<string, File> {
240
+ const files: Record<string, File> = {};
241
+ for (const [key, value] of Object.entries(formData)) {
242
+ if (isFileUpload(value)) {
243
+ files[key] = value.file;
244
+ }
245
+ }
246
+ return files;
247
+ }
248
+
249
+ // Get all non-file fields from form data
250
+ export function getFormFields(formData: Record<string, any>): Record<string, string> {
251
+ const fields: Record<string, string> = {};
252
+ for (const [key, value] of Object.entries(formData)) {
253
+ if (!isFileUpload(value)) {
254
+ fields[key] = String(value);
255
+ }
256
+ }
257
+ return fields;
258
+ }