firestore-batch-updater 1.5.0 → 1.6.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/README.ko.md CHANGED
@@ -20,6 +20,8 @@
20
20
  - 전체 문서 조회 - `getAll()`로 매칭되는 모든 문서 데이터 조회
21
21
  - 집계 쿼리 - `aggregate()`로 서버 사이드 `sum`, `average`, `count` 연산
22
22
  - 커서 페이지네이션 - `paginate()`로 메모리 효율적인 페이지 단위 조회
23
+ - ID 직접 조회 - `getOne()`으로 문서 ID로 빠른 조회
24
+ - 벌크 업데이트 - `bulkUpdate()`로 여러 문서에 각기 다른 데이터 업데이트
23
25
  - FieldValue 지원 - `increment()`, `arrayUnion()`, `delete()`, `serverTimestamp()` 등 사용 가능
24
26
  - 서브컬렉션 & 컬렉션 그룹 - 서브컬렉션 쿼리 또는 동일 이름의 모든 컬렉션 쿼리
25
27
  - Dry Run 모드 - 실제 변경 없이 작업 시뮬레이션
@@ -93,6 +95,7 @@ console.log(`${result.successCount}개 문서 업데이트 완료`);
93
95
  | `count()` | 매칭되는 문서 개수 조회 | `CountResult` |
94
96
  | `exists()` | 매칭되는 문서 존재 여부 확인 | `boolean` |
95
97
  | `findOne()` | 첫 번째 매칭 문서 조회 | `{ id, data } \| null` |
98
+ | `getOne(id)` | ID로 문서 직접 조회 | `{ id, data } \| null` |
96
99
  | `getAll()` | 모든 매칭 문서 조회 | `{ id, data }[]` |
97
100
  | `preview(data)` | 업데이트 전 미리보기 | `PreviewResult` |
98
101
  | `update(data, options?)` | 매칭되는 문서 업데이트 | `UpdateResult` |
@@ -104,6 +107,7 @@ console.log(`${result.successCount}개 문서 업데이트 완료`);
104
107
  | `deleteOne()` | 첫 번째 매칭 문서 삭제 | `{ success, id }` |
105
108
  | `aggregate(spec)` | sum/average/count 집계 쿼리 | `AggregateResult` |
106
109
  | `paginate(options)` | 커서 기반 페이지네이션 | `PaginateResult` |
110
+ | `bulkUpdate(updates, options?)` | 여러 문서에 각기 다른 데이터 업데이트 | `BulkUpdateResult` |
107
111
  | `getFields(field)` | 특정 필드 값 조회 | `FieldValueResult[]` |
108
112
 
109
113
  ### 옵션
@@ -153,6 +157,7 @@ console.log(`${result.successCount}개 문서 업데이트 완료`);
153
157
  | `DeleteResult` | `successCount`, `failureCount`, `totalCount`, `deletedIds[]`, `failedDocIds?`, `logFilePath?` |
154
158
  | `AggregateResult` | `{ [alias]: number \| null }` |
155
159
  | `PaginateResult` | `docs[]`, `nextCursor`, `hasMore` |
160
+ | `BulkUpdateResult` | `successCount`, `failureCount`, `totalCount`, `failedDocIds?`, `logFilePath?` |
156
161
  | `FieldValueResult` | `id`, `value` |
157
162
 
158
163
  ## 사용 예시
@@ -511,6 +516,62 @@ const page = await updater
511
516
  .paginate({ pageSize: 50 });
