firestore-batch-updater 1.1.0 → 1.2.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
@@ -14,7 +14,10 @@
14
14
  - 진행 상황 추적 - 실시간 진행률 콜백
15
15
  - 일괄 생성/Upsert/삭제 - 여러 문서를 한 번에 생성, upsert 또는 삭제
16
16
  - 정렬 및 제한 - `orderBy()`와 `limit()`으로 정밀한 제어
17
- - FieldValue 지원 - `increment()`, `arrayUnion()`, `serverTimestamp()` 등 사용 가능
17
+ - FieldValue 지원 - `increment()`, `arrayUnion()`, `delete()`, `serverTimestamp()` 등 사용 가능
18
+ - 서브컬렉션 & 컬렉션 그룹 - 서브컬렉션 쿼리 또는 동일 이름의 모든 컬렉션 쿼리
19
+ - Dry Run 모드 - 실제 변경 없이 작업 시뮬레이션
20
+ - 문서 개수 조회 - 문서를 로드하지 않고 빠르게 개수 확인
18
21
  - 로그 파일 생성 - 감사를 위한 상세 작업 로그 (선택사항)
19
22
 
20
23
  ## 설치
@@ -75,10 +78,12 @@ console.log(`${result.successCount}개 문서 업데이트 완료`);
75
78
 
76
79
  | 메서드 | 설명 | 반환값 |
77
80
  |--------|------|--------|
78
- | `collection(path)` | 작업할 컬렉션 선택 | `this` |
81
+ | `collection(path)` | 작업할 컬렉션 선택 (서브컬렉션 경로 지원) | `this` |
82
+ | `collectionGroup(id)` | 동일 ID의 모든 컬렉션 쿼리 | `this` |
79
83
  | `where(field, op, value)` | 필터 조건 추가 (체이닝 가능) | `this` |
80
84
  | `orderBy(field, direction?)` | 정렬 추가 (체이닝 가능) | `this` |
81
85
  | `limit(count)` | 문서 수 제한 (체이닝 가능) | `this` |
86
+ | `count()` | 매칭되는 문서 개수 조회 | `CountResult` |
82
87
  | `preview(data)` | 업데이트 전 미리보기 | `PreviewResult` |
83
88
  | `update(data, options?)` | 매칭되는 문서 업데이트 | `UpdateResult` |
84
89
  | `create(docs, options?)` | 새 문서 생성 | `CreateResult` |
@@ -95,6 +100,7 @@ console.log(`${result.successCount}개 문서 업데이트 완료`);
95
100
  onProgress?: (progress: ProgressInfo) => void;
96
101
  log?: LogOptions;
97
102
  batchSize?: number; // update/upsert/delete 전용
103
+ dryRun?: boolean; // update/upsert/delete 전용 - 실제 쓰기 없이 시뮬레이션
98
104
  }
99
105
 
100
106
  // ProgressInfo
@@ -116,10 +122,15 @@ console.log(`${result.successCount}개 문서 업데이트 완료`);
116
122
  - 미설정: 모든 문서를 메모리에 한 번에 로드 (소규모 컬렉션에 적합)
117
123
  - 설정 시 (예: `batchSize: 1000`): 커서 페이지네이션을 사용하여 배치 단위로 처리 (대규모 컬렉션의 메모리 문제 방지)
118
124
 
125
+ **dryRun 옵션:**
126
+ - `true` 설정 시: 실제 변경 없이 `DryRunResult` 반환 (`wouldAffect` 개수와 `sampleIds` 포함)
127
+
119
128
  ### 반환 타입
120
129
 
121
130
  | 타입 | 필드 |
122
131
  |------|------|
132
+ | `CountResult` | `count` |
133
+ | `DryRunResult` | `wouldAffect`, `sampleIds[]`, `operation` |
123
134
  | `PreviewResult` | `affectedCount`, `samples[]`, `affectedFields[]` |
124
135
  | `UpdateResult` | `successCount`, `failureCount`, `totalCount`, `failedDocIds?`, `logFilePath?` |
125
136
  | `CreateResult` | `successCount`, `failureCount`, `totalCount`, `createdIds[]`, `failedDocIds?`, `logFilePath?` |
@@ -285,6 +296,80 @@ await updater
285
296
  .collection("users")
286
297
  .where("status", "==", "active")
287
298
  .update({ updatedAt: FieldValue.serverTimestamp() });
