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