512
517
  ```
513
518
 
519
+ ### ID로 문서 조회
520
+
521
+ ```typescript
522
+ // 문서 ID로 직접 조회 (쿼리 필터 없이 가장 빠름)
523
+ const user = await updater.collection("users").getOne("user-123");
524
+
525
+ if (user) {
526
+ console.log(`찾음: ${user.data.name} (${user.data.email})`);
527
+ } else {
528
+ console.log("사용자를 찾을 수 없음");
529
+ }
530
+
531
+ // select와 함께 사용하여 특정 필드만 가져오기
532
+ const userBasic = await updater
533
+ .collection("users")
534
+ .select("name", "email")
535
+ .getOne("user-123");
536
+
537
+ // 서브컬렉션에서 문서 조회
538
+ const order = await updater
539
+ .collection("users/user-123/orders")
540
+ .getOne("order-456");
541
+ ```
542
+
543
+ ### 벌크 업데이트
544
+
545
+ ```typescript
546
+ // 여러 문서를 각각 다른 데이터로 업데이트
547
+ const result = await updater.collection("users").bulkUpdate([
548
+ { id: "user-1", data: { name: "Alice", age: 30 } },
549
+ { id: "user-2", data: { name: "Bob", status: "active" } },
550
+ { id: "user-3", data: { email: "charlie@example.com" } },
551
+ ]);
552
+
553
+ console.log(`성공: ${result.successCount}, 실패: ${result.failureCount}`);
554
+
555
+ // 진행 상황 콜백과 함께 사용
556
+ const result = await updater.collection("products").bulkUpdate(
557
+ [
558
+ { id: "prod-1", data: { price: 29.99, stock: 100 } },
559
+ { id: "prod-2", data: { price: 49.99, stock: 50 } },
560
+ // ... 더 많은 업데이트
561
+ ],
562
+ {
563
+ onProgress: (progress) => {
564
+ console.log(`${progress.processedCount}/${progress.totalCount} 처리됨`);
565
+ },
566
+ }
567
+ );
568
+
569
+ // 실패 처리
570
+ if (result.failureCount > 0) {
571
+ console.log("실패한 문서 ID:", result.failedDocIds);
572
+ }
573
+ ```
574
+
514
575
  ### Dry Run 모드
515
576
 
516
577
  ```typescript
package/README.md CHANGED
@@ -20,6 +20,8 @@ English | [한국어](./README.ko.md)
20
20
  - Get all documents - Use `getAll()` to retrieve all matching documents with data
21
21
  - Aggregation - Use `aggregate()` for server-side `sum`, `average`, and `count` operations
22
22
  - Cursor pagination - Use `paginate()` for memory-efficient page-by-page iteration
23
+ - Direct ID lookup - Use `getOne()` for fast document retrieval by ID
24
+ - Bulk updates - Use `bulkUpdate()` to update multiple documents with different data each
23
25
  - FieldValue support - Use `increment()`, `arrayUnion()`, `delete()`, `serverTimestamp()`, etc.
24
26
  - Subcollection & Collection Group - Query subcollections or all collections with the same name
25
27
  - Dry run mode - Simulate operations without making changes
@@ -93,6 +95,7 @@ console.log(`Updated ${result.successCount} documents`);
93
95
  | `count()` | Count matching documents | `CountResult` |
94
96
  | `exists()` | Check if matching documents exist | `boolean` |
95
97
  | `findOne()` | Find first matching document | `{ id, data } \| null` |
98
+ | `getOne(id)` | Get document by ID directly | `{ id, data } \| null` |
96
99
  | `getAll()` | Get all matching documents | `{ id, data }[]` |
97
100
  | `preview(data)` | Preview changes before update | `PreviewResult` |
98
101
  | `update(data, options?)` | Update matching documents | `UpdateResult` |
@@ -104,6 +107,7 @@ console.log(`Updated ${result.successCount} documents`);
104
107
  | `deleteOne()` | Delete first matching document | `{ success, id }` |
105
108
  | `aggregate(spec)` | Run sum/average/count queries | `AggregateResult` |
106
109
  | `paginate(options)` | Cursor-based pagination | `PaginateResult` |
110
+ | `bulkUpdate(updates, options?)` | Update multiple docs with different data | `BulkUpdateResult` |
107
111
  | `getFields(field)` | Get specific field values | `FieldValueResult[]` |
108
112
 
109
113
  ### Options
@@ -153,6 +157,7 @@ All write operations support an optional `options` parameter:
153
157
  | `DeleteResult` | `successCount`, `failureCount`, `totalCount`, `deletedIds[]`, `failedDocIds?`, `logFilePath?` |
154
158
  | `AggregateResult` | `{ [alias]: number \| null }` |
155
159
  | `PaginateResult` | `docs[]`, `nextCursor`, `hasMore` |