299
+
300
+ // 필드 삭제
301
+ await updater
302
+ .collection("users")
303
+ .where("status", "==", "inactive")
304
+ .update({ temporaryData: FieldValue.delete() });
305
+ ```
306
+
307
+ ### 문서 개수 조회
308
+
309
+ ```typescript
310
+ // 문서를 로드하지 않고 빠르게 개수 조회
311
+ const result = await updater
312
+ .collection("users")
313
+ .where("status", "==", "inactive")
314
+ .count();
315
+
316
+ console.log(`${result.count}명의 비활성 사용자 발견`);
317
+ ```
318
+
319
+ ### Dry Run 모드
320
+
321
+ ```typescript
322
+ // 실제 변경 없이 작업 시뮬레이션
323
+ const simulation = await updater
324
+ .collection("users")
325
+ .where("status", "==", "inactive")
326
+ .update(
327
+ { status: "archived" },
328
+ { dryRun: true }
329
+ );
330
+
331
+ console.log(`${simulation.wouldAffect}개 문서가 영향을 받을 예정`);
332
+ console.log("샘플 ID:", simulation.sampleIds);
333
+
334
+ // 삭제에도 사용 가능
335
+ const deleteSimulation = await updater
336
+ .collection("logs")
337
+ .where("createdAt", "<", thirtyDaysAgo)
338
+ .delete({ dryRun: true });
339
+
340
+ console.log(`${deleteSimulation.wouldAffect}개 문서가 삭제될 예정`);
341
+ ```
342
+
343
+ ### 서브컬렉션
344
+
345
+ ```typescript
346
+ // 특정 서브컬렉션 경로 쿼리
347
+ const result = await updater
348
+ .collection("users/user-123/orders")
349
+ .where("status", "==", "pending")
350
+ .update({ status: "cancelled" });
351
+
352
+ // 동적 경로 사용
353
+ const userId = "user-123";
354
+ await updater
355
+ .collection(`users/${userId}/notifications`)
356
+ .where("read", "==", false)
357
+ .delete();
358
+ ```
359
+
360
+ ### 컬렉션 그룹 쿼리
361
+
362
+ ```typescript
363
+ // 모든 사용자의 "orders" 서브컬렉션을 한 번에 쿼리
364
+ const result = await updater
365
+ .collectionGroup("orders")
366
+ .where("status", "==", "pending")
367
+ .where("createdAt", "<", thirtyDaysAgo)
368
+ .update({ status: "expired" });
369
+
370
+ console.log(`${result.successCount}개 주문 업데이트 완료`);
371
+
372
+ // 참고: collectionGroup은 쿼리 필드에 대한 Firestore 인덱스가 필요합니다
288
373
  ```
289
374
 
290
375
  > **참고:** 서로 다른 필드에 여러 `where()` 조건을 사용하거나, `where()`와 `orderBy()`를 다른 필드에 사용할 경우, Firestore에서 [복합 인덱스](https://firebase.google.com/docs/firestore/query-data/indexing)가 필요할 수 있습니다. `FAILED_PRECONDITION` 오류가 발생하면 오류 메시지의 링크를 통해 필요한 인덱스를 생성하세요.
package/README.md CHANGED
@@ -14,7 +14,10 @@ English | [한국어](./README.ko.md)
14
14
  - Progress tracking - Real-time progress callbacks
15
15
  - Batch create/upsert/delete - Create, upsert, or delete multiple documents at once
16
16
  - Sorting and limiting - Use `orderBy()` and `limit()` for precise control
17
- - FieldValue support - Use `increment()`, `arrayUnion()`, `serverTimestamp()`, etc.
17
+ - FieldValue support - Use `increment()`, `arrayUnion()`, `delete()`, `serverTimestamp()`, etc.
18
+ - Subcollection & Collection Group - Query subcollections or all collections with the same name
19
+ - Dry run mode - Simulate operations without making changes
20
+ - Count documents - Quickly count matching documents without loading them
18
21
  - Log file generation - Optional detailed operation logs for auditing
19
22
 
20
23
  ## Installation
@@ -75,10 +78,12 @@ console.log(`Updated ${result.successCount} documents`);
75
78
 
76
79
  | Method | Description | Returns |
77
80
  |--------|-------------|---------|
78
- | `collection(path)` | Select collection to operate on | `this` |
81
+ | `collection(path)` | Select collection to operate on (supports subcollection paths) | `this` |
82
+ | `collectionGroup(id)` | Query all collections with the same ID | `this` |
79
83
  | `where(field, op, value)` | Add filter condition (chainable) | `this` |
80
84
  | `orderBy(field, direction?)` | Add sorting (chainable) | `this` |
81
85
  | `limit(count)` | Limit number of documents (chainable) | `this` |
86
+ | `count()` | Count matching documents | `CountResult` |
82
87
  | `preview(data)` | Preview changes before update | `PreviewResult` |
83
88
  | `update(data, options?)` | Update matching documents | `UpdateResult` |
84
89
  | `create(docs, options?)` | Create new documents | `CreateResult` |
@@ -95,6 +100,7 @@ All write operations support an optional `options` parameter:
95
100
  onProgress?: (progress: ProgressInfo) => void;
96
101
  log?: LogOptions;
97
102
  batchSize?: number; // For update/upsert/delete
103
+ dryRun?: boolean; // For update/upsert/delete - simulate without writing
98
104
  }
99
105
 
100
106
  // ProgressInfo
@@ -116,10 +122,15 @@ All write operations support an optional `options` parameter:
116
122
  - When not set: All documents are loaded into memory at once (suitable for small collections)
117
123
  - When set (e.g., `batchSize: 1000`): Documents are processed in batches using cursor pagination (suitable for large collections to prevent memory issues)
118
124
 
125
+ **dryRun option:**
126
+ - When `true`: Returns `DryRunResult` with `wouldAffect` count and `sampleIds` without making any changes
127
+
119
128
  ### Return Types
120
129
 
121
130
  | Type | Fields |
122
131
  |------|--------|
132
+ | `CountResult` | `count` |
133
+ | `DryRunResult` | `wouldAffect`, `sampleIds[]`, `operation` |
123
134
  | `PreviewResult` | `affectedCount`, `samples[]`, `affectedFields[]` |
124
135
  | `UpdateResult` | `successCount`, `failureCount`, `totalCount`, `failedDocIds?`, `logFilePath?` |
125
136
  | `CreateResult` | `successCount`, `failureCount`, `totalCount`, `createdIds[]`, `failedDocIds?`, `logFilePath?` |
@@ -284,6 +295,80 @@ await updater
284
295
  .collection("users")
285
296
  .where("status", "==", "active")
286
297
  .update({ lastSeen: FieldValue.serverTimestamp() });
298
+
299
+ // Delete a field
300
+ await updater
301
+ .collection("users")
302
+ .where("status", "==", "inactive")
303
+ .update({ temporaryData: FieldValue.delete() });
304
+ ```
305
+
306
+ ### Count Documents
307
+
308
+ ```typescript
309
+ // Quickly count matching documents without loading them
310
+ const result = await updater
311
+ .collection("users")
312
+ .where("status", "==", "inactive")
313
+ .count();
314
+
315
+ console.log(`Found ${result.count} inactive users`);
316
+ ```
317
+
318
+ ### Dry Run Mode
319
+
320
+ ```typescript
321
+ // Simulate an operation without making any changes
322
+ const simulation = await updater
323
+ .collection("users")
324
+ .where("status", "==", "inactive")
325
+ .update(
326
+ { status: "archived" },
327
+ { dryRun: true }
328
+ );
329
+
330
+ console.log(`Would affect ${simulation.wouldAffect} documents`);
331
+ console.log("Sample IDs:", simulation.sampleIds);
332
+
333
+ // Also works with delete
334
+ const deleteSimulation = await updater
335
+ .collection("logs")
336
+ .where("createdAt", "<", thirtyDaysAgo)
337
+ .delete({ dryRun: true });
338
+
339
+ console.log(`Would delete ${deleteSimulation.wouldAffect} documents`);
340
+ ```
341
+
342
+ ### Subcollections
343
+
344
+ ```typescript
345
+ // Query a specific subcollection path
346
+ const result = await updater
347
+ .collection("users/user-123/orders")
348
+ .where("status", "==", "pending")
349
+ .update({ status: "cancelled" });
350
+
351
+ // Or use dynamic paths
352
+ const userId = "user-123";
353
+ await updater
354
+ .collection(`users/${userId}/notifications`)
355
+ .where("read", "==", false)
356
+ .delete();
357
+ ```
358
+
359
+ ### Collection Group Queries
360
+
361
+ ```typescript
362
+ // Query ALL "orders" subcollections across all users
363
+ const result = await updater
364
+ .collectionGroup("orders")
365
+ .where("status", "==", "pending")
366
+ .where("createdAt", "<", thirtyDaysAgo)
367
+ .update({ status: "expired" });
368
+
369
+ console.log(`Updated ${result.successCount} orders across all users`);
370
+
371
+ // Note: collectionGroup requires a Firestore index on the queried fields
287
372
  ```
