svelte-firekit 0.0.25 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +445 -213
  2. package/dist/components/Collection.svelte +150 -0
  3. package/dist/components/Collection.svelte.d.ts +27 -0
  4. package/dist/components/Ddoc.svelte +131 -0
  5. package/dist/components/Ddoc.svelte.d.ts +28 -0
  6. package/dist/components/Node.svelte +97 -0
  7. package/dist/components/Node.svelte.d.ts +23 -0
  8. package/dist/components/auth-guard.svelte +89 -0
  9. package/dist/components/auth-guard.svelte.d.ts +26 -0
  10. package/dist/components/custom-guard.svelte +122 -0
  11. package/dist/components/custom-guard.svelte.d.ts +31 -0
  12. package/dist/components/download-url.svelte +92 -0
  13. package/dist/components/download-url.svelte.d.ts +19 -0
  14. package/dist/components/firebase-app.svelte +30 -0
  15. package/dist/components/firebase-app.svelte.d.ts +7 -0
  16. package/dist/components/node-list.svelte +102 -0
  17. package/dist/components/node-list.svelte.d.ts +27 -0
  18. package/dist/components/signed-in.svelte +42 -0
  19. package/dist/components/signed-in.svelte.d.ts +11 -0
  20. package/dist/components/signed-out.svelte +42 -0
  21. package/dist/components/signed-out.svelte.d.ts +11 -0
  22. package/dist/components/storage-list.svelte +97 -0
  23. package/dist/components/storage-list.svelte.d.ts +26 -0
  24. package/dist/components/upload-task.svelte +108 -0
  25. package/dist/components/upload-task.svelte.d.ts +24 -0
  26. package/dist/config.js +17 -39
  27. package/dist/firebase.d.ts +43 -21
  28. package/dist/firebase.js +121 -35
  29. package/dist/index.d.ts +21 -13
  30. package/dist/index.js +27 -15
  31. package/dist/services/auth.d.ts +397 -0
  32. package/dist/services/auth.js +882 -0
  33. package/dist/services/collection.svelte.d.ts +286 -0
  34. package/dist/services/collection.svelte.js +871 -0
  35. package/dist/services/document.svelte.d.ts +288 -0
  36. package/dist/services/document.svelte.js +555 -0
  37. package/dist/services/mutations.d.ts +336 -0
  38. package/dist/services/mutations.js +1079 -0
  39. package/dist/services/presence.svelte.d.ts +141 -0
  40. package/dist/services/presence.svelte.js +727 -0
  41. package/dist/{realtime → services}/realtime.svelte.d.ts +3 -1
  42. package/dist/{realtime → services}/realtime.svelte.js +13 -7
  43. package/dist/services/storage.svelte.d.ts +257 -0
  44. package/dist/services/storage.svelte.js +374 -0
  45. package/dist/services/user.svelte.d.ts +296 -0
  46. package/dist/services/user.svelte.js +609 -0
  47. package/dist/types/auth.d.ts +158 -0
  48. package/dist/types/auth.js +106 -0
  49. package/dist/types/collection.d.ts +360 -0
  50. package/dist/types/collection.js +167 -0
  51. package/dist/types/document.d.ts +342 -0
  52. package/dist/types/document.js +148 -0
  53. package/dist/types/firebase.d.ts +44 -0
  54. package/dist/types/firebase.js +33 -0
  55. package/dist/types/index.d.ts +6 -0
  56. package/dist/types/index.js +4 -0
  57. package/dist/types/mutations.d.ts +387 -0
  58. package/dist/types/mutations.js +205 -0
  59. package/dist/types/presence.d.ts +282 -0
  60. package/dist/types/presence.js +80 -0
  61. package/dist/utils/errors.d.ts +21 -0
  62. package/dist/utils/errors.js +35 -0
  63. package/dist/utils/firestore.d.ts +9 -0
  64. package/dist/utils/firestore.js +33 -0
  65. package/dist/utils/index.d.ts +4 -0
  66. package/dist/utils/index.js +8 -0
  67. package/dist/utils/providers.d.ts +16 -0
  68. package/dist/utils/providers.js +30 -0
  69. package/dist/utils/user.d.ts +8 -0
  70. package/dist/utils/user.js +29 -0
  71. package/package.json +64 -64
  72. package/dist/auth/auth.d.ts +0 -117
  73. package/dist/auth/auth.js +0 -194
  74. package/dist/auth/presence.svelte.d.ts +0 -139
  75. package/dist/auth/presence.svelte.js +0 -373
  76. package/dist/auth/user.svelte.d.ts +0 -112
  77. package/dist/auth/user.svelte.js +0 -155
  78. package/dist/firestore/awaitable-doc.svelte.d.ts +0 -141
  79. package/dist/firestore/awaitable-doc.svelte.js +0 -183
  80. package/dist/firestore/batch-mutations.svelte.d.ts +0 -140
  81. package/dist/firestore/batch-mutations.svelte.js +0 -218
  82. package/dist/firestore/collection-group.svelte.d.ts +0 -78
  83. package/dist/firestore/collection-group.svelte.js +0 -120
  84. package/dist/firestore/collection.svelte.d.ts +0 -96
  85. package/dist/firestore/collection.svelte.js +0 -137
  86. package/dist/firestore/doc.svelte.d.ts +0 -90
  87. package/dist/firestore/doc.svelte.js +0 -131
  88. package/dist/firestore/document-mutations.svelte.d.ts +0 -164
  89. package/dist/firestore/document-mutations.svelte.js +0 -273
  90. package/dist/storage/download-url.svelte.d.ts +0 -83
  91. package/dist/storage/download-url.svelte.js +0 -114
  92. package/dist/storage/storage-list.svelte.d.ts +0 -89
  93. package/dist/storage/storage-list.svelte.js +0 -123
  94. package/dist/storage/upload-task.svelte.d.ts +0 -94
  95. package/dist/storage/upload-task.svelte.js +0 -138