160
+ | `BulkUpdateResult` | `successCount`, `failureCount`, `totalCount`, `failedDocIds?`, `logFilePath?` |
156
161
  | `FieldValueResult` | `id`, `value` |
157
162
 
158
163
  ## Usage Examples
@@ -510,6 +515,51 @@ const page = await updater
510
515
  .paginate({ pageSize: 50 });
511
516
  ```
512
517
 
518
+ ### Get Document by ID
519
+
520
+ ```typescript
521
+ // Fast lookup when you know the document ID
522
+ const user = await updater.collection("users").getOne("user-123");
523
+
524
+ if (user) {
525
+ console.log(`Found: ${user.data.name}`);
526
+ } else {
527
+ console.log("User not found");
528
+ }
529
+
530
+ // Works with select for field filtering
531
+ const profile = await updater
532
+ .collection("users")
533
+ .select("name", "avatar")
534
+ .getOne("user-123");
535
+ ```
536
+
537
+ ### Bulk Update with Different Data
538
+
539
+ ```typescript
540
+ // Update multiple documents with different data for each
541
+ const result = await updater.collection("users").bulkUpdate([
542
+ { id: "user-1", data: { score: 100, rank: 1 } },
543
+ { id: "user-2", data: { score: 85, rank: 2 } },
544
+ { id: "user-3", data: { score: 70, rank: 3 } },
545
+ ]);
546
+
547
+ console.log(`Updated ${result.successCount} documents`);
548
+
549
+ // With progress tracking
550
+ const result2 = await updater.collection("products").bulkUpdate(
551
+ [
552
+ { id: "prod-1", data: { price: 29.99, stock: 100 } },
553
+ { id: "prod-2", data: { price: 49.99, stock: 50 } },
554
+ ],
555
+ {
556
+ onProgress: (progress) => {
557
+ console.log(`${progress.percentage}% complete`);
558
+ },
559
+ }
560
+ );
561
+ ```
562
+
513
563
  ### Dry Run Mode
514
564
 
515
565
  ```typescript
package/dist/index.d.mts CHANGED
@@ -302,6 +302,36 @@ interface PaginateResult {
302
302
  nextCursor: unknown | null;
303
303
  hasMore: boolean;
304
304
  }
305
+ /**
306
+ * Input for bulk update operation
307
+ */
308
+ interface BulkUpdateInput {
309
+ id: string;
310
+ data: Record<string, any>;
311
+ }
312
+ /**
313
+ * Options for bulk update operation
314
+ */
315
+ interface BulkUpdateOptions {
316
+ /**
317
+ * Callback function for progress updates
318
+ * @param progress - Current progress information
319
+ */
320
+ onProgress?: (progress: ProgressInfo) => void;
321
+ /**
322
+ * Log file generation options
323
+ */
324
+ log?: LogOptions;
325
+ }
326
+ /**
327
+ * Result of bulk update operation
328
+ */
329
+ interface BulkUpdateResult {
330
+ successCount: number;
331
+ failureCount: number;
332
+ totalCount: number;
333
+ failedDocIds?: string[];
334
+ }
305
335
 
306
336
  /**
307
337
  * BatchUpdater - Core class for batch operations on Firestore
@@ -389,6 +419,15 @@ declare class BatchUpdater {
389
419
  id: string;
390
420
  data: Record<string, any>;
391
421
  }[]>;
422
+ /**
423
+ * Get a document by its ID directly (faster than findOne with where)
424
+ * @param id - Document ID
425
+ * @returns Document with id and data, or null if not found
426
+ */
427
+ getOne(id: string): Promise<{
428
+ id: string;
429
+ data: Record<string, any>;
430
+ } | null>;
392
431
  /**
393
432
  * Update the first document matching the query conditions
394
433
  * @param updateData - Data to update
@@ -459,6 +498,15 @@ declare class BatchUpdater {
459
498
  create(documents: CreateDocumentInput[], options?: CreateOptions): Promise<CreateResult & {
460
499
  logFilePath?: string;
461
500
  }>;
501
+ /**
502
+ * Update multiple documents with different data for each
503
+ * @param updates - Array of { id, data } objects specifying updates for each document
504
+ * @param options - Bulk update options (e.g., progress callback, log options)
505
+ * @returns Bulk update result with success/failure counts and optional log file path
506
+ */
507
+ bulkUpdate(updates: BulkUpdateInput[], options?: BulkUpdateOptions): Promise<BulkUpdateResult & {
508
+ logFilePath?: string;
509
+ }>;
462
510
  /**
463
511
  * Upsert documents matching query conditions
464
512
  * Updates existing documents or creates them if they don't exist
@@ -554,4 +602,4 @@ declare function isValidUpdateData(value: any): value is Record<string, any>;
554
602
  */
