firestore-batch-updater 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.
package/dist/index.js ADDED
@@ -0,0 +1,661 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ BatchUpdater: () => BatchUpdater,
34
+ calculateProgress: () => calculateProgress,
35
+ createLogCollector: () => createLogCollector,
36
+ formatError: () => formatError,
37
+ formatOperationLog: () => formatOperationLog,
38
+ getAffectedFields: () => getAffectedFields,
39
+ isValidUpdateData: () => isValidUpdateData,
40
+ mergeUpdateData: () => mergeUpdateData,
41
+ writeOperationLog: () => writeOperationLog
42
+ });
43
+ module.exports = __toCommonJS(index_exports);
44
+
45
+ // src/utils/logger.ts
46
+ var fs = __toESM(require("fs"));
47
+ var path = __toESM(require("path"));
48
+ function getTimestamp() {
49
+ return (/* @__PURE__ */ new Date()).toISOString();
50
+ }
51
+ function generateLogFilename(operation) {
52
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
53
+ return `${operation}-${timestamp}.log`;
54
+ }
55
+ function ensureLogDirectory(logPath) {
56
+ if (!fs.existsSync(logPath)) {
57
+ fs.mkdirSync(logPath, { recursive: true });
58
+ }
59
+ }
60
+ function formatOperationLog(log) {
61
+ const lines = [];
62
+ const separator = "=".repeat(60);
63
+ lines.push(separator);
64
+ lines.push(`FIRESTORE BATCH OPERATION LOG`);
65
+ lines.push(separator);
66
+ lines.push("");
67
+ lines.push(`Operation: ${log.operation.toUpperCase()}`);
68
+ lines.push(`Collection: ${log.collection}`);
69
+ lines.push(`Started: ${log.startedAt}`);
70
+ lines.push(`Completed: ${log.completedAt}`);
71
+ lines.push("");
72
+ if (log.conditions && log.conditions.length > 0) {
73
+ lines.push("Conditions:");
74
+ for (const condition of log.conditions) {
75
+ lines.push(` - ${condition.field} ${condition.operator} ${formatValue(condition.value)}`);
76
+ }
77
+ lines.push("");
78
+ }
79
+ if (log.updateData) {
80
+ lines.push("Update Data:");
81
+ lines.push(` ${JSON.stringify(log.updateData, null, 2).replace(/\n/g, "\n ")}`);
82
+ lines.push("");
83
+ }
84
+ lines.push(separator);
85
+ lines.push("SUMMARY");
86
+ lines.push(separator);
87
+ lines.push(`Total: ${log.summary.totalCount}`);
88
+ lines.push(`Success: ${log.summary.successCount}`);
89
+ lines.push(`Failure: ${log.summary.failureCount}`);
90
+ lines.push("");
91
+ if (log.entries.length > 0) {
92
+ lines.push(separator);
93
+ lines.push("DETAILS");
94
+ lines.push(separator);
95
+ lines.push("");
96
+ for (const entry of log.entries) {
97
+ const statusLabel = entry.status === "success" ? "[SUCCESS]" : "[FAILURE]";
98
+ lines.push(`${entry.timestamp} ${statusLabel} ${entry.documentId}`);
99
+ if (entry.error) {
100
+ lines.push(` Error: ${entry.error}`);
101
+ }
102
+ }
103
+ }
104
+ lines.push("");
105
+ lines.push(separator);
106
+ lines.push("END OF LOG");
107
+ lines.push(separator);
108
+ return lines.join("\n");
109
+ }
110
+ function formatValue(value) {
111
+ if (value instanceof Date) {
112
+ return value.toISOString();
113
+ }
114
+ if (typeof value === "string") {
115
+ return `"${value}"`;
116
+ }
117
+ return String(value);
118
+ }
119
+ function writeOperationLog(log, options) {
120
+ const logPath = options.path || "./logs";
121
+ const filename = options.filename || generateLogFilename(log.operation);
122
+ const fullPath = path.join(logPath, filename);
123
+ ensureLogDirectory(logPath);
124
+ const content = formatOperationLog(log);
125
+ fs.writeFileSync(fullPath, content, "utf-8");
126
+ return fullPath;
127
+ }
128
+ function createLogCollector(operation, collection, conditions, updateData) {
129
+ const startedAt = getTimestamp();
130
+ const entries = [];
131
+ return {
132
+ addEntry(documentId, status, error) {
133
+ entries.push({
134
+ timestamp: getTimestamp(),
135
+ documentId,
136
+ status,
137
+ error
138
+ });
139
+ },
140
+ getLog() {
141
+ const successCount = entries.filter((e) => e.status === "success").length;
142
+ const failureCount = entries.filter((e) => e.status === "failure").length;
143
+ return {
144
+ operation,
145
+ collection,
146
+ startedAt,
147
+ completedAt: getTimestamp(),
148
+ conditions,
149
+ updateData,
150
+ summary: {
151
+ totalCount: entries.length,
152
+ successCount,
153
+ failureCount
154
+ },
155
+ entries
156
+ };
157
+ },
158
+ finalize(options) {
159
+ const log = this.getLog();
160
+ return writeOperationLog(log, options);
161
+ }
162
+ };
163
+ }
164
+
165
+ // src/utils/index.ts
166
+ function calculateProgress(current, total) {
167
+ const percentage = total === 0 ? 0 : Math.round(current / total * 100);
168
+ return {
169
+ current,
170
+ total,
171
+ percentage
172
+ };
173
+ }
174
+ function getAffectedFields(updateData) {
175
+ return Object.keys(updateData);
176
+ }
177
+ function mergeUpdateData(existingData, updateData) {
178
+ return {
179
+ ...existingData,
180
+ ...updateData
181
+ };
182
+ }
183
+ function isValidUpdateData(value) {
184
+ return value !== null && typeof value === "object" && !Array.isArray(value) && Object.keys(value).length > 0;
185
+ }
186
+ function formatError(error, context) {
187
+ const errorMessage = error instanceof Error ? error.message : String(error);
188
+ return context ? `Error at ${context}: ${errorMessage}` : errorMessage;
189
+ }
190
+
191
+ // src/core/batch-updater.ts
192
+ var BatchUpdater = class {
193
+ /**
194
+ * Create a new BatchUpdater instance
195
+ * @param firestore - Initialized Firestore instance from firebase-admin
196
+ */
197
+ constructor(firestore) {
198
+ this.conditions = [];
199
+ this.firestore = firestore;
200
+ }
201
+ /**
202
+ * Select a collection to operate on
203
+ * @param path - Collection path
204
+ * @returns This instance for chaining
205
+ */
206
+ collection(path2) {
207
+ this.collectionPath = path2;
208
+ this.conditions = [];
209
+ return this;
210
+ }
211
+ /**
212
+ * Add a where condition to filter documents
213
+ * @param field - Field path
214
+ * @param operator - Comparison operator
215
+ * @param value - Value to compare
216
+ * @returns This instance for chaining
217
+ */
218
+ where(field, operator, value) {
219
+ this.conditions.push({ field, operator, value });
220
+ return this;
221
+ }
222
+ /**
223
+ * Preview changes before executing update
224
+ * @param updateData - Data to update
225
+ * @returns Preview result with affected count and samples
226
+ */
227
+ async preview(updateData) {
228
+ this.validateSetup();
229
+ if (!isValidUpdateData(updateData)) {
230
+ throw new Error("Update data must be a non-empty object");
231
+ }
232
+ const query = this.buildQuery();
233
+ const snapshot = await query.get();
234
+ const affectedCount = snapshot.size;
235
+ const affectedFields = getAffectedFields(updateData);
236
+ const samples = [];
237
+ const sampleDocs = snapshot.docs.slice(0, 10);
238
+ for (const doc of sampleDocs) {
239
+ const before = doc.data();
240
+ const after = mergeUpdateData(before, updateData);
241
+ samples.push({
242
+ id: doc.id,
243
+ before,
244
+ after
245
+ });
246
+ }
247
+ return {
248
+ affectedCount,
249
+ samples,
250
+ affectedFields
251
+ };
252
+ }
253
+ /**
254
+ * Execute batch update operation
255
+ * @param updateData - Data to update
256
+ * @param options - Update options (e.g., progress callback, log options, batchSize for pagination)
257
+ * @returns Update result with success/failure counts and optional log file path
258
+ */
259
+ async update(updateData, options = {}) {
260
+ this.validateSetup();
261
+ if (!isValidUpdateData(updateData)) {
262
+ throw new Error("Update data must be a non-empty object");
263
+ }
264
+ const logCollector = options.log?.enabled ? createLogCollector("update", this.collectionPath, this.conditions, updateData) : null;
265
+ let successCount = 0;
266
+ let failureCount = 0;
267
+ let totalCount = 0;
268
+ const failedDocIds = [];
269
+ if (options.batchSize && options.batchSize > 0) {
270
+ const countQuery = this.buildQuery();
271
+ const countSnapshot = await countQuery.count().get();
272
+ totalCount = countSnapshot.data().count;
273
+ if (totalCount === 0) {
274
+ const result2 = {
275
+ successCount: 0,
276
+ failureCount: 0,
277
+ totalCount: 0
278
+ };
279
+ if (logCollector && options.log) {
280
+ result2.logFilePath = logCollector.finalize(options.log);
281
+ }
282
+ return result2;
283
+ }
284
+ let processedCount = 0;
285
+ let lastDoc = null;
286
+ while (true) {
287
+ let paginatedQuery = this.buildQuery().limit(options.batchSize);
288
+ if (lastDoc) {
289
+ paginatedQuery = paginatedQuery.startAfter(lastDoc);
290
+ }
291
+ const snapshot = await paginatedQuery.get();
292
+ if (snapshot.empty) {
293
+ break;
294
+ }
295
+ const bulkWriter = this.firestore.bulkWriter();
296
+ const docIdMap = /* @__PURE__ */ new Map();
297
+ for (const doc of snapshot.docs) {
298
+ docIdMap.set(doc.ref.path, doc.id);
299
+ }
300
+ bulkWriter.onWriteResult((ref) => {
301
+ successCount++;
302
+ processedCount++;
303
+ const docId = docIdMap.get(ref.path) || ref.id;
304
+ logCollector?.addEntry(docId, "success");
305
+ if (options.onProgress) {
306
+ const progress = calculateProgress(processedCount, totalCount);
307
+ options.onProgress(progress);
308
+ }
309
+ });
310
+ bulkWriter.onWriteError((error) => {
311
+ failureCount++;
312
+ processedCount++;
313
+ const docId = error.documentRef?.id || "unknown";
314
+ failedDocIds.push(docId);
315
+ logCollector?.addEntry(docId, "failure", error.message);
316
+ if (options.onProgress) {
317
+ const progress = calculateProgress(processedCount, totalCount);
318
+ options.onProgress(progress);
319
+ }
320
+ return false;
321
+ });
322
+ for (const doc of snapshot.docs) {
323
+ bulkWriter.update(doc.ref, updateData);
324
+ }
325
+ await bulkWriter.close();
326
+ lastDoc = snapshot.docs[snapshot.docs.length - 1];
327
+ if (snapshot.docs.length < options.batchSize) {
328
+ break;
329
+ }
330
+ }
331
+ } else {
332
+ const query = this.buildQuery();
333
+ const snapshot = await query.get();
334
+ totalCount = snapshot.size;
335
+ if (totalCount === 0) {
336
+ const result2 = {
337
+ successCount: 0,
338
+ failureCount: 0,
339
+ totalCount: 0
340
+ };
341
+ if (logCollector && options.log) {
342
+ result2.logFilePath = logCollector.finalize(options.log);
343
+ }
344
+ return result2;
345
+ }
346
+ const bulkWriter = this.firestore.bulkWriter();
347
+ let processedCount = 0;
348
+ const docIdMap = /* @__PURE__ */ new Map();
349
+ for (const doc of snapshot.docs) {
350
+ docIdMap.set(doc.ref.path, doc.id);
351
+ }
352
+ bulkWriter.onWriteResult((ref) => {
353
+ successCount++;
354
+ processedCount++;
355
+ const docId = docIdMap.get(ref.path) || ref.id;
356
+ logCollector?.addEntry(docId, "success");
357
+ if (options.onProgress) {
358
+ const progress = calculateProgress(processedCount, totalCount);
359
+ options.onProgress(progress);
360
+ }
361
+ });
362
+ bulkWriter.onWriteError((error) => {
363
+ failureCount++;
364
+ processedCount++;
365
+ const docId = error.documentRef?.id || "unknown";
366
+ failedDocIds.push(docId);
367
+ logCollector?.addEntry(docId, "failure", error.message);
368
+ if (options.onProgress) {
369
+ const progress = calculateProgress(processedCount, totalCount);
370
+ options.onProgress(progress);
371
+ }
372
+ return false;
373
+ });
374
+ for (const doc of snapshot.docs) {
375
+ bulkWriter.update(doc.ref, updateData);
376
+ }
377
+ await bulkWriter.close();
378
+ }
379
+ const result = {
380
+ successCount,
381
+ failureCount,
382
+ totalCount,
383
+ failedDocIds: failedDocIds.length > 0 ? failedDocIds : void 0
384
+ };
385
+ if (logCollector && options.log) {
386
+ result.logFilePath = logCollector.finalize(options.log);
387
+ }
388
+ return result;
389
+ }
390
+ /**
391
+ * Get specific field values from matching documents
392
+ * @param fieldPath - Field path to retrieve
393
+ * @returns Array of field values with document IDs
394
+ */
395
+ async getFields(fieldPath) {
396
+ this.validateSetup();
397
+ const query = this.buildQuery();
398
+ const snapshot = await query.get();
399
+ const results = [];
400
+ for (const doc of snapshot.docs) {
401
+ const data = doc.data();
402
+ const value = this.getNestedValue(data, fieldPath);
403
+ results.push({
404
+ id: doc.id,
405
+ value
406
+ });
407
+ }
408
+ return results;
409
+ }
410
+ /**
411
+ * Create multiple documents in batch
412
+ * @param documents - Array of documents to create
413
+ * @param options - Create options (e.g., progress callback, log options)
414
+ * @returns Create result with success/failure counts, created IDs, and optional log file path
415
+ */
416
+ async create(documents, options = {}) {
417
+ this.validateSetup();
418
+ if (!Array.isArray(documents) || documents.length === 0) {
419
+ throw new Error("Documents array must be non-empty");
420
+ }
421
+ for (const doc of documents) {
422
+ if (!isValidUpdateData(doc.data)) {
423
+ throw new Error("Each document must have valid data");
424
+ }
425
+ }
426
+ const totalCount = documents.length;
427
+ let successCount = 0;
428
+ let failureCount = 0;
429
+ const createdIds = [];
430
+ const failedDocIds = [];
431
+ const logCollector = options.log?.enabled ? createLogCollector("create", this.collectionPath) : null;
432
+ const bulkWriter = this.firestore.bulkWriter();
433
+ const collection = this.firestore.collection(this.collectionPath);
434
+ let processedCount = 0;
435
+ bulkWriter.onWriteResult((ref) => {
436
+ successCount++;
437
+ processedCount++;
438
+ createdIds.push(ref.id);
439
+ logCollector?.addEntry(ref.id, "success");
440
+ if (options.onProgress) {
441
+ const progress = calculateProgress(processedCount, totalCount);
442
+ options.onProgress(progress);
443
+ }
444
+ });
445
+ bulkWriter.onWriteError((error) => {
446
+ failureCount++;
447
+ processedCount++;
448
+ const docId = error.documentRef?.id || "unknown";
449
+ failedDocIds.push(docId);
450
+ logCollector?.addEntry(docId, "failure", error.message);
451
+ if (options.onProgress) {
452
+ const progress = calculateProgress(processedCount, totalCount);
453
+ options.onProgress(progress);
454
+ }
455
+ return false;
456
+ });
457
+ for (const doc of documents) {
458
+ const docRef = doc.id ? collection.doc(doc.id) : collection.doc();
459
+ bulkWriter.create(docRef, doc.data);
460
+ }
461
+ await bulkWriter.close();
462
+ const result = {
463
+ successCount,
464
+ failureCount,
465
+ totalCount,
466
+ createdIds,
467
+ failedDocIds: failedDocIds.length > 0 ? failedDocIds : void 0
468
+ };
469
+ if (logCollector && options.log) {
470
+ result.logFilePath = logCollector.finalize(options.log);
471
+ }
472
+ return result;
473
+ }
474
+ /**
475
+ * Upsert documents matching query conditions
476
+ * Updates existing documents or creates them if they don't exist
477
+ * @param updateData - Data to set/merge
478
+ * @param options - Upsert options (e.g., progress callback, log options, batchSize for pagination)
479
+ * @returns Upsert result with success/failure counts and optional log file path
480
+ */
481
+ async upsert(updateData, options = {}) {
482
+ this.validateSetup();
483
+ if (!isValidUpdateData(updateData)) {
484
+ throw new Error("Update data must be a non-empty object");
485
+ }
486
+ const logCollector = options.log?.enabled ? createLogCollector("upsert", this.collectionPath, this.conditions, updateData) : null;
487
+ let successCount = 0;
488
+ let failureCount = 0;
489
+ let totalCount = 0;
490
+ const failedDocIds = [];
491
+ if (options.batchSize && options.batchSize > 0) {
492
+ const countQuery = this.buildQuery();
493
+ const countSnapshot = await countQuery.count().get();
494
+ totalCount = countSnapshot.data().count;
495
+ if (totalCount === 0) {
496
+ const result2 = {
497
+ successCount: 0,
498
+ failureCount: 0,
499
+ totalCount: 0
500
+ };
501
+ if (logCollector && options.log) {
502
+ result2.logFilePath = logCollector.finalize(options.log);
503
+ }
504
+ return result2;
505
+ }
506
+ let processedCount = 0;
507
+ let lastDoc = null;
508
+ while (true) {
509
+ let paginatedQuery = this.buildQuery().limit(options.batchSize);
510
+ if (lastDoc) {
511
+ paginatedQuery = paginatedQuery.startAfter(lastDoc);
512
+ }
513
+ const snapshot = await paginatedQuery.get();
514
+ if (snapshot.empty) {
515
+ break;
516
+ }
517
+ const bulkWriter = this.firestore.bulkWriter();
518
+ const docIdMap = /* @__PURE__ */ new Map();
519
+ for (const doc of snapshot.docs) {
520
+ docIdMap.set(doc.ref.path, doc.id);
521
+ }
522
+ bulkWriter.onWriteResult((ref) => {
523
+ successCount++;
524
+ processedCount++;
525
+ const docId = docIdMap.get(ref.path) || ref.id;
526
+ logCollector?.addEntry(docId, "success");
527
+ if (options.onProgress) {
528
+ const progress = calculateProgress(processedCount, totalCount);
529
+ options.onProgress(progress);
530
+ }
531
+ });
532
+ bulkWriter.onWriteError((error) => {
533
+ failureCount++;
534
+ processedCount++;
535
+ const docId = error.documentRef?.id || "unknown";
536
+ failedDocIds.push(docId);
537
+ logCollector?.addEntry(docId, "failure", error.message);
538
+ if (options.onProgress) {
539
+ const progress = calculateProgress(processedCount, totalCount);
540
+ options.onProgress(progress);
541
+ }
542
+ return false;
543
+ });
544
+ for (const doc of snapshot.docs) {
545
+ bulkWriter.set(doc.ref, updateData, { merge: true });
546
+ }
547
+ await bulkWriter.close();
548
+ lastDoc = snapshot.docs[snapshot.docs.length - 1];
549
+ if (snapshot.docs.length < options.batchSize) {
550
+ break;
551
+ }
552
+ }
553
+ } else {
554
+ const query = this.buildQuery();
555
+ const snapshot = await query.get();
556
+ totalCount = snapshot.size;
557
+ if (totalCount === 0) {
558
+ const result2 = {
559
+ successCount: 0,
560
+ failureCount: 0,
561
+ totalCount: 0
562
+ };
563
+ if (logCollector && options.log) {
564
+ result2.logFilePath = logCollector.finalize(options.log);
565
+ }
566
+ return result2;
567
+ }
568
+ const bulkWriter = this.firestore.bulkWriter();
569
+ let processedCount = 0;
570
+ const docIdMap = /* @__PURE__ */ new Map();
571
+ for (const doc of snapshot.docs) {
572
+ docIdMap.set(doc.ref.path, doc.id);
573
+ }
574
+ bulkWriter.onWriteResult((ref) => {
575
+ successCount++;
576
+ processedCount++;
577
+ const docId = docIdMap.get(ref.path) || ref.id;
578
+ logCollector?.addEntry(docId, "success");
579
+ if (options.onProgress) {
580
+ const progress = calculateProgress(processedCount, totalCount);
581
+ options.onProgress(progress);
582
+ }
583
+ });
584
+ bulkWriter.onWriteError((error) => {
585
+ failureCount++;
586
+ processedCount++;
587
+ const docId = error.documentRef?.id || "unknown";
588
+ failedDocIds.push(docId);
589
+ logCollector?.addEntry(docId, "failure", error.message);
590
+ if (options.onProgress) {
591
+ const progress = calculateProgress(processedCount, totalCount);
592
+ options.onProgress(progress);
593
+ }
594
+ return false;
595
+ });
596
+ for (const doc of snapshot.docs) {
597
+ bulkWriter.set(doc.ref, updateData, { merge: true });
598
+ }
599
+ await bulkWriter.close();
600
+ }
601
+ const result = {
602
+ successCount,
603
+ failureCount,
604
+ totalCount,
605
+ failedDocIds: failedDocIds.length > 0 ? failedDocIds : void 0
606
+ };
607
+ if (logCollector && options.log) {
608
+ result.logFilePath = logCollector.finalize(options.log);
609
+ }
610
+ return result;
611
+ }
612
+ /**
613
+ * Validate that collection is set
614
+ * @private
615
+ */
616
+ validateSetup() {
617
+ if (!this.collectionPath) {
618
+ throw new Error("Collection path is required. Call .collection() first.");
619
+ }
620
+ }
621
+ /**
622
+ * Build Firestore query with all conditions
623
+ * @private
624
+ */
625
+ buildQuery() {
626
+ let query = this.firestore.collection(
627
+ this.collectionPath
628
+ );
629
+ for (const condition of this.conditions) {
630
+ query = query.where(condition.field, condition.operator, condition.value);
631
+ }
632
+ return query;
633
+ }
634
+ /**
635
+ * Get nested value from object using dot notation
636
+ * @private
637
+ */
638
+ getNestedValue(obj, path2) {
639
+ const keys = path2.split(".");
640
+ let current = obj;
641
+ for (const key of keys) {
642
+ if (current === null || current === void 0) {
643
+ return void 0;
644
+ }
645
+ current = current[key];
646
+ }
647
+ return current;
648
+ }
649
+ };
650
+ // Annotate the CommonJS export names for ESM import in node:
651
+ 0 && (module.exports = {
652
+ BatchUpdater,
653
+ calculateProgress,
654
+ createLogCollector,
655
+ formatError,
656
+ formatOperationLog,
657
+ getAffectedFields,
658
+ isValidUpdateData,
659
+ mergeUpdateData,
660
+ writeOperationLog
661
+ });