288
373
 
289
374
  ### Error Handling
package/dist/index.d.mts CHANGED
@@ -32,6 +32,11 @@ interface UpdateOptions {
32
32
  * When not set, all documents are loaded at once
33
33
  */
34
34
  batchSize?: number;
35
+ /**
36
+ * Dry run mode - simulate the operation without actually writing
37
+ * Returns what would happen without making any changes
38
+ */
39
+ dryRun?: boolean;
35
40
  }
36
41
  /**
37
42
  * Result of batch update operation
@@ -130,6 +135,11 @@ interface UpsertOptions {
130
135
  * When not set, all documents are loaded at once
131
136
  */
132
137
  batchSize?: number;
138
+ /**
139
+ * Dry run mode - simulate the operation without actually writing
140
+ * Returns what would happen without making any changes
141
+ */
142
+ dryRun?: boolean;
133
143
  }
134
144
  /**
135
145
  * Result of batch upsert operation
@@ -159,6 +169,11 @@ interface DeleteOptions {
159
169
  * When not set, all documents are loaded at once
160
170
  */
161
171
  batchSize?: number;
172
+ /**
173
+ * Dry run mode - simulate the operation without actually writing
174
+ * Returns what would happen without making any changes
175
+ */
176
+ dryRun?: boolean;
162
177
  }
163
178
  /**
164
179
  * Result of batch delete operation
@@ -170,6 +185,20 @@ interface DeleteResult {
170
185
  deletedIds: string[];
171
186
  failedDocIds?: string[];
172
187
  }
188
+ /**
189
+ * Result of count operation
190
+ */
191
+ interface CountResult {
192
+ count: number;
193
+ }
194
+ /**
195
+ * Result of dry run operation
196
+ */
197
+ interface DryRunResult {
198
+ wouldAffect: number;
199
+ sampleIds: string[];
200
+ operation: "update" | "upsert" | "delete";
201
+ }
173
202
  /**
174
203
  * Log options for batch operations
175
204
  */