555
603
  declare function formatError(error: unknown, context?: string): string;
556
604
 
557
- export { type AggregateResult, type AggregateSpec, BatchUpdater, type CountResult, type CreateDocumentInput, type CreateOneResult, type CreateOptions, type CreateResult, type DeleteOptions, type DeleteResult, type DocumentSnapshot, type DryRunResult, type FieldValueResult, type LogEntry, type LogOptions, type OperationLog, type OrderByCondition, type PaginateOptions, type PaginateResult, type PreviewResult, type ProgressInfo, type UpdateOptions, type UpdateResult, type UpsertOptions, type UpsertResult, type WhereCondition, calculateProgress, createLogCollector, formatError, formatOperationLog, getAffectedFields, isValidUpdateData, mergeUpdateData, writeOperationLog };
605
+ export { type AggregateResult, type AggregateSpec, BatchUpdater, type BulkUpdateInput, type BulkUpdateOptions, type BulkUpdateResult, type CountResult, type CreateDocumentInput, type CreateOneResult, type CreateOptions, type CreateResult, type DeleteOptions, type DeleteResult, type DocumentSnapshot, type DryRunResult, type FieldValueResult, type LogEntry, type LogOptions, type OperationLog, type OrderByCondition, type PaginateOptions, type PaginateResult, type PreviewResult, type ProgressInfo, type UpdateOptions, type UpdateResult, type UpsertOptions, type UpsertResult, type WhereCondition, calculateProgress, createLogCollector, formatError, formatOperationLog, getAffectedFields, isValidUpdateData, mergeUpdateData, writeOperationLog };
package/dist/index.d.ts CHANGED
@@ -302,6 +302,36 @@ interface PaginateResult {
302
302
  nextCursor: unknown | null;
303
303
  hasMore: boolean;
304
304
  }
305
+ /**
306
+ * Input for bulk update operation
307
+ */
308
+ interface BulkUpdateInput {
309
+ id: string;
310
+ data: Record<string, any>;
311
+ }
312
+ /**
313
+ * Options for bulk update operation
314
+ */
315
+ interface BulkUpdateOptions {
316
+ /**
317
+ * Callback function for progress updates
318
+ * @param progress - Current progress information
319
+ */
320
+ onProgress?: (progress: ProgressInfo) => void;
321
+ /**
322
+ * Log file generation options
323
+ */
324
+ log?: LogOptions;
325
+ }
326
+ /**
327
+ * Result of bulk update operation
328
+ */
329
+ interface BulkUpdateResult {
330
+ successCount: number;
331
+ failureCount: number;
332
+ totalCount: number;
333
+ failedDocIds?: string[];
334
+ }
305
335
 