@@ -0,0 +1,1079 @@
1
+ /**
2
+ * @fileoverview FirekitDocumentMutations - Optimized document mutation service for Svelte
3
+ * @module FirekitDocumentMutations
4
+ * @version 1.0.0
5
+ */
6
+ import { addDoc, setDoc, updateDoc, deleteDoc, doc, getDoc, collection, serverTimestamp, increment, arrayUnion, arrayRemove, deleteField, writeBatch, runTransaction } from 'firebase/firestore';
7
+ import { firebaseService } from '../firebase.js';
8
+ import { firekitUser } from './user.svelte.js';
9
+ import { MutationOperationType, MutationErrorCode, MutationError } from '../types/mutations.js';
10
+ import { CacheSource } from '../types/document.js';
11
+ /**
12
+ * Comprehensive Firestore document mutation service with advanced features.
13
+ * Handles CRUD operations, batch processing, validation, error handling, and analytics.
14
+ *
15
+ * @class FirekitDocumentMutations
16
+ * @example
17
+ * ```typescript
18
+ * import { firekitDocMutations } from 'svelte-firekit';
19
+ *
20
+ * // Add a new document
21
+ * const result = await firekitDocMutations.add('users', {
22
+ * name: 'John Doe',
23
+ * email: 'john@example.com'
24
+ * }, {
25
+ * timestamps: true,
26
+ * validate: true
27
+ * });
28
+ *
29
+ * // Batch operations
30
+ * const batchResult = await firekitDocMutations.batch([
31
+ * { type: 'create', path: 'users', data: userData },
32
+ * { type: 'update', path: 'profiles/123', data: profileUpdate }
33
+ * ]);
34
+ *
35
+ * // Listen to mutation events
36
+ * const unsubscribe = firekitDocMutations.addEventListener((event) => {
37
+ * console.log('Mutation event:', event.type, event.data);
38
+ * });
39
+ * ```
40
+ */
41
+ class FirekitDocumentMutations {
42
+ eventListeners = new Set();
43
+ analytics = this.initializeAnalytics();
44
+ defaultOptions = {
45
+ timestamps: true,
46
+ merge: false,
47
+ validate: false,
48
+ optimistic: false,
49
+ retry: {
50
+ enabled: true,
51
+ maxAttempts: 3,
52
+ baseDelay: 1000,
53
+ strategy: 'exponential'
54
+ }
55
+ };
56
+ /**
57
+ * Initialize analytics object
58
+ */
59
+ initializeAnalytics() {
60
+ return {
61
+ totalMutations: 0,
62
+ successfulMutations: 0,
63
+ failedMutations: 0,
64
+ successRate: 0,
65
+ averageDuration: 0,
66
+ commonErrors: [],
67
+ performanceByOperation: {},
68
+ retryStats: {
69
+ totalRetries: 0,
70
+ retriesPerMutation: 0,
71
+ retrySuccessRate: 0
72
+ }
73
+ };
74
+ }
75
+ /**
76
+ * Generate timestamp data for document mutations
77
+ */
78
+ getTimestampData(isNew = true) {
79
+ const timestamps = {
80
+ updatedAt: serverTimestamp(),
81
+ updatedBy: firekitUser.uid || 'anonymous'
82
+ };
83
+ if (isNew) {
84
+ timestamps.createdAt = serverTimestamp();
85
+ timestamps.createdBy = firekitUser.uid || 'anonymous';
86
+ }
87
+ return timestamps;
88
+ }
89
+ /**
90
+ * Validate data using custom validator or basic validation
91
+ */
92
+ validateData(data, validator) {
93
+ if (validator) {
94
+ return validator(data);
95
+ }
96
+ // Basic validation
97
+ if (!data || typeof data !== 'object') {
98
+ return {
99
+ valid: false,
100
+ message: 'Data must be a valid object'
101
+ };
102
+ }
103
+ // Check for null/undefined values in top-level fields
104
+ const invalidFields = {};
105
+ for (const [key, value] of Object.entries(data)) {
106
+ if (value === null || value === undefined) {
107
+ invalidFields[key] = 'Field cannot be null or undefined';
108
+ }
109
+ }
110
+ if (Object.keys(invalidFields).length > 0) {
111
+ return {
112
+ valid: false,
113
+ message: 'Some fields contain invalid values',
114
+ fieldErrors: invalidFields
115
+ };
116
+ }
117
+ return { valid: true };
118
+ }
119
+ /**
120
+ * Handle and format mutation errors
121
+ */
122
+ handleError(error, operation, path) {
123
+ let mutationError;
124
+ if (error instanceof MutationError) {
125
+ mutationError = error;
126
+ }
127
+ else {
128
+ // Map Firestore errors to MutationError
129
+ const code = this.mapFirestoreErrorCode(error.code);
130
+ mutationError = new MutationError(code, error.message || 'An unknown error occurred', operation, path, error);
131
+ }
132
+ // Update analytics
133
+ this.analytics.failedMutations++;
134
+ this.updateErrorAnalytics(mutationError);
135
+ // Emit error event
136
+ this.emitEvent({
137
+ type: 'mutation_error',
138
+ error: mutationError,
139
+ timestamp: new Date(),
140
+ userId: firekitUser.uid ?? 'anonymous'
141
+ });
142
+ console.error('FirekitDocumentMutations error:', mutationError);
143
+ return mutationError;
144
+ }
145
+ /**
146
+ * Map Firestore error codes to MutationErrorCode
147
+ */
148
+ mapFirestoreErrorCode(firestoreCode) {
149
+ switch (firestoreCode) {
150
+ case 'permission-denied':
151
+ return MutationErrorCode.PERMISSION_DENIED;
152
+ case 'not-found':
153
+ return MutationErrorCode.NOT_FOUND;
154
+ case 'already-exists':
155
+ return MutationErrorCode.ALREADY_EXISTS;
156
+ case 'failed-precondition':
157
+ return MutationErrorCode.FAILED_PRECONDITION;
158
+ case 'aborted':
159
+ return MutationErrorCode.ABORTED;
160
+ case 'out-of-range':
161
+ return MutationErrorCode.OUT_OF_RANGE;
162
+ case 'unimplemented':
163
+ return MutationErrorCode.UNIMPLEMENTED;
164
+ case 'internal':
165
+ return MutationErrorCode.INTERNAL_ERROR;
166
+ case 'unavailable':
167
+ return MutationErrorCode.UNAVAILABLE;
168
+ case 'deadline-exceeded':
169
+ return MutationErrorCode.DEADLINE_EXCEEDED;
170
+ case 'unauthenticated':
171
+ return MutationErrorCode.UNAUTHENTICATED;
172
+ case 'resource-exhausted':
173
+ return MutationErrorCode.RESOURCE_EXHAUSTED;
174
+ case 'cancelled':
175
+ return MutationErrorCode.CANCELLED;
176
+ default:
177
+ return MutationErrorCode.UNKNOWN;
178
+ }
179
+ }
180
+ /**
181
+ * Update error analytics
182
+ */
183
+ updateErrorAnalytics(error) {
184
+ const existingError = this.analytics.commonErrors.find((e) => e.code === error.code);
185
+ if (existingError) {
186
+ existingError.count++;
187
+ }
188
+ else {
189
+ this.analytics.commonErrors.push({
190
+ code: error.code,
191
+ count: 1,
192
+ percentage: 0
193
+ });
194
+ }
195
+ // Recalculate percentages
196
+ const totalErrors = this.analytics.commonErrors.reduce((sum, e) => sum + e.count, 0);
197
+ this.analytics.commonErrors.forEach((e) => {
198
+ e.percentage = (e.count / totalErrors) * 100;
199
+ });
200
+ // Sort by count
201
+ this.analytics.commonErrors.sort((a, b) => b.count - a.count);
202
+ }
203
+ /**
204
+ * Emit event to all listeners
205
+ */
206
+ emitEvent(event) {
207
+ this.eventListeners.forEach((callback) => {
208
+ try {
209
+ callback(event);
210
+ }
211
+ catch (error) {
212
+ console.error('Error in mutation event listener:', error);
213
+ }
214
+ });
215
+ }
216
+ /**
217
+ * Execute operation with retry logic
218
+ */
219
+ async executeWithRetry(operation, options, operationName, path) {
220
+ const retryConfig = { ...this.defaultOptions.retry, ...options.retry };
221
+ let lastError;
222
+ let attempt = 0;
223
+ while (attempt < retryConfig.maxAttempts) {
224
+ try {
225
+ if (attempt > 0) {
226
+ this.emitEvent({
227
+ type: 'mutation_retry',
228
+ data: { attempt, maxAttempts: retryConfig.maxAttempts, operation: operationName, path },
229
+ timestamp: new Date(),
230
+ userId: firekitUser.uid ?? 'anonymous'
231
+ });
232
+ this.analytics.retryStats.totalRetries++;
233
+ }
234
+ const result = await operation();
235
+ if (attempt > 0) {
236
+ // Successful retry
237
+ this.analytics.retryStats.retrySuccessRate =
238
+ (this.analytics.retryStats.retrySuccessRate + 1) / 2;
239
+ }
240
+ return result;
241
+ }
242
+ catch (error) {
243
+ lastError = this.handleError(error, operationName, path);
244
+ if (!lastError.isRetryable() ||
245
+ (retryConfig.shouldRetry && !retryConfig.shouldRetry(lastError, attempt))) {
246
+ break;
247
+ }
248
+ attempt++;
249
+ if (attempt < retryConfig.maxAttempts) {
250
+ const delay = retryConfig.strategy === 'exponential'
251
+ ? Math.min(retryConfig.baseDelay * Math.pow(2, attempt), retryConfig.maxDelay || 30000)
252
+ : retryConfig.baseDelay;
253
+ await new Promise((resolve) => setTimeout(resolve, delay));
254
+ }
255
+ }
256
+ }
257
+ throw lastError;
258
+ }
259
+ // ========================================
260
+ // CRUD OPERATIONS
261
+ // ========================================
262
+ /**
263
+ * Add a new document to a collection
264
+ *
265
+ * @template T Document data type
266
+ * @param collectionPath Collection path
267
+ * @param data Document data
268
+ * @param options Mutation options
269
+ * @returns Promise resolving to mutation response
270
+ *
271
+ * @example
272
+ * ```typescript
273
+ * const result = await firekitDocMutations.add('users', {
274
+ * name: 'John Doe',
275
+ * email: 'john@example.com'
276
+ * }, {
277
+ * timestamps: true,
278
+ * customId: 'custom-user-id',
279
+ * validate: true
280
+ * });
281
+ *
282
+ * if (result.success) {
283
+ * console.log('Created document:', result.id);
284
+ * }
285
+ * ```
286
+ */
287
+ async add(collectionPath, data, options = {}) {
288
+ const startTime = Date.now();
289
+ const mergedOptions = { ...this.defaultOptions, ...options };
290
+ try {
291
+ // Emit start event
292
+ this.emitEvent({
293
+ type: 'mutation_start',
294
+ data: { operation: 'create', path: collectionPath, data },
295
+ timestamp: new Date(),
296
+ userId: firekitUser.uid ?? 'anonymous'
297
+ });
298
+ // Validate data if requested
299
+ if (mergedOptions.validate) {
300
+ const validation = this.validateData(data, mergedOptions.validator);
301
+ if (!validation.valid) {
302
+ throw new MutationError(MutationErrorCode.VALIDATION_FAILED, validation.message || 'Validation failed', MutationOperationType.CREATE, collectionPath, null, { fieldErrors: validation.fieldErrors });
303
+ }
304
+ }
305
+ const result = await this.executeWithRetry(async () => {
306
+ const firestore = firebaseService.getDbInstance();
307
+ if (!firestore) {
308
+ throw new MutationError(MutationErrorCode.SERVICE_UNAVAILABLE, 'Firestore service not available');
309
+ }
310
+ const colRef = collection(firestore, collectionPath);
311
+ let dataToAdd = {
312
+ ...data,
313
+ ...(mergedOptions.timestamps && this.getTimestampData())
314
+ };
315
+ let docRef;
316
+ if (mergedOptions.customId) {
317
+ docRef = doc(colRef, mergedOptions.customId);
318
+ dataToAdd = { ...dataToAdd, id: docRef.id };
319
+ await setDoc(docRef, dataToAdd);
320
+ }
321
+ else {
322
+ docRef = (await addDoc(colRef, dataToAdd));
323
+ dataToAdd = { ...dataToAdd, id: docRef.id };
324
+ await setDoc(docRef, dataToAdd);
325
+ }
326
+ return {
327
+ success: true,
328
+ id: docRef.id,
329
+ data: { ...dataToAdd, id: docRef.id },
330
+ metadata: {
331
+ timestamp: new Date(),
332
+ operation: MutationOperationType.CREATE,
333
+ source: CacheSource.CLIENT,
334
+ performedBy: firekitUser.uid ?? 'anonymous',
335
+ duration: Date.now() - startTime
336
+ }
337
+ };
338
+ }, mergedOptions, MutationOperationType.CREATE, collectionPath);
339
+ // Update analytics
340
+ this.analytics.totalMutations++;
341
+ this.analytics.successfulMutations++;
342
+ this.updateOperationAnalytics(MutationOperationType.CREATE, Date.now() - startTime, true);
343
+ // Emit success event
344
+ this.emitEvent({
345
+ type: 'mutation_success',
346
+ data: { operation: 'create', path: collectionPath, result },
347
+ timestamp: new Date(),
348
+ userId: firekitUser.uid ?? 'anonymous'
349
+ });
350
+ return result;
351
+ }
352
+ catch (error) {
353
+ this.analytics.totalMutations++;
354
+ const mutationError = error instanceof MutationError
355
+ ? error
356
+ : this.handleError(error, MutationOperationType.CREATE, collectionPath);
357
+ this.updateOperationAnalytics(MutationOperationType.CREATE, Date.now() - startTime, false);
358
+ return {
359
+ success: false,
360
+ error: mutationError,
361
+ metadata: {
362
+ timestamp: new Date(),
363
+ operation: MutationOperationType.CREATE,
364
+ source: CacheSource.CLIENT,
365
+ performedBy: firekitUser.uid ?? 'anonymous' ?? 'anonymous',
366
+ duration: Date.now() - startTime
367
+ }
368
+ };
369
+ }
370
+ }
371
+ /**
372
+ * Set document data at specified path
373
+ *
374
+ * @template T Document data type
375
+ * @param path Document path
376
+ * @param data Document data
377
+ * @param options Mutation options
378
+ * @returns Promise resolving to mutation response
379
+ *
380
+ * @example
381
+ * ```typescript
382
+ * const result = await firekitDocMutations.set('users/123', {
383
+ * name: 'John Doe',
384
+ * email: 'john@example.com'
385
+ * }, {
386
+ * merge: true,
387
+ * timestamps: true
388
+ * });
389
+ * ```
390
+ */
391
+ async set(path, data, options = {}) {
392
+ const startTime = Date.now();
393
+ const mergedOptions = { ...this.defaultOptions, ...options };
394
+ try {
395
+ this.emitEvent({
396
+ type: 'mutation_start',
397
+ data: { operation: 'set', path, data },
398
+ timestamp: new Date(),
399
+ userId: firekitUser.uid ?? 'anonymous'
400
+ });
401
+ if (mergedOptions.validate) {
402
+ const validation = this.validateData(data, mergedOptions.validator);
403
+ if (!validation.valid) {
404
+ throw new MutationError(MutationErrorCode.VALIDATION_FAILED, validation.message || 'Validation failed', MutationOperationType.SET, path, null, { fieldErrors: validation.fieldErrors });
405
+ }
406
+ }
407
+ const result = await this.executeWithRetry(async () => {
408
+ const firestore = firebaseService.getDbInstance();
409
+ if (!firestore) {
410
+ throw new MutationError(MutationErrorCode.SERVICE_UNAVAILABLE, 'Firestore service not available');
411
+ }
412
+ const docRef = doc(firestore, path);
413
+ const dataToSet = {
414
+ ...data,
415
+ ...(mergedOptions.timestamps && this.getTimestampData()),
416
+ id: docRef.id
417
+ };
418
+ await setDoc(docRef, dataToSet, { merge: mergedOptions.merge });
419
+ return {
420
+ success: true,
421
+ id: docRef.id,
422
+ data: dataToSet,
423
+ metadata: {
424
+ timestamp: new Date(),
425
+ operation: MutationOperationType.SET,
426
+ source: CacheSource.CLIENT,
427
+ performedBy: firekitUser.uid ?? 'anonymous' ?? 'anonymous',
428
+ duration: Date.now() - startTime
429
+ }
430
+ };
431
+ }, mergedOptions, MutationOperationType.SET, path);
432
+ this.analytics.totalMutations++;
433
+ this.analytics.successfulMutations++;
434
+ this.updateOperationAnalytics(MutationOperationType.SET, Date.now() - startTime, true);
435
+ this.emitEvent({
436
+ type: 'mutation_success',
437
+ data: { operation: 'set', path, result },
438
+ timestamp: new Date(),
439
+ userId: firekitUser.uid ?? 'anonymous'
440
+ });
441
+ return result;
442
+ }
443
+ catch (error) {
444
+ this.analytics.totalMutations++;
445
+ const mutationError = error instanceof MutationError
446
+ ? error
447
+ : this.handleError(error, MutationOperationType.SET, path);
448
+ this.updateOperationAnalytics(MutationOperationType.SET, Date.now() - startTime, false);
449
+ return {
450
+ success: false,
451
+ error: mutationError,
452
+ metadata: {
453
+ timestamp: new Date(),
454
+ operation: MutationOperationType.SET,
455
+ source: CacheSource.CLIENT,
456
+ performedBy: firekitUser.uid ?? 'anonymous' ?? 'anonymous',
457
+ duration: Date.now() - startTime
458
+ }
459
+ };
460
+ }
461
+ }
462
+ /**
463
+ * Update a document at specified path
464
+ *
465
+ * @template T Document data type
466
+ * @param path Document path
467
+ * @param data Update data
468
+ * @param options Mutation options
469
+ * @returns Promise resolving to mutation response
470
+ *
471
+ * @example
472
+ * ```typescript
473
+ * const result = await firekitDocMutations.update('users/123', {
474
+ * name: 'Jane Doe',
475
+ * lastLogin: serverTimestamp()
476
+ * });
477
+ * ```
478
+ */
479
+ async update(path, data, options = {}) {
480
+ const startTime = Date.now();
481
+ const mergedOptions = { ...this.defaultOptions, ...options };
482
+ try {
483
+ this.emitEvent({
484
+ type: 'mutation_start',
485
+ data: { operation: 'update', path, data },
486
+ timestamp: new Date(),
487
+ userId: firekitUser.uid ?? 'anonymous'
488
+ });
489
+ if (mergedOptions.validate) {
490
+ const validation = this.validateData(data, mergedOptions.validator);
491
+ if (!validation.valid) {
492
+ throw new MutationError(MutationErrorCode.VALIDATION_FAILED, validation.message || 'Validation failed', MutationOperationType.UPDATE, path, null, { fieldErrors: validation.fieldErrors });
493
+ }
494
+ }
495
+ const result = await this.executeWithRetry(async () => {
496
+ const firestore = firebaseService.getDbInstance();
497
+ if (!firestore) {
498
+ throw new MutationError(MutationErrorCode.SERVICE_UNAVAILABLE, 'Firestore service not available');
499
+ }
500
+ const docRef = doc(firestore, path);
501
+ const dataToUpdate = {
502
+ ...data,
503
+ ...(mergedOptions.timestamps && this.getTimestampData(false))
504
+ };
505
+ await updateDoc(docRef, dataToUpdate);
506
+ return {
507
+ success: true,
508
+ id: docRef.id,
509
+ data: dataToUpdate,
510
+ metadata: {
511
+ timestamp: new Date(),
512
+ operation: MutationOperationType.UPDATE,
513
+ source: CacheSource.CLIENT,
514
+ performedBy: firekitUser.uid ?? 'anonymous' ?? 'anonymous',
515
+ duration: Date.now() - startTime
516
+ }
517
+ };
518
+ }, mergedOptions, MutationOperationType.UPDATE, path);
519
+ this.analytics.totalMutations++;
520
+ this.analytics.successfulMutations++;
521
+ this.updateOperationAnalytics(MutationOperationType.UPDATE, Date.now() - startTime, true);
522
+ this.emitEvent({
523
+ type: 'mutation_success',
524
+ data: { operation: 'update', path, result },
525
+ timestamp: new Date(),
526
+ userId: firekitUser.uid ?? 'anonymous'
527
+ });
528
+ return result;
529
+ }
530
+ catch (error) {
531
+ this.analytics.totalMutations++;
532
+ const mutationError = error instanceof MutationError
533
+ ? error
534
+ : this.handleError(error, MutationOperationType.UPDATE, path);
535
+ this.updateOperationAnalytics(MutationOperationType.UPDATE, Date.now() - startTime, false);
536
+ return {
537
+ success: false,
538
+ error: mutationError,
539
+ metadata: {
540
+ timestamp: new Date(),
541
+ operation: MutationOperationType.UPDATE,
542
+ source: CacheSource.CLIENT,
543
+ performedBy: firekitUser.uid ?? 'anonymous' ?? 'anonymous',
544
+ duration: Date.now() - startTime
545
+ }
546
+ };
547
+ }
548
+ }
549
+ /**
550
+ * Delete a document at specified path
551
+ *
552
+ * @param path Document path
553
+ * @param options Mutation options
554
+ * @returns Promise resolving to mutation response
555
+ *
556
+ * @example
557
+ * ```typescript
558
+ * const result = await firekitDocMutations.delete('users/123');
559
+ * if (result.success) {
560
+ * console.log('Document deleted');
561
+ * }
562
+ * ```
563
+ */
564
+ async delete(path, options = {}) {
565
+ const startTime = Date.now();
566
+ const mergedOptions = { ...this.defaultOptions, ...options };
567
+ try {
568
+ this.emitEvent({
569
+ type: 'mutation_start',
570
+ data: { operation: 'delete', path },
571
+ timestamp: new Date(),
572
+ userId: firekitUser.uid ?? 'anonymous'
573
+ });
574
+ const result = await this.executeWithRetry(async () => {
575
+ const firestore = firebaseService.getDbInstance();
576
+ if (!firestore) {
577
+ throw new MutationError(MutationErrorCode.SERVICE_UNAVAILABLE, 'Firestore service not available');
578
+ }
579
+ const docRef = doc(firestore, path);
580
+ await deleteDoc(docRef);
581
+ return {
582
+ success: true,
583
+ id: docRef.id,
584
+ metadata: {
585
+ timestamp: new Date(),
586
+ operation: MutationOperationType.DELETE,
587
+ source: CacheSource.CLIENT,
588
+ performedBy: firekitUser.uid ?? 'anonymous' ?? 'anonymous',
589
+ duration: Date.now() - startTime
590
+ }
591
+ };
592
+ }, mergedOptions, MutationOperationType.DELETE, path);
593
+ this.analytics.totalMutations++;
594
+ this.analytics.successfulMutations++;
595
+ this.updateOperationAnalytics(MutationOperationType.DELETE, Date.now() - startTime, true);
596
+ this.emitEvent({
597
+ type: 'mutation_success',
598
+ data: { operation: 'delete', path, result },
599
+ timestamp: new Date(),
600
+ userId: firekitUser.uid ?? 'anonymous'
601
+ });
602
+ return result;
603
+ }
604
+ catch (error) {
605
+ this.analytics.totalMutations++;
606
+ const mutationError = error instanceof MutationError
607
+ ? error
608
+ : this.handleError(error, MutationOperationType.DELETE, path);
609
+ this.updateOperationAnalytics(MutationOperationType.DELETE, Date.now() - startTime, false);
610
+ return {
611
+ success: false,
612
+ error: mutationError,
613
+ metadata: {
614
+ timestamp: new Date(),
615
+ operation: MutationOperationType.DELETE,
616
+ source: CacheSource.CLIENT,
617
+ performedBy: firekitUser.uid ?? 'anonymous' ?? 'anonymous',
618
+ duration: Date.now() - startTime
619
+ }
620
+ };
621
+ }
622
+ }
623
+ // ========================================
624
+ // FIELD VALUE OPERATIONS
625
+ // ========================================
626
+ /**
627
+ * Increment a numeric field
628
+ *
629
+ * @param path Document path
630
+ * @param field Field name
631
+ * @param value Increment value
632
+ * @param options Mutation options
633
+ * @returns Promise resolving to mutation response
634
+ */
635
+ async increment(path, field, value, options = {}) {
636
+ return this.update(path, { [field]: increment(value) }, options);
637
+ }
638
+ /**
639
+ * Add elements to an array field
640
+ *
641
+ * @param path Document path
642
+ * @param field Field name
643
+ * @param elements Elements to add
644
+ * @param options Mutation options
645
+ * @returns Promise resolving to mutation response
646
+ */
647
+ async arrayUnion(path, field, elements, options = {}) {
648
+ return this.update(path, { [field]: arrayUnion(...elements) }, options);
649
+ }
650
+ /**
651
+ * Remove elements from an array field
652
+ *
653
+ * @param path Document path
654
+ * @param field Field name
655
+ * @param elements Elements to remove
656
+ * @param options Mutation options
657
+ * @returns Promise resolving to mutation response
658
+ */
659
+ async arrayRemove(path, field, elements, options = {}) {
660
+ return this.update(path, { [field]: arrayRemove(...elements) }, options);
661
+ }
662
+ /**
663
+ * Delete a field from document
664
+ *
665
+ * @param path Document path
666
+ * @param field Field name
667
+ * @param options Mutation options
668
+ * @returns Promise resolving to mutation response
669
+ */
670
+ async deleteField(path, field, options = {}) {
671
+ return this.update(path, { [field]: deleteField() }, options);
672
+ }
673
+ // ========================================
674
+ // BATCH OPERATIONS
675
+ // ========================================
676
+ /**
677
+ * Execute multiple operations in a batch
678
+ *
679
+ * @param operations Array of batch operations
680
+ * @param config Bulk mutation configuration
681
+ * @returns Promise resolving to batch result
682
+ *
683
+ * @example
684
+ * ```typescript
685
+ * const result = await firekitDocMutations.batch([
686
+ * { type: 'create', path: 'users', data: userData },
687
+ * { type: 'update', path: 'profiles/123', data: profileUpdate },
688
+ * { type: 'delete', path: 'temp/456' }
689
+ * ], {
690
+ * batchSize: 500,
691
+ * failFast: false,
692
+ * onProgress: (completed, total) => console.log(`${completed}/${total}`)
693
+ * });
694
+ * ```
695
+ */
696
+ async batch(operations, config = {}) {
697
+ const startTime = Date.now();
698
+ const batchConfig = {
699
+ batchSize: 500,
700
+ parallel: false,
701
+ failFast: true,
702
+ ...config
703
+ };
704
+ this.emitEvent({
705
+ type: 'batch_start',
706
+ data: { operationCount: operations.length, config: batchConfig },
707
+ timestamp: new Date(),
708
+ userId: firekitUser.uid ?? 'anonymous'
709
+ });
710
+ try {
711
+ const firestore = firebaseService.getDbInstance();
712
+ if (!firestore) {
713
+ throw new MutationError(MutationErrorCode.SERVICE_UNAVAILABLE, 'Firestore service not available');
714
+ }
715
+ const results = [];
716
+ const batches = this.chunkArray(operations, batchConfig.batchSize);
717
+ let successCount = 0;
718
+ let failureCount = 0;
719
+ for (let i = 0; i < batches.length; i++) {
720
+ const batch = batches[i];
721
+ const batchWriter = writeBatch(firestore);
722
+ const batchResults = [];
723
+ for (const operation of batch) {
724
+ try {
725
+ await this.addOperationToBatch(batchWriter, operation);
726
+ batchResults.push({
727
+ operation,
728
+ success: true,
729
+ duration: 0 // Will be updated after batch commit
730
+ });
731
+ }
732
+ catch (error) {
733
+ const mutationError = this.handleError(error, operation.type, operation.path);
734
+ batchResults.push({
735
+ operation,
736
+ success: false,
737
+ error: mutationError,
738
+ duration: 0
739
+ });
740
+ failureCount++;
741
+ if (batchConfig.failFast) {
742
+ throw mutationError;
743
+ }
744
+ }
745
+ }
746
+ // Commit batch
747
+ const batchStartTime = Date.now();
748
+ await batchWriter.commit();
749
+ const batchDuration = Date.now() - batchStartTime;
750
+ // Update durations
751
+ batchResults.forEach((result) => {
752
+ result.duration = batchDuration / batch.length;
753
+ if (result.success)
754
+ successCount++;
755
+ });
756
+ results.push(...batchResults);
757
+ // Emit progress
758
+ const completed = (i + 1) * batchConfig.batchSize;
759
+ this.emitEvent({
760
+ type: 'batch_progress',
761
+ data: { completed: Math.min(completed, operations.length), total: operations.length },
762
+ timestamp: new Date(),
763
+ userId: firekitUser.uid ?? 'anonymous'
764
+ });
765
+ batchConfig.onProgress?.(Math.min(completed, operations.length), operations.length);
766
+ }
767
+ const batchResult = {
768
+ success: failureCount === 0,
769
+ successCount,
770
+ failureCount,
771
+ results,
772
+ metadata: {
773
+ startTime: new Date(startTime),
774
+ endTime: new Date(),
775
+ duration: Date.now() - startTime,
776
+ operationCount: operations.length,
777
+ executedBy: firekitUser.uid ?? 'anonymous',
778
+ strategy: batchConfig.parallel ? 'parallel' : 'sequential'
779
+ }
780
+ };
781
+ this.emitEvent({
782
+ type: 'batch_complete',
783
+ data: batchResult,
784
+ timestamp: new Date(),
785
+ userId: firekitUser.uid ?? 'anonymous'
786
+ });
787
+ return batchResult;
788
+ }
789
+ catch (error) {
790
+ const mutationError = error instanceof MutationError ? error : this.handleError(error, 'batch');
791
+ throw mutationError;
792
+ }
793
+ }
794
+ /**
795
+ * Add operation to batch writer
796
+ */
797
+ async addOperationToBatch(batch, operation) {
798
+ const firestore = firebaseService.getDbInstance();
799
+ if (!firestore)
800
+ throw new Error('Firestore not available');
801
+ const options = { ...this.defaultOptions, ...operation.options };
802
+ switch (operation.type) {
803
+ case 'create':
804
+ case 'set': {
805
+ const docRef = doc(firestore, operation.path);
806
+ let data = operation.data || {};
807
+ if (options.timestamps) {
808
+ data = { ...data, ...this.getTimestampData() };
809
+ }
810
+ if (options.validate && operation.data) {
811
+ const validation = this.validateData(operation.data, options.validator);
812
+ if (!validation.valid) {
813
+ throw new MutationError(MutationErrorCode.VALIDATION_FAILED, validation.message || 'Validation failed', operation.type, operation.path);
814
+ }
815
+ }
816
+ batch.set(docRef, { ...data, id: docRef.id }, { merge: options.merge });
817
+ break;
818
+ }
819
+ case 'update': {
820
+ const docRef = doc(firestore, operation.path);
821
+ let data = operation.data || {};
822
+ if (options.timestamps) {
823
+ data = { ...data, ...this.getTimestampData(false) };
824
+ }
825
+ if (options.validate && operation.data) {
826
+ const validation = this.validateData(operation.data, options.validator);
827
+ if (!validation.valid) {
828
+ throw new MutationError(MutationErrorCode.VALIDATION_FAILED, validation.message || 'Validation failed', operation.type, operation.path);
829
+ }
830
+ }
831
+ batch.update(docRef, data);
832
+ break;
833
+ }
834
+ case 'delete': {
835
+ const docRef = doc(firestore, operation.path);
836
+ batch.delete(docRef);
837
+ break;
838
+ }
839
+ default:
840
+ throw new MutationError(MutationErrorCode.UNIMPLEMENTED, `Operation type ${operation.type} not supported in batch`);
841
+ }
842
+ }
843
+ /**
844
+ * Chunk array into smaller arrays
845
+ */
846
+ chunkArray(array, chunkSize) {
847
+ const chunks = [];
848
+ for (let i = 0; i < array.length; i += chunkSize) {
849
+ chunks.push(array.slice(i, i + chunkSize));
850
+ }
851
+ return chunks;
852
+ }
853
+ // ========================================
854
+ // UTILITY METHODS
855
+ // ========================================
856
+ /**
857
+ * Check if a document exists at specified path
858
+ *
859
+ * @param path Document path
860
+ * @returns Promise resolving to existence check result
861
+ *
862
+ * @example
863
+ * ```typescript
864
+ * const checkResult = await firekitDocMutations.exists('users/123');
865
+ * if (checkResult.exists) {
866
+ * console.log('Document exists');
867
+ * }
868
+ * ```
869
+ */
870
+ async exists(path) {
871
+ try {
872
+ const firestore = firebaseService.getDbInstance();
873
+ if (!firestore) {
874
+ throw new MutationError(MutationErrorCode.SERVICE_UNAVAILABLE, 'Firestore service not available');
875
+ }
876
+ const docRef = doc(firestore, path);
877
+ const docSnap = await getDoc(docRef);
878
+ return {
879
+ exists: docSnap.exists(),
880
+ id: docSnap.exists() ? docSnap.id : undefined,
881
+ lastModified: docSnap.exists()
882
+ ? docSnap.metadata.fromCache
883
+ ? undefined
884
+ : new Date()
885
+ : undefined,
886
+ metadata: docSnap.exists()
887
+ ? {
888
+ size: JSON.stringify(docSnap.data()).length,
889
+ updateTime: docSnap.metadata.fromCache ? new Date() : new Date(),
890
+ createTime: docSnap.metadata.fromCache ? new Date() : new Date()
891
+ }
892
+ : undefined
893
+ };
894
+ }
895
+ catch (error) {
896
+ console.error('Error checking document existence:', error);
897
+ return { exists: false };
898
+ }
899
+ }
900
+ /**
901
+ * Get a document at specified path
902
+ *
903
+ * @template T Document data type
904
+ * @param path Document path
905
+ * @returns Promise resolving to mutation response with document data
906
+ *
907
+ * @example
908
+ * ```typescript
909
+ * const result = await firekitDocMutations.getDoc<UserData>('users/123');
910
+ * if (result.success && result.data) {
911
+ * console.log('User data:', result.data);
912
+ * }
913
+ * ```
914
+ */
915
+ async getDoc(path) {
916
+ try {
917
+ const firestore = firebaseService.getDbInstance();
918
+ if (!firestore) {
919
+ throw new MutationError(MutationErrorCode.SERVICE_UNAVAILABLE, 'Firestore service not available');
920
+ }
921
+ const docRef = doc(firestore, path);
922
+ const docSnap = await getDoc(docRef);
923
+ if (!docSnap.exists()) {
924
+ return {
925
+ success: false,
926
+ error: new MutationError(MutationErrorCode.NOT_FOUND, 'Document does not exist', undefined, path)
927
+ };
928
+ }
929
+ return {
930
+ success: true,
931
+ id: docSnap.id,
932
+ data: { id: docSnap.id, ...docSnap.data() },
933
+ metadata: {
934
+ timestamp: new Date(),
935
+ operation: MutationOperationType.READ,
936
+ source: docSnap.metadata.fromCache ? CacheSource.CACHE : CacheSource.SERVER,
937
+ performedBy: firekitUser.uid ?? 'anonymous',
938
+ fromCache: docSnap.metadata.fromCache
939
+ }
940
+ };
941
+ }
942
+ catch (error) {
943
+ const mutationError = this.handleError(error, 'read', path);
944
+ return {
945
+ success: false,
946
+ error: mutationError
947
+ };
948
+ }
949
+ }
950
+ /**
951
+ * Update operation analytics
952
+ */
953
+ updateOperationAnalytics(operation, duration, success) {
954
+ if (!this.analytics.performanceByOperation[operation]) {
955
+ this.analytics.performanceByOperation[operation] = {
956
+ count: 0,
957
+ averageDuration: 0,
958
+ successRate: 0
959
+ };
960
+ }
961
+ const stats = this.analytics.performanceByOperation[operation];
962
+ stats.count++;
963
+ stats.averageDuration = (stats.averageDuration * (stats.count - 1) + duration) / stats.count;
964
+ if (success) {
965
+ stats.successRate = (stats.successRate * (stats.count - 1) + 100) / stats.count;
966
+ }
967
+ else {
968
+ stats.successRate = (stats.successRate * (stats.count - 1)) / stats.count;
969
+ }
970
+ // Update overall analytics
971
+ this.analytics.successRate =
972
+ (this.analytics.successfulMutations / this.analytics.totalMutations) * 100;
973
+ this.analytics.averageDuration =
974
+ (this.analytics.averageDuration * (this.analytics.totalMutations - 1) + duration) /
975
+ this.analytics.totalMutations;
976
+ }
977
+ // ========================================
978
+ // EVENT MANAGEMENT
979
+ // ========================================
980
+ /**
981
+ * Add event listener for mutation events
982
+ *
983
+ * @param callback Event callback function
984
+ * @returns Cleanup function to remove listener
985
+ *
986
+ * @example
987
+ * ```typescript
988
+ * const unsubscribe = firekitDocMutations.addEventListener((event) => {
989
+ * console.log('Mutation event:', event.type, event.data);
990
+ * });
991
+ *
992
+ * // Clean up when done
993
+ * unsubscribe();
994
+ * ```
995
+ */
996
+ addEventListener(callback) {
997
+ this.eventListeners.add(callback);
998
+ return () => this.eventListeners.delete(callback);
999
+ }
1000
+ /**
1001
+ * Remove all event listeners
1002
+ */
1003
+ clearEventListeners() {
1004
+ this.eventListeners.clear();
1005
+ }
1006
+ /**
1007
+ * Get current mutation analytics
1008
+ *
1009
+ * @returns Current analytics data
1010
+ */
1011
+ getAnalytics() {
1012
+ return { ...this.analytics };
1013
+ }
1014
+ /**
1015
+ * Reset analytics data
1016
+ */
1017
+ resetAnalytics() {
1018
+ this.analytics = this.initializeAnalytics();
1019
+ }
1020
+ // ========================================
1021
+ // STATIC FIELD VALUE HELPERS
1022
+ // ========================================
1023
+ /**
1024
+ * Create server timestamp field value
1025
+ */
1026
+ static serverTimestamp() {
1027
+ return serverTimestamp();
1028
+ }
1029
+ /**
1030
+ * Create increment field value
1031
+ */
1032
+ static increment(value) {
1033
+ return increment(value);
1034
+ }
1035
+ /**
1036
+ * Create array union field value
1037
+ */
1038
+ static arrayUnion(...elements) {
1039
+ return arrayUnion(...elements);
1040
+ }
1041
+ /**
1042
+ * Create array remove field value
1043
+ */
1044
+ static arrayRemove(...elements) {
1045
+ return arrayRemove(...elements);
1046
+ }
1047
+ /**
1048
+ * Create delete field value
1049
+ */
1050
+ static deleteField() {
1051
+ return deleteField();
1052
+ }
1053
+ }
1054
+ /**
1055
+ * Pre-initialized singleton instance of FirekitDocumentMutations.
1056
+ *
1057
+ * @example
1058
+ * ```typescript
1059
+ * import { firekitDocMutations } from 'svelte-firekit';
1060
+ *
1061
+ * // Add document
1062
+ * const result = await firekitDocMutations.add('users', userData);
1063
+ *
1064
+ * // Update document
1065
+ * await firekitDocMutations.update('users/123', { name: 'New Name' });
1066
+ *
1067
+ * // Batch operations
1068
+ * const batchResult = await firekitDocMutations.batch([
1069
+ * { type: 'create', path: 'users', data: userData },
1070
+ * { type: 'update', path: 'profiles/123', data: updateData }
1071
+ * ]);
1072
+ *
1073
+ * // Listen to events
1074
+ * const unsubscribe = firekitDocMutations.addEventListener((event) => {
1075
+ * console.log('Mutation:', event.type, event.data);
1076
+ * });
1077
+ * ```
1078
+ */
1079
+ export const firekitDocMutations = new FirekitDocumentMutations();