firestore-batch-updater 1.1.0 → 1.3.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 +135 -2
- package/README.md +135 -2
- package/dist/index.d.mts +102 -16
- package/dist/index.d.ts +102 -16
- package/dist/index.js +110 -11
- package/dist/index.mjs +110 -11
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -14,7 +14,12 @@
|
|
|
14
14
|
- 진행 상황 추적 - 실시간 진행률 콜백
|
|
15
15
|
- 일괄 생성/Upsert/삭제 - 여러 문서를 한 번에 생성, upsert 또는 삭제
|
|
16
16
|
- 정렬 및 제한 - `orderBy()`와 `limit()`으로 정밀한 제어
|
|
17
|
-
-
|
|
17
|
+
- 필드 선택 - `select()`로 필요한 필드만 로드 (메모리 및 비용 절약)
|
|
18
|
+
- 단일 문서 조회 - `findOne()`으로 효율적인 단일 문서 검색
|
|
19
|
+
- FieldValue 지원 - `increment()`, `arrayUnion()`, `delete()`, `serverTimestamp()` 등 사용 가능
|
|
20
|
+
- 서브컬렉션 & 컬렉션 그룹 - 서브컬렉션 쿼리 또는 동일 이름의 모든 컬렉션 쿼리
|
|
21
|
+
- Dry Run 모드 - 실제 변경 없이 작업 시뮬레이션
|
|
22
|
+
- 문서 개수 조회 - 문서를 로드하지 않고 빠르게 개수 확인
|
|
18
23
|
- 로그 파일 생성 - 감사를 위한 상세 작업 로그 (선택사항)
|
|
19
24
|
|
|
20
25
|
## 설치
|
|
@@ -75,10 +80,14 @@ console.log(`${result.successCount}개 문서 업데이트 완료`);
|
|
|
75
80
|
|
|
76
81
|
| 메서드 | 설명 | 반환값 |
|
|
77
82
|
|--------|------|--------|
|
|
78
|
-
| `collection(path)` | 작업할 컬렉션 선택 | `this` |
|
|
83
|
+
| `collection(path)` | 작업할 컬렉션 선택 (서브컬렉션 경로 지원) | `this` |
|
|
84
|
+
| `collectionGroup(id)` | 동일 ID의 모든 컬렉션 쿼리 | `this` |
|
|
79
85
|
| `where(field, op, value)` | 필터 조건 추가 (체이닝 가능) | `this` |
|
|
80
86
|
| `orderBy(field, direction?)` | 정렬 추가 (체이닝 가능) | `this` |
|
|
81
87
|
| `limit(count)` | 문서 수 제한 (체이닝 가능) | `this` |
|
|
88
|
+
| `select(...fields)` | 특정 필드만 조회 (체이닝 가능) | `this` |
|
|
89
|
+
| `count()` | 매칭되는 문서 개수 조회 | `CountResult` |
|
|
90
|
+
| `findOne()` | 첫 번째 매칭 문서 조회 | `{ id, data } \| null` |
|
|
82
91
|
| `preview(data)` | 업데이트 전 미리보기 | `PreviewResult` |
|
|
83
92
|
| `update(data, options?)` | 매칭되는 문서 업데이트 | `UpdateResult` |
|
|
84
93
|
| `create(docs, options?)` | 새 문서 생성 | `CreateResult` |
|
|
@@ -95,6 +104,7 @@ console.log(`${result.successCount}개 문서 업데이트 완료`);
|
|
|
95
104
|
onProgress?: (progress: ProgressInfo) => void;
|
|
96
105
|
log?: LogOptions;
|
|
97
106
|
batchSize?: number; // update/upsert/delete 전용
|
|
107
|
+
dryRun?: boolean; // update/upsert/delete 전용 - 실제 쓰기 없이 시뮬레이션
|
|
98
108
|
}
|
|
99
109
|
|
|
100
110
|
// ProgressInfo
|
|
@@ -116,10 +126,15 @@ console.log(`${result.successCount}개 문서 업데이트 완료`);
|
|
|
116
126
|
- 미설정: 모든 문서를 메모리에 한 번에 로드 (소규모 컬렉션에 적합)
|
|
117
127
|
- 설정 시 (예: `batchSize: 1000`): 커서 페이지네이션을 사용하여 배치 단위로 처리 (대규모 컬렉션의 메모리 문제 방지)
|
|
118
128
|
|
|
129
|
+
**dryRun 옵션:**
|
|
130
|
+
- `true` 설정 시: 실제 변경 없이 `DryRunResult` 반환 (`wouldAffect` 개수와 `sampleIds` 포함)
|
|
131
|
+
|
|
119
132
|
### 반환 타입
|
|
120
133
|
|
|
121
134
|
| 타입 | 필드 |
|
|
122
135
|
|------|------|
|
|
136
|
+
| `CountResult` | `count` |
|
|
137
|
+
| `DryRunResult` | `wouldAffect`, `sampleIds[]`, `operation` |
|
|
123
138
|
| `PreviewResult` | `affectedCount`, `samples[]`, `affectedFields[]` |
|
|
124
139
|
| `UpdateResult` | `successCount`, `failureCount`, `totalCount`, `failedDocIds?`, `logFilePath?` |
|
|
125
140
|
| `CreateResult` | `successCount`, `failureCount`, `totalCount`, `createdIds[]`, `failedDocIds?`, `logFilePath?` |
|
|
@@ -285,6 +300,124 @@ await updater
|
|
|
285
300
|
.collection("users")
|
|
286
301
|
.where("status", "==", "active")
|
|
287
302
|
.update({ updatedAt: FieldValue.serverTimestamp() });
|
|
303
|
+
|
|
304
|
+
// 필드 삭제
|
|
305
|
+
await updater
|
|
306
|
+
.collection("users")
|
|
307
|
+
.where("status", "==", "inactive")
|
|
308
|
+
.update({ temporaryData: FieldValue.delete() });
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
### 문서 개수 조회
|
|
312
|
+
|
|
313
|
+
```typescript
|
|
314
|
+
// 문서를 로드하지 않고 빠르게 개수 조회
|
|
315
|
+
const result = await updater
|
|
316
|
+
.collection("users")
|
|
317
|
+
.where("status", "==", "inactive")
|
|
318
|
+
.count();
|
|
319
|
+
|
|
320
|
+
console.log(`${result.count}명의 비활성 사용자 발견`);
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
### 특정 필드만 조회
|
|
324
|
+
|
|
325
|
+
```typescript
|
|
326
|
+
// name, email 필드만 로드 (메모리 및 읽기 비용 절약)
|
|
327
|
+
const result = await updater
|
|
328
|
+
.collection("users")
|
|
329
|
+
.select("name", "email")
|
|
330
|
+
.where("status", "==", "active")
|
|
331
|
+
.findOne();
|
|
332
|
+
|
|
333
|
+
console.log(result?.data); // { name, email }만 포함
|
|
334
|
+
|
|
335
|
+
// 모든 작업에서 사용 가능 - 문서에 선택된 필드만 포함됨
|
|
336
|
+
const emails = await updater
|
|
337
|
+
.collection("users")
|
|
338
|
+
.select("email")
|
|
339
|
+
.where("verified", "==", true)
|
|
340
|
+
.getFields("email");
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
### 단일 문서 조회
|
|
344
|
+
|
|
345
|
+
```typescript
|
|
346
|
+
// 첫 번째 매칭 문서 찾기
|
|
347
|
+
const user = await updater
|
|
348
|
+
.collection("users")
|
|
349
|
+
.where("email", "==", "user@example.com")
|
|
350
|
+
.findOne();
|
|
351
|
+
|
|
352
|
+
if (user) {
|
|
353
|
+
console.log("사용자 발견:", user.id);
|
|
354
|
+
console.log("사용자 데이터:", user.data);
|
|
355
|
+
} else {
|
|
356
|
+
console.log("사용자를 찾을 수 없음");
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// select와 함께 사용하여 효율적인 조회
|
|
360
|
+
const profile = await updater
|
|
361
|
+
.collection("users")
|
|
362
|
+
.select("name", "avatar", "tier")
|
|
363
|
+
.where("username", "==", "johndoe")
|
|
364
|
+
.findOne();
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
### Dry Run 모드
|
|
368
|
+
|
|
369
|
+
```typescript
|
|
370
|
+
// 실제 변경 없이 작업 시뮬레이션
|
|
371
|
+
const simulation = await updater
|
|
372
|
+
.collection("users")
|
|
373
|
+
.where("status", "==", "inactive")
|
|
374
|
+
.update(
|
|
375
|
+
{ status: "archived" },
|
|
376
|
+
{ dryRun: true }
|
|
377
|
+
);
|
|
378
|
+
|
|
379
|
+
console.log(`${simulation.wouldAffect}개 문서가 영향을 받을 예정`);
|
|
380
|
+
console.log("샘플 ID:", simulation.sampleIds);
|
|
381
|
+
|
|
382
|
+
// 삭제에도 사용 가능
|
|
383
|
+
const deleteSimulation = await updater
|
|
384
|
+
.collection("logs")
|
|
385
|
+
.where("createdAt", "<", thirtyDaysAgo)
|
|
386
|
+
.delete({ dryRun: true });
|
|
387
|
+
|
|
388
|
+
console.log(`${deleteSimulation.wouldAffect}개 문서가 삭제될 예정`);
|
|
389
|
+
```
|
|
390
|
+
|
|
391
|
+
### 서브컬렉션
|
|
392
|
+
|
|
393
|
+
```typescript
|
|
394
|
+
// 특정 서브컬렉션 경로 쿼리
|
|
395
|
+
const result = await updater
|
|
396
|
+
.collection("users/user-123/orders")
|
|
397
|
+
.where("status", "==", "pending")
|
|
398
|
+
.update({ status: "cancelled" });
|
|
399
|
+
|
|
400
|
+
// 동적 경로 사용
|
|
401
|
+
const userId = "user-123";
|
|
402
|
+
await updater
|
|
403
|
+
.collection(`users/${userId}/notifications`)
|
|
404
|
+
.where("read", "==", false)
|
|
405
|
+
.delete();
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
### 컬렉션 그룹 쿼리
|
|
409
|
+
|
|
410
|
+
```typescript
|
|
411
|
+
// 모든 사용자의 "orders" 서브컬렉션을 한 번에 쿼리
|
|
412
|
+
const result = await updater
|
|
413
|
+
.collectionGroup("orders")
|
|
414
|
+
.where("status", "==", "pending")
|
|
415
|
+
.where("createdAt", "<", thirtyDaysAgo)
|
|
416
|
+
.update({ status: "expired" });
|
|
417
|
+
|
|
418
|
+
console.log(`${result.successCount}개 주문 업데이트 완료`);
|
|
419
|
+
|
|
420
|
+
// 참고: collectionGroup은 쿼리 필드에 대한 Firestore 인덱스가 필요합니다
|
|
288
421
|
```
|
|
289
422
|
|
|
290
423
|
> **참고:** 서로 다른 필드에 여러 `where()` 조건을 사용하거나, `where()`와 `orderBy()`를 다른 필드에 사용할 경우, Firestore에서 [복합 인덱스](https://firebase.google.com/docs/firestore/query-data/indexing)가 필요할 수 있습니다. `FAILED_PRECONDITION` 오류가 발생하면 오류 메시지의 링크를 통해 필요한 인덱스를 생성하세요.
|
package/README.md
CHANGED
|
@@ -14,7 +14,12 @@ 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
|
-
-
|
|
17
|
+
- Field selection - Use `select()` to load only needed fields (saves memory and costs)
|
|
18
|
+
- Find single document - Use `findOne()` for efficient single-document retrieval
|
|
19
|
+
- FieldValue support - Use `increment()`, `arrayUnion()`, `delete()`, `serverTimestamp()`, etc.
|
|
20
|
+
- Subcollection & Collection Group - Query subcollections or all collections with the same name
|
|
21
|
+
- Dry run mode - Simulate operations without making changes
|
|
22
|
+
- Count documents - Quickly count matching documents without loading them
|
|
18
23
|
- Log file generation - Optional detailed operation logs for auditing
|
|
19
24
|
|
|
20
25
|
## Installation
|
|
@@ -75,10 +80,14 @@ console.log(`Updated ${result.successCount} documents`);
|
|
|
75
80
|
|
|
76
81
|
| Method | Description | Returns |
|
|
77
82
|
|--------|-------------|---------|
|
|
78
|
-
| `collection(path)` | Select collection to operate on | `this` |
|
|
83
|
+
| `collection(path)` | Select collection to operate on (supports subcollection paths) | `this` |
|
|
84
|
+
| `collectionGroup(id)` | Query all collections with the same ID | `this` |
|
|
79
85
|
| `where(field, op, value)` | Add filter condition (chainable) | `this` |
|
|
80
86
|
| `orderBy(field, direction?)` | Add sorting (chainable) | `this` |
|
|
81
87
|
| `limit(count)` | Limit number of documents (chainable) | `this` |
|
|
88
|
+
| `select(...fields)` | Select specific fields to retrieve (chainable) | `this` |
|
|
89
|
+
| `count()` | Count matching documents | `CountResult` |
|
|
90
|
+
| `findOne()` | Find first matching document | `{ id, data } \| null` |
|
|
82
91
|
| `preview(data)` | Preview changes before update | `PreviewResult` |
|
|
83
92
|
| `update(data, options?)` | Update matching documents | `UpdateResult` |
|
|
84
93
|
| `create(docs, options?)` | Create new documents | `CreateResult` |
|
|
@@ -95,6 +104,7 @@ All write operations support an optional `options` parameter:
|
|
|
95
104
|
onProgress?: (progress: ProgressInfo) => void;
|
|
96
105
|
log?: LogOptions;
|
|
97
106
|
batchSize?: number; // For update/upsert/delete
|
|
107
|
+
dryRun?: boolean; // For update/upsert/delete - simulate without writing
|
|
98
108
|
}
|
|
99
109
|
|
|
100
110
|
// ProgressInfo
|
|
@@ -116,10 +126,15 @@ All write operations support an optional `options` parameter:
|
|
|
116
126
|
- When not set: All documents are loaded into memory at once (suitable for small collections)
|
|
117
127
|
- When set (e.g., `batchSize: 1000`): Documents are processed in batches using cursor pagination (suitable for large collections to prevent memory issues)
|
|
118
128
|
|
|
129
|
+
**dryRun option:**
|
|
130
|
+
- When `true`: Returns `DryRunResult` with `wouldAffect` count and `sampleIds` without making any changes
|
|
131
|
+
|
|
119
132
|
### Return Types
|
|
120
133
|
|
|
121
134
|
| Type | Fields |
|
|
122
135
|
|------|--------|
|
|
136
|
+
| `CountResult` | `count` |
|
|
137
|
+
| `DryRunResult` | `wouldAffect`, `sampleIds[]`, `operation` |
|
|
123
138
|
| `PreviewResult` | `affectedCount`, `samples[]`, `affectedFields[]` |
|
|
124
139
|
| `UpdateResult` | `successCount`, `failureCount`, `totalCount`, `failedDocIds?`, `logFilePath?` |
|
|
125
140
|
| `CreateResult` | `successCount`, `failureCount`, `totalCount`, `createdIds[]`, `failedDocIds?`, `logFilePath?` |
|
|
@@ -284,6 +299,124 @@ await updater
|
|
|
284
299
|
.collection("users")
|
|
285
300
|
.where("status", "==", "active")
|
|
286
301
|
.update({ lastSeen: FieldValue.serverTimestamp() });
|
|
302
|
+
|
|
303
|
+
// Delete a field
|
|
304
|
+
await updater
|
|
305
|
+
.collection("users")
|
|
306
|
+
.where("status", "==", "inactive")
|
|
307
|
+
.update({ temporaryData: FieldValue.delete() });
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Count Documents
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
// Quickly count matching documents without loading them
|
|
314
|
+
const result = await updater
|
|
315
|
+
.collection("users")
|
|
316
|
+
.where("status", "==", "inactive")
|
|
317
|
+
.count();
|
|
318
|
+
|
|
319
|
+
console.log(`Found ${result.count} inactive users`);
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
### Select Specific Fields
|
|
323
|
+
|
|
324
|
+
```typescript
|
|
325
|
+
// Only load name and email fields (reduces memory and read costs)
|
|
326
|
+
const result = await updater
|
|
327
|
+
.collection("users")
|
|
328
|
+
.select("name", "email")
|
|
329
|
+
.where("status", "==", "active")
|
|
330
|
+
.findOne();
|
|
331
|
+
|
|
332
|
+
console.log(result?.data); // Only contains { name, email }
|
|
333
|
+
|
|
334
|
+
// Works with all operations - documents will only have selected fields
|
|
335
|
+
const emails = await updater
|
|
336
|
+
.collection("users")
|
|
337
|
+
.select("email")
|
|
338
|
+
.where("verified", "==", true)
|
|
339
|
+
.getFields("email");
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
### Find Single Document
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
// Find first matching document
|
|
346
|
+
const user = await updater
|
|
347
|
+
.collection("users")
|
|
348
|
+
.where("email", "==", "user@example.com")
|
|
349
|
+
.findOne();
|
|
350
|
+
|
|
351
|
+
if (user) {
|
|
352
|
+
console.log("Found user:", user.id);
|
|
353
|
+
console.log("User data:", user.data);
|
|
354
|
+
} else {
|
|
355
|
+
console.log("User not found");
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Combine with select for efficient lookup
|
|
359
|
+
const profile = await updater
|
|
360
|
+
.collection("users")
|
|
361
|
+
.select("name", "avatar", "tier")
|
|
362
|
+
.where("username", "==", "johndoe")
|
|
363
|
+
.findOne();
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
### Dry Run Mode
|
|
367
|
+
|
|
368
|
+
```typescript
|
|
369
|
+
// Simulate an operation without making any changes
|
|
370
|
+
const simulation = await updater
|
|
371
|
+
.collection("users")
|
|
372
|
+
.where("status", "==", "inactive")
|
|
373
|
+
.update(
|
|
374
|
+
{ status: "archived" },
|
|
375
|
+
{ dryRun: true }
|
|
376
|
+
);
|
|
377
|
+
|
|
378
|
+
console.log(`Would affect ${simulation.wouldAffect} documents`);
|
|
379
|
+
console.log("Sample IDs:", simulation.sampleIds);
|
|
380
|
+
|
|
381
|
+
// Also works with delete
|
|
382
|
+
const deleteSimulation = await updater
|
|
383
|
+
.collection("logs")
|
|
384
|
+
.where("createdAt", "<", thirtyDaysAgo)
|
|
385
|
+
.delete({ dryRun: true });
|
|
386
|
+
|
|
387
|
+
console.log(`Would delete ${deleteSimulation.wouldAffect} documents`);
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
### Subcollections
|
|
391
|
+
|
|
392
|
+
```typescript
|
|
393
|
+
// Query a specific subcollection path
|
|
394
|
+
const result = await updater
|
|
395
|
+
.collection("users/user-123/orders")
|
|
396
|
+
.where("status", "==", "pending")
|
|
397
|
+
.update({ status: "cancelled" });
|
|
398
|
+
|
|
399
|
+
// Or use dynamic paths
|
|
400
|
+
const userId = "user-123";
|
|
401
|
+
await updater
|
|
402
|
+
.collection(`users/${userId}/notifications`)
|
|
403
|
+
.where("read", "==", false)
|
|
404
|
+
.delete();
|
|
405
|
+
```
|
|
406
|
+
|
|
407
|
+
### Collection Group Queries
|
|
408
|
+
|
|
409
|
+
```typescript
|
|
410
|
+
// Query ALL "orders" subcollections across all users
|
|
411
|
+
const result = await updater
|
|
412
|
+
.collectionGroup("orders")
|
|
413
|
+
.where("status", "==", "pending")
|
|
414
|
+
.where("createdAt", "<", thirtyDaysAgo)
|
|
415
|
+
.update({ status: "expired" });
|
|
416
|
+
|
|
417
|
+
console.log(`Updated ${result.successCount} orders across all users`);
|
|
418
|
+
|
|
419
|
+
// Note: collectionGroup requires a Firestore index on the queried fields
|
|
287
420
|
```
|
|
288
421
|
|
|
289
422
|
### Error Handling
|
package/dist/index.d.mts
CHANGED
|
@@ -12,6 +12,9 @@ interface ProgressInfo {
|
|
|
12
12
|
current: number;
|
|
13
13
|
total: number;
|
|
14
14
|
percentage: number;
|
|
15
|
+
elapsedTime: number;
|
|
16
|
+
docsPerSecond: number;
|
|
17
|
+
eta: number;
|
|
15
18
|
}
|
|
16
19
|
/**
|
|
17
20
|
* Options for update operations
|
|
@@ -32,6 +35,19 @@ interface UpdateOptions {
|
|
|
32
35
|
* When not set, all documents are loaded at once
|
|
33
36
|
*/
|
|
34
37
|
batchSize?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Dry run mode - simulate the operation without actually writing
|
|
40
|
+
* Returns what would happen without making any changes
|
|
41
|
+
*/
|
|
42
|
+
dryRun?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Maximum number of retry attempts for failed operations (default: 0)
|
|
45
|
+
*/
|
|
46
|
+
retries?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Delay between retry attempts in milliseconds (default: 1000)
|
|
49
|
+
*/
|
|
50
|
+
retryDelay?: number;
|
|
35
51
|
}
|
|
36
52
|
/**
|
|
37
53
|
* Result of batch update operation
|
|
@@ -130,6 +146,19 @@ interface UpsertOptions {
|
|
|
130
146
|
* When not set, all documents are loaded at once
|
|
131
147
|
*/
|
|
132
148
|
batchSize?: number;
|
|
149
|
+
/**
|
|
150
|
+
* Dry run mode - simulate the operation without actually writing
|
|
151
|
+
* Returns what would happen without making any changes
|
|
152
|
+
*/
|
|
153
|
+
dryRun?: boolean;
|
|
154
|
+
/**
|
|
155
|
+
* Maximum number of retry attempts for failed operations (default: 0)
|
|
156
|
+
*/
|
|
157
|
+
retries?: number;
|
|
158
|
+
/**
|
|
159
|
+
* Delay between retry attempts in milliseconds (default: 1000)
|
|
160
|
+
*/
|
|
161
|
+
retryDelay?: number;
|
|
133
162
|
}
|
|
134
163
|
/**
|
|
135
164
|
* Result of batch upsert operation
|
|
@@ -159,6 +188,19 @@ interface DeleteOptions {
|
|
|
159
188
|
* When not set, all documents are loaded at once
|
|
160
189
|
*/
|
|
161
190
|
batchSize?: number;
|
|
191
|
+
/**
|
|
192
|
+
* Dry run mode - simulate the operation without actually writing
|
|
193
|
+
* Returns what would happen without making any changes
|
|
194
|
+
*/
|
|
195
|
+
dryRun?: boolean;
|
|
196
|
+
/**
|
|
197
|
+
* Maximum number of retry attempts for failed operations (default: 0)
|
|
198
|
+
*/
|
|
199
|
+
retries?: number;
|
|
200
|
+
/**
|
|
201
|
+
* Delay between retry attempts in milliseconds (default: 1000)
|
|
202
|
+
*/
|
|
203
|
+
retryDelay?: number;
|
|
162
204
|
}
|
|
163
205
|
/**
|
|
164
206
|
* Result of batch delete operation
|
|
@@ -170,6 +212,20 @@ interface DeleteResult {
|
|
|
170
212
|
deletedIds: string[];
|
|
171
213
|
failedDocIds?: string[];
|
|
172
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Result of count operation
|
|
217
|
+
*/
|
|
218
|
+
interface CountResult {
|
|
219
|
+
count: number;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Result of dry run operation
|
|
223
|
+
*/
|
|
224
|
+
interface DryRunResult {
|
|
225
|
+
wouldAffect: number;
|
|
226
|
+
sampleIds: string[];
|
|
227
|
+
operation: "update" | "upsert" | "delete";
|
|
228
|
+
}
|
|
173
229
|
/**
|
|
174
230
|
* Log options for batch operations
|
|
175
231
|
*/
|
|
@@ -215,9 +271,11 @@ interface OperationLog {
|
|
|
215
271
|
declare class BatchUpdater {
|
|
216
272
|
private firestore;
|
|
217
273
|
private collectionPath?;
|
|
274
|
+
private isCollectionGroup;
|
|
218
275
|
private conditions;
|
|
219
276
|
private orderByConditions;
|
|
220
277
|
private limitCount?;
|
|
278
|
+
private selectedFields?;
|
|
221
279
|
/**
|
|
222
280
|
* Create a new BatchUpdater instance
|
|
223
281
|
* @param firestore - Initialized Firestore instance from firebase-admin
|
|
@@ -225,10 +283,17 @@ declare class BatchUpdater {
|
|
|
225
283
|
constructor(firestore: Firestore);
|
|
226
284
|
/**
|
|
227
285
|
* Select a collection to operate on
|
|
286
|
+
* Supports subcollection paths like "users/userId/orders"
|
|
228
287
|
* @param path - Collection path
|
|
229
288
|
* @returns This instance for chaining
|
|
230
289
|
*/
|
|
231
290
|
collection(path: string): this;
|
|
291
|
+
/**
|
|
292
|
+
* Select a collection group to operate on (queries across all subcollections with the same name)
|
|
293
|
+
* @param collectionId - Collection ID (not a path, just the collection name)
|
|
294
|
+
* @returns This instance for chaining
|
|
295
|
+
*/
|
|
296
|
+
collectionGroup(collectionId: string): this;
|
|
232
297
|
/**
|
|
233
298
|
* Add a where condition to filter documents
|
|
234
299
|
* @param field - Field path
|
|
@@ -250,6 +315,25 @@ declare class BatchUpdater {
|
|
|
250
315
|
* @returns This instance for chaining
|
|
251
316
|
*/
|
|
252
317
|
limit(count: number): this;
|
|
318
|
+
/**
|
|
319
|
+
* Select specific fields to retrieve (reduces memory usage and read costs)
|
|
320
|
+
* @param fields - Field paths to retrieve
|
|
321
|
+
* @returns This instance for chaining
|
|
322
|
+
*/
|
|
323
|
+
select(...fields: string[]): this;
|
|
324
|
+
/**
|
|
325
|
+
* Count documents matching the query conditions
|
|
326
|
+
* @returns Count result with number of matching documents
|
|
327
|
+
*/
|
|
328
|
+
count(): Promise<CountResult>;
|
|
329
|
+
/**
|
|
330
|
+
* Find the first document matching the query conditions
|
|
331
|
+
* @returns First matching document with id and data, or null if not found
|
|
332
|
+
*/
|
|
333
|
+
findOne(): Promise<{
|
|
334
|
+
id: string;
|
|
335
|
+
data: Record<string, any>;
|
|
336
|
+
} | null>;
|
|
253
337
|
/**
|
|
254
338
|
* Preview changes before executing update
|
|
255
339
|
* @param updateData - Data to update
|
|
@@ -259,12 +343,12 @@ declare class BatchUpdater {
|
|
|
259
343
|
/**
|
|
260
344
|
* Execute batch update operation
|
|
261
345
|
* @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
|
|
346
|
+
* @param options - Update options (e.g., progress callback, log options, batchSize for pagination, dryRun)
|
|
347
|
+
* @returns Update result with success/failure counts and optional log file path, or DryRunResult if dryRun is true
|
|
264
348
|
*/
|
|
265
|
-
update(updateData: Record<string, any>, options?: UpdateOptions): Promise<UpdateResult & {
|
|
349
|
+
update(updateData: Record<string, any>, options?: UpdateOptions): Promise<(UpdateResult & {
|
|
266
350
|
logFilePath?: string;
|
|
267
|
-
}>;
|
|
351
|
+
}) | DryRunResult>;
|
|
268
352
|
/**
|
|
269
353
|
* Get specific field values from matching documents
|
|
270
354
|
* @param fieldPath - Field path to retrieve
|
|
@@ -273,6 +357,7 @@ declare class BatchUpdater {
|
|
|
273
357
|
getFields(fieldPath: string): Promise<FieldValueResult[]>;
|
|
274
358
|
/**
|
|
275
359
|
* Create multiple documents in batch
|
|
360
|
+
* Note: This method does not work with collectionGroup()
|
|
276
361
|
* @param documents - Array of documents to create
|
|
277
362
|
* @param options - Create options (e.g., progress callback, log options)
|
|
278
363
|
* @returns Create result with success/failure counts, created IDs, and optional log file path
|
|
@@ -284,20 +369,20 @@ declare class BatchUpdater {
|
|
|
284
369
|
* Upsert documents matching query conditions
|
|
285
370
|
* Updates existing documents or creates them if they don't exist
|
|
286
371
|
* @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
|
|
372
|
+
* @param options - Upsert options (e.g., progress callback, log options, batchSize for pagination, dryRun)
|
|
373
|
+
* @returns Upsert result with success/failure counts and optional log file path, or DryRunResult if dryRun is true
|
|
289
374
|
*/
|
|
290
|
-
upsert(updateData: Record<string, any>, options?: UpsertOptions): Promise<UpsertResult & {
|
|
375
|
+
upsert(updateData: Record<string, any>, options?: UpsertOptions): Promise<(UpsertResult & {
|
|
291
376
|
logFilePath?: string;
|
|
292
|
-
}>;
|
|
377
|
+
}) | DryRunResult>;
|
|
293
378
|
/**
|
|
294
379
|
* 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
|
|
380
|
+
* @param options - Delete options (e.g., progress callback, log options, batchSize for pagination, dryRun)
|
|
381
|
+
* @returns Delete result with success/failure counts, deleted IDs, and optional log file path, or DryRunResult if dryRun is true
|
|
297
382
|
*/
|
|
298
|
-
delete(options?: DeleteOptions): Promise<DeleteResult & {
|
|
383
|
+
delete(options?: DeleteOptions): Promise<(DeleteResult & {
|
|
299
384
|
logFilePath?: string;
|
|
300
|
-
}>;
|
|
385
|
+
}) | DryRunResult>;
|
|
301
386
|
/**
|
|
302
387
|
* Validate that collection is set
|
|
303
388
|
* @private
|
|
@@ -341,12 +426,13 @@ declare function createLogCollector(operation: "update" | "create" | "upsert" |
|
|
|
341
426
|
*/
|
|
342
427
|
|
|
343
428
|
/**
|
|
344
|
-
* Calculate progress information
|
|
429
|
+
* Calculate progress information with timing details
|
|
345
430
|
* @param current - Number of documents processed so far
|
|
346
431
|
* @param total - Total number of documents to process
|
|
347
|
-
* @
|
|
432
|
+
* @param startTime - Start time of the operation (from Date.now())
|
|
433
|
+
* @returns Progress information with percentage, timing, and ETA
|
|
348
434
|
*/
|
|
349
|
-
declare function calculateProgress(current: number, total: number): ProgressInfo;
|
|
435
|
+
declare function calculateProgress(current: number, total: number, startTime?: number): ProgressInfo;
|
|
350
436
|
/**
|
|
351
437
|
* Extract field names from update data
|
|
352
438
|
* @param updateData - Data to be updated
|
|
@@ -374,4 +460,4 @@ declare function isValidUpdateData(value: any): value is Record<string, any>;
|
|
|
374
460
|
*/
|
|
375
461
|
declare function formatError(error: unknown, context?: string): string;
|
|
376
462
|
|
|
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 };
|
|
463
|
+
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
|
@@ -12,6 +12,9 @@ interface ProgressInfo {
|
|
|
12
12
|
current: number;
|
|
13
13
|
total: number;
|
|
14
14
|
percentage: number;
|
|
15
|
+
elapsedTime: number;
|
|
16
|
+
docsPerSecond: number;
|
|
17
|
+
eta: number;
|
|
15
18
|
}
|
|
16
19
|
/**
|
|
17
20
|
* Options for update operations
|
|
@@ -32,6 +35,19 @@ interface UpdateOptions {
|
|
|
32
35
|
* When not set, all documents are loaded at once
|
|
33
36
|
*/
|
|
34
37
|
batchSize?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Dry run mode - simulate the operation without actually writing
|
|
40
|
+
* Returns what would happen without making any changes
|
|
41
|
+
*/
|
|
42
|
+
dryRun?: boolean;
|
|
43
|
+
/**
|
|
44
|
+
* Maximum number of retry attempts for failed operations (default: 0)
|
|
45
|
+
*/
|
|
46
|
+
retries?: number;
|
|
47
|
+
/**
|
|
48
|
+
* Delay between retry attempts in milliseconds (default: 1000)
|
|
49
|
+
*/
|
|
50
|
+
retryDelay?: number;
|
|
35
51
|
}
|
|
36
52
|
/**
|
|
37
53
|
* Result of batch update operation
|
|
@@ -130,6 +146,19 @@ interface UpsertOptions {
|
|
|
130
146
|
* When not set, all documents are loaded at once
|
|
131
147
|
*/
|
|
132
148
|
batchSize?: number;
|
|
149
|
+
/**
|
|
150
|
+
* Dry run mode - simulate the operation without actually writing
|
|
151
|
+
* Returns what would happen without making any changes
|
|
152
|
+
*/
|
|
153
|
+
dryRun?: boolean;
|
|
154
|
+
/**
|
|
155
|
+
* Maximum number of retry attempts for failed operations (default: 0)
|
|
156
|
+
*/
|
|
157
|
+
retries?: number;
|
|
158
|
+
/**
|
|
159
|
+
* Delay between retry attempts in milliseconds (default: 1000)
|
|
160
|
+
*/
|
|
161
|
+
retryDelay?: number;
|
|
133
162
|
}
|
|
134
163
|
/**
|
|
135
164
|
* Result of batch upsert operation
|
|
@@ -159,6 +188,19 @@ interface DeleteOptions {
|
|
|
159
188
|
* When not set, all documents are loaded at once
|
|
160
189
|
*/
|
|
161
190
|
batchSize?: number;
|
|
191
|
+
/**
|
|
192
|
+
* Dry run mode - simulate the operation without actually writing
|
|
193
|
+
* Returns what would happen without making any changes
|
|
194
|
+
*/
|
|
195
|
+
dryRun?: boolean;
|
|
196
|
+
/**
|
|
197
|
+
* Maximum number of retry attempts for failed operations (default: 0)
|
|
198
|
+
*/
|
|
199
|
+
retries?: number;
|
|
200
|
+
/**
|
|
201
|
+
* Delay between retry attempts in milliseconds (default: 1000)
|
|
202
|
+
*/
|
|
203
|
+
retryDelay?: number;
|
|
162
204
|
}
|
|
163
205
|
/**
|
|
164
206
|
* Result of batch delete operation
|
|
@@ -170,6 +212,20 @@ interface DeleteResult {
|
|
|
170
212
|
deletedIds: string[];
|
|
171
213
|
failedDocIds?: string[];
|
|
172
214
|
}
|
|
215
|
+
/**
|
|
216
|
+
* Result of count operation
|
|
217
|
+
*/
|
|
218
|
+
interface CountResult {
|
|
219
|
+
count: number;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Result of dry run operation
|
|
223
|
+
*/
|
|
224
|
+
interface DryRunResult {
|
|
225
|
+
wouldAffect: number;
|
|
226
|
+
sampleIds: string[];
|
|
227
|
+
operation: "update" | "upsert" | "delete";
|
|
228
|
+
}
|
|
173
229
|
/**
|
|
174
230
|
* Log options for batch operations
|
|
175
231
|
*/
|
|
@@ -215,9 +271,11 @@ interface OperationLog {
|
|
|
215
271
|
declare class BatchUpdater {
|
|
216
272
|
private firestore;
|
|
217
273
|
private collectionPath?;
|
|
274
|
+
private isCollectionGroup;
|
|
218
275
|
private conditions;
|
|
219
276
|
private orderByConditions;
|
|
220
277
|
private limitCount?;
|
|
278
|
+
private selectedFields?;
|
|
221
279
|
/**
|
|
222
280
|
* Create a new BatchUpdater instance
|
|
223
281
|
* @param firestore - Initialized Firestore instance from firebase-admin
|
|
@@ -225,10 +283,17 @@ declare class BatchUpdater {
|
|
|
225
283
|
constructor(firestore: Firestore);
|
|
226
284
|
/**
|
|
227
285
|
* Select a collection to operate on
|
|
286
|
+
* Supports subcollection paths like "users/userId/orders"
|
|
228
287
|
* @param path - Collection path
|
|
229
288
|
* @returns This instance for chaining
|
|
230
289
|
*/
|
|
231
290
|
collection(path: string): this;
|
|
291
|
+
/**
|
|
292
|
+
* Select a collection group to operate on (queries across all subcollections with the same name)
|
|
293
|
+
* @param collectionId - Collection ID (not a path, just the collection name)
|
|
294
|
+
* @returns This instance for chaining
|
|
295
|
+
*/
|
|
296
|
+
collectionGroup(collectionId: string): this;
|
|
232
297
|
/**
|
|
233
298
|
* Add a where condition to filter documents
|
|
234
299
|
* @param field - Field path
|
|
@@ -250,6 +315,25 @@ declare class BatchUpdater {
|
|
|
250
315
|
* @returns This instance for chaining
|
|
251
316
|
*/
|
|
252
317
|
limit(count: number): this;
|
|
318
|
+
/**
|
|
319
|
+
* Select specific fields to retrieve (reduces memory usage and read costs)
|
|
320
|
+
* @param fields - Field paths to retrieve
|
|
321
|
+
* @returns This instance for chaining
|
|
322
|
+
*/
|
|
323
|
+
select(...fields: string[]): this;
|
|
324
|
+
/**
|
|
325
|
+
* Count documents matching the query conditions
|
|
326
|
+
* @returns Count result with number of matching documents
|
|
327
|
+
*/
|
|
328
|
+
count(): Promise<CountResult>;
|
|
329
|
+
/**
|
|
330
|
+
* Find the first document matching the query conditions
|
|
331
|
+
* @returns First matching document with id and data, or null if not found
|
|
332
|
+
*/
|
|
333
|
+
findOne(): Promise<{
|
|
334
|
+
id: string;
|
|
335
|
+
data: Record<string, any>;
|
|
336
|
+
} | null>;
|
|
253
337
|
/**
|
|
254
338
|
* Preview changes before executing update
|
|
255
339
|
* @param updateData - Data to update
|
|
@@ -259,12 +343,12 @@ declare class BatchUpdater {
|
|
|
259
343
|
/**
|
|
260
344
|
* Execute batch update operation
|
|
261
345
|
* @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
|
|
346
|
+
* @param options - Update options (e.g., progress callback, log options, batchSize for pagination, dryRun)
|
|
347
|
+
* @returns Update result with success/failure counts and optional log file path, or DryRunResult if dryRun is true
|
|
264
348
|
*/
|
|
265
|
-
update(updateData: Record<string, any>, options?: UpdateOptions): Promise<UpdateResult & {
|
|
349
|
+
update(updateData: Record<string, any>, options?: UpdateOptions): Promise<(UpdateResult & {
|
|
266
350
|
logFilePath?: string;
|
|
267
|
-
}>;
|
|
351
|
+
}) | DryRunResult>;
|
|
268
352
|
/**
|
|
269
353
|
* Get specific field values from matching documents
|
|
270
354
|
* @param fieldPath - Field path to retrieve
|
|
@@ -273,6 +357,7 @@ declare class BatchUpdater {
|
|
|
273
357
|
getFields(fieldPath: string): Promise<FieldValueResult[]>;
|
|
274
358
|
/**
|
|
275
359
|
* Create multiple documents in batch
|
|
360
|
+
* Note: This method does not work with collectionGroup()
|
|
276
361
|
* @param documents - Array of documents to create
|
|
277
362
|
* @param options - Create options (e.g., progress callback, log options)
|
|
278
363
|
* @returns Create result with success/failure counts, created IDs, and optional log file path
|
|
@@ -284,20 +369,20 @@ declare class BatchUpdater {
|
|
|
284
369
|
* Upsert documents matching query conditions
|
|
285
370
|
* Updates existing documents or creates them if they don't exist
|
|
286
371
|
* @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
|
|
372
|
+
* @param options - Upsert options (e.g., progress callback, log options, batchSize for pagination, dryRun)
|
|
373
|
+
* @returns Upsert result with success/failure counts and optional log file path, or DryRunResult if dryRun is true
|
|
289
374
|
*/
|
|
290
|
-
upsert(updateData: Record<string, any>, options?: UpsertOptions): Promise<UpsertResult & {
|
|
375
|
+
upsert(updateData: Record<string, any>, options?: UpsertOptions): Promise<(UpsertResult & {
|
|
291
376
|
logFilePath?: string;
|
|
292
|
-
}>;
|
|
377
|
+
}) | DryRunResult>;
|
|
293
378
|
/**
|
|
294
379
|
* 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
|
|
380
|
+
* @param options - Delete options (e.g., progress callback, log options, batchSize for pagination, dryRun)
|
|
381
|
+
* @returns Delete result with success/failure counts, deleted IDs, and optional log file path, or DryRunResult if dryRun is true
|
|
297
382
|
*/
|
|
298
|
-
delete(options?: DeleteOptions): Promise<DeleteResult & {
|
|
383
|
+
delete(options?: DeleteOptions): Promise<(DeleteResult & {
|
|
299
384
|
logFilePath?: string;
|
|
300
|
-
}>;
|
|
385
|
+
}) | DryRunResult>;
|
|
301
386
|
/**
|
|
302
387
|
* Validate that collection is set
|
|
303
388
|
* @private
|
|
@@ -341,12 +426,13 @@ declare function createLogCollector(operation: "update" | "create" | "upsert" |
|
|
|
341
426
|
*/
|
|
342
427
|
|
|
343
428
|
/**
|
|
344
|
-
* Calculate progress information
|
|
429
|
+
* Calculate progress information with timing details
|
|
345
430
|
* @param current - Number of documents processed so far
|
|
346
431
|
* @param total - Total number of documents to process
|
|
347
|
-
* @
|
|
432
|
+
* @param startTime - Start time of the operation (from Date.now())
|
|
433
|
+
* @returns Progress information with percentage, timing, and ETA
|
|
348
434
|
*/
|
|
349
|
-
declare function calculateProgress(current: number, total: number): ProgressInfo;
|
|
435
|
+
declare function calculateProgress(current: number, total: number, startTime?: number): ProgressInfo;
|
|
350
436
|
/**
|
|
351
437
|
* Extract field names from update data
|
|
352
438
|
* @param updateData - Data to be updated
|
|
@@ -374,4 +460,4 @@ declare function isValidUpdateData(value: any): value is Record<string, any>;
|
|
|
374
460
|
*/
|
|
375
461
|
declare function formatError(error: unknown, context?: string): string;
|
|
376
462
|
|
|
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 };
|
|
463
|
+
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
|
@@ -164,12 +164,20 @@ function createLogCollector(operation, collection, conditions, updateData) {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
// src/utils/index.ts
|
|
167
|
-
function calculateProgress(current, total) {
|
|
167
|
+
function calculateProgress(current, total, startTime) {
|
|
168
168
|
const percentage = total === 0 ? 0 : Math.round(current / total * 100);
|
|
169
|
+
const now = Date.now();
|
|
170
|
+
const elapsedTime = startTime ? now - startTime : 0;
|
|
171
|
+
const docsPerSecond = elapsedTime > 0 ? Math.round(current / elapsedTime * 1e3 * 100) / 100 : 0;
|
|
172
|
+
const remaining = total - current;
|
|
173
|
+
const eta = docsPerSecond > 0 ? Math.round(remaining / docsPerSecond * 100) / 100 : 0;
|
|
169
174
|
return {
|
|
170
175
|
current,
|
|
171
176
|
total,
|
|
172
|
-
percentage
|
|
177
|
+
percentage,
|
|
178
|
+
elapsedTime,
|
|
179
|
+
docsPerSecond,
|
|
180
|
+
eta
|
|
173
181
|
};
|
|
174
182
|
}
|
|
175
183
|
function getAffectedFields(updateData) {
|
|
@@ -196,20 +204,38 @@ var BatchUpdater = class {
|
|
|
196
204
|
* @param firestore - Initialized Firestore instance from firebase-admin
|
|
197
205
|
*/
|
|
198
206
|
constructor(firestore) {
|
|
207
|
+
this.isCollectionGroup = false;
|
|
199
208
|
this.conditions = [];
|
|
200
209
|
this.orderByConditions = [];
|
|
201
210
|
this.firestore = firestore;
|
|
202
211
|
}
|
|
203
212
|
/**
|
|
204
213
|
* Select a collection to operate on
|
|
214
|
+
* Supports subcollection paths like "users/userId/orders"
|
|
205
215
|
* @param path - Collection path
|
|
206
216
|
* @returns This instance for chaining
|
|
207
217
|
*/
|
|
208
218
|
collection(path2) {
|
|
209
219
|
this.collectionPath = path2;
|
|
220
|
+
this.isCollectionGroup = false;
|
|
210
221
|
this.conditions = [];
|
|
211
222
|
this.orderByConditions = [];
|
|
212
223
|
this.limitCount = void 0;
|
|
224
|
+
this.selectedFields = void 0;
|
|
225
|
+
return this;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Select a collection group to operate on (queries across all subcollections with the same name)
|
|
229
|
+
* @param collectionId - Collection ID (not a path, just the collection name)
|
|
230
|
+
* @returns This instance for chaining
|
|
231
|
+
*/
|
|
232
|
+
collectionGroup(collectionId) {
|
|
233
|
+
this.collectionPath = collectionId;
|
|
234
|
+
this.isCollectionGroup = true;
|
|
235
|
+
this.conditions = [];
|
|
236
|
+
this.orderByConditions = [];
|
|
237
|
+
this.limitCount = void 0;
|
|
238
|
+
this.selectedFields = void 0;
|
|
213
239
|
return this;
|
|
214
240
|
}
|
|
215
241
|
/**
|
|
@@ -242,6 +268,44 @@ var BatchUpdater = class {
|
|
|
242
268
|
this.limitCount = count;
|
|
243
269
|
return this;
|
|
244
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Select specific fields to retrieve (reduces memory usage and read costs)
|
|
273
|
+
* @param fields - Field paths to retrieve
|
|
274
|
+
* @returns This instance for chaining
|
|
275
|
+
*/
|
|
276
|
+
select(...fields) {
|
|
277
|
+
this.selectedFields = fields;
|
|
278
|
+
return this;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Count documents matching the query conditions
|
|
282
|
+
* @returns Count result with number of matching documents
|
|
283
|
+
*/
|
|
284
|
+
async count() {
|
|
285
|
+
this.validateSetup();
|
|
286
|
+
const query = this.buildQuery();
|
|
287
|
+
const snapshot = await query.count().get();
|
|
288
|
+
return {
|
|
289
|
+
count: snapshot.data().count
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* Find the first document matching the query conditions
|
|
294
|
+
* @returns First matching document with id and data, or null if not found
|
|
295
|
+
*/
|
|
296
|
+
async findOne() {
|
|
297
|
+
this.validateSetup();
|
|
298
|
+
const query = this.buildQuery().limit(1);
|
|
299
|
+
const snapshot = await query.get();
|
|
300
|
+
if (snapshot.empty) {
|
|
301
|
+
return null;
|
|
302
|
+
}
|
|
303
|
+
const doc = snapshot.docs[0];
|
|
304
|
+
return {
|
|
305
|
+
id: doc.id,
|
|
306
|
+
data: doc.data()
|
|
307
|
+
};
|
|
308
|
+
}
|
|
245
309
|
/**
|
|
246
310
|
* Preview changes before executing update
|
|
247
311
|
* @param updateData - Data to update
|
|
@@ -276,14 +340,24 @@ var BatchUpdater = class {
|
|
|
276
340
|
/**
|
|
277
341
|
* Execute batch update operation
|
|
278
342
|
* @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
|
|
343
|
+
* @param options - Update options (e.g., progress callback, log options, batchSize for pagination, dryRun)
|
|
344
|
+
* @returns Update result with success/failure counts and optional log file path, or DryRunResult if dryRun is true
|
|
281
345
|
*/
|
|
282
346
|
async update(updateData, options = {}) {
|
|
283
347
|
this.validateSetup();
|
|
284
348
|
if (!isValidUpdateData(updateData)) {
|
|
285
349
|
throw new Error("Update data must be a non-empty object");
|
|
286
350
|
}
|
|
351
|
+
if (options.dryRun) {
|
|
352
|
+
const query = this.buildQuery();
|
|
353
|
+
const snapshot = await query.limit(10).get();
|
|
354
|
+
const countSnapshot = await this.buildQuery().count().get();
|
|
355
|
+
return {
|
|
356
|
+
wouldAffect: countSnapshot.data().count,
|
|
357
|
+
sampleIds: snapshot.docs.map((doc) => doc.id),
|
|
358
|
+
operation: "update"
|
|
359
|
+
};
|
|
360
|
+
}
|
|
287
361
|
const logCollector = options.log?.enabled ? createLogCollector("update", this.collectionPath, this.conditions, updateData) : null;
|
|
288
362
|
let successCount = 0;
|
|
289
363
|
let failureCount = 0;
|
|
@@ -432,12 +506,16 @@ var BatchUpdater = class {
|
|
|
432
506
|
}
|
|
433
507
|
/**
|
|
434
508
|
* Create multiple documents in batch
|
|
509
|
+
* Note: This method does not work with collectionGroup()
|
|
435
510
|
* @param documents - Array of documents to create
|
|
436
511
|
* @param options - Create options (e.g., progress callback, log options)
|
|
437
512
|
* @returns Create result with success/failure counts, created IDs, and optional log file path
|
|
438
513
|
*/
|
|
439
514
|
async create(documents, options = {}) {
|
|
440
515
|
this.validateSetup();
|
|
516
|
+
if (this.isCollectionGroup) {
|
|
517
|
+
throw new Error("create() cannot be used with collectionGroup(). Use collection() with a specific path instead.");
|
|
518
|
+
}
|
|
441
519
|
if (!Array.isArray(documents) || documents.length === 0) {
|
|
442
520
|
throw new Error("Documents array must be non-empty");
|
|
443
521
|
}
|
|
@@ -498,14 +576,24 @@ var BatchUpdater = class {
|
|
|
498
576
|
* Upsert documents matching query conditions
|
|
499
577
|
* Updates existing documents or creates them if they don't exist
|
|
500
578
|
* @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
|
|
579
|
+
* @param options - Upsert options (e.g., progress callback, log options, batchSize for pagination, dryRun)
|
|
580
|
+
* @returns Upsert result with success/failure counts and optional log file path, or DryRunResult if dryRun is true
|
|
503
581
|
*/
|
|
504
582
|
async upsert(updateData, options = {}) {
|
|
505
583
|
this.validateSetup();
|
|
506
584
|
if (!isValidUpdateData(updateData)) {
|
|
507
585
|
throw new Error("Update data must be a non-empty object");
|
|
508
586
|
}
|
|
587
|
+
if (options.dryRun) {
|
|
588
|
+
const query = this.buildQuery();
|
|
589
|
+
const snapshot = await query.limit(10).get();
|
|
590
|
+
const countSnapshot = await this.buildQuery().count().get();
|
|
591
|
+
return {
|
|
592
|
+
wouldAffect: countSnapshot.data().count,
|
|
593
|
+
sampleIds: snapshot.docs.map((doc) => doc.id),
|
|
594
|
+
operation: "upsert"
|
|
595
|
+
};
|
|
596
|
+
}
|
|
509
597
|
const logCollector = options.log?.enabled ? createLogCollector("upsert", this.collectionPath, this.conditions, updateData) : null;
|
|
510
598
|
let successCount = 0;
|
|
511
599
|
let failureCount = 0;
|
|
@@ -634,11 +722,21 @@ var BatchUpdater = class {
|
|
|
634
722
|
}
|
|
635
723
|
/**
|
|
636
724
|
* 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
|
|
725
|
+
* @param options - Delete options (e.g., progress callback, log options, batchSize for pagination, dryRun)
|
|
726
|
+
* @returns Delete result with success/failure counts, deleted IDs, and optional log file path, or DryRunResult if dryRun is true
|
|
639
727
|
*/
|
|
640
728
|
async delete(options = {}) {
|
|
641
729
|
this.validateSetup();
|
|
730
|
+
if (options.dryRun) {
|
|
731
|
+
const query = this.buildQuery();
|
|
732
|
+
const snapshot = await query.limit(10).get();
|
|
733
|
+
const countSnapshot = await this.buildQuery().count().get();
|
|
734
|
+
return {
|
|
735
|
+
wouldAffect: countSnapshot.data().count,
|
|
736
|
+
sampleIds: snapshot.docs.map((doc) => doc.id),
|
|
737
|
+
operation: "delete"
|
|
738
|
+
};
|
|
739
|
+
}
|
|
642
740
|
const logCollector = options.log?.enabled ? createLogCollector("delete", this.collectionPath, this.conditions) : null;
|
|
643
741
|
let successCount = 0;
|
|
644
742
|
let failureCount = 0;
|
|
@@ -785,9 +883,7 @@ var BatchUpdater = class {
|
|
|
785
883
|
* @private
|
|
786
884
|
*/
|
|
787
885
|
buildQuery() {
|
|
788
|
-
let query = this.firestore.collection(
|
|
789
|
-
this.collectionPath
|
|
790
|
-
);
|
|
886
|
+
let query = this.isCollectionGroup ? this.firestore.collectionGroup(this.collectionPath) : this.firestore.collection(this.collectionPath);
|
|
791
887
|
for (const condition of this.conditions) {
|
|
792
888
|
query = query.where(condition.field, condition.operator, condition.value);
|
|
793
889
|
}
|
|
@@ -797,6 +893,9 @@ var BatchUpdater = class {
|
|
|
797
893
|
if (this.limitCount !== void 0 && this.limitCount > 0) {
|
|
798
894
|
query = query.limit(this.limitCount);
|
|
799
895
|
}
|
|
896
|
+
if (this.selectedFields && this.selectedFields.length > 0) {
|
|
897
|
+
query = query.select(...this.selectedFields);
|
|
898
|
+
}
|
|
800
899
|
return query;
|
|
801
900
|
}
|
|
802
901
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -119,12 +119,20 @@ function createLogCollector(operation, collection, conditions, updateData) {
|
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
// src/utils/index.ts
|
|
122
|
-
function calculateProgress(current, total) {
|
|
122
|
+
function calculateProgress(current, total, startTime) {
|
|
123
123
|
const percentage = total === 0 ? 0 : Math.round(current / total * 100);
|
|
124
|
+
const now = Date.now();
|
|
125
|
+
const elapsedTime = startTime ? now - startTime : 0;
|
|
126
|
+
const docsPerSecond = elapsedTime > 0 ? Math.round(current / elapsedTime * 1e3 * 100) / 100 : 0;
|
|
127
|
+
const remaining = total - current;
|
|
128
|
+
const eta = docsPerSecond > 0 ? Math.round(remaining / docsPerSecond * 100) / 100 : 0;
|
|
124
129
|
return {
|
|
125
130
|
current,
|
|
126
131
|
total,
|
|
127
|
-
percentage
|
|
132
|
+
percentage,
|
|
133
|
+
elapsedTime,
|
|
134
|
+
docsPerSecond,
|
|
135
|
+
eta
|
|
128
136
|
};
|
|
129
137
|
}
|
|
130
138
|
function getAffectedFields(updateData) {
|
|
@@ -151,20 +159,38 @@ var BatchUpdater = class {
|
|
|
151
159
|
* @param firestore - Initialized Firestore instance from firebase-admin
|
|
152
160
|
*/
|
|
153
161
|
constructor(firestore) {
|
|
162
|
+
this.isCollectionGroup = false;
|
|
154
163
|
this.conditions = [];
|
|
155
164
|
this.orderByConditions = [];
|
|
156
165
|
this.firestore = firestore;
|
|
157
166
|
}
|
|
158
167
|
/**
|
|
159
168
|
* Select a collection to operate on
|
|
169
|
+
* Supports subcollection paths like "users/userId/orders"
|
|
160
170
|
* @param path - Collection path
|
|
161
171
|
* @returns This instance for chaining
|
|
162
172
|
*/
|
|
163
173
|
collection(path2) {
|
|
164
174
|
this.collectionPath = path2;
|
|
175
|
+
this.isCollectionGroup = false;
|
|
165
176
|
this.conditions = [];
|
|
166
177
|
this.orderByConditions = [];
|
|
167
178
|
this.limitCount = void 0;
|
|
179
|
+
this.selectedFields = void 0;
|
|
180
|
+
return this;
|
|
181
|
+
}
|
|
182
|
+
/**
|
|
183
|
+
* Select a collection group to operate on (queries across all subcollections with the same name)
|
|
184
|
+
* @param collectionId - Collection ID (not a path, just the collection name)
|
|
185
|
+
* @returns This instance for chaining
|
|
186
|
+
*/
|
|
187
|
+
collectionGroup(collectionId) {
|
|
188
|
+
this.collectionPath = collectionId;
|
|
189
|
+
this.isCollectionGroup = true;
|
|
190
|
+
this.conditions = [];
|
|
191
|
+
this.orderByConditions = [];
|
|
192
|
+
this.limitCount = void 0;
|
|
193
|
+
this.selectedFields = void 0;
|
|
168
194
|
return this;
|
|
169
195
|
}
|
|
170
196
|
/**
|
|
@@ -197,6 +223,44 @@ var BatchUpdater = class {
|
|
|
197
223
|
this.limitCount = count;
|
|
198
224
|
return this;
|
|
199
225
|
}
|
|
226
|
+
/**
|
|
227
|
+
* Select specific fields to retrieve (reduces memory usage and read costs)
|
|
228
|
+
* @param fields - Field paths to retrieve
|
|
229
|
+
* @returns This instance for chaining
|
|
230
|
+
*/
|
|
231
|
+
select(...fields) {
|
|
232
|
+
this.selectedFields = fields;
|
|
233
|
+
return this;
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Count documents matching the query conditions
|
|
237
|
+
* @returns Count result with number of matching documents
|
|
238
|
+
*/
|
|
239
|
+
async count() {
|
|
240
|
+
this.validateSetup();
|
|
241
|
+
const query = this.buildQuery();
|
|
242
|
+
const snapshot = await query.count().get();
|
|
243
|
+
return {
|
|
244
|
+
count: snapshot.data().count
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Find the first document matching the query conditions
|
|
249
|
+
* @returns First matching document with id and data, or null if not found
|
|
250
|
+
*/
|
|
251
|
+
async findOne() {
|
|
252
|
+
this.validateSetup();
|
|
253
|
+
const query = this.buildQuery().limit(1);
|
|
254
|
+
const snapshot = await query.get();
|
|
255
|
+
if (snapshot.empty) {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
const doc = snapshot.docs[0];
|
|
259
|
+
return {
|
|
260
|
+
id: doc.id,
|
|
261
|
+
data: doc.data()
|
|
262
|
+
};
|
|
263
|
+
}
|
|
200
264
|
/**
|
|
201
265
|
* Preview changes before executing update
|
|
202
266
|
* @param updateData - Data to update
|
|
@@ -231,14 +295,24 @@ var BatchUpdater = class {
|
|
|
231
295
|
/**
|
|
232
296
|
* Execute batch update operation
|
|
233
297
|
* @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
|
|
298
|
+
* @param options - Update options (e.g., progress callback, log options, batchSize for pagination, dryRun)
|
|
299
|
+
* @returns Update result with success/failure counts and optional log file path, or DryRunResult if dryRun is true
|
|
236
300
|
*/
|
|
237
301
|
async update(updateData, options = {}) {
|
|
238
302
|
this.validateSetup();
|
|
239
303
|
if (!isValidUpdateData(updateData)) {
|
|
240
304
|
throw new Error("Update data must be a non-empty object");
|
|
241
305
|
}
|
|
306
|
+
if (options.dryRun) {
|
|
307
|
+
const query = this.buildQuery();
|
|
308
|
+
const snapshot = await query.limit(10).get();
|
|
309
|
+
const countSnapshot = await this.buildQuery().count().get();
|
|
310
|
+
return {
|
|
311
|
+
wouldAffect: countSnapshot.data().count,
|
|
312
|
+
sampleIds: snapshot.docs.map((doc) => doc.id),
|
|
313
|
+
operation: "update"
|
|
314
|
+
};
|
|
315
|
+
}
|
|
242
316
|
const logCollector = options.log?.enabled ? createLogCollector("update", this.collectionPath, this.conditions, updateData) : null;
|
|
243
317
|
let successCount = 0;
|
|
244
318
|
let failureCount = 0;
|
|
@@ -387,12 +461,16 @@ var BatchUpdater = class {
|
|
|
387
461
|
}
|
|
388
462
|
/**
|
|
389
463
|
* Create multiple documents in batch
|
|
464
|
+
* Note: This method does not work with collectionGroup()
|
|
390
465
|
* @param documents - Array of documents to create
|
|
391
466
|
* @param options - Create options (e.g., progress callback, log options)
|
|
392
467
|
* @returns Create result with success/failure counts, created IDs, and optional log file path
|
|
393
468
|
*/
|
|
394
469
|
async create(documents, options = {}) {
|
|
395
470
|
this.validateSetup();
|
|
471
|
+
if (this.isCollectionGroup) {
|
|
472
|
+
throw new Error("create() cannot be used with collectionGroup(). Use collection() with a specific path instead.");
|
|
473
|
+
}
|
|
396
474
|
if (!Array.isArray(documents) || documents.length === 0) {
|
|
397
475
|
throw new Error("Documents array must be non-empty");
|
|
398
476
|
}
|
|
@@ -453,14 +531,24 @@ var BatchUpdater = class {
|
|
|
453
531
|
* Upsert documents matching query conditions
|
|
454
532
|
* Updates existing documents or creates them if they don't exist
|
|
455
533
|
* @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
|
|
534
|
+
* @param options - Upsert options (e.g., progress callback, log options, batchSize for pagination, dryRun)
|
|
535
|
+
* @returns Upsert result with success/failure counts and optional log file path, or DryRunResult if dryRun is true
|
|
458
536
|
*/
|
|
459
537
|
async upsert(updateData, options = {}) {
|
|
460
538
|
this.validateSetup();
|
|
461
539
|
if (!isValidUpdateData(updateData)) {
|
|
462
540
|
throw new Error("Update data must be a non-empty object");
|
|
463
541
|
}
|
|
542
|
+
if (options.dryRun) {
|
|
543
|
+
const query = this.buildQuery();
|
|
544
|
+
const snapshot = await query.limit(10).get();
|
|
545
|
+
const countSnapshot = await this.buildQuery().count().get();
|
|
546
|
+
return {
|
|
547
|
+
wouldAffect: countSnapshot.data().count,
|
|
548
|
+
sampleIds: snapshot.docs.map((doc) => doc.id),
|
|
549
|
+
operation: "upsert"
|
|
550
|
+
};
|
|
551
|
+
}
|
|
464
552
|
const logCollector = options.log?.enabled ? createLogCollector("upsert", this.collectionPath, this.conditions, updateData) : null;
|
|
465
553
|
let successCount = 0;
|
|
466
554
|
let failureCount = 0;
|
|
@@ -589,11 +677,21 @@ var BatchUpdater = class {
|
|
|
589
677
|
}
|
|
590
678
|
/**
|
|
591
679
|
* 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
|
|
680
|
+
* @param options - Delete options (e.g., progress callback, log options, batchSize for pagination, dryRun)
|
|
681
|
+
* @returns Delete result with success/failure counts, deleted IDs, and optional log file path, or DryRunResult if dryRun is true
|
|
594
682
|
*/
|
|
595
683
|
async delete(options = {}) {
|
|
596
684
|
this.validateSetup();
|
|
685
|
+
if (options.dryRun) {
|
|
686
|
+
const query = this.buildQuery();
|
|
687
|
+
const snapshot = await query.limit(10).get();
|
|
688
|
+
const countSnapshot = await this.buildQuery().count().get();
|
|
689
|
+
return {
|
|
690
|
+
wouldAffect: countSnapshot.data().count,
|
|
691
|
+
sampleIds: snapshot.docs.map((doc) => doc.id),
|
|
692
|
+
operation: "delete"
|
|
693
|
+
};
|
|
694
|
+
}
|
|
597
695
|
const logCollector = options.log?.enabled ? createLogCollector("delete", this.collectionPath, this.conditions) : null;
|
|
598
696
|
let successCount = 0;
|
|
599
697
|
let failureCount = 0;
|
|
@@ -740,9 +838,7 @@ var BatchUpdater = class {
|
|
|
740
838
|
* @private
|
|
741
839
|
*/
|
|
742
840
|
buildQuery() {
|
|
743
|
-
let query = this.firestore.collection(
|
|
744
|
-
this.collectionPath
|
|
745
|
-
);
|
|
841
|
+
let query = this.isCollectionGroup ? this.firestore.collectionGroup(this.collectionPath) : this.firestore.collection(this.collectionPath);
|
|
746
842
|
for (const condition of this.conditions) {
|
|
747
843
|
query = query.where(condition.field, condition.operator, condition.value);
|
|
748
844
|
}
|
|
@@ -752,6 +848,9 @@ var BatchUpdater = class {
|
|
|
752
848
|
if (this.limitCount !== void 0 && this.limitCount > 0) {
|
|
753
849
|
query = query.limit(this.limitCount);
|
|
754
850
|
}
|
|
851
|
+
if (this.selectedFields && this.selectedFields.length > 0) {
|
|
852
|
+
query = query.select(...this.selectedFields);
|
|
853
|
+
}
|
|
755
854
|
return query;
|
|
756
855
|
}
|
|
757
856
|
/**
|