306
336
  /**
307
337
  * BatchUpdater - Core class for batch operations on Firestore
@@ -389,6 +419,15 @@ declare class BatchUpdater {
389
419
  id: string;
390
420
  data: Record<string, any>;
391
421
  }[]>;
422
+ /**
423
+ * Get a document by its ID directly (faster than findOne with where)
424
+ * @param id - Document ID
425
+ * @returns Document with id and data, or null if not found
426
+ */
427
+ getOne(id: string): Promise<{
428
+ id: string;
429
+ data: Record<string, any>;
430
+ } | null>;
392
431
  /**
393
432
  * Update the first document matching the query conditions
394
433
  * @param updateData - Data to update
@@ -459,6 +498,15 @@ declare class BatchUpdater {
459
498
  create(documents: CreateDocumentInput[], options?: CreateOptions): Promise<CreateResult & {
460
499
  logFilePath?: string;
461
500
  }>;
501
+ /**
502
+ * Update multiple documents with different data for each
503
+ * @param updates - Array of { id, data } objects specifying updates for each document
504
+ * @param options - Bulk update options (e.g., progress callback, log options)
505
+ * @returns Bulk update result with success/failure counts and optional log file path
506
+ */
507
+ bulkUpdate(updates: BulkUpdateInput[], options?: BulkUpdateOptions): Promise<BulkUpdateResult & {
508
+ logFilePath?: string;
509
+ }>;
462
510
  /**
463
511
  * Upsert documents matching query conditions
464
512
  * Updates existing documents or creates them if they don't exist
@@ -554,4 +602,4 @@ declare function isValidUpdateData(value: any): value is Record<string, any>;
554
602
  */
555
603
  declare function formatError(error: unknown, context?: string): string;
556
604
 
557
- export { type AggregateResult, type AggregateSpec, BatchUpdater, type CountResult, type CreateDocumentInput, type CreateOneResult, type CreateOptions, type CreateResult, type DeleteOptions, type DeleteResult, type DocumentSnapshot, type DryRunResult, type FieldValueResult, type LogEntry, type LogOptions, type OperationLog, type OrderByCondition, type PaginateOptions, type PaginateResult, type PreviewResult, type ProgressInfo, type UpdateOptions, type UpdateResult, type UpsertOptions, type UpsertResult, type WhereCondition, calculateProgress, createLogCollector, formatError, formatOperationLog, getAffectedFields, isValidUpdateData, mergeUpdateData, writeOperationLog };
605
+ export { type AggregateResult, type AggregateSpec, BatchUpdater, type BulkUpdateInput, type BulkUpdateOptions, type BulkUpdateResult, type CountResult, type CreateDocumentInput, type CreateOneResult, type CreateOptions, type CreateResult, type DeleteOptions, type DeleteResult, type DocumentSnapshot, type DryRunResult, type FieldValueResult, type LogEntry, type LogOptions, type OperationLog, type OrderByCondition, type PaginateOptions, type PaginateResult, type PreviewResult, type ProgressInfo, type UpdateOptions, type UpdateResult, type UpsertOptions, type UpsertResult, type WhereCondition, calculateProgress, createLogCollector, formatError, formatOperationLog, getAffectedFields, isValidUpdateData, mergeUpdateData, writeOperationLog };
package/dist/index.js CHANGED
@@ -332,6 +332,38 @@ var BatchUpdater = class {
332
332
  data: doc.data()
333
333
  }));
334
334
  }