@@ -215,6 +244,7 @@ interface OperationLog {
215
244
  declare class BatchUpdater {
216
245
  private firestore;
217
246
  private collectionPath?;
247
+ private isCollectionGroup;
218
248
  private conditions;
219
249
  private orderByConditions;
220
250
  private limitCount?;
@@ -225,10 +255,17 @@ declare class BatchUpdater {
225
255
  constructor(firestore: Firestore);
226
256
  /**
227
257
  * Select a collection to operate on
258
+ * Supports subcollection paths like "users/userId/orders"
228
259
  * @param path - Collection path
229
260
  * @returns This instance for chaining
230
261
  */
231
262
  collection(path: string): this;
263
+ /**
264
+ * Select a collection group to operate on (queries across all subcollections with the same name)
265
+ * @param collectionId - Collection ID (not a path, just the collection name)
266
+ * @returns This instance for chaining
267
+ */
268
+ collectionGroup(collectionId: string): this;
232
269
  /**
233
270
  * Add a where condition to filter documents
234
271
  * @param field - Field path
@@ -250,6 +287,11 @@ declare class BatchUpdater {
250
287
  * @returns This instance for chaining
251
288
  */
252
289
  limit(count: number): this;
290
+ /**
291
+ * Count documents matching the query conditions
292
+ * @returns Count result with number of matching documents
293
+ */
294
+ count(): Promise<CountResult>;
253
295
  /**
254
296
  * Preview changes before executing update
255
297
  * @param updateData - Data to update
@@ -259,12 +301,12 @@ declare class BatchUpdater {
259
301
  /**
260
302
  * Execute batch update operation
261
303
  * @param updateData - Data to update
262
- * @param options - Update options (e.g., progress callback, log options, batchSize for pagination)
263
- * @returns Update result with success/failure counts and optional log file path
304
+ * @param options - Update options (e.g., progress callback, log options, batchSize for pagination, dryRun)
305
+ * @returns Update result with success/failure counts and optional log file path, or DryRunResult if dryRun is true
264
306
  */
265
- update(updateData: Record<string, any>, options?: UpdateOptions): Promise<UpdateResult & {
307
+ update(updateData: Record<string, any>, options?: UpdateOptions): Promise<(UpdateResult & {
266
308
  logFilePath?: string;
267
- }>;
309
+ }) | DryRunResult>;
268
310
  /**
269
311
  * Get specific field values from matching documents
270
312
  * @param fieldPath - Field path to retrieve
@@ -273,6 +315,7 @@ declare class BatchUpdater {
273
315
  getFields(fieldPath: string): Promise<FieldValueResult[]>;
274
316
  /**
275
317
  * Create multiple documents in batch
318
+ * Note: This method does not work with collectionGroup()
276
319
  * @param documents - Array of documents to create
277
320
  * @param options - Create options (e.g., progress callback, log options)
278
321
  * @returns Create result with success/failure counts, created IDs, and optional log file path
@@ -284,20 +327,20 @@ declare class BatchUpdater {
284
327
  * Upsert documents matching query conditions
285
328
  * Updates existing documents or creates them if they don't exist
286
329
  * @param updateData - Data to set/merge
287
- * @param options - Upsert options (e.g., progress callback, log options, batchSize for pagination)
288
- * @returns Upsert result with success/failure counts and optional log file path
330
+ * @param options - Upsert options (e.g., progress callback, log options, batchSize for pagination, dryRun)
331
+ * @returns Upsert result with success/failure counts and optional log file path, or DryRunResult if dryRun is true
289
332
  */
290
- upsert(updateData: Record<string, any>, options?: UpsertOptions): Promise<UpsertResult & {
333
+ upsert(updateData: Record<string, any>, options?: UpsertOptions): Promise<(UpsertResult & {
291
334
  logFilePath?: string;
292
- }>;
335
+ }) | DryRunResult>;
293
336
  /**
294
337
  * Delete documents matching query conditions
295
- * @param options - Delete options (e.g., progress callback, log options, batchSize for pagination)
296
- * @returns Delete result with success/failure counts, deleted IDs, and optional log file path
338
+ * @param options - Delete options (e.g., progress callback, log options, batchSize for pagination, dryRun)
339
+ * @returns Delete result with success/failure counts, deleted IDs, and optional log file path, or DryRunResult if dryRun is true
297
340
  */
298
- delete(options?: DeleteOptions): Promise<DeleteResult & {
341
+ delete(options?: DeleteOptions): Promise<(DeleteResult & {
299
342
  logFilePath?: string;
300
- }>;
343
+ }) | DryRunResult>;
301
344
  /**
302
345
  * Validate that collection is set
303
346
  * @private
@@ -374,4 +417,4 @@ declare function isValidUpdateData(value: any): value is Record<string, any>;
374
417
  */
375
418
  declare function formatError(error: unknown, context?: string): string;
376
419
 
377
- export { BatchUpdater, type CreateDocumentInput, type CreateOptions, type CreateResult, type DeleteOptions, type DeleteResult, type DocumentSnapshot, type FieldValueResult, type LogEntry, type LogOptions, type OperationLog, type OrderByCondition, type PreviewResult, type ProgressInfo, type UpdateOptions, type UpdateResult, type UpsertOptions, type UpsertResult, type WhereCondition, calculateProgress, createLogCollector, formatError, formatOperationLog, getAffectedFields, isValidUpdateData, mergeUpdateData, writeOperationLog };
420
+ export { BatchUpdater, type CountResult, type CreateDocumentInput, type CreateOptions, type CreateResult, type DeleteOptions, type DeleteResult, type DocumentSnapshot, type DryRunResult, type FieldValueResult, type LogEntry, type LogOptions, type OperationLog, type OrderByCondition, 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
@@ -32,6 +32,11 @@ interface UpdateOptions {
32
32
  * When not set, all documents are loaded at once
33
33
  */
34
34
  batchSize?: number;
35
+ /**
36
+ * Dry run mode - simulate the operation without actually writing
37
+ * Returns what would happen without making any changes
38
+ */
39
+ dryRun?: boolean;
35
40
  }
36
41
  /**
37
42
  * Result of batch update operation
@@ -130,6 +135,11 @@ interface UpsertOptions {
130
135
  * When not set, all documents are loaded at once
131
136
  */
132
137
  batchSize?: number;
138
+ /**
139
+ * Dry run mode - simulate the operation without actually writing
140
+ * Returns what would happen without making any changes
141
+ */
142
+ dryRun?: boolean;
133
143
  }
134
144
  /**
135
145
  * Result of batch upsert operation
@@ -159,6 +169,11 @@ interface DeleteOptions {
159
169
  * When not set, all documents are loaded at once
160
170
  */
161
171
  batchSize?: number;
172
+ /**
173
+ * Dry run mode - simulate the operation without actually writing
174
+ * Returns what would happen without making any changes
175
+ */
176
+ dryRun?: boolean;
162
177
  }
163
178
  /**
164
179
  * Result of batch delete operation
@@ -170,6 +185,20 @@ interface DeleteResult {
170
185
  deletedIds: string[];
171
186
  failedDocIds?: string[];
172
187
  }
188
+ /**
189
+ * Result of count operation
190
+ */
191
+ interface CountResult {
192
+ count: number;
193
+ }
194
+ /**
195
+ * Result of dry run operation
196
+ */
197
+ interface DryRunResult {
198
+ wouldAffect: number;
199
+ sampleIds: string[];
200
+ operation: "update" | "upsert" | "delete";
201
+ }
173
202
  /**
174
203
  * Log options for batch operations
175
204
  */
@@ -215,6 +244,7 @@ interface OperationLog {
215
244
  declare class BatchUpdater {
216
245
  private firestore;
217
246
  private collectionPath?;
247
+ private isCollectionGroup;
218
248
  private conditions;
219
249
  private orderByConditions;
220
250
  private limitCount?;
@@ -225,10 +255,17 @@ declare class BatchUpdater {
225
255
  constructor(firestore: Firestore);
226
256
  /**
227
257
  * Select a collection to operate on
258
+ * Supports subcollection paths like "users/userId/orders"
228
259
  * @param path - Collection path
229
260
  * @returns This instance for chaining
230
261
  */
231
262
  collection(path: string): this;
263
+ /**
264
+ * Select a collection group to operate on (queries across all subcollections with the same name)
265
+ * @param collectionId - Collection ID (not a path, just the collection name)
266
+ * @returns This instance for chaining
267
+ */
268
+ collectionGroup(collectionId: string): this;
232
269
  /**
233
270
  * Add a where condition to filter documents
234
271
  * @param field - Field path
@@ -250,6 +287,11 @@ declare class BatchUpdater {
250
287
  * @returns This instance for chaining
251
288
  */
252
289
  limit(count: number): this;
290
+ /**
291
+ * Count documents matching the query conditions
292
+ * @returns Count result with number of matching documents
293
+ */
294
+ count(): Promise<CountResult>;
253
295
  /**
254
296
  * Preview changes before executing update
255
297
  * @param updateData - Data to update
@@ -259,12 +301,12 @@ declare class BatchUpdater {
259
301
  /**
260
302
  * Execute batch update operation
261
303
  * @param updateData - Data to update
262
- * @param options - Update options (e.g., progress callback, log options, batchSize for pagination)
263
- * @returns Update result with success/failure counts and optional log file path
304
+ * @param options - Update options (e.g., progress callback, log options, batchSize for pagination, dryRun)
305
+ * @returns Update result with success/failure counts and optional log file path, or DryRunResult if dryRun is true
264
306
  */
265
- update(updateData: Record<string, any>, options?: UpdateOptions): Promise<UpdateResult & {
307
+ update(updateData: Record<string, any>, options?: UpdateOptions): Promise<(UpdateResult & {
266
308
  logFilePath?: string;
267
- }>;
309
+ }) | DryRunResult>;
268
310
  /**
269
311
  * Get specific field values from matching documents
270
312
  * @param fieldPath - Field path to retrieve
@@ -273,6 +315,7 @@ declare class BatchUpdater {
273
315
  getFields(fieldPath: string): Promise<FieldValueResult[]>;
274
316
  /**
275
317
  * Create multiple documents in batch
318
+ * Note: This method does not work with collectionGroup()
276
319
  * @param documents - Array of documents to create
277
320
  * @param options - Create options (e.g., progress callback, log options)
278
321
  * @returns Create result with success/failure counts, created IDs, and optional log file path
@@ -284,20 +327,20 @@ declare class BatchUpdater {
284
327
  * Upsert documents matching query conditions
285
328
  * Updates existing documents or creates them if they don't exist
286
329
  * @param updateData - Data to set/merge
287
- * @param options - Upsert options (e.g., progress callback, log options, batchSize for pagination)
288
- * @returns Upsert result with success/failure counts and optional log file path
330
+ * @param options - Upsert options (e.g., progress callback, log options, batchSize for pagination, dryRun)
331
+ * @returns Upsert result with success/failure counts and optional log file path, or DryRunResult if dryRun is true
289
332
  */
290
- upsert(updateData: Record<string, any>, options?: UpsertOptions): Promise<UpsertResult & {
333
+ upsert(updateData: Record<string, any>, options?: UpsertOptions): Promise<(UpsertResult & {
291
334
  logFilePath?: string;
292
- }>;
335
+ }) | DryRunResult>;
293
336
  /**
294
337
  * Delete documents matching query conditions
295
- * @param options - Delete options (e.g., progress callback, log options, batchSize for pagination)
296
- * @returns Delete result with success/failure counts, deleted IDs, and optional log file path
338
+ * @param options - Delete options (e.g., progress callback, log options, batchSize for pagination, dryRun)
339
+ * @returns Delete result with success/failure counts, deleted IDs, and optional log file path, or DryRunResult if dryRun is true
297
340
  */
298
- delete(options?: DeleteOptions): Promise<DeleteResult & {
341
+ delete(options?: DeleteOptions): Promise<(DeleteResult & {
299
342
  logFilePath?: string;
300
- }>;
343
+ }) | DryRunResult>;
301
344
  /**
302
345
  * Validate that collection is set
303
346
  * @private
@@ -374,4 +417,4 @@ declare function isValidUpdateData(value: any): value is Record<string, any>;
374
417
  */
375
418
  declare function formatError(error: unknown, context?: string): string;
376
419
 
377
- export { BatchUpdater, type CreateDocumentInput, type CreateOptions, type CreateResult, type DeleteOptions, type DeleteResult, type DocumentSnapshot, type FieldValueResult, type LogEntry, type LogOptions, type OperationLog, type OrderByCondition, type PreviewResult, type ProgressInfo, type UpdateOptions, type UpdateResult, type UpsertOptions, type UpsertResult, type WhereCondition, calculateProgress, createLogCollector, formatError, formatOperationLog, getAffectedFields, isValidUpdateData, mergeUpdateData, writeOperationLog };
420
+ export { BatchUpdater, type CountResult, type CreateDocumentInput, type CreateOptions, type CreateResult, type DeleteOptions, type DeleteResult, type DocumentSnapshot, type DryRunResult, type FieldValueResult, type LogEntry, type LogOptions, type OperationLog, type OrderByCondition, 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
@@ -196,17 +196,33 @@ var BatchUpdater = class {
196
196
  * @param firestore - Initialized Firestore instance from firebase-admin
197
197
  */
198
198
  constructor(firestore) {
199
+ this.isCollectionGroup = false;
199
200
  this.conditions = [];
200
201
  this.orderByConditions = [];
201
202
  this.firestore = firestore;
202
203
  }
203
204
  /**
204
205
  * Select a collection to operate on
206
+ * Supports subcollection paths like "users/userId/orders"
205
207
  * @param path - Collection path
206
208
  * @returns This instance for chaining
207
209
  */
208
210
  collection(path2) {
209
211
  this.collectionPath = path2;
212
+ this.isCollectionGroup = false;
213
+ this.conditions = [];
214
+ this.orderByConditions = [];
215
+ this.limitCount = void 0;
216
+ return this;
217
+ }
218
+ /**
219
+ * Select a collection group to operate on (queries across all subcollections with the same name)
220
+ * @param collectionId - Collection ID (not a path, just the collection name)
221
+ * @returns This instance for chaining
222
+ */
223
+ collectionGroup(collectionId) {
224
+ this.collectionPath = collectionId;
225
+ this.isCollectionGroup = true;
210
226
  this.conditions = [];
211
227
  this.orderByConditions = [];
212
228
  this.limitCount = void 0;
@@ -242,6 +258,18 @@ var BatchUpdater = class {
242
258
  this.limitCount = count;
243
259
  return this;
244
260
  }
261
+ /**
262
+ * Count documents matching the query conditions
263
+ * @returns Count result with number of matching documents
264
+ */
265
+ async count() {
266
+ this.validateSetup();
267
+ const query = this.buildQuery();
268
+ const snapshot = await query.count().get();
269
+ return {
270
+ count: snapshot.data().count
271
+ };
272
+ }
245
273
  /**
246
274
  * Preview changes before executing update
247
275
  * @param updateData - Data to update
@@ -276,14 +304,24 @@ var BatchUpdater = class {
276
304
  /**
277
305
  * Execute batch update operation
278
306
  * @param updateData - Data to update
279
- * @param options - Update options (e.g., progress callback, log options, batchSize for pagination)
280
- * @returns Update result with success/failure counts and optional log file path
307
+ * @param options - Update options (e.g., progress callback, log options, batchSize for pagination, dryRun)
308
+ * @returns Update result with success/failure counts and optional log file path, or DryRunResult if dryRun is true
281
309
  */
282
310
  async update(updateData, options = {}) {
283
311
  this.validateSetup();
284
312
  if (!isValidUpdateData(updateData)) {
285
313
  throw new Error("Update data must be a non-empty object");
286
314
  }
315
+ if (options.dryRun) {
316
+ const query = this.buildQuery();
317
+ const snapshot = await query.limit(10).get();
318
+ const countSnapshot = await this.buildQuery().count().get();
319
+ return {
320
+ wouldAffect: countSnapshot.data().count,
321
+ sampleIds: snapshot.docs.map((doc) => doc.id),
322
+ operation: "update"
323
+ };
324
+ }
287
325
  const logCollector = options.log?.enabled ? createLogCollector("update", this.collectionPath, this.conditions, updateData) : null;
288
326
  let successCount = 0;
289
327
  let failureCount = 0;
@@ -432,12 +470,16 @@ var BatchUpdater = class {
432
470
  }
433
471
  /**
434
472
  * Create multiple documents in batch
473
+ * Note: This method does not work with collectionGroup()
435
474
  * @param documents - Array of documents to create
436
475
  * @param options - Create options (e.g., progress callback, log options)
437
476
  * @returns Create result with success/failure counts, created IDs, and optional log file path
438
477
  */
439
478
  async create(documents, options = {}) {
440
479
  this.validateSetup();
480
+ if (this.isCollectionGroup) {
481
+ throw new Error("create() cannot be used with collectionGroup(). Use collection() with a specific path instead.");
482
+ }
441
483
  if (!Array.isArray(documents) || documents.length === 0) {
442
484
  throw new Error("Documents array must be non-empty");
443
485
  }
@@ -498,14 +540,24 @@ var BatchUpdater = class {
498
540
  * Upsert documents matching query conditions
499
541
  * Updates existing documents or creates them if they don't exist
500
542
  * @param updateData - Data to set/merge
501
- * @param options - Upsert options (e.g., progress callback, log options, batchSize for pagination)
502
- * @returns Upsert result with success/failure counts and optional log file path
543
+ * @param options - Upsert options (e.g., progress callback, log options, batchSize for pagination, dryRun)
544
+ * @returns Upsert result with success/failure counts and optional log file path, or DryRunResult if dryRun is true
503
545
  */
504
546
  async upsert(updateData, options = {}) {
505
547
  this.validateSetup();
506
548
  if (!isValidUpdateData(updateData)) {
507
549
  throw new Error("Update data must be a non-empty object");
508
550
  }
551
+ if (options.dryRun) {
552
+ const query = this.buildQuery();
553
+ const snapshot = await query.limit(10).get();
554
+ const countSnapshot = await this.buildQuery().count().get();
555
+ return {
556
+ wouldAffect: countSnapshot.data().count,
557
+ sampleIds: snapshot.docs.map((doc) => doc.id),
558
+ operation: "upsert"
559
+ };
560
+ }
509
561
  const logCollector = options.log?.enabled ? createLogCollector("upsert", this.collectionPath, this.conditions, updateData) : null;
510
562
  let successCount = 0;
511
563
  let failureCount = 0;
@@ -634,11 +686,21 @@ var BatchUpdater = class {
634
686
  }
635
687
  /**
636
688
  * Delete documents matching query conditions
637
- * @param options - Delete options (e.g., progress callback, log options, batchSize for pagination)
638
- * @returns Delete result with success/failure counts, deleted IDs, and optional log file path
689
+ * @param options - Delete options (e.g., progress callback, log options, batchSize for pagination, dryRun)
690
+ * @returns Delete result with success/failure counts, deleted IDs, and optional log file path, or DryRunResult if dryRun is true
639
691
  */
640
692
  async delete(options = {}) {
641
693
  this.validateSetup();
694
+ if (options.dryRun) {
695
+ const query = this.buildQuery();
696
+ const snapshot = await query.limit(10).get();
697
+ const countSnapshot = await this.buildQuery().count().get();
698
+ return {
699
+ wouldAffect: countSnapshot.data().count,
700
+ sampleIds: snapshot.docs.map((doc) => doc.id),
701
+ operation: "delete"
702
+ };
703
+ }
642
704
  const logCollector = options.log?.enabled ? createLogCollector("delete", this.collectionPath, this.conditions) : null;
643
705
  let successCount = 0;
644
706
  let failureCount = 0;
@@ -785,9 +847,7 @@ var BatchUpdater = class {
785
847
  * @private
786
848
  */
787
849
  buildQuery() {
788
- let query = this.firestore.collection(
789
- this.collectionPath
790
- );
850
+ let query = this.isCollectionGroup ? this.firestore.collectionGroup(this.collectionPath) : this.firestore.collection(this.collectionPath);
791
851
  for (const condition of this.conditions) {
792
852
  query = query.where(condition.field, condition.operator, condition.value);
793
853
  }
package/dist/index.mjs CHANGED
@@ -151,17 +151,33 @@ var BatchUpdater = class {
151
151
  * @param firestore - Initialized Firestore instance from firebase-admin
152
152
  */
153
153
  constructor(firestore) {
154
+ this.isCollectionGroup = false;
154
155
  this.conditions = [];
155
156
  this.orderByConditions = [];
156
157
  this.firestore = firestore;
157
158
  }
158
159
  /**
159
160
  * Select a collection to operate on
161
+ * Supports subcollection paths like "users/userId/orders"
160
162
  * @param path - Collection path
161
163
  * @returns This instance for chaining
162
164
  */
163
165
  collection(path2) {
164
166
  this.collectionPath = path2;
167
+ this.isCollectionGroup = false;
168
+ this.conditions = [];
169
+ this.orderByConditions = [];
170
+ this.limitCount = void 0;
171
+ return this;
172
+ }
173
+ /**
174
+ * Select a collection group to operate on (queries across all subcollections with the same name)
175
+ * @param collectionId - Collection ID (not a path, just the collection name)
176
+ * @returns This instance for chaining
177
+ */
178
+ collectionGroup(collectionId) {
179
+ this.collectionPath = collectionId;
180
+ this.isCollectionGroup = true;
165
181
  this.conditions = [];
166
182
  this.orderByConditions = [];
167
183
  this.limitCount = void 0;
@@ -197,6 +213,18 @@ var BatchUpdater = class {
197
213
  this.limitCount = count;
198
214
  return this;
199
215
  }
216
+ /**
217
+ * Count documents matching the query conditions
218
+ * @returns Count result with number of matching documents
219
+ */
220
+ async count() {
221
+ this.validateSetup();
222
+ const query = this.buildQuery();
223
+ const snapshot = await query.count().get();
224
+ return {
225
+ count: snapshot.data().count
226
+ };
227
+ }
200
228
  /**
201
229
  * Preview changes before executing update
202
230
  * @param updateData - Data to update
@@ -231,14 +259,24 @@ var BatchUpdater = class {
231
259
  /**
232
260
  * Execute batch update operation
233
261
  * @param updateData - Data to update
234
- * @param options - Update options (e.g., progress callback, log options, batchSize for pagination)
235
- * @returns Update result with success/failure counts and optional log file path
262
+ * @param options - Update options (e.g., progress callback, log options, batchSize for pagination, dryRun)
263
+ * @returns Update result with success/failure counts and optional log file path, or DryRunResult if dryRun is true
236
264
  */
237
265
  async update(updateData, options = {}) {
238
266
  this.validateSetup();
239
267
  if (!isValidUpdateData(updateData)) {
240
268
  throw new Error("Update data must be a non-empty object");
241
269
  }
270
+ if (options.dryRun) {
271
+ const query = this.buildQuery();
272
+ const snapshot = await query.limit(10).get();
273
+ const countSnapshot = await this.buildQuery().count().get();
274
+ return {
275
+ wouldAffect: countSnapshot.data().count,
276
+ sampleIds: snapshot.docs.map((doc) => doc.id),
277
+ operation: "update"
278
+ };
279
+ }
242
280
  const logCollector = options.log?.enabled ? createLogCollector("update", this.collectionPath, this.conditions, updateData) : null;
243
281
  let successCount = 0;
244
282
  let failureCount = 0;
@@ -387,12 +425,16 @@ var BatchUpdater = class {
387
425
  }
388
426
  /**
389
427
  * Create multiple documents in batch
428
+ * Note: This method does not work with collectionGroup()
390
429
  * @param documents - Array of documents to create
391
430
  * @param options - Create options (e.g., progress callback, log options)
392
431
  * @returns Create result with success/failure counts, created IDs, and optional log file path
393
432
  */
394
433
  async create(documents, options = {}) {
395
434
  this.validateSetup();
435
+ if (this.isCollectionGroup) {
436
+ throw new Error("create() cannot be used with collectionGroup(). Use collection() with a specific path instead.");
437
+ }
396
438
  if (!Array.isArray(documents) || documents.length === 0) {
397
439
  throw new Error("Documents array must be non-empty");
398
440
  }
@@ -453,14 +495,24 @@ var BatchUpdater = class {
453
495
  * Upsert documents matching query conditions
454
496
  * Updates existing documents or creates them if they don't exist
455
497
  * @param updateData - Data to set/merge
456
- * @param options - Upsert options (e.g., progress callback, log options, batchSize for pagination)
457
- * @returns Upsert result with success/failure counts and optional log file path
498
+ * @param options - Upsert options (e.g., progress callback, log options, batchSize for pagination, dryRun)
499
+ * @returns Upsert result with success/failure counts and optional log file path, or DryRunResult if dryRun is true
458
500
  */
459
501
  async upsert(updateData, options = {}) {
460
502
  this.validateSetup();
461
503
  if (!isValidUpdateData(updateData)) {
462
504
  throw new Error("Update data must be a non-empty object");
463
505
  }
506
+ if (options.dryRun) {
507
+ const query = this.buildQuery();
508
+ const snapshot = await query.limit(10).get();
509
+ const countSnapshot = await this.buildQuery().count().get();
510
+ return {
511
+ wouldAffect: countSnapshot.data().count,
512
+ sampleIds: snapshot.docs.map((doc) => doc.id),
513
+ operation: "upsert"
514
+ };
515
+ }
464
516
  const logCollector = options.log?.enabled ? createLogCollector("upsert", this.collectionPath, this.conditions, updateData) : null;
465
517
  let successCount = 0;
466
518
  let failureCount = 0;
@@ -589,11 +641,21 @@ var BatchUpdater = class {
589
641
  }
590
642
  /**
591
643
  * Delete documents matching query conditions
592
- * @param options - Delete options (e.g., progress callback, log options, batchSize for pagination)
593
- * @returns Delete result with success/failure counts, deleted IDs, and optional log file path
644
+ * @param options - Delete options (e.g., progress callback, log options, batchSize for pagination, dryRun)
645
+ * @returns Delete result with success/failure counts, deleted IDs, and optional log file path, or DryRunResult if dryRun is true
594
646
  */
595
647
  async delete(options = {}) {
596
648
  this.validateSetup();
649
+ if (options.dryRun) {
650
+ const query = this.buildQuery();
651
+ const snapshot = await query.limit(10).get();
652
+ const countSnapshot = await this.buildQuery().count().get();
653
+ return {
654
+ wouldAffect: countSnapshot.data().count,
655
+ sampleIds: snapshot.docs.map((doc) => doc.id),
656
+ operation: "delete"
657
+ };
658
+ }
597
659
  const logCollector = options.log?.enabled ? createLogCollector("delete", this.collectionPath, this.conditions) : null;
598
660
  let successCount = 0;
599
661
  let failureCount = 0;
@@ -740,9 +802,7 @@ var BatchUpdater = class {
740
802
  * @private
741
803
  */
742
804
  buildQuery() {
743
- let query = this.firestore.collection(
744
- this.collectionPath
745
- );
805
+ let query = this.isCollectionGroup ? this.firestore.collectionGroup(this.collectionPath) : this.firestore.collection(this.collectionPath);
746
806
  for (const condition of this.conditions) {
747
807
  query = query.where(condition.field, condition.operator, condition.value);
748
808
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firestore-batch-updater",
3
- "version": "1.1.0",
3
+ "version": "1.2.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",