@gofranz/formshive-submit 1.0.0

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,1714 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ /**
6
+ * TypeScript type definitions for Formshive Submit library
7
+ */
8
+ // Default configurations
9
+ const DEFAULT_RETRY_CONFIG = {
10
+ maxAttempts: 3,
11
+ baseDelay: 1000,
12
+ maxDelay: 30000,
13
+ enableJitter: true,
14
+ backoffMultiplier: 2,
15
+ };
16
+ const DEFAULT_FILE_CONFIG = {
17
+ maxFileSize: 10 * 1024 * 1024, // 10MB
18
+ allowedTypes: [], // Empty array means allow all types
19
+ trackProgress: true,
20
+ };
21
+ const DEFAULT_OPTIONS = {
22
+ endpoint: 'https://api.formshive.com/v1',
23
+ httpClient: 'fetch',
24
+ timeout: 30000,
25
+ debug: false,
26
+ retry: DEFAULT_RETRY_CONFIG,
27
+ files: DEFAULT_FILE_CONFIG,
28
+ };
29
+ // Export error codes
30
+ const ERROR_CODES = {
31
+ NETWORK_ERROR: 'NETWORK_ERROR',
32
+ TIMEOUT_ERROR: 'TIMEOUT_ERROR',
33
+ VALIDATION_ERROR: 'VALIDATION_ERROR',
34
+ FILE_TOO_LARGE: 'FILE_TOO_LARGE',
35
+ INVALID_FILE_TYPE: 'INVALID_FILE_TYPE',
36
+ FORM_NOT_FOUND: 'FORM_NOT_FOUND',
37
+ SERVER_ERROR: 'SERVER_ERROR',
38
+ RATE_LIMITED: 'RATE_LIMITED',
39
+ UNKNOWN_ERROR: 'UNKNOWN_ERROR',
40
+ };
41
+
42
+ /**
43
+ * HTTP Client abstraction for both fetch and axios
44
+ */
45
+ /**
46
+ * Fetch-based HTTP client adapter
47
+ */
48
+ class FetchAdapter {
49
+ async request(config) {
50
+ const controller = new AbortController();
51
+ let timeoutId = null;
52
+ if (config.timeout) {
53
+ timeoutId = setTimeout(() => controller.abort(), config.timeout);
54
+ }
55
+ try {
56
+ const response = await fetch(config.url, {
57
+ method: config.method,
58
+ headers: {
59
+ ...config.headers,
60
+ },
61
+ body: config.data,
62
+ signal: controller.signal,
63
+ });
64
+ if (timeoutId)
65
+ clearTimeout(timeoutId);
66
+ // Convert headers to record
67
+ const headers = {};
68
+ response.headers.forEach((value, key) => {
69
+ headers[key] = value;
70
+ });
71
+ let data;
72
+ const contentType = response.headers.get('content-type');
73
+ if (contentType?.includes('application/json')) {
74
+ data = await response.json();
75
+ }
76
+ else {
77
+ data = await response.text();
78
+ }
79
+ return {
80
+ data,
81
+ status: response.status,
82
+ statusText: response.statusText,
83
+ headers,
84
+ };
85
+ }
86
+ catch (error) {
87
+ if (timeoutId)
88
+ clearTimeout(timeoutId);
89
+ if (error instanceof Error && error.name === 'AbortError') {
90
+ throw new Error('Request timeout');
91
+ }
92
+ throw error;
93
+ }
94
+ }
95
+ supportsProgress() {
96
+ // Note: fetch doesn't support upload progress natively
97
+ // This would require using streams which is complex
98
+ return false;
99
+ }
100
+ }
101
+ /**
102
+ * Axios-based HTTP client adapter
103
+ */
104
+ class AxiosAdapter {
105
+ constructor(axiosInstance) {
106
+ this.axiosInstance = axiosInstance;
107
+ }
108
+ async request(config) {
109
+ try {
110
+ const response = await this.axiosInstance.request({
111
+ url: config.url,
112
+ method: config.method,
113
+ data: config.data,
114
+ headers: config.headers,
115
+ timeout: config.timeout,
116
+ onUploadProgress: config.onUploadProgress,
117
+ });
118
+ return {
119
+ data: response.data,
120
+ status: response.status,
121
+ statusText: response.statusText,
122
+ headers: response.headers,
123
+ };
124
+ }
125
+ catch (error) {
126
+ // Re-throw axios errors as they contain useful information
127
+ throw error;
128
+ }
129
+ }
130
+ supportsProgress() {
131
+ return true;
132
+ }
133
+ }
134
+ /**
135
+ * Create HTTP client adapter based on the provided client type
136
+ */
137
+ function createHttpClient(client) {
138
+ if (typeof client === 'string') {
139
+ switch (client) {
140
+ case 'fetch':
141
+ return new FetchAdapter();
142
+ case 'axios':
143
+ // Try to import axios dynamically
144
+ try {
145
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
146
+ const axios = require('axios');
147
+ return new AxiosAdapter(axios.create());
148
+ }
149
+ catch (error) {
150
+ throw new Error('Axios is not available. Please install axios or use fetch client.');
151
+ }
152
+ default:
153
+ throw new Error(`Unsupported HTTP client: ${client}`);
154
+ }
155
+ }
156
+ else {
157
+ // Assume it's an Axios instance
158
+ return new AxiosAdapter(client);
159
+ }
160
+ }
161
+ /**
162
+ * Try to parse field validation errors from response data
163
+ */
164
+ function parseFieldValidationErrors(responseData) {
165
+ if (!responseData || typeof responseData !== 'object') {
166
+ return {};
167
+ }
168
+ // Check if this is a Formshive field validation error response
169
+ if (responseData.error === 'validation_error' &&
170
+ Array.isArray(responseData.errors)) {
171
+ const validationResponse = responseData;
172
+ return {
173
+ fieldErrors: validationResponse.errors,
174
+ validationResponse: validationResponse
175
+ };
176
+ }
177
+ return {};
178
+ }
179
+ /**
180
+ * Create a SubmitError from various error types
181
+ */
182
+ function createSubmitError(error, attempt, code = 'UNKNOWN_ERROR') {
183
+ let message = 'Unknown error';
184
+ let statusCode;
185
+ let response;
186
+ let isRetryable = true;
187
+ let fieldErrors;
188
+ let validationResponse;
189
+ // Handle axios errors
190
+ if (error.isAxiosError) {
191
+ const axiosError = error;
192
+ message = axiosError.message;
193
+ statusCode = axiosError.response?.status;
194
+ response = axiosError.response?.data;
195
+ // Parse field validation errors if this is a 400 response
196
+ if (statusCode === 400) {
197
+ const fieldValidation = parseFieldValidationErrors(response);
198
+ fieldErrors = fieldValidation.fieldErrors;
199
+ validationResponse = fieldValidation.validationResponse;
200
+ // Use more specific message from validation response
201
+ if (validationResponse?.message) {
202
+ message = validationResponse.message;
203
+ }
204
+ }
205
+ // Determine if error is retryable
206
+ if (statusCode) {
207
+ // Don't retry client errors except 429 (rate limiting)
208
+ if (statusCode >= 400 && statusCode < 500 && statusCode !== 429) {
209
+ isRetryable = false;
210
+ }
211
+ // Set appropriate error code
212
+ if (statusCode === 404) {
213
+ code = 'FORM_NOT_FOUND';
214
+ }
215
+ else if (statusCode === 429) {
216
+ code = 'RATE_LIMITED';
217
+ }
218
+ else if (statusCode >= 400 && statusCode < 500) {
219
+ code = 'VALIDATION_ERROR';
220
+ }
221
+ else if (statusCode >= 500) {
222
+ code = 'SERVER_ERROR';
223
+ }
224
+ }
225
+ }
226
+ // Handle fetch errors
227
+ else if (error instanceof Error) {
228
+ message = error.message;
229
+ if (error.message.includes('timeout') || error.message.includes('Timeout')) {
230
+ code = 'TIMEOUT_ERROR';
231
+ }
232
+ else if (error.message.includes('network') || error.message.includes('fetch')) {
233
+ code = 'NETWORK_ERROR';
234
+ }
235
+ }
236
+ // Handle Response objects (from fetch)
237
+ else if (error instanceof Response) {
238
+ statusCode = error.status;
239
+ message = `HTTP ${error.status}: ${error.statusText}`;
240
+ // Try to extract response data for field validation errors
241
+ if (statusCode === 400) {
242
+ try {
243
+ // If response body is available, try to parse it
244
+ if (error.body) {
245
+ error.json().then(data => {
246
+ const fieldValidation = parseFieldValidationErrors(data);
247
+ if (fieldValidation.validationResponse?.message) {
248
+ message = fieldValidation.validationResponse.message;
249
+ }
250
+ }).catch(() => {
251
+ // Ignore JSON parsing errors for Response objects
252
+ });
253
+ }
254
+ }
255
+ catch (e) {
256
+ // Ignore parsing errors
257
+ }
258
+ }
259
+ // Determine if error is retryable
260
+ if (statusCode >= 400 && statusCode < 500 && statusCode !== 429) {
261
+ isRetryable = false;
262
+ }
263
+ // Set appropriate error code
264
+ if (statusCode === 404) {
265
+ code = 'FORM_NOT_FOUND';
266
+ }
267
+ else if (statusCode === 429) {
268
+ code = 'RATE_LIMITED';
269
+ }
270
+ else if (statusCode >= 400 && statusCode < 500) {
271
+ code = 'VALIDATION_ERROR';
272
+ }
273
+ else if (statusCode >= 500) {
274
+ code = 'SERVER_ERROR';
275
+ }
276
+ }
277
+ const submitError = new Error(message);
278
+ submitError.name = 'SubmitError';
279
+ submitError.code = ERROR_CODES[code];
280
+ submitError.statusCode = statusCode || undefined;
281
+ submitError.response = response;
282
+ submitError.attempt = attempt;
283
+ submitError.isRetryable = isRetryable;
284
+ submitError.originalError = error;
285
+ submitError.fieldErrors = fieldErrors;
286
+ submitError.validationResponse = validationResponse;
287
+ return submitError;
288
+ }
289
+ /**
290
+ * Check if an error is retryable
291
+ */
292
+ function isRetryableError(error) {
293
+ // Network errors are retryable
294
+ if (error instanceof TypeError && error.message.includes('fetch')) {
295
+ return true;
296
+ }
297
+ // Timeout errors are retryable
298
+ if (error.message?.includes('timeout') || error.message?.includes('Timeout')) {
299
+ return true;
300
+ }
301
+ // Server errors (5xx) are retryable
302
+ if (error.response?.status >= 500) {
303
+ return true;
304
+ }
305
+ // Rate limiting (429) is retryable
306
+ if (error.response?.status === 429) {
307
+ return true;
308
+ }
309
+ // Axios network errors are retryable
310
+ if (error.isAxiosError && !error.response) {
311
+ return true;
312
+ }
313
+ // Client errors (4xx except 429) are not retryable
314
+ if (error.response?.status >= 400 && error.response?.status < 500 && error.response?.status !== 429) {
315
+ return false;
316
+ }
317
+ return true;
318
+ }
319
+ /**
320
+ * Get user-friendly error message based on error type
321
+ */
322
+ function getErrorMessage(error) {
323
+ switch (error.code) {
324
+ case ERROR_CODES.FORM_NOT_FOUND:
325
+ return 'Form not found. Please check the form ID.';
326
+ case ERROR_CODES.VALIDATION_ERROR:
327
+ return error.response?.message || 'Form validation failed. Please check your data.';
328
+ case ERROR_CODES.FILE_TOO_LARGE:
329
+ return 'One or more files are too large. Please reduce file size and try again.';
330
+ case ERROR_CODES.INVALID_FILE_TYPE:
331
+ return 'Invalid file type. Please check allowed file types.';
332
+ case ERROR_CODES.RATE_LIMITED:
333
+ return 'Too many requests. Please wait a moment and try again.';
334
+ case ERROR_CODES.TIMEOUT_ERROR:
335
+ return 'Request timed out. Please check your connection and try again.';
336
+ case ERROR_CODES.NETWORK_ERROR:
337
+ return 'Network error. Please check your internet connection.';
338
+ case ERROR_CODES.SERVER_ERROR:
339
+ return 'Server error. Please try again in a few minutes.';
340
+ default:
341
+ return error.message || 'An unexpected error occurred.';
342
+ }
343
+ }
344
+
345
+ /**
346
+ * Retry mechanism with exponential backoff and jitter
347
+ */
348
+ /**
349
+ * Sleep utility for retry delays
350
+ */
351
+ function sleep(ms) {
352
+ return new Promise(resolve => setTimeout(resolve, ms));
353
+ }
354
+ /**
355
+ * Calculate delay with exponential backoff and optional jitter
356
+ */
357
+ function calculateDelay(attempt, config) {
358
+ const exponentialDelay = config.baseDelay * Math.pow(config.backoffMultiplier, attempt - 1);
359
+ // Apply maximum delay limit
360
+ const cappedDelay = Math.min(exponentialDelay, config.maxDelay);
361
+ // Add jitter to avoid thundering herd problem
362
+ if (config.enableJitter) {
363
+ // Random jitter between 0% and 25% of the delay
364
+ const jitterRange = cappedDelay * 0.25;
365
+ const jitter = Math.random() * jitterRange;
366
+ return Math.floor(cappedDelay + jitter);
367
+ }
368
+ return cappedDelay;
369
+ }
370
+ /**
371
+ * Retry function with exponential backoff
372
+ */
373
+ async function withRetry(operation, config = {}, logger, onRetry) {
374
+ const finalConfig = {
375
+ ...DEFAULT_RETRY_CONFIG,
376
+ ...config,
377
+ };
378
+ let lastError;
379
+ for (let attempt = 1; attempt <= finalConfig.maxAttempts; attempt++) {
380
+ try {
381
+ logger?.debug(`Attempt ${attempt}/${finalConfig.maxAttempts}`);
382
+ const result = await operation();
383
+ if (attempt > 1) {
384
+ logger?.info(`Operation succeeded on attempt ${attempt}`);
385
+ }
386
+ return result;
387
+ }
388
+ catch (error) {
389
+ lastError = error;
390
+ logger?.warn(`Attempt ${attempt} failed:`, error.message);
391
+ // Check if this is the last attempt
392
+ if (attempt === finalConfig.maxAttempts) {
393
+ logger?.error(`All ${finalConfig.maxAttempts} attempts failed. Giving up.`);
394
+ break;
395
+ }
396
+ // Check if error is retryable
397
+ if (!isRetryableError(error)) {
398
+ logger?.info('Error is not retryable. Stopping retry attempts.');
399
+ break;
400
+ }
401
+ // Calculate delay for next attempt
402
+ const delay = calculateDelay(attempt, finalConfig);
403
+ logger?.debug(`Waiting ${delay}ms before next attempt...`);
404
+ // Call retry callback if provided
405
+ if (onRetry) {
406
+ try {
407
+ onRetry(attempt, finalConfig.maxAttempts, error);
408
+ }
409
+ catch (callbackError) {
410
+ logger?.warn('Retry callback threw an error:', callbackError);
411
+ }
412
+ }
413
+ // Wait before next attempt
414
+ await sleep(delay);
415
+ }
416
+ }
417
+ // All attempts failed
418
+ throw lastError;
419
+ }
420
+ /**
421
+ * Create a retry-enabled version of a function
422
+ */
423
+ function createRetryableFunction(fn, config = {}, logger) {
424
+ return async (...args) => {
425
+ return withRetry(() => fn(...args), config, logger);
426
+ };
427
+ }
428
+ /**
429
+ * Smart retry decorator that can be used with different error types
430
+ */
431
+ class RetryManager {
432
+ constructor(config = {}, logger) {
433
+ this.config = { ...DEFAULT_RETRY_CONFIG, ...config };
434
+ this.logger = logger;
435
+ }
436
+ /**
437
+ * Execute operation with retry logic
438
+ */
439
+ async execute(operation, onRetry) {
440
+ return withRetry(operation, this.config, this.logger, onRetry);
441
+ }
442
+ /**
443
+ * Update retry configuration
444
+ */
445
+ updateConfig(newConfig) {
446
+ this.config = { ...this.config, ...newConfig };
447
+ }
448
+ /**
449
+ * Get current configuration
450
+ */
451
+ getConfig() {
452
+ return { ...this.config };
453
+ }
454
+ /**
455
+ * Check if an attempt should be retried based on attempt number and error
456
+ */
457
+ shouldRetry(attempt, error) {
458
+ if (attempt >= this.config.maxAttempts) {
459
+ return false;
460
+ }
461
+ return isRetryableError(error);
462
+ }
463
+ /**
464
+ * Calculate the delay for a specific attempt
465
+ */
466
+ getDelay(attempt) {
467
+ return calculateDelay(attempt, this.config);
468
+ }
469
+ }
470
+ /**
471
+ * Utility to create common retry configurations
472
+ */
473
+ const RetryPresets = {
474
+ /**
475
+ * Quick retry for fast operations
476
+ */
477
+ quick: () => ({
478
+ maxAttempts: 2,
479
+ baseDelay: 500,
480
+ maxDelay: 2000,
481
+ enableJitter: true,
482
+ backoffMultiplier: 2,
483
+ }),
484
+ /**
485
+ * Standard retry for most operations
486
+ */
487
+ standard: () => ({
488
+ ...DEFAULT_RETRY_CONFIG,
489
+ }),
490
+ /**
491
+ * Patient retry for heavy operations
492
+ */
493
+ patient: () => ({
494
+ maxAttempts: 5,
495
+ baseDelay: 2000,
496
+ maxDelay: 60000,
497
+ enableJitter: true,
498
+ backoffMultiplier: 1.5,
499
+ }),
500
+ /**
501
+ * Aggressive retry for critical operations
502
+ */
503
+ aggressive: () => ({
504
+ maxAttempts: 10,
505
+ baseDelay: 100,
506
+ maxDelay: 30000,
507
+ enableJitter: true,
508
+ backoffMultiplier: 1.8,
509
+ }),
510
+ /**
511
+ * Custom retry configuration
512
+ */
513
+ custom: (overrides) => ({
514
+ ...DEFAULT_RETRY_CONFIG,
515
+ ...overrides,
516
+ }),
517
+ };
518
+ /**
519
+ * Utility function to get delay information for display purposes
520
+ */
521
+ function getRetryDelayInfo(attempt, config = {}) {
522
+ const finalConfig = {
523
+ ...DEFAULT_RETRY_CONFIG,
524
+ ...config,
525
+ };
526
+ let totalElapsed = 0;
527
+ for (let i = 1; i < attempt; i++) {
528
+ totalElapsed += calculateDelay(i, finalConfig);
529
+ }
530
+ const delay = calculateDelay(attempt, finalConfig);
531
+ return { delay, totalElapsed };
532
+ }
533
+
534
+ /**
535
+ * File handling and validation utilities
536
+ */
537
+ /**
538
+ * Validate file against configuration rules
539
+ */
540
+ function validateFile(file, config) {
541
+ // Check file size
542
+ if (file.size > config.maxFileSize) {
543
+ return `File "${file.name}" is too large. Maximum size is ${formatFileSize(config.maxFileSize)}, but file is ${formatFileSize(file.size)}.`;
544
+ }
545
+ // Check file type if restrictions are set
546
+ if (config.allowedTypes.length > 0) {
547
+ const isAllowed = config.allowedTypes.some(allowedType => {
548
+ // Handle wildcard types like "image/*"
549
+ if (allowedType.includes('*')) {
550
+ const baseType = allowedType.split('/')[0];
551
+ return file.type.startsWith(baseType + '/');
552
+ }
553
+ // Handle exact type matches
554
+ return file.type === allowedType;
555
+ });
556
+ if (!isAllowed) {
557
+ return `File "${file.name}" has unsupported type "${file.type}". Allowed types: ${config.allowedTypes.join(', ')}.`;
558
+ }
559
+ }
560
+ return null; // File is valid
561
+ }
562
+ /**
563
+ * Format file size for human-readable display
564
+ */
565
+ function formatFileSize(bytes) {
566
+ if (bytes === 0)
567
+ return '0 Bytes';
568
+ const k = 1024;
569
+ const sizes = ['Bytes', 'KB', 'MB', 'GB'];
570
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
571
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
572
+ }
573
+ /**
574
+ * Extract files from form data object
575
+ */
576
+ function extractFiles(data) {
577
+ const files = [];
578
+ function processValue(key, value) {
579
+ if (value instanceof File) {
580
+ files.push({
581
+ field: key,
582
+ file: value,
583
+ name: value.name,
584
+ size: value.size,
585
+ type: value.type,
586
+ });
587
+ }
588
+ else if (value instanceof FileList) {
589
+ // Handle multiple files from input[type="file"][multiple]
590
+ for (let i = 0; i < value.length; i++) {
591
+ const file = value.item(i);
592
+ if (file) {
593
+ files.push({
594
+ field: `${key}[${i}]`,
595
+ file,
596
+ name: file.name,
597
+ size: file.size,
598
+ type: file.type,
599
+ });
600
+ }
601
+ }
602
+ }
603
+ else if (Array.isArray(value)) {
604
+ // Handle array of files
605
+ value.forEach((item, index) => {
606
+ if (item instanceof File) {
607
+ files.push({
608
+ field: `${key}[${index}]`,
609
+ file: item,
610
+ name: item.name,
611
+ size: item.size,
612
+ type: item.type,
613
+ });
614
+ }
615
+ });
616
+ }
617
+ }
618
+ // Process all form fields
619
+ for (const [key, value] of Object.entries(data)) {
620
+ processValue(key, value);
621
+ }
622
+ return files;
623
+ }
624
+ /**
625
+ * Validate all files in a collection
626
+ */
627
+ function validateFiles(files, config, logger) {
628
+ const errors = [];
629
+ for (const fileInfo of files) {
630
+ logger?.debug(`Validating file: ${fileInfo.name} (${formatFileSize(fileInfo.size)}, ${fileInfo.type})`);
631
+ const error = validateFile(fileInfo.file, config);
632
+ if (error) {
633
+ errors.push(error);
634
+ logger?.warn(`File validation failed: ${error}`);
635
+ }
636
+ }
637
+ return errors;
638
+ }
639
+ /**
640
+ * Process form data and handle files
641
+ */
642
+ function processFormData(inputData, fileConfig = {}, logger) {
643
+ const config = { ...DEFAULT_FILE_CONFIG, ...fileConfig };
644
+ // If input is already FormData, extract information from it
645
+ if (inputData instanceof FormData) {
646
+ const files = [];
647
+ // Process FormData entries
648
+ for (const [key, value] of inputData.entries()) {
649
+ if (value instanceof File) {
650
+ files.push({
651
+ field: key,
652
+ file: value,
653
+ name: value.name,
654
+ size: value.size,
655
+ type: value.type,
656
+ });
657
+ }
658
+ }
659
+ // Validate files
660
+ const validationErrors = validateFiles(files, config, logger);
661
+ if (validationErrors.length > 0) {
662
+ const error = new Error(validationErrors.join(' '));
663
+ error.name = 'SubmitError';
664
+ error.code = ERROR_CODES.FILE_TOO_LARGE; // Could be file size or type error
665
+ error.attempt = 0;
666
+ error.isRetryable = false;
667
+ throw error;
668
+ }
669
+ return {
670
+ data: inputData,
671
+ hasFiles: files.length > 0,
672
+ files,
673
+ };
674
+ }
675
+ // Handle object data
676
+ const files = extractFiles(inputData);
677
+ const hasFiles = files.length > 0;
678
+ logger?.debug(`Found ${files.length} files in form data`);
679
+ // Validate files
680
+ if (hasFiles) {
681
+ const validationErrors = validateFiles(files, config, logger);
682
+ if (validationErrors.length > 0) {
683
+ const error = new Error(validationErrors.join(' '));
684
+ error.name = 'SubmitError';
685
+ error.code = validationErrors[0]?.includes('too large')
686
+ ? ERROR_CODES.FILE_TOO_LARGE
687
+ : ERROR_CODES.INVALID_FILE_TYPE;
688
+ error.attempt = 0;
689
+ error.isRetryable = false;
690
+ throw error;
691
+ }
692
+ }
693
+ // Convert to FormData if files are present
694
+ let processedData;
695
+ if (hasFiles) {
696
+ logger?.debug('Converting to FormData due to file presence');
697
+ processedData = new FormData();
698
+ // Add all non-file fields
699
+ for (const [key, value] of Object.entries(inputData)) {
700
+ if (!(value instanceof File) && !(value instanceof FileList) && !Array.isArray(value)) {
701
+ // Handle simple values
702
+ if (value !== undefined && value !== null) {
703
+ processedData.append(key, String(value));
704
+ }
705
+ }
706
+ else if (Array.isArray(value)) {
707
+ // Handle arrays (might contain files or other values)
708
+ value.forEach((item, index) => {
709
+ if (item instanceof File) {
710
+ processedData.append(`${key}[${index}]`, item);
711
+ }
712
+ else if (item !== undefined && item !== null) {
713
+ processedData.append(`${key}[${index}]`, String(item));
714
+ }
715
+ });
716
+ }
717
+ else if (value instanceof File) {
718
+ processedData.append(key, value);
719
+ }
720
+ else if (value instanceof FileList) {
721
+ for (let i = 0; i < value.length; i++) {
722
+ const file = value.item(i);
723
+ if (file) {
724
+ processedData.append(`${key}[${i}]`, file);
725
+ }
726
+ }
727
+ }
728
+ }
729
+ }
730
+ else {
731
+ // No files, keep as object (will be sent as JSON)
732
+ logger?.debug('No files found, keeping as object for JSON submission');
733
+ processedData = { ...inputData };
734
+ }
735
+ return {
736
+ data: processedData,
737
+ hasFiles,
738
+ files,
739
+ };
740
+ }
741
+ /**
742
+ * Create progress tracker for file uploads
743
+ */
744
+ function createProgressTracker(files, onProgress) {
745
+ const totalSize = files.reduce((sum, file) => sum + file.size, 0);
746
+ if (!onProgress || totalSize === 0) {
747
+ return undefined;
748
+ }
749
+ return (progressEvent) => {
750
+ if (progressEvent.lengthComputable) {
751
+ const percent = Math.round((progressEvent.loaded / progressEvent.total) * 100);
752
+ onProgress(percent, progressEvent.loaded, progressEvent.total);
753
+ }
754
+ };
755
+ }
756
+ /**
757
+ * Get MIME type from file extension (fallback if browser doesn't detect)
758
+ */
759
+ function getMimeTypeFromExtension(filename) {
760
+ const extension = filename.split('.').pop()?.toLowerCase();
761
+ const mimeTypes = {
762
+ // Images
763
+ jpg: 'image/jpeg',
764
+ jpeg: 'image/jpeg',
765
+ png: 'image/png',
766
+ gif: 'image/gif',
767
+ webp: 'image/webp',
768
+ svg: 'image/svg+xml',
769
+ // Documents
770
+ pdf: 'application/pdf',
771
+ doc: 'application/msword',
772
+ docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
773
+ xls: 'application/vnd.ms-excel',
774
+ xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
775
+ ppt: 'application/vnd.ms-powerpoint',
776
+ pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
777
+ // Text
778
+ txt: 'text/plain',
779
+ csv: 'text/csv',
780
+ json: 'application/json',
781
+ xml: 'application/xml',
782
+ // Archives
783
+ zip: 'application/zip',
784
+ rar: 'application/x-rar-compressed',
785
+ '7z': 'application/x-7z-compressed',
786
+ // Audio
787
+ mp3: 'audio/mpeg',
788
+ wav: 'audio/wav',
789
+ ogg: 'audio/ogg',
790
+ // Video
791
+ mp4: 'video/mp4',
792
+ avi: 'video/x-msvideo',
793
+ mov: 'video/quicktime',
794
+ wmv: 'video/x-ms-wmv',
795
+ };
796
+ return extension ? mimeTypes[extension] || null : null;
797
+ }
798
+ /**
799
+ * Enhanced file information with additional metadata
800
+ */
801
+ function getEnhancedFileInfo(file) {
802
+ const extension = file.name.split('.').pop()?.toLowerCase() || '';
803
+ const detectedMimeType = getMimeTypeFromExtension(file.name);
804
+ return {
805
+ field: '', // Will be set by caller
806
+ file,
807
+ name: file.name,
808
+ size: file.size,
809
+ type: file.type,
810
+ extension,
811
+ detectedMimeType,
812
+ isImage: file.type.startsWith('image/'),
813
+ isDocument: file.type.includes('pdf') || file.type.includes('document') || file.type.includes('word') || file.type.includes('excel') || file.type.includes('powerpoint'),
814
+ isArchive: file.type.includes('zip') || file.type.includes('rar') || file.type.includes('7z'),
815
+ };
816
+ }
817
+
818
+ /**
819
+ * Utility functions and logging
820
+ */
821
+ /**
822
+ * Simple console-based logger implementation
823
+ */
824
+ class ConsoleLogger {
825
+ constructor(enabled = false) {
826
+ this.enabled = enabled;
827
+ }
828
+ debug(message, ...args) {
829
+ if (this.enabled) {
830
+ console.debug('[FormshiveSubmit DEBUG]', message, ...args);
831
+ }
832
+ }
833
+ info(message, ...args) {
834
+ if (this.enabled) {
835
+ console.info('[FormshiveSubmit INFO]', message, ...args);
836
+ }
837
+ }
838
+ warn(message, ...args) {
839
+ if (this.enabled) {
840
+ console.warn('[FormshiveSubmit WARN]', message, ...args);
841
+ }
842
+ }
843
+ error(message, ...args) {
844
+ if (this.enabled) {
845
+ console.error('[FormshiveSubmit ERROR]', message, ...args);
846
+ }
847
+ }
848
+ setEnabled(enabled) {
849
+ this.enabled = enabled;
850
+ }
851
+ }
852
+ /**
853
+ * No-op logger for production builds
854
+ */
855
+ class NoOpLogger {
856
+ debug() { }
857
+ info() { }
858
+ warn() { }
859
+ error() { }
860
+ }
861
+ /**
862
+ * Create logger based on debug flag
863
+ */
864
+ function createLogger(debug = false) {
865
+ return debug ? new ConsoleLogger(true) : new NoOpLogger();
866
+ }
867
+ /**
868
+ * Validate URL format
869
+ */
870
+ function isValidUrl(url) {
871
+ try {
872
+ new URL(url);
873
+ return true;
874
+ }
875
+ catch {
876
+ return false;
877
+ }
878
+ }
879
+ /**
880
+ * Ensure URL has protocol
881
+ */
882
+ function ensureProtocol(url) {
883
+ if (!url.startsWith('http://') && !url.startsWith('https://')) {
884
+ return `https://${url}`;
885
+ }
886
+ return url;
887
+ }
888
+ /**
889
+ * Build form submission URL
890
+ */
891
+ function buildSubmissionUrl(endpoint, formId) {
892
+ const baseUrl = ensureProtocol(endpoint);
893
+ const trimmedUrl = baseUrl.replace(/\/$/, '');
894
+ return `${trimmedUrl}/digest/${encodeURIComponent(formId)}`;
895
+ }
896
+ /**
897
+ * Generate unique request ID for tracking
898
+ */
899
+ function generateRequestId() {
900
+ return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
901
+ }
902
+ /**
903
+ * Format duration in milliseconds to human readable string
904
+ */
905
+ function formatDuration(ms) {
906
+ if (ms < 1000) {
907
+ return `${ms}ms`;
908
+ }
909
+ else if (ms < 60000) {
910
+ return `${(ms / 1000).toFixed(1)}s`;
911
+ }
912
+ else {
913
+ const minutes = Math.floor(ms / 60000);
914
+ const seconds = ((ms % 60000) / 1000).toFixed(0);
915
+ return `${minutes}m ${seconds}s`;
916
+ }
917
+ }
918
+ /**
919
+ * Deep clone object (simple implementation)
920
+ */
921
+ function deepClone(obj) {
922
+ if (obj === null || typeof obj !== 'object') {
923
+ return obj;
924
+ }
925
+ if (obj instanceof Date) {
926
+ return new Date(obj.getTime());
927
+ }
928
+ if (obj instanceof Array) {
929
+ return obj.map(item => deepClone(item));
930
+ }
931
+ if (typeof obj === 'object') {
932
+ const cloned = {};
933
+ for (const key in obj) {
934
+ if (obj.hasOwnProperty(key)) {
935
+ cloned[key] = deepClone(obj[key]);
936
+ }
937
+ }
938
+ return cloned;
939
+ }
940
+ return obj;
941
+ }
942
+ /**
943
+ * Safely get nested object property
944
+ */
945
+ function getNestedProperty(obj, path) {
946
+ return path.split('.').reduce((current, key) => {
947
+ return current && current[key] !== undefined ? current[key] : undefined;
948
+ }, obj);
949
+ }
950
+ /**
951
+ * Debounce function
952
+ */
953
+ function debounce(func, wait) {
954
+ let timeout = null;
955
+ return (...args) => {
956
+ if (timeout) {
957
+ clearTimeout(timeout);
958
+ }
959
+ timeout = setTimeout(() => func(...args), wait);
960
+ };
961
+ }
962
+ /**
963
+ * Throttle function
964
+ */
965
+ function throttle(func, limit) {
966
+ let inThrottle = false;
967
+ return (...args) => {
968
+ if (!inThrottle) {
969
+ func(...args);
970
+ inThrottle = true;
971
+ setTimeout(() => (inThrottle = false), limit);
972
+ }
973
+ };
974
+ }
975
+ /**
976
+ * Check if running in browser environment
977
+ */
978
+ function isBrowser() {
979
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
980
+ }
981
+ /**
982
+ * Check if fetch is available
983
+ */
984
+ function isFetchAvailable() {
985
+ return typeof fetch !== 'undefined';
986
+ }
987
+ /**
988
+ * Check if FormData is available
989
+ */
990
+ function isFormDataAvailable() {
991
+ return typeof FormData !== 'undefined';
992
+ }
993
+ /**
994
+ * Get environment info for debugging
995
+ */
996
+ function getEnvironmentInfo() {
997
+ const userAgent = isBrowser() ? navigator.userAgent : undefined;
998
+ return {
999
+ browser: isBrowser(),
1000
+ fetch: isFetchAvailable(),
1001
+ formData: isFormDataAvailable(),
1002
+ userAgent,
1003
+ };
1004
+ }
1005
+ /**
1006
+ * Parse error message from various error types
1007
+ */
1008
+ function parseErrorMessage(error) {
1009
+ if (typeof error === 'string') {
1010
+ return error;
1011
+ }
1012
+ if (error && typeof error === 'object') {
1013
+ // Check for common error properties
1014
+ if (error.message) {
1015
+ return String(error.message);
1016
+ }
1017
+ if (error.error) {
1018
+ return String(error.error);
1019
+ }
1020
+ if (error.statusText) {
1021
+ return String(error.statusText);
1022
+ }
1023
+ // Try to stringify if it's an object
1024
+ try {
1025
+ return JSON.stringify(error);
1026
+ }
1027
+ catch {
1028
+ return 'Unknown error';
1029
+ }
1030
+ }
1031
+ return 'Unknown error';
1032
+ }
1033
+ /**
1034
+ * Sanitize string for logging (remove sensitive information)
1035
+ */
1036
+ function sanitizeForLogging(input) {
1037
+ // Remove potential passwords, tokens, keys, etc.
1038
+ return input
1039
+ .replace(/password[^&\s]*=[^&\s]*/gi, 'password=***')
1040
+ .replace(/token[^&\s]*=[^&\s]*/gi, 'token=***')
1041
+ .replace(/key[^&\s]*=[^&\s]*/gi, 'key=***')
1042
+ .replace(/secret[^&\s]*=[^&\s]*/gi, 'secret=***')
1043
+ .replace(/authorization:\s*[^\s]+/gi, 'authorization: ***');
1044
+ }
1045
+ /**
1046
+ * Create a timeout promise that rejects after specified time
1047
+ */
1048
+ function createTimeoutPromise(ms) {
1049
+ return new Promise((_, reject) => {
1050
+ setTimeout(() => {
1051
+ reject(new Error(`Operation timed out after ${ms}ms`));
1052
+ }, ms);
1053
+ });
1054
+ }
1055
+ /**
1056
+ * Race a promise against a timeout
1057
+ */
1058
+ function withTimeout(promise, ms) {
1059
+ return Promise.race([
1060
+ promise,
1061
+ createTimeoutPromise(ms)
1062
+ ]);
1063
+ }
1064
+ /**
1065
+ * Retry a promise with simple linear backoff
1066
+ */
1067
+ async function simpleRetry(fn, maxRetries = 3, delay = 1000) {
1068
+ let lastError;
1069
+ for (let i = 0; i <= maxRetries; i++) {
1070
+ try {
1071
+ return await fn();
1072
+ }
1073
+ catch (error) {
1074
+ lastError = error;
1075
+ if (i < maxRetries) {
1076
+ await new Promise(resolve => setTimeout(resolve, delay * (i + 1)));
1077
+ }
1078
+ }
1079
+ }
1080
+ throw lastError;
1081
+ }
1082
+ /**
1083
+ * Convert object to query string
1084
+ */
1085
+ function objectToQueryString(obj) {
1086
+ const params = new URLSearchParams();
1087
+ for (const [key, value] of Object.entries(obj)) {
1088
+ if (value !== undefined && value !== null) {
1089
+ params.append(key, String(value));
1090
+ }
1091
+ }
1092
+ return params.toString();
1093
+ }
1094
+ /**
1095
+ * Merge objects with deep merging for nested objects
1096
+ */
1097
+ function mergeObjects(target, ...sources) {
1098
+ if (!sources.length)
1099
+ return target;
1100
+ const source = sources.shift();
1101
+ if (source) {
1102
+ for (const key in source) {
1103
+ if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
1104
+ if (!target[key]) {
1105
+ Object.assign(target, { [key]: {} });
1106
+ }
1107
+ mergeObjects(target[key], source[key]);
1108
+ }
1109
+ else {
1110
+ Object.assign(target, { [key]: source[key] });
1111
+ }
1112
+ }
1113
+ }
1114
+ return mergeObjects(target, ...sources);
1115
+ }
1116
+
1117
+ /**
1118
+ * Main form submission functionality
1119
+ */
1120
+ /**
1121
+ * Main form submission function
1122
+ */
1123
+ async function submitForm(options) {
1124
+ // Validate required options
1125
+ if (!options.formId) {
1126
+ throw new Error('formId is required');
1127
+ }
1128
+ if (!options.data) {
1129
+ throw new Error('data is required');
1130
+ }
1131
+ // Merge with defaults
1132
+ const config = { ...DEFAULT_OPTIONS, ...options };
1133
+ // Create logger
1134
+ const logger = createLogger(config.debug);
1135
+ // Generate unique request ID for tracking
1136
+ const requestId = generateRequestId();
1137
+ const startTime = Date.now();
1138
+ logger.info(`Starting form submission (${requestId})`, {
1139
+ formId: config.formId,
1140
+ endpoint: config.endpoint,
1141
+ hasFiles: config.data instanceof FormData,
1142
+ environment: getEnvironmentInfo(),
1143
+ });
1144
+ // Validate endpoint URL
1145
+ if (!isValidUrl(config.endpoint)) {
1146
+ const error = new Error(`Invalid endpoint URL: ${config.endpoint}`);
1147
+ error.name = 'SubmitError';
1148
+ error.code = ERROR_CODES.VALIDATION_ERROR;
1149
+ error.attempt = 0;
1150
+ error.isRetryable = false;
1151
+ throw error;
1152
+ }
1153
+ try {
1154
+ // Create HTTP client
1155
+ const httpClient = createHttpClient(config.httpClient);
1156
+ logger.debug('HTTP client created', {
1157
+ type: typeof config.httpClient === 'string' ? config.httpClient : 'custom',
1158
+ supportsProgress: httpClient.supportsProgress(),
1159
+ });
1160
+ // Process form data and handle files
1161
+ const processedData = processFormData(config.data, config.files, logger);
1162
+ logger.debug('Form data processed', {
1163
+ hasFiles: processedData.hasFiles,
1164
+ fileCount: processedData.files.length,
1165
+ totalFileSize: processedData.files.reduce((sum, f) => sum + f.size, 0),
1166
+ });
1167
+ // Build submission URL
1168
+ const submissionUrl = buildSubmissionUrl(config.endpoint, config.formId);
1169
+ logger.debug(`Submission URL: ${submissionUrl}`);
1170
+ // Prepare request configuration
1171
+ const requestConfig = {
1172
+ url: submissionUrl,
1173
+ method: 'POST',
1174
+ data: processedData.data,
1175
+ headers: {
1176
+ ...config.headers,
1177
+ },
1178
+ timeout: config.timeout || 30000,
1179
+ };
1180
+ // Set appropriate content type if not manually specified
1181
+ if (!requestConfig.headers['Content-Type'] && !processedData.hasFiles) {
1182
+ requestConfig.headers['Content-Type'] = 'application/json';
1183
+ // Convert object to JSON string for non-multipart requests
1184
+ if (!(processedData.data instanceof FormData)) {
1185
+ requestConfig.data = JSON.stringify(processedData.data);
1186
+ }
1187
+ }
1188
+ // Set up progress tracking for file uploads
1189
+ if (processedData.hasFiles && httpClient.supportsProgress() && config.callbacks?.onProgress) {
1190
+ const progressTracker = createProgressTracker(processedData.files, config.callbacks.onProgress);
1191
+ if (progressTracker) {
1192
+ requestConfig.onUploadProgress = progressTracker;
1193
+ logger.debug('Progress tracking enabled for file upload');
1194
+ }
1195
+ }
1196
+ // Call onStart callback
1197
+ if (config.callbacks?.onStart) {
1198
+ try {
1199
+ config.callbacks.onStart();
1200
+ }
1201
+ catch (callbackError) {
1202
+ logger.warn('onStart callback error:', callbackError);
1203
+ }
1204
+ }
1205
+ // Create the submission function with retry logic
1206
+ const submitWithRetry = async () => {
1207
+ return withRetry(async () => {
1208
+ logger.debug('Making HTTP request...');
1209
+ const response = await httpClient.request(requestConfig);
1210
+ const duration = Date.now() - startTime;
1211
+ logger.info(`Form submission successful (${requestId})`, {
1212
+ statusCode: response.status,
1213
+ duration: formatDuration(duration),
1214
+ });
1215
+ // Prepare successful response
1216
+ const submitResponse = {
1217
+ success: true,
1218
+ data: response.data,
1219
+ statusCode: response.status,
1220
+ headers: response.headers,
1221
+ attempt: 1, // Will be updated by retry logic
1222
+ duration,
1223
+ };
1224
+ // Check for redirect URL in response
1225
+ if (response.headers['location']) {
1226
+ submitResponse.redirectUrl = response.headers['location'];
1227
+ }
1228
+ else if (response.data && typeof response.data === 'object' && response.data.redirect_url) {
1229
+ submitResponse.redirectUrl = response.data.redirect_url;
1230
+ }
1231
+ return submitResponse;
1232
+ }, config.retry, logger, (attempt, maxAttempts, error) => {
1233
+ logger.warn(`Retry attempt ${attempt}/${maxAttempts}:`, error.message);
1234
+ // Call retry callback
1235
+ if (config.callbacks?.onRetry) {
1236
+ try {
1237
+ config.callbacks.onRetry(attempt, maxAttempts, error);
1238
+ }
1239
+ catch (callbackError) {
1240
+ logger.warn('onRetry callback error:', callbackError);
1241
+ }
1242
+ }
1243
+ });
1244
+ };
1245
+ // Execute submission with retry
1246
+ const result = await submitWithRetry();
1247
+ // Update attempt count (this would be set by the retry mechanism)
1248
+ // For now, we'll estimate based on timing or assume success on attempt 1
1249
+ result.attempt = 1; // This should be provided by the retry mechanism
1250
+ // Call success callback
1251
+ if (config.callbacks?.onSuccess) {
1252
+ try {
1253
+ config.callbacks.onSuccess(result);
1254
+ }
1255
+ catch (callbackError) {
1256
+ logger.warn('onSuccess callback error:', callbackError);
1257
+ }
1258
+ }
1259
+ return result;
1260
+ }
1261
+ catch (error) {
1262
+ const duration = Date.now() - startTime;
1263
+ // Convert error to SubmitError if it isn't already
1264
+ let submitError;
1265
+ if (error.name === 'SubmitError') {
1266
+ submitError = error;
1267
+ }
1268
+ else {
1269
+ submitError = createSubmitError(error, 1); // Default to attempt 1
1270
+ }
1271
+ // Add duration and request ID for debugging
1272
+ submitError.message = `${submitError.message} (${requestId}, ${formatDuration(duration)})`;
1273
+ logger.error(`Form submission failed (${requestId})`, {
1274
+ error: submitError.code,
1275
+ message: submitError.message,
1276
+ statusCode: submitError.statusCode,
1277
+ duration: formatDuration(duration),
1278
+ isRetryable: submitError.isRetryable,
1279
+ });
1280
+ // Call error callback
1281
+ if (config.callbacks?.onError) {
1282
+ try {
1283
+ config.callbacks.onError(submitError);
1284
+ }
1285
+ catch (callbackError) {
1286
+ logger.warn('onError callback error:', callbackError);
1287
+ }
1288
+ }
1289
+ throw submitError;
1290
+ }
1291
+ }
1292
+ /**
1293
+ * Simplified form submission function with common defaults
1294
+ */
1295
+ async function submitFormSimple(formId, data, options = {}) {
1296
+ return submitForm({
1297
+ formId,
1298
+ data,
1299
+ ...options,
1300
+ });
1301
+ }
1302
+ /**
1303
+ * Submit form with success/error callbacks in a more functional style
1304
+ */
1305
+ function submitFormWithCallbacks(options, onSuccess, onError) {
1306
+ const config = {
1307
+ ...options,
1308
+ callbacks: {
1309
+ ...options.callbacks,
1310
+ onSuccess: onSuccess || options.callbacks?.onSuccess,
1311
+ onError: onError || options.callbacks?.onError,
1312
+ },
1313
+ };
1314
+ return submitForm(config);
1315
+ }
1316
+ /**
1317
+ * Create a reusable form submitter with pre-configured options
1318
+ */
1319
+ class FormSubmitter {
1320
+ constructor(defaultOptions = {}) {
1321
+ this.defaultOptions = defaultOptions;
1322
+ this.logger = createLogger(defaultOptions.debug);
1323
+ }
1324
+ /**
1325
+ * Submit a form using the default configuration
1326
+ */
1327
+ async submit(formId, data, overrideOptions = {}) {
1328
+ const baseOptions = { formId, data };
1329
+ const options = {
1330
+ ...this.defaultOptions,
1331
+ ...baseOptions,
1332
+ ...overrideOptions
1333
+ };
1334
+ return submitForm(options);
1335
+ }
1336
+ /**
1337
+ * Update default options
1338
+ */
1339
+ updateDefaults(newDefaults) {
1340
+ this.defaultOptions = { ...this.defaultOptions, ...newDefaults };
1341
+ this.logger = createLogger(this.defaultOptions.debug || false);
1342
+ }
1343
+ /**
1344
+ * Get current default options
1345
+ */
1346
+ getDefaults() {
1347
+ return { ...this.defaultOptions };
1348
+ }
1349
+ /**
1350
+ * Test connection to the API endpoint
1351
+ */
1352
+ async testConnection(endpoint) {
1353
+ const testEndpoint = endpoint || this.defaultOptions.endpoint || DEFAULT_OPTIONS.endpoint;
1354
+ try {
1355
+ const httpClient = createHttpClient(this.defaultOptions.httpClient || 'fetch');
1356
+ // Try to make a simple request to the base endpoint
1357
+ await httpClient.request({
1358
+ url: testEndpoint,
1359
+ method: 'GET',
1360
+ timeout: 5000,
1361
+ });
1362
+ return true;
1363
+ }
1364
+ catch (error) {
1365
+ this.logger.debug('Connection test failed:', error);
1366
+ return false;
1367
+ }
1368
+ }
1369
+ }
1370
+ /**
1371
+ * Utility to validate form data before submission
1372
+ */
1373
+ function validateFormData(data) {
1374
+ const errors = [];
1375
+ const warnings = [];
1376
+ if (!data) {
1377
+ errors.push('Form data is required');
1378
+ return { isValid: false, errors, warnings };
1379
+ }
1380
+ // Check for common issues
1381
+ if (data instanceof FormData) {
1382
+ let hasData = false;
1383
+ for (const [key, value] of data.entries()) {
1384
+ hasData = true;
1385
+ if (typeof value === 'string' && value.trim() === '') {
1386
+ warnings.push(`Field '${key}' is empty`);
1387
+ }
1388
+ }
1389
+ if (!hasData) {
1390
+ errors.push('FormData contains no fields');
1391
+ }
1392
+ }
1393
+ else {
1394
+ const keys = Object.keys(data);
1395
+ if (keys.length === 0) {
1396
+ errors.push('Form data object is empty');
1397
+ }
1398
+ // Check for potential issues
1399
+ keys.forEach(key => {
1400
+ const value = data[key];
1401
+ if (value === null || value === undefined) {
1402
+ warnings.push(`Field '${key}' is null or undefined`);
1403
+ }
1404
+ else if (typeof value === 'string' && value.trim() === '') {
1405
+ warnings.push(`Field '${key}' is empty`);
1406
+ }
1407
+ else if (typeof value === 'object' && !(value instanceof File) && !(value instanceof FileList)) {
1408
+ warnings.push(`Field '${key}' contains complex object - may not be serialized correctly`);
1409
+ }
1410
+ });
1411
+ }
1412
+ return {
1413
+ isValid: errors.length === 0,
1414
+ errors,
1415
+ warnings,
1416
+ };
1417
+ }
1418
+
1419
+ /**
1420
+ * Field validation error handling utilities
1421
+ */
1422
+ /**
1423
+ * Check if an error contains field validation errors
1424
+ */
1425
+ function isFieldValidationError(error) {
1426
+ return !!(error.fieldErrors && error.fieldErrors.length > 0);
1427
+ }
1428
+ /**
1429
+ * Get all field validation errors from a SubmitError
1430
+ */
1431
+ function getFieldErrors(error) {
1432
+ return error.fieldErrors || [];
1433
+ }
1434
+ /**
1435
+ * Get validation error for a specific field
1436
+ */
1437
+ function getFieldError(error, fieldName) {
1438
+ const fieldErrors = getFieldErrors(error);
1439
+ return fieldErrors.find(err => err.field === fieldName) || null;
1440
+ }
1441
+ /**
1442
+ * Check if a specific field has validation errors
1443
+ */
1444
+ function hasFieldError(error, fieldName) {
1445
+ return getFieldError(error, fieldName) !== null;
1446
+ }
1447
+ /**
1448
+ * Get all field names that have validation errors
1449
+ */
1450
+ function getErrorFieldNames(error) {
1451
+ const fieldErrors = getFieldErrors(error);
1452
+ return fieldErrors.map(err => err.field);
1453
+ }
1454
+ /**
1455
+ * Get validation response if available
1456
+ */
1457
+ function getValidationResponse(error) {
1458
+ return error.validationResponse || null;
1459
+ }
1460
+ /**
1461
+ * Format field errors into a simple object for easy UI consumption
1462
+ */
1463
+ function formatFieldErrors(error) {
1464
+ const fieldErrors = getFieldErrors(error);
1465
+ const formatted = {};
1466
+ fieldErrors.forEach(err => {
1467
+ formatted[err.field] = err.message;
1468
+ });
1469
+ return formatted;
1470
+ }
1471
+ /**
1472
+ * Format field errors with codes for advanced error handling
1473
+ */
1474
+ function formatFieldErrorsWithCodes(error) {
1475
+ const fieldErrors = getFieldErrors(error);
1476
+ const formatted = {};
1477
+ fieldErrors.forEach(err => {
1478
+ formatted[err.field] = err;
1479
+ });
1480
+ return formatted;
1481
+ }
1482
+ /**
1483
+ * Get a human-readable summary of validation errors
1484
+ */
1485
+ function getValidationErrorSummary(error) {
1486
+ if (!isFieldValidationError(error)) {
1487
+ return error.message || 'Validation error occurred';
1488
+ }
1489
+ const validationResponse = getValidationResponse(error);
1490
+ if (validationResponse?.message) {
1491
+ return validationResponse.message;
1492
+ }
1493
+ const fieldErrors = getFieldErrors(error);
1494
+ if (fieldErrors.length === 1) {
1495
+ return `${fieldErrors[0].field}: ${fieldErrors[0].message}`;
1496
+ }
1497
+ else if (fieldErrors.length > 1) {
1498
+ const fieldNames = fieldErrors.map(err => err.field).join(', ');
1499
+ return `Validation failed for fields: ${fieldNames}`;
1500
+ }
1501
+ return 'Form validation failed';
1502
+ }
1503
+ /**
1504
+ * Create a map of field names to their error messages for easy form field highlighting
1505
+ */
1506
+ function createFieldErrorMap(error) {
1507
+ const fieldErrors = getFieldErrors(error);
1508
+ const errorMap = new Map();
1509
+ fieldErrors.forEach(err => {
1510
+ errorMap.set(err.field, err.message);
1511
+ });
1512
+ return errorMap;
1513
+ }
1514
+ /**
1515
+ * Get the first field error (useful for focusing on the first invalid field)
1516
+ */
1517
+ function getFirstFieldError(error) {
1518
+ const fieldErrors = getFieldErrors(error);
1519
+ return fieldErrors.length > 0 ? fieldErrors[0] : null;
1520
+ }
1521
+ /**
1522
+ * Check if validation errors include specific error codes
1523
+ */
1524
+ function hasErrorCode(error, code) {
1525
+ const fieldErrors = getFieldErrors(error);
1526
+ return fieldErrors.some(err => err.code === code);
1527
+ }
1528
+ /**
1529
+ * Check if a specific field has a specific error code
1530
+ */
1531
+ function hasFieldErrorCode(error, fieldName, code) {
1532
+ const fieldError = getFieldError(error, fieldName);
1533
+ return fieldError?.code === code;
1534
+ }
1535
+ /**
1536
+ * Get all errors with a specific code
1537
+ */
1538
+ function getErrorsByCode(error, code) {
1539
+ const fieldErrors = getFieldErrors(error);
1540
+ return fieldErrors.filter(err => err.code === code);
1541
+ }
1542
+ /**
1543
+ * Group errors by their error codes
1544
+ */
1545
+ function groupErrorsByCode(error) {
1546
+ const fieldErrors = getFieldErrors(error);
1547
+ const grouped = {};
1548
+ fieldErrors.forEach(err => {
1549
+ if (!grouped[err.code]) {
1550
+ grouped[err.code] = [];
1551
+ }
1552
+ grouped[err.code].push(err);
1553
+ });
1554
+ return grouped;
1555
+ }
1556
+ /**
1557
+ * Create field error helpers for easy UI integration
1558
+ */
1559
+ function createFieldErrorHelpers(error, defaultErrorClass = 'error') {
1560
+ return {
1561
+ getMessage: (fieldName) => {
1562
+ if (!error)
1563
+ return '';
1564
+ const fieldError = getFieldError(error, fieldName);
1565
+ return fieldError?.message || '';
1566
+ },
1567
+ hasError: (fieldName) => {
1568
+ if (!error)
1569
+ return false;
1570
+ return hasFieldError(error, fieldName);
1571
+ },
1572
+ getFieldClass: (fieldName, errorClass) => {
1573
+ if (!error)
1574
+ return '';
1575
+ const hasError = hasFieldError(error, fieldName);
1576
+ return hasError ? (errorClass || defaultErrorClass) : '';
1577
+ },
1578
+ getError: (fieldName) => {
1579
+ if (!error)
1580
+ return null;
1581
+ return getFieldError(error, fieldName);
1582
+ }
1583
+ };
1584
+ }
1585
+ /**
1586
+ * Extract field validation errors from any error object
1587
+ * Useful when you're not sure if the error is a SubmitError
1588
+ */
1589
+ function extractFieldErrors(error) {
1590
+ if (!error)
1591
+ return [];
1592
+ // If it's already a SubmitError
1593
+ if (error.fieldErrors && Array.isArray(error.fieldErrors)) {
1594
+ return error.fieldErrors;
1595
+ }
1596
+ // If it has a validationResponse
1597
+ if (error.validationResponse?.errors && Array.isArray(error.validationResponse.errors)) {
1598
+ return error.validationResponse.errors;
1599
+ }
1600
+ // Try to extract from response data
1601
+ if (error.response?.errors && Array.isArray(error.response.errors)) {
1602
+ return error.response.errors;
1603
+ }
1604
+ return [];
1605
+ }
1606
+
1607
+ /**
1608
+ * Formshive Submit Library
1609
+ *
1610
+ * A JavaScript library for submitting forms to Formshive with retry logic,
1611
+ * file support, and flexible HTTP client options.
1612
+ */
1613
+ // Export main functionality
1614
+ // Set up global object for browser usage
1615
+ if (typeof window !== 'undefined') {
1616
+ window.FormshiveSubmit = {
1617
+ submitForm,
1618
+ submitFormSimple,
1619
+ FormSubmitter,
1620
+ validateFormData,
1621
+ RetryPresets,
1622
+ ERROR_CODES,
1623
+ formatFileSize,
1624
+ createLogger,
1625
+ // Field validation utilities
1626
+ isFieldValidationError,
1627
+ getFieldErrors,
1628
+ createFieldErrorHelpers,
1629
+ };
1630
+ }
1631
+ // Default export for convenience
1632
+ var main = {
1633
+ submitForm,
1634
+ submitFormSimple,
1635
+ FormSubmitter,
1636
+ validateFormData,
1637
+ RetryPresets,
1638
+ ERROR_CODES,
1639
+ formatFileSize,
1640
+ createLogger,
1641
+ // Field validation utilities
1642
+ isFieldValidationError,
1643
+ getFieldErrors,
1644
+ createFieldErrorHelpers,
1645
+ };
1646
+
1647
+ exports.ConsoleLogger = ConsoleLogger;
1648
+ exports.DEFAULT_FILE_CONFIG = DEFAULT_FILE_CONFIG;
1649
+ exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
1650
+ exports.DEFAULT_RETRY_CONFIG = DEFAULT_RETRY_CONFIG;
1651
+ exports.ERROR_CODES = ERROR_CODES;
1652
+ exports.FormSubmitter = FormSubmitter;
1653
+ exports.NoOpLogger = NoOpLogger;
1654
+ exports.RetryManager = RetryManager;
1655
+ exports.RetryPresets = RetryPresets;
1656
+ exports.buildSubmissionUrl = buildSubmissionUrl;
1657
+ exports.createFieldErrorHelpers = createFieldErrorHelpers;
1658
+ exports.createFieldErrorMap = createFieldErrorMap;
1659
+ exports.createHttpClient = createHttpClient;
1660
+ exports.createLogger = createLogger;
1661
+ exports.createProgressTracker = createProgressTracker;
1662
+ exports.createRetryableFunction = createRetryableFunction;
1663
+ exports.createSubmitError = createSubmitError;
1664
+ exports.createTimeoutPromise = createTimeoutPromise;
1665
+ exports.debounce = debounce;
1666
+ exports.deepClone = deepClone;
1667
+ exports.default = main;
1668
+ exports.ensureProtocol = ensureProtocol;
1669
+ exports.extractFieldErrors = extractFieldErrors;
1670
+ exports.extractFiles = extractFiles;
1671
+ exports.formatDuration = formatDuration;
1672
+ exports.formatFieldErrors = formatFieldErrors;
1673
+ exports.formatFieldErrorsWithCodes = formatFieldErrorsWithCodes;
1674
+ exports.formatFileSize = formatFileSize;
1675
+ exports.generateRequestId = generateRequestId;
1676
+ exports.getEnhancedFileInfo = getEnhancedFileInfo;
1677
+ exports.getEnvironmentInfo = getEnvironmentInfo;
1678
+ exports.getErrorFieldNames = getErrorFieldNames;
1679
+ exports.getErrorMessage = getErrorMessage;
1680
+ exports.getErrorsByCode = getErrorsByCode;
1681
+ exports.getFieldError = getFieldError;
1682
+ exports.getFieldErrors = getFieldErrors;
1683
+ exports.getFirstFieldError = getFirstFieldError;
1684
+ exports.getMimeTypeFromExtension = getMimeTypeFromExtension;
1685
+ exports.getNestedProperty = getNestedProperty;
1686
+ exports.getRetryDelayInfo = getRetryDelayInfo;
1687
+ exports.getValidationErrorSummary = getValidationErrorSummary;
1688
+ exports.getValidationResponse = getValidationResponse;
1689
+ exports.groupErrorsByCode = groupErrorsByCode;
1690
+ exports.hasErrorCode = hasErrorCode;
1691
+ exports.hasFieldError = hasFieldError;
1692
+ exports.hasFieldErrorCode = hasFieldErrorCode;
1693
+ exports.isBrowser = isBrowser;
1694
+ exports.isFetchAvailable = isFetchAvailable;
1695
+ exports.isFieldValidationError = isFieldValidationError;
1696
+ exports.isFormDataAvailable = isFormDataAvailable;
1697
+ exports.isRetryableError = isRetryableError;
1698
+ exports.isValidUrl = isValidUrl;
1699
+ exports.mergeObjects = mergeObjects;
1700
+ exports.objectToQueryString = objectToQueryString;
1701
+ exports.parseErrorMessage = parseErrorMessage;
1702
+ exports.processFormData = processFormData;
1703
+ exports.sanitizeForLogging = sanitizeForLogging;
1704
+ exports.simpleRetry = simpleRetry;
1705
+ exports.submitForm = submitForm;
1706
+ exports.submitFormSimple = submitFormSimple;
1707
+ exports.submitFormWithCallbacks = submitFormWithCallbacks;
1708
+ exports.throttle = throttle;
1709
+ exports.validateFile = validateFile;
1710
+ exports.validateFiles = validateFiles;
1711
+ exports.validateFormData = validateFormData;
1712
+ exports.withRetry = withRetry;
1713
+ exports.withTimeout = withTimeout;
1714
+ //# sourceMappingURL=formshive-submit.cjs.map