335
+ /**
336
+ * Get a document by its ID directly (faster than findOne with where)
337
+ * @param id - Document ID
338
+ * @returns Document with id and data, or null if not found
339
+ */
340
+ async getOne(id) {
341
+ this.validateSetup();
342
+ if (this.isCollectionGroup) {
343
+ throw new Error(
344
+ "getOne() cannot be used with collectionGroup(). Use findOne() with where conditions instead."
345
+ );
346
+ }
347
+ const docRef = this.firestore.collection(this.collectionPath).doc(id);
348
+ let docSnapshot;
349
+ if (this.selectedFields && this.selectedFields.length > 0) {
350
+ const query = this.firestore.collection(this.collectionPath).where("__name__", "==", docRef).select(...this.selectedFields);
351
+ const snapshot = await query.get();
352
+ if (snapshot.empty) {
353
+ return null;
354
+ }
355
+ docSnapshot = snapshot.docs[0];
356
+ } else {
357
+ docSnapshot = await docRef.get();
358
+ if (!docSnapshot.exists) {
359
+ return null;
360
+ }
361
+ }
362
+ return {
363
+ id: docSnapshot.id,
364
+ data: docSnapshot.data()
365
+ };
366
+ }
335
367
  /**
336
368
  * Update the first document matching the query conditions
337
369
  * @param updateData - Data to update
@@ -721,6 +753,81 @@ var BatchUpdater = class {
721
753
  }
722
754
  return result;
723
755
  }
756
+ /**
757
+ * Update multiple documents with different data for each
758
+ * @param updates - Array of { id, data } objects specifying updates for each document
759
+ * @param options - Bulk update options (e.g., progress callback, log options)
760
+ * @returns Bulk update result with success/failure counts and optional log file path
761
+ */
762
+ async bulkUpdate(updates, options = {}) {
763
+ this.validateSetup();
764
+ if (this.isCollectionGroup) {
765
+ throw new Error(
766
+ "bulkUpdate() cannot be used with collectionGroup(). Use collection() with a specific path instead."
767
+ );
768
+ }
769
+ if (!Array.isArray(updates) || updates.length === 0) {
770
+ throw new Error("Updates array must be non-empty");
771
+ }
772
+ for (const update of updates) {
773
+ if (!update.id || typeof update.id !== "string") {
774
+ throw new Error("Each update must have a valid id");
775
+ }
776
+ if (!isValidUpdateData(update.data)) {
777
+ throw new Error("Each update must have valid data");
778
+ }
779
+ }
780
+ const totalCount = updates.length;
781
+ let successCount = 0;
782
+ let failureCount = 0;
783
+ const failedDocIds = [];
784
+ const logCollector = options.log?.enabled ? createLogCollector("update", this.collectionPath) : null;
785
+ const bulkWriter = this.firestore.bulkWriter();
786
+ const collection = this.firestore.collection(this.collectionPath);
787
+ let processedCount = 0;
788
+ const docIdMap = /* @__PURE__ */ new Map();
789
+ for (const update of updates) {
790
+ const docRef = collection.doc(update.id);
791
+ docIdMap.set(docRef.path, update.id);
792
+ }
793
+ bulkWriter.onWriteResult((ref) => {
794
+ successCount++;
795
+ processedCount++;
796
+ const docId = docIdMap.get(ref.path) || ref.id;
797
+ logCollector?.addEntry(docId, "success");
798
+ if (options.onProgress) {
799
+ const progress = calculateProgress(processedCount, totalCount);
800
+ options.onProgress(progress);
801
+ }
802
+ });
803
+ bulkWriter.onWriteError((error) => {
804
+ failureCount++;
805
+ processedCount++;
806
+ const docId = error.documentRef?.id || "unknown";
807
+ failedDocIds.push(docId);
808
+ logCollector?.addEntry(docId, "failure", error.message);
809
+ if (options.onProgress) {
810
+ const progress = calculateProgress(processedCount, totalCount);
811
+ options.onProgress(progress);
812
+ }
813
+ return false;
814
+ });
815
+ for (const update of updates) {
816
+ const docRef = collection.doc(update.id);
817
+ bulkWriter.update(docRef, update.data);
818
+ }
819
+ await bulkWriter.close();
820
+ const result = {
821
+ successCount,
822
+ failureCount,
823
+ totalCount,
824
+ failedDocIds: failedDocIds.length > 0 ? failedDocIds : void 0
825
+ };
826
+ if (logCollector && options.log) {
827
+ result.logFilePath = logCollector.finalize(options.log);
828
+ }
829
+ return result;
830
+ }
724
831
  /**
725
832
  * Upsert documents matching query conditions
726
833
  * Updates existing documents or creates them if they don't exist
package/dist/index.mjs CHANGED
@@ -287,6 +287,38 @@ var BatchUpdater = class {
287
287
  data: doc.data()
288
288
  }));
289
289
  }
290
+ /**
291
+ * Get a document by its ID directly (faster than findOne with where)
292
+ * @param id - Document ID
293
+ * @returns Document with id and data, or null if not found
294
+ */
295
+ async getOne(id) {
296
+ this.validateSetup();
297
+ if (this.isCollectionGroup) {
298
+ throw new Error(
299
+ "getOne() cannot be used with collectionGroup(). Use findOne() with where conditions instead."
300
+ );
301
+ }
302
+ const docRef = this.firestore.collection(this.collectionPath).doc(id);
303
+ let docSnapshot;
304
+ if (this.selectedFields && this.selectedFields.length > 0) {
305
+ const query = this.firestore.collection(this.collectionPath).where("__name__", "==", docRef).select(...this.selectedFields);
306
+ const snapshot = await query.get();
307
+ if (snapshot.empty) {
308
+ return null;
309
+ }
310
+ docSnapshot = snapshot.docs[0];
311
+ } else {
312
+ docSnapshot = await docRef.get();
313
+ if (!docSnapshot.exists) {
314
+ return null;
315
+ }
316
+ }
317
+ return {
318
+ id: docSnapshot.id,
319
+ data: docSnapshot.data()
320
+ };
321
+ }
290
322
  /**
291
323
  * Update the first document matching the query conditions
292
324
  * @param updateData - Data to update
@@ -676,6 +708,81 @@ var BatchUpdater = class {
676
708
  }
677
709
  return result;
678
710
  }
711
+ /**
712
+ * Update multiple documents with different data for each
713
+ * @param updates - Array of { id, data } objects specifying updates for each document
714
+ * @param options - Bulk update options (e.g., progress callback, log options)
715
+ * @returns Bulk update result with success/failure counts and optional log file path
716
+ */
717
+ async bulkUpdate(updates, options = {}) {
718
+ this.validateSetup();
719
+ if (this.isCollectionGroup) {
720
+ throw new Error(
721
+ "bulkUpdate() cannot be used with collectionGroup(). Use collection() with a specific path instead."
722
+ );
723
+ }
724
+ if (!Array.isArray(updates) || updates.length === 0) {
725
+ throw new Error("Updates array must be non-empty");
726
+ }
727
+ for (const update of updates) {
728
+ if (!update.id || typeof update.id !== "string") {
729
+ throw new Error("Each update must have a valid id");
730
+ }
731
+ if (!isValidUpdateData(update.data)) {
732
+ throw new Error("Each update must have valid data");
733
+ }
734
+ }
735
+ const totalCount = updates.length;
736
+ let successCount = 0;
737
+ let failureCount = 0;
738
+ const failedDocIds = [];
739
+ const logCollector = options.log?.enabled ? createLogCollector("update", this.collectionPath) : null;
740
+ const bulkWriter = this.firestore.bulkWriter();
741
+ const collection = this.firestore.collection(this.collectionPath);
742
+ let processedCount = 0;
743
+ const docIdMap = /* @__PURE__ */ new Map();
744
+ for (const update of updates) {
745
+ const docRef = collection.doc(update.id);
746
+ docIdMap.set(docRef.path, update.id);
747
+ }
748
+ bulkWriter.onWriteResult((ref) => {
749
+ successCount++;
750
+ processedCount++;
751
+ const docId = docIdMap.get(ref.path) || ref.id;
752
+ logCollector?.addEntry(docId, "success");
753
+ if (options.onProgress) {
754
+ const progress = calculateProgress(processedCount, totalCount);
755
+ options.onProgress(progress);
756
+ }
757
+ });
758
+ bulkWriter.onWriteError((error) => {
759
+ failureCount++;
760
+ processedCount++;
761
+ const docId = error.documentRef?.id || "unknown";
762
+ failedDocIds.push(docId);
763
+ logCollector?.addEntry(docId, "failure", error.message);
764
+ if (options.onProgress) {
765
+ const progress = calculateProgress(processedCount, totalCount);
766
+ options.onProgress(progress);
767
+ }
768
+ return false;
769
+ });
770
+ for (const update of updates) {
771
+ const docRef = collection.doc(update.id);
772
+ bulkWriter.update(docRef, update.data);
773
+ }
774
+ await bulkWriter.close();
775
+ const result = {
776
+ successCount,
777
+ failureCount,
778
+ totalCount,
779
+ failedDocIds: failedDocIds.length > 0 ? failedDocIds : void 0
780
+ };
781
+ if (logCollector && options.log) {
782
+ result.logFilePath = logCollector.finalize(options.log);
783
+ }
784
+ return result;
785
+ }
679
786
  /**
680
787
  * Upsert documents matching query conditions
681
788
  * Updates existing documents or creates them if they don't exist
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firestore-batch-updater",
3
- "version": "1.5.0",
3
+ "version": "1.6.0",
4
4
  "description": "Batch update Firestore documents with query-based filtering and preview",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",