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.
- package/README.md +445 -213
- package/dist/components/Collection.svelte +150 -0
- package/dist/components/Collection.svelte.d.ts +27 -0
- package/dist/components/Ddoc.svelte +131 -0
- package/dist/components/Ddoc.svelte.d.ts +28 -0
- package/dist/components/Node.svelte +97 -0
- package/dist/components/Node.svelte.d.ts +23 -0
- package/dist/components/auth-guard.svelte +89 -0
- package/dist/components/auth-guard.svelte.d.ts +26 -0
- package/dist/components/custom-guard.svelte +122 -0
- package/dist/components/custom-guard.svelte.d.ts +31 -0
- package/dist/components/download-url.svelte +92 -0
- package/dist/components/download-url.svelte.d.ts +19 -0
- package/dist/components/firebase-app.svelte +30 -0
- package/dist/components/firebase-app.svelte.d.ts +7 -0
- package/dist/components/node-list.svelte +102 -0
- package/dist/components/node-list.svelte.d.ts +27 -0
- package/dist/components/signed-in.svelte +42 -0
- package/dist/components/signed-in.svelte.d.ts +11 -0
- package/dist/components/signed-out.svelte +42 -0
- package/dist/components/signed-out.svelte.d.ts +11 -0
- package/dist/components/storage-list.svelte +97 -0
- package/dist/components/storage-list.svelte.d.ts +26 -0
- package/dist/components/upload-task.svelte +108 -0
- package/dist/components/upload-task.svelte.d.ts +24 -0
- package/dist/config.js +17 -39
- package/dist/firebase.d.ts +43 -21
- package/dist/firebase.js +121 -35
- package/dist/index.d.ts +21 -13
- package/dist/index.js +27 -15
- package/dist/services/auth.d.ts +397 -0
- package/dist/services/auth.js +882 -0
- package/dist/services/collection.svelte.d.ts +286 -0
- package/dist/services/collection.svelte.js +871 -0
- package/dist/services/document.svelte.d.ts +288 -0
- package/dist/services/document.svelte.js +555 -0
- package/dist/services/mutations.d.ts +336 -0
- package/dist/services/mutations.js +1079 -0
- package/dist/services/presence.svelte.d.ts +141 -0
- package/dist/services/presence.svelte.js +727 -0
- package/dist/{realtime → services}/realtime.svelte.d.ts +3 -1
- package/dist/{realtime → services}/realtime.svelte.js +13 -7
- package/dist/services/storage.svelte.d.ts +257 -0
- package/dist/services/storage.svelte.js +374 -0
- package/dist/services/user.svelte.d.ts +296 -0
- package/dist/services/user.svelte.js +609 -0
- package/dist/types/auth.d.ts +158 -0
- package/dist/types/auth.js +106 -0
- package/dist/types/collection.d.ts +360 -0
- package/dist/types/collection.js +167 -0
- package/dist/types/document.d.ts +342 -0
- package/dist/types/document.js +148 -0
- package/dist/types/firebase.d.ts +44 -0
- package/dist/types/firebase.js +33 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.js +4 -0
- package/dist/types/mutations.d.ts +387 -0
- package/dist/types/mutations.js +205 -0
- package/dist/types/presence.d.ts +282 -0
- package/dist/types/presence.js +80 -0
- package/dist/utils/errors.d.ts +21 -0
- package/dist/utils/errors.js +35 -0
- package/dist/utils/firestore.d.ts +9 -0
- package/dist/utils/firestore.js +33 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.js +8 -0
- package/dist/utils/providers.d.ts +16 -0
- package/dist/utils/providers.js +30 -0
- package/dist/utils/user.d.ts +8 -0
- package/dist/utils/user.js +29 -0
- package/package.json +64 -64
- package/dist/auth/auth.d.ts +0 -117
- package/dist/auth/auth.js +0 -194
- package/dist/auth/presence.svelte.d.ts +0 -139
- package/dist/auth/presence.svelte.js +0 -373
- package/dist/auth/user.svelte.d.ts +0 -112
- package/dist/auth/user.svelte.js +0 -155
- package/dist/firestore/awaitable-doc.svelte.d.ts +0 -141
- package/dist/firestore/awaitable-doc.svelte.js +0 -183
- package/dist/firestore/batch-mutations.svelte.d.ts +0 -140
- package/dist/firestore/batch-mutations.svelte.js +0 -218
- package/dist/firestore/collection-group.svelte.d.ts +0 -78
- package/dist/firestore/collection-group.svelte.js +0 -120
- package/dist/firestore/collection.svelte.d.ts +0 -96
- package/dist/firestore/collection.svelte.js +0 -137
- package/dist/firestore/doc.svelte.d.ts +0 -90
- package/dist/firestore/doc.svelte.js +0 -131
- package/dist/firestore/document-mutations.svelte.d.ts +0 -164
- package/dist/firestore/document-mutations.svelte.js +0 -273
- package/dist/storage/download-url.svelte.d.ts +0 -83
- package/dist/storage/download-url.svelte.js +0 -114
- package/dist/storage/storage-list.svelte.d.ts +0 -89
- package/dist/storage/storage-list.svelte.js +0 -123
- package/dist/storage/upload-task.svelte.d.ts +0 -94
- 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();
|