firestore-batch-updater 1.4.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ko.md +130 -1
- package/README.md +119 -1
- package/dist/index.d.mts +113 -1
- package/dist/index.d.ts +113 -1
- package/dist/index.js +201 -2
- package/dist/index.mjs +199 -0
- package/package.json +1 -1
package/README.ko.md
CHANGED
|
@@ -15,9 +15,13 @@
|
|
|
15
15
|
- 일괄 생성/Upsert/삭제 - 여러 문서를 한 번에 생성, upsert 또는 삭제
|
|
16
16
|
- 정렬 및 제한 - `orderBy()`와 `limit()`으로 정밀한 제어
|
|
17
17
|
- 필드 선택 - `select()`로 필요한 필드만 로드 (메모리 및 비용 절약)
|
|
18
|
-
- 단일 문서 작업 - `findOne()`, `updateOne()`, `deleteOne()`으로 효율적인 단일 문서 처리
|
|
18
|
+
- 단일 문서 작업 - `findOne()`, `createOne()`, `updateOne()`, `deleteOne()`으로 효율적인 단일 문서 처리
|
|
19
19
|
- 존재 여부 확인 - `exists()`로 매칭 문서 존재 여부 빠르게 확인
|
|
20
20
|
- 전체 문서 조회 - `getAll()`로 매칭되는 모든 문서 데이터 조회
|
|
21
|
+
- 집계 쿼리 - `aggregate()`로 서버 사이드 `sum`, `average`, `count` 연산
|
|
22
|
+
- 커서 페이지네이션 - `paginate()`로 메모리 효율적인 페이지 단위 조회
|
|
23
|
+
- ID 직접 조회 - `getOne()`으로 문서 ID로 빠른 조회
|
|
24
|
+
- 벌크 업데이트 - `bulkUpdate()`로 여러 문서에 각기 다른 데이터 업데이트
|
|
21
25
|
- FieldValue 지원 - `increment()`, `arrayUnion()`, `delete()`, `serverTimestamp()` 등 사용 가능
|
|
22
26
|
- 서브컬렉션 & 컬렉션 그룹 - 서브컬렉션 쿼리 또는 동일 이름의 모든 컬렉션 쿼리
|
|
23
27
|
- Dry Run 모드 - 실제 변경 없이 작업 시뮬레이션
|
|
@@ -91,14 +95,19 @@ console.log(`${result.successCount}개 문서 업데이트 완료`);
|
|
|
91
95
|
| `count()` | 매칭되는 문서 개수 조회 | `CountResult` |
|
|
92
96
|
| `exists()` | 매칭되는 문서 존재 여부 확인 | `boolean` |
|
|
93
97
|
| `findOne()` | 첫 번째 매칭 문서 조회 | `{ id, data } \| null` |
|
|
98
|
+
| `getOne(id)` | ID로 문서 직접 조회 | `{ id, data } \| null` |
|
|
94
99
|
| `getAll()` | 모든 매칭 문서 조회 | `{ id, data }[]` |
|
|
95
100
|
| `preview(data)` | 업데이트 전 미리보기 | `PreviewResult` |
|
|
96
101
|
| `update(data, options?)` | 매칭되는 문서 업데이트 | `UpdateResult` |
|
|
97
102
|
| `updateOne(data)` | 첫 번째 매칭 문서 업데이트 | `{ success, id }` |
|
|
98
103
|
| `create(docs, options?)` | 새 문서 생성 | `CreateResult` |
|
|
104
|
+
| `createOne(data, id?)` | 단일 문서 생성 | `{ success, id }` |
|
|
99
105
|
| `upsert(data, options?)` | 업데이트 또는 생성 (set with merge) | `UpsertResult` |
|
|
100
106
|
| `delete(options?)` | 매칭되는 문서 삭제 | `DeleteResult` |
|
|
101
107
|
| `deleteOne()` | 첫 번째 매칭 문서 삭제 | `{ success, id }` |
|
|
108
|
+
| `aggregate(spec)` | sum/average/count 집계 쿼리 | `AggregateResult` |
|
|
109
|
+
| `paginate(options)` | 커서 기반 페이지네이션 | `PaginateResult` |
|
|
110
|
+
| `bulkUpdate(updates, options?)` | 여러 문서에 각기 다른 데이터 업데이트 | `BulkUpdateResult` |
|
|
102
111
|
| `getFields(field)` | 특정 필드 값 조회 | `FieldValueResult[]` |
|
|
103
112
|
|
|
104
113
|
### 옵션
|
|
@@ -146,6 +155,9 @@ console.log(`${result.successCount}개 문서 업데이트 완료`);
|
|
|
146
155
|
| `CreateResult` | `successCount`, `failureCount`, `totalCount`, `createdIds[]`, `failedDocIds?`, `logFilePath?` |
|
|
147
156
|
| `UpsertResult` | `successCount`, `failureCount`, `totalCount`, `failedDocIds?`, `logFilePath?` |
|
|
148
157
|
| `DeleteResult` | `successCount`, `failureCount`, `totalCount`, `deletedIds[]`, `failedDocIds?`, `logFilePath?` |
|
|
158
|
+
| `AggregateResult` | `{ [alias]: number \| null }` |
|
|
159
|
+
| `PaginateResult` | `docs[]`, `nextCursor`, `hasMore` |
|
|
160
|
+
| `BulkUpdateResult` | `successCount`, `failureCount`, `totalCount`, `failedDocIds?`, `logFilePath?` |
|
|
149
161
|
| `FieldValueResult` | `id`, `value` |
|
|
150
162
|
|
|
151
163
|
## 사용 예시
|
|
@@ -443,6 +455,123 @@ if (result.success) {
|
|
|
443
455
|
}
|
|
444
456
|
```
|
|
445
457
|
|
|
458
|
+
### 단일 문서 생성
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
// 자동 생성 ID로 문서 생성
|
|
462
|
+
const result = await updater
|
|
463
|
+
.collection("users")
|
|
464
|
+
.createOne({ name: "Alice", status: "active", score: 100 });
|
|
465
|
+
|
|
466
|
+
console.log(`문서 생성 완료: ${result.id}`);
|
|
467
|
+
|
|
468
|
+
// 커스텀 ID로 문서 생성
|
|
469
|
+
const result2 = await updater
|
|
470
|
+
.collection("users")
|
|
471
|
+
.createOne({ name: "Bob", status: "active" }, "custom-bob-id");
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
### 집계 쿼리
|
|
475
|
+
|
|
476
|
+
```typescript
|
|
477
|
+
// 매칭 문서에 대해 sum, average, count 집계
|
|
478
|
+
const stats = await updater
|
|
479
|
+
.collection("orders")
|
|
480
|
+
.where("status", "==", "completed")
|
|
481
|
+
.aggregate({
|
|
482
|
+
totalAmount: { op: "sum", field: "amount" },
|
|
483
|
+
avgAmount: { op: "average", field: "amount" },
|
|
484
|
+
orderCount: { op: "count" },
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
console.log(`총액: ${stats.totalAmount}원`);
|
|
488
|
+
console.log(`평균: ${stats.avgAmount}원`);
|
|
489
|
+
console.log(`주문 수: ${stats.orderCount}건`);
|
|
490
|
+
```
|
|
491
|
+
|
|
492
|
+
### 커서 기반 페이지네이션
|
|
493
|
+
|
|
494
|
+
```typescript
|
|
495
|
+
// 페이지 단위로 효율적으로 문서 조회
|
|
496
|
+
let nextCursor = undefined;
|
|
497
|
+
|
|
498
|
+
do {
|
|
499
|
+
const page = await updater
|
|
500
|
+
.collection("users")
|
|
501
|
+
.orderBy("createdAt", "desc")
|
|
502
|
+
.paginate({ pageSize: 20, startAfter: nextCursor });
|
|
503
|
+
|
|
504
|
+
page.docs.forEach((doc) => {
|
|
505
|
+
console.log(`${doc.id}: ${doc.data.name}`);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
nextCursor = page.nextCursor;
|
|
509
|
+
} while (nextCursor);
|
|
510
|
+
|
|
511
|
+
// select와 함께 사용하여 메모리 효율 극대화
|
|
512
|
+
const page = await updater
|
|
513
|
+
.collection("users")
|
|
514
|
+
.select("name", "email")
|
|
515
|
+
.orderBy("name")
|
|
516
|
+
.paginate({ pageSize: 50 });
|
|
517
|
+
```
|
|
518
|
+
|
|
519
|
+
### ID로 문서 조회
|
|
520
|
+
|
|
521
|
+
```typescript
|
|
522
|
+
// 문서 ID로 직접 조회 (쿼리 필터 없이 가장 빠름)
|
|
523
|
+
const user = await updater.collection("users").getOne("user-123");
|
|
524
|
+
|
|
525
|
+
if (user) {
|
|
526
|
+
console.log(`찾음: ${user.data.name} (${user.data.email})`);
|
|
527
|
+
} else {
|
|
528
|
+
console.log("사용자를 찾을 수 없음");
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
// select와 함께 사용하여 특정 필드만 가져오기
|
|
532
|
+
const userBasic = await updater
|
|
533
|
+
.collection("users")
|
|
534
|
+
.select("name", "email")
|
|
535
|
+
.getOne("user-123");
|
|
536
|
+
|
|
537
|
+
// 서브컬렉션에서 문서 조회
|
|
538
|
+
const order = await updater
|
|
539
|
+
.collection("users/user-123/orders")
|
|
540
|
+
.getOne("order-456");
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
### 벌크 업데이트
|
|
544
|
+
|
|
545
|
+
```typescript
|
|
546
|
+
// 여러 문서를 각각 다른 데이터로 업데이트
|
|
547
|
+
const result = await updater.collection("users").bulkUpdate([
|
|
548
|
+
{ id: "user-1", data: { name: "Alice", age: 30 } },
|
|
549
|
+
{ id: "user-2", data: { name: "Bob", status: "active" } },
|
|
550
|
+
{ id: "user-3", data: { email: "charlie@example.com" } },
|
|
551
|
+
]);
|
|
552
|
+
|
|
553
|
+
console.log(`성공: ${result.successCount}, 실패: ${result.failureCount}`);
|
|
554
|
+
|
|
555
|
+
// 진행 상황 콜백과 함께 사용
|
|
556
|
+
const result = await updater.collection("products").bulkUpdate(
|
|
557
|
+
[
|
|
558
|
+
{ id: "prod-1", data: { price: 29.99, stock: 100 } },
|
|
559
|
+
{ id: "prod-2", data: { price: 49.99, stock: 50 } },
|
|
560
|
+
// ... 더 많은 업데이트
|
|
561
|
+
],
|
|
562
|
+
{
|
|
563
|
+
onProgress: (progress) => {
|
|
564
|
+
console.log(`${progress.processedCount}/${progress.totalCount} 처리됨`);
|
|
565
|
+
},
|
|
566
|
+
}
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
// 실패 처리
|
|
570
|
+
if (result.failureCount > 0) {
|
|
571
|
+
console.log("실패한 문서 ID:", result.failedDocIds);
|
|
572
|
+
}
|
|
573
|
+
```
|
|
574
|
+
|
|
446
575
|
### Dry Run 모드
|
|
447
576
|
|
|
448
577
|
```typescript
|
package/README.md
CHANGED
|
@@ -15,9 +15,13 @@ English | [한국어](./README.ko.md)
|
|
|
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
|
-
- Single document operations - Use `findOne()`, `updateOne()`, `deleteOne()` for efficient single-doc ops
|
|
18
|
+
- Single document operations - Use `findOne()`, `createOne()`, `updateOne()`, `deleteOne()` for efficient single-doc ops
|
|
19
19
|
- Existence check - Use `exists()` to quickly check if matching documents exist
|
|
20
20
|
- Get all documents - Use `getAll()` to retrieve all matching documents with data
|
|
21
|
+
- Aggregation - Use `aggregate()` for server-side `sum`, `average`, and `count` operations
|
|
22
|
+
- Cursor pagination - Use `paginate()` for memory-efficient page-by-page iteration
|
|
23
|
+
- Direct ID lookup - Use `getOne()` for fast document retrieval by ID
|
|
24
|
+
- Bulk updates - Use `bulkUpdate()` to update multiple documents with different data each
|
|
21
25
|
- FieldValue support - Use `increment()`, `arrayUnion()`, `delete()`, `serverTimestamp()`, etc.
|
|
22
26
|
- Subcollection & Collection Group - Query subcollections or all collections with the same name
|
|
23
27
|
- Dry run mode - Simulate operations without making changes
|
|
@@ -91,14 +95,19 @@ console.log(`Updated ${result.successCount} documents`);
|
|
|
91
95
|
| `count()` | Count matching documents | `CountResult` |
|
|
92
96
|
| `exists()` | Check if matching documents exist | `boolean` |
|
|
93
97
|
| `findOne()` | Find first matching document | `{ id, data } \| null` |
|
|
98
|
+
| `getOne(id)` | Get document by ID directly | `{ id, data } \| null` |
|
|
94
99
|
| `getAll()` | Get all matching documents | `{ id, data }[]` |
|
|
95
100
|
| `preview(data)` | Preview changes before update | `PreviewResult` |
|
|
96
101
|
| `update(data, options?)` | Update matching documents | `UpdateResult` |
|
|
97
102
|
| `updateOne(data)` | Update first matching document | `{ success, id }` |
|
|
98
103
|
| `create(docs, options?)` | Create new documents | `CreateResult` |
|
|
104
|
+
| `createOne(data, id?)` | Create a single document | `{ success, id }` |
|
|
99
105
|
| `upsert(data, options?)` | Update or create (set with merge) | `UpsertResult` |
|
|
100
106
|
| `delete(options?)` | Delete matching documents | `DeleteResult` |
|
|
101
107
|
| `deleteOne()` | Delete first matching document | `{ success, id }` |
|
|
108
|
+
| `aggregate(spec)` | Run sum/average/count queries | `AggregateResult` |
|
|
109
|
+
| `paginate(options)` | Cursor-based pagination | `PaginateResult` |
|
|
110
|
+
| `bulkUpdate(updates, options?)` | Update multiple docs with different data | `BulkUpdateResult` |
|
|
102
111
|
| `getFields(field)` | Get specific field values | `FieldValueResult[]` |
|
|
103
112
|
|
|
104
113
|
### Options
|
|
@@ -146,6 +155,9 @@ All write operations support an optional `options` parameter:
|
|
|
146
155
|
| `CreateResult` | `successCount`, `failureCount`, `totalCount`, `createdIds[]`, `failedDocIds?`, `logFilePath?` |
|
|
147
156
|
| `UpsertResult` | `successCount`, `failureCount`, `totalCount`, `failedDocIds?`, `logFilePath?` |
|
|
148
157
|
| `DeleteResult` | `successCount`, `failureCount`, `totalCount`, `deletedIds[]`, `failedDocIds?`, `logFilePath?` |
|
|
158
|
+
| `AggregateResult` | `{ [alias]: number \| null }` |
|
|
159
|
+
| `PaginateResult` | `docs[]`, `nextCursor`, `hasMore` |
|
|
160
|
+
| `BulkUpdateResult` | `successCount`, `failureCount`, `totalCount`, `failedDocIds?`, `logFilePath?` |
|
|
149
161
|
| `FieldValueResult` | `id`, `value` |
|
|
150
162
|
|
|
151
163
|
## Usage Examples
|
|
@@ -442,6 +454,112 @@ if (result.success) {
|
|
|
442
454
|
}
|
|
443
455
|
```
|
|
444
456
|
|
|
457
|
+
### Create Single Document
|
|
458
|
+
|
|
459
|
+
```typescript
|
|
460
|
+
// Create with auto-generated ID
|
|
461
|
+
const result = await updater
|
|
462
|
+
.collection("users")
|
|
463
|
+
.createOne({ name: "Alice", status: "active", score: 100 });
|
|
464
|
+
|
|
465
|
+
console.log(`Created document: ${result.id}`);
|
|
466
|
+
|
|
467
|
+
// Create with custom ID
|
|
468
|
+
const result2 = await updater
|
|
469
|
+
.collection("users")
|
|
470
|
+
.createOne({ name: "Bob", status: "active" }, "custom-bob-id");
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
### Aggregate Queries
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
// Sum, average, count on matching documents
|
|
477
|
+
const stats = await updater
|
|
478
|
+
.collection("orders")
|
|
479
|
+
.where("status", "==", "completed")
|
|
480
|
+
.aggregate({
|
|
481
|
+
totalAmount: { op: "sum", field: "amount" },
|
|
482
|
+
avgAmount: { op: "average", field: "amount" },
|
|
483
|
+
orderCount: { op: "count" },
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
console.log(`Total: $${stats.totalAmount}`);
|
|
487
|
+
console.log(`Average: $${stats.avgAmount}`);
|
|
488
|
+
console.log(`Orders: ${stats.orderCount}`);
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
### Cursor-Based Pagination
|
|
492
|
+
|
|
493
|
+
```typescript
|
|
494
|
+
// Page through documents efficiently
|
|
495
|
+
let nextCursor = undefined;
|
|
496
|
+
|
|
497
|
+
do {
|
|
498
|
+
const page = await updater
|
|
499
|
+
.collection("users")
|
|
500
|
+
.orderBy("createdAt", "desc")
|
|
501
|
+
.paginate({ pageSize: 20, startAfter: nextCursor });
|
|
502
|
+
|
|
503
|
+
page.docs.forEach((doc) => {
|
|
504
|
+
console.log(`${doc.id}: ${doc.data.name}`);
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
nextCursor = page.nextCursor;
|
|
508
|
+
} while (nextCursor);
|
|
509
|
+
|
|
510
|
+
// Works with select for memory efficiency
|
|
511
|
+
const page = await updater
|
|
512
|
+
.collection("users")
|
|
513
|
+
.select("name", "email")
|
|
514
|
+
.orderBy("name")
|
|
515
|
+
.paginate({ pageSize: 50 });
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
### Get Document by ID
|
|
519
|
+
|
|
520
|
+
```typescript
|
|
521
|
+
// Fast lookup when you know the document ID
|
|
522
|
+
const user = await updater.collection("users").getOne("user-123");
|
|
523
|
+
|
|
524
|
+
if (user) {
|
|
525
|
+
console.log(`Found: ${user.data.name}`);
|
|
526
|
+
} else {
|
|
527
|
+
console.log("User not found");
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Works with select for field filtering
|
|
531
|
+
const profile = await updater
|
|
532
|
+
.collection("users")
|
|
533
|
+
.select("name", "avatar")
|
|
534
|
+
.getOne("user-123");
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
### Bulk Update with Different Data
|
|
538
|
+
|
|
539
|
+
```typescript
|
|
540
|
+
// Update multiple documents with different data for each
|
|
541
|
+
const result = await updater.collection("users").bulkUpdate([
|
|
542
|
+
{ id: "user-1", data: { score: 100, rank: 1 } },
|
|
543
|
+
{ id: "user-2", data: { score: 85, rank: 2 } },
|
|
544
|
+
{ id: "user-3", data: { score: 70, rank: 3 } },
|
|
545
|
+
]);
|
|
546
|
+
|
|
547
|
+
console.log(`Updated ${result.successCount} documents`);
|
|
548
|
+
|
|
549
|
+
// With progress tracking
|
|
550
|
+
const result2 = await updater.collection("products").bulkUpdate(
|
|
551
|
+
[
|
|
552
|
+
{ id: "prod-1", data: { price: 29.99, stock: 100 } },
|
|
553
|
+
{ id: "prod-2", data: { price: 49.99, stock: 50 } },
|
|
554
|
+
],
|
|
555
|
+
{
|
|
556
|
+
onProgress: (progress) => {
|
|
557
|
+
console.log(`${progress.percentage}% complete`);
|
|
558
|
+
},
|
|
559
|
+
}
|
|
560
|
+
);
|
|
561
|
+
```
|
|
562
|
+
|
|
445
563
|
### Dry Run Mode
|
|
446
564
|
|
|
447
565
|
```typescript
|
package/dist/index.d.mts
CHANGED
|
@@ -260,6 +260,78 @@ interface OperationLog {
|
|
|
260
260
|
};
|
|
261
261
|
entries: LogEntry[];
|
|
262
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Result of createOne operation
|
|
265
|
+
*/
|
|
266
|
+
interface CreateOneResult {
|
|
267
|
+
success: boolean;
|
|
268
|
+
id: string;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Aggregate operation specification
|
|
272
|
+
* Each key is the alias for the result, value defines the operation
|
|
273
|
+
*/
|
|
274
|
+
interface AggregateSpec {
|
|
275
|
+
[alias: string]: {
|
|
276
|
+
op: "sum" | "average" | "count";
|
|
277
|
+
field?: string;
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Result of aggregate operation
|
|
282
|
+
* Keys match the aliases from AggregateSpec
|
|
283
|
+
*/
|
|
284
|
+
interface AggregateResult {
|
|
285
|
+
[alias: string]: number | null;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Options for paginate operation
|
|
289
|
+
*/
|
|
290
|
+
interface PaginateOptions {
|
|
291
|
+
pageSize: number;
|
|
292
|
+
startAfter?: unknown;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Result of paginate operation
|
|
296
|
+
*/
|
|
297
|
+
interface PaginateResult {
|
|
298
|
+
docs: {
|
|
299
|
+
id: string;
|
|
300
|
+
data: Record<string, any>;
|
|
301
|
+
}[];
|
|
302
|
+
nextCursor: unknown | null;
|
|
303
|
+
hasMore: boolean;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Input for bulk update operation
|
|
307
|
+
*/
|
|
308
|
+
interface BulkUpdateInput {
|
|
309
|
+
id: string;
|
|
310
|
+
data: Record<string, any>;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Options for bulk update operation
|
|
314
|
+
*/
|
|
315
|
+
interface BulkUpdateOptions {
|
|
316
|
+
/**
|
|
317
|
+
* Callback function for progress updates
|
|
318
|
+
* @param progress - Current progress information
|
|
319
|
+
*/
|
|
320
|
+
onProgress?: (progress: ProgressInfo) => void;
|
|
321
|
+
/**
|
|
322
|
+
* Log file generation options
|
|
323
|
+
*/
|
|
324
|
+
log?: LogOptions;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Result of bulk update operation
|
|
328
|
+
*/
|
|
329
|
+
interface BulkUpdateResult {
|
|
330
|
+
successCount: number;
|
|
331
|
+
failureCount: number;
|
|
332
|
+
totalCount: number;
|
|
333
|
+
failedDocIds?: string[];
|
|
334
|
+
}
|
|
263
335
|
|
|
264
336
|
/**
|
|
265
337
|
* BatchUpdater - Core class for batch operations on Firestore
|
|
@@ -347,6 +419,15 @@ declare class BatchUpdater {
|
|
|
347
419
|
id: string;
|
|
348
420
|
data: Record<string, any>;
|
|
349
421
|
}[]>;
|
|
422
|
+
/**
|
|
423
|
+
* Get a document by its ID directly (faster than findOne with where)
|
|
424
|
+
* @param id - Document ID
|
|
425
|
+
* @returns Document with id and data, or null if not found
|
|
426
|
+
*/
|
|
427
|
+
getOne(id: string): Promise<{
|
|
428
|
+
id: string;
|
|
429
|
+
data: Record<string, any>;
|
|
430
|
+
} | null>;
|
|
350
431
|
/**
|
|
351
432
|
* Update the first document matching the query conditions
|
|
352
433
|
* @param updateData - Data to update
|
|
@@ -364,6 +445,28 @@ declare class BatchUpdater {
|
|
|
364
445
|
success: boolean;
|
|
365
446
|
id: string | null;
|
|
366
447
|
}>;
|
|
448
|
+
/**
|
|
449
|
+
* Create a single document in the collection
|
|
450
|
+
* @param data - Document data
|
|
451
|
+
* @param id - Optional document ID (auto-generated if not provided)
|
|
452
|
+
* @returns Result with success status and document id
|
|
453
|
+
*/
|
|
454
|
+
createOne(data: Record<string, any>, id?: string): Promise<{
|
|
455
|
+
success: boolean;
|
|
456
|
+
id: string;
|
|
457
|
+
}>;
|
|
458
|
+
/**
|
|
459
|
+
* Run aggregate queries (sum, average, count) on matching documents
|
|
460
|
+
* @param spec - Aggregate specification defining operations and fields
|
|
461
|
+
* @returns Object with alias keys and numeric results
|
|
462
|
+
*/
|
|
463
|
+
aggregate(spec: AggregateSpec): Promise<AggregateResult>;
|
|
464
|
+
/**
|
|
465
|
+
* Get documents with cursor-based pagination
|
|
466
|
+
* @param options - Pagination options (pageSize, startAfter cursor)
|
|
467
|
+
* @returns Page of documents with cursor for next page
|
|
468
|
+
*/
|
|
469
|
+
paginate(options: PaginateOptions): Promise<PaginateResult>;
|
|
367
470
|
/**
|
|
368
471
|
* Preview changes before executing update
|
|
369
472
|
* @param updateData - Data to update
|
|
@@ -395,6 +498,15 @@ declare class BatchUpdater {
|
|
|
395
498
|
create(documents: CreateDocumentInput[], options?: CreateOptions): Promise<CreateResult & {
|
|
396
499
|
logFilePath?: string;
|
|
397
500
|
}>;
|
|
501
|
+
/**
|
|
502
|
+
* Update multiple documents with different data for each
|
|
503
|
+
* @param updates - Array of { id, data } objects specifying updates for each document
|
|
504
|
+
* @param options - Bulk update options (e.g., progress callback, log options)
|
|
505
|
+
* @returns Bulk update result with success/failure counts and optional log file path
|
|
506
|
+
*/
|
|
507
|
+
bulkUpdate(updates: BulkUpdateInput[], options?: BulkUpdateOptions): Promise<BulkUpdateResult & {
|
|
508
|
+
logFilePath?: string;
|
|
509
|
+
}>;
|
|
398
510
|
/**
|
|
399
511
|
* Upsert documents matching query conditions
|
|
400
512
|
* Updates existing documents or creates them if they don't exist
|
|
@@ -490,4 +602,4 @@ declare function isValidUpdateData(value: any): value is Record<string, any>;
|
|
|
490
602
|
*/
|
|
491
603
|
declare function formatError(error: unknown, context?: string): string;
|
|
492
604
|
|
|
493
|
-
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 };
|
|
605
|
+
export { type AggregateResult, type AggregateSpec, BatchUpdater, type BulkUpdateInput, type BulkUpdateOptions, type BulkUpdateResult, type CountResult, type CreateDocumentInput, type CreateOneResult, type CreateOptions, type CreateResult, type DeleteOptions, type DeleteResult, type DocumentSnapshot, type DryRunResult, type FieldValueResult, type LogEntry, type LogOptions, type OperationLog, type OrderByCondition, type PaginateOptions, type PaginateResult, type PreviewResult, type ProgressInfo, type UpdateOptions, type UpdateResult, type UpsertOptions, type UpsertResult, type WhereCondition, calculateProgress, createLogCollector, formatError, formatOperationLog, getAffectedFields, isValidUpdateData, mergeUpdateData, writeOperationLog };
|
package/dist/index.d.ts
CHANGED
|
@@ -260,6 +260,78 @@ interface OperationLog {
|
|
|
260
260
|
};
|
|
261
261
|
entries: LogEntry[];
|
|
262
262
|
}
|
|
263
|
+
/**
|
|
264
|
+
* Result of createOne operation
|
|
265
|
+
*/
|
|
266
|
+
interface CreateOneResult {
|
|
267
|
+
success: boolean;
|
|
268
|
+
id: string;
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Aggregate operation specification
|
|
272
|
+
* Each key is the alias for the result, value defines the operation
|
|
273
|
+
*/
|
|
274
|
+
interface AggregateSpec {
|
|
275
|
+
[alias: string]: {
|
|
276
|
+
op: "sum" | "average" | "count";
|
|
277
|
+
field?: string;
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Result of aggregate operation
|
|
282
|
+
* Keys match the aliases from AggregateSpec
|
|
283
|
+
*/
|
|
284
|
+
interface AggregateResult {
|
|
285
|
+
[alias: string]: number | null;
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Options for paginate operation
|
|
289
|
+
*/
|
|
290
|
+
interface PaginateOptions {
|
|
291
|
+
pageSize: number;
|
|
292
|
+
startAfter?: unknown;
|
|
293
|
+
}
|
|
294
|
+
/**
|
|
295
|
+
* Result of paginate operation
|
|
296
|
+
*/
|
|
297
|
+
interface PaginateResult {
|
|
298
|
+
docs: {
|
|
299
|
+
id: string;
|
|
300
|
+
data: Record<string, any>;
|
|
301
|
+
}[];
|
|
302
|
+
nextCursor: unknown | null;
|
|
303
|
+
hasMore: boolean;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Input for bulk update operation
|
|
307
|
+
*/
|
|
308
|
+
interface BulkUpdateInput {
|
|
309
|
+
id: string;
|
|
310
|
+
data: Record<string, any>;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Options for bulk update operation
|
|
314
|
+
*/
|
|
315
|
+
interface BulkUpdateOptions {
|
|
316
|
+
/**
|
|
317
|
+
* Callback function for progress updates
|
|
318
|
+
* @param progress - Current progress information
|
|
319
|
+
*/
|
|
320
|
+
onProgress?: (progress: ProgressInfo) => void;
|
|
321
|
+
/**
|
|
322
|
+
* Log file generation options
|
|
323
|
+
*/
|
|
324
|
+
log?: LogOptions;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Result of bulk update operation
|
|
328
|
+
*/
|
|
329
|
+
interface BulkUpdateResult {
|
|
330
|
+
successCount: number;
|
|
331
|
+
failureCount: number;
|
|
332
|
+
totalCount: number;
|
|
333
|
+
failedDocIds?: string[];
|
|
334
|
+
}
|
|
263
335
|
|
|
264
336
|
/**
|
|
265
337
|
* BatchUpdater - Core class for batch operations on Firestore
|
|
@@ -347,6 +419,15 @@ declare class BatchUpdater {
|
|
|
347
419
|
id: string;
|
|
348
420
|
data: Record<string, any>;
|
|
349
421
|
}[]>;
|
|
422
|
+
/**
|
|
423
|
+
* Get a document by its ID directly (faster than findOne with where)
|
|
424
|
+
* @param id - Document ID
|
|
425
|
+
* @returns Document with id and data, or null if not found
|
|
426
|
+
*/
|
|
427
|
+
getOne(id: string): Promise<{
|
|
428
|
+
id: string;
|
|
429
|
+
data: Record<string, any>;
|
|
430
|
+
} | null>;
|
|
350
431
|
/**
|
|
351
432
|
* Update the first document matching the query conditions
|
|
352
433
|
* @param updateData - Data to update
|
|
@@ -364,6 +445,28 @@ declare class BatchUpdater {
|
|
|
364
445
|
success: boolean;
|
|
365
446
|
id: string | null;
|
|
366
447
|
}>;
|
|
448
|
+
/**
|
|
449
|
+
* Create a single document in the collection
|
|
450
|
+
* @param data - Document data
|
|
451
|
+
* @param id - Optional document ID (auto-generated if not provided)
|
|
452
|
+
* @returns Result with success status and document id
|
|
453
|
+
*/
|
|
454
|
+
createOne(data: Record<string, any>, id?: string): Promise<{
|
|
455
|
+
success: boolean;
|
|
456
|
+
id: string;
|
|
457
|
+
}>;
|
|
458
|
+
/**
|
|
459
|
+
* Run aggregate queries (sum, average, count) on matching documents
|
|
460
|
+
* @param spec - Aggregate specification defining operations and fields
|
|
461
|
+
* @returns Object with alias keys and numeric results
|
|
462
|
+
*/
|
|
463
|
+
aggregate(spec: AggregateSpec): Promise<AggregateResult>;
|
|
464
|
+
/**
|
|
465
|
+
* Get documents with cursor-based pagination
|
|
466
|
+
* @param options - Pagination options (pageSize, startAfter cursor)
|
|
467
|
+
* @returns Page of documents with cursor for next page
|
|
468
|
+
*/
|
|
469
|
+
paginate(options: PaginateOptions): Promise<PaginateResult>;
|
|
367
470
|
/**
|
|
368
471
|
* Preview changes before executing update
|
|
369
472
|
* @param updateData - Data to update
|
|
@@ -395,6 +498,15 @@ declare class BatchUpdater {
|
|
|
395
498
|
create(documents: CreateDocumentInput[], options?: CreateOptions): Promise<CreateResult & {
|
|
396
499
|
logFilePath?: string;
|
|
397
500
|
}>;
|
|
501
|
+
/**
|
|
502
|
+
* Update multiple documents with different data for each
|
|
503
|
+
* @param updates - Array of { id, data } objects specifying updates for each document
|
|
504
|
+
* @param options - Bulk update options (e.g., progress callback, log options)
|
|
505
|
+
* @returns Bulk update result with success/failure counts and optional log file path
|
|
506
|
+
*/
|
|
507
|
+
bulkUpdate(updates: BulkUpdateInput[], options?: BulkUpdateOptions): Promise<BulkUpdateResult & {
|
|
508
|
+
logFilePath?: string;
|
|
509
|
+
}>;
|
|
398
510
|
/**
|
|
399
511
|
* Upsert documents matching query conditions
|
|
400
512
|
* Updates existing documents or creates them if they don't exist
|
|
@@ -490,4 +602,4 @@ declare function isValidUpdateData(value: any): value is Record<string, any>;
|
|
|
490
602
|
*/
|
|
491
603
|
declare function formatError(error: unknown, context?: string): string;
|
|
492
604
|
|
|
493
|
-
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 };
|
|
605
|
+
export { type AggregateResult, type AggregateSpec, BatchUpdater, type BulkUpdateInput, type BulkUpdateOptions, type BulkUpdateResult, type CountResult, type CreateDocumentInput, type CreateOneResult, type CreateOptions, type CreateResult, type DeleteOptions, type DeleteResult, type DocumentSnapshot, type DryRunResult, type FieldValueResult, type LogEntry, type LogOptions, type OperationLog, type OrderByCondition, type PaginateOptions, type PaginateResult, type PreviewResult, type ProgressInfo, type UpdateOptions, type UpdateResult, type UpsertOptions, type UpsertResult, type WhereCondition, calculateProgress, createLogCollector, formatError, formatOperationLog, getAffectedFields, isValidUpdateData, mergeUpdateData, writeOperationLog };
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
31
31
|
var index_exports = {};
|
|
32
32
|
__export(index_exports, {
|
|
33
33
|
BatchUpdater: () => BatchUpdater,
|
|
34
|
-
FieldValue: () =>
|
|
34
|
+
FieldValue: () => import_firestore2.FieldValue,
|
|
35
35
|
calculateProgress: () => calculateProgress,
|
|
36
36
|
createLogCollector: () => createLogCollector,
|
|
37
37
|
formatError: () => formatError,
|
|
@@ -43,6 +43,9 @@ __export(index_exports, {
|
|
|
43
43
|
});
|
|
44
44
|
module.exports = __toCommonJS(index_exports);
|
|
45
45
|
|
|
46
|
+
// src/core/batch-updater.ts
|
|
47
|
+
var import_firestore = require("firebase-admin/firestore");
|
|
48
|
+
|
|
46
49
|
// src/utils/logger.ts
|
|
47
50
|
var fs = __toESM(require("fs"));
|
|
48
51
|
var path = __toESM(require("path"));
|
|
@@ -329,6 +332,38 @@ var BatchUpdater = class {
|
|
|
329
332
|
data: doc.data()
|
|
330
333
|
}));
|
|
331
334
|
}
|
|
335
|
+
/**
|
|
336
|
+
* Get a document by its ID directly (faster than findOne with where)
|
|
337
|
+
* @param id - Document ID
|
|
338
|
+
* @returns Document with id and data, or null if not found
|
|
339
|
+
*/
|
|
340
|
+
async getOne(id) {
|
|
341
|
+
this.validateSetup();
|
|
342
|
+
if (this.isCollectionGroup) {
|
|
343
|
+
throw new Error(
|
|
344
|
+
"getOne() cannot be used with collectionGroup(). Use findOne() with where conditions instead."
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
const docRef = this.firestore.collection(this.collectionPath).doc(id);
|
|
348
|
+
let docSnapshot;
|
|
349
|
+
if (this.selectedFields && this.selectedFields.length > 0) {
|
|
350
|
+
const query = this.firestore.collection(this.collectionPath).where("__name__", "==", docRef).select(...this.selectedFields);
|
|
351
|
+
const snapshot = await query.get();
|
|
352
|
+
if (snapshot.empty) {
|
|
353
|
+
return null;
|
|
354
|
+
}
|
|
355
|
+
docSnapshot = snapshot.docs[0];
|
|
356
|
+
} else {
|
|
357
|
+
docSnapshot = await docRef.get();
|
|
358
|
+
if (!docSnapshot.exists) {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
id: docSnapshot.id,
|
|
364
|
+
data: docSnapshot.data()
|
|
365
|
+
};
|
|
366
|
+
}
|
|
332
367
|
/**
|
|
333
368
|
* Update the first document matching the query conditions
|
|
334
369
|
* @param updateData - Data to update
|
|
@@ -363,6 +398,95 @@ var BatchUpdater = class {
|
|
|
363
398
|
await doc.ref.delete();
|
|
364
399
|
return { success: true, id: doc.id };
|
|
365
400
|
}
|
|
401
|
+
/**
|
|
402
|
+
* Create a single document in the collection
|
|
403
|
+
* @param data - Document data
|
|
404
|
+
* @param id - Optional document ID (auto-generated if not provided)
|
|
405
|
+
* @returns Result with success status and document id
|
|
406
|
+
*/
|
|
407
|
+
async createOne(data, id) {
|
|
408
|
+
this.validateSetup();
|
|
409
|
+
if (this.isCollectionGroup) {
|
|
410
|
+
throw new Error(
|
|
411
|
+
"createOne() cannot be used with collectionGroup(). Use collection() with a specific path instead."
|
|
412
|
+
);
|
|
413
|
+
}
|
|
414
|
+
if (!isValidUpdateData(data)) {
|
|
415
|
+
throw new Error("Document data must be a non-empty object");
|
|
416
|
+
}
|
|
417
|
+
const collection = this.firestore.collection(this.collectionPath);
|
|
418
|
+
const docRef = id ? collection.doc(id) : collection.doc();
|
|
419
|
+
await docRef.set(data);
|
|
420
|
+
return { success: true, id: docRef.id };
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Run aggregate queries (sum, average, count) on matching documents
|
|
424
|
+
* @param spec - Aggregate specification defining operations and fields
|
|
425
|
+
* @returns Object with alias keys and numeric results
|
|
426
|
+
*/
|
|
427
|
+
async aggregate(spec) {
|
|
428
|
+
this.validateSetup();
|
|
429
|
+
if (!spec || Object.keys(spec).length === 0) {
|
|
430
|
+
throw new Error("Aggregate spec must be a non-empty object");
|
|
431
|
+
}
|
|
432
|
+
const query = this.buildQuery();
|
|
433
|
+
const aggregateFields = {};
|
|
434
|
+
for (const [alias, definition] of Object.entries(spec)) {
|
|
435
|
+
switch (definition.op) {
|
|
436
|
+
case "sum":
|
|
437
|
+
if (!definition.field) {
|
|
438
|
+
throw new Error(`Field is required for sum operation (alias: ${alias})`);
|
|
439
|
+
}
|
|
440
|
+
aggregateFields[alias] = import_firestore.AggregateField.sum(definition.field);
|
|
441
|
+
break;
|
|
442
|
+
case "average":
|
|
443
|
+
if (!definition.field) {
|
|
444
|
+
throw new Error(`Field is required for average operation (alias: ${alias})`);
|
|
445
|
+
}
|
|
446
|
+
aggregateFields[alias] = import_firestore.AggregateField.average(definition.field);
|
|
447
|
+
break;
|
|
448
|
+
case "count":
|
|
449
|
+
aggregateFields[alias] = import_firestore.AggregateField.count();
|
|
450
|
+
break;
|
|
451
|
+
default:
|
|
452
|
+
throw new Error(`Unknown aggregate operation: ${definition.op}`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
const snapshot = await query.aggregate(aggregateFields).get();
|
|
456
|
+
const data = snapshot.data();
|
|
457
|
+
const result = {};
|
|
458
|
+
for (const alias of Object.keys(spec)) {
|
|
459
|
+
result[alias] = data[alias] ?? null;
|
|
460
|
+
}
|
|
461
|
+
return result;
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Get documents with cursor-based pagination
|
|
465
|
+
* @param options - Pagination options (pageSize, startAfter cursor)
|
|
466
|
+
* @returns Page of documents with cursor for next page
|
|
467
|
+
*/
|
|
468
|
+
async paginate(options) {
|
|
469
|
+
this.validateSetup();
|
|
470
|
+
if (!options.pageSize || options.pageSize <= 0) {
|
|
471
|
+
throw new Error("pageSize must be a positive number");
|
|
472
|
+
}
|
|
473
|
+
let query = this.buildQuery().limit(options.pageSize + 1);
|
|
474
|
+
if (options.startAfter) {
|
|
475
|
+
query = query.startAfter(options.startAfter);
|
|
476
|
+
}
|
|
477
|
+
const snapshot = await query.get();
|
|
478
|
+
const hasMore = snapshot.docs.length > options.pageSize;
|
|
479
|
+
const docs = snapshot.docs.slice(0, options.pageSize).map((doc) => ({
|
|
480
|
+
id: doc.id,
|
|
481
|
+
data: doc.data()
|
|
482
|
+
}));
|
|
483
|
+
const lastDoc = snapshot.docs.length > 0 ? snapshot.docs[Math.min(snapshot.docs.length - 1, options.pageSize - 1)] : null;
|
|
484
|
+
return {
|
|
485
|
+
docs,
|
|
486
|
+
nextCursor: hasMore ? lastDoc : null,
|
|
487
|
+
hasMore
|
|
488
|
+
};
|
|
489
|
+
}
|
|
366
490
|
/**
|
|
367
491
|
* Preview changes before executing update
|
|
368
492
|
* @param updateData - Data to update
|
|
@@ -629,6 +753,81 @@ var BatchUpdater = class {
|
|
|
629
753
|
}
|
|
630
754
|
return result;
|
|
631
755
|
}
|
|
756
|
+
/**
|
|
757
|
+
* Update multiple documents with different data for each
|
|
758
|
+
* @param updates - Array of { id, data } objects specifying updates for each document
|
|
759
|
+
* @param options - Bulk update options (e.g., progress callback, log options)
|
|
760
|
+
* @returns Bulk update result with success/failure counts and optional log file path
|
|
761
|
+
*/
|
|
762
|
+
async bulkUpdate(updates, options = {}) {
|
|
763
|
+
this.validateSetup();
|
|
764
|
+
if (this.isCollectionGroup) {
|
|
765
|
+
throw new Error(
|
|
766
|
+
"bulkUpdate() cannot be used with collectionGroup(). Use collection() with a specific path instead."
|
|
767
|
+
);
|
|
768
|
+
}
|
|
769
|
+
if (!Array.isArray(updates) || updates.length === 0) {
|
|
770
|
+
throw new Error("Updates array must be non-empty");
|
|
771
|
+
}
|
|
772
|
+
for (const update of updates) {
|
|
773
|
+
if (!update.id || typeof update.id !== "string") {
|
|
774
|
+
throw new Error("Each update must have a valid id");
|
|
775
|
+
}
|
|
776
|
+
if (!isValidUpdateData(update.data)) {
|
|
777
|
+
throw new Error("Each update must have valid data");
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const totalCount = updates.length;
|
|
781
|
+
let successCount = 0;
|
|
782
|
+
let failureCount = 0;
|
|
783
|
+
const failedDocIds = [];
|
|
784
|
+
const logCollector = options.log?.enabled ? createLogCollector("update", this.collectionPath) : null;
|
|
785
|
+
const bulkWriter = this.firestore.bulkWriter();
|
|
786
|
+
const collection = this.firestore.collection(this.collectionPath);
|
|
787
|
+
let processedCount = 0;
|
|
788
|
+
const docIdMap = /* @__PURE__ */ new Map();
|
|
789
|
+
for (const update of updates) {
|
|
790
|
+
const docRef = collection.doc(update.id);
|
|
791
|
+
docIdMap.set(docRef.path, update.id);
|
|
792
|
+
}
|
|
793
|
+
bulkWriter.onWriteResult((ref) => {
|
|
794
|
+
successCount++;
|
|
795
|
+
processedCount++;
|
|
796
|
+
const docId = docIdMap.get(ref.path) || ref.id;
|
|
797
|
+
logCollector?.addEntry(docId, "success");
|
|
798
|
+
if (options.onProgress) {
|
|
799
|
+
const progress = calculateProgress(processedCount, totalCount);
|
|
800
|
+
options.onProgress(progress);
|
|
801
|
+
}
|
|
802
|
+
});
|
|
803
|
+
bulkWriter.onWriteError((error) => {
|
|
804
|
+
failureCount++;
|
|
805
|
+
processedCount++;
|
|
806
|
+
const docId = error.documentRef?.id || "unknown";
|
|
807
|
+
failedDocIds.push(docId);
|
|
808
|
+
logCollector?.addEntry(docId, "failure", error.message);
|
|
809
|
+
if (options.onProgress) {
|
|
810
|
+
const progress = calculateProgress(processedCount, totalCount);
|
|
811
|
+
options.onProgress(progress);
|
|
812
|
+
}
|
|
813
|
+
return false;
|
|
814
|
+
});
|
|
815
|
+
for (const update of updates) {
|
|
816
|
+
const docRef = collection.doc(update.id);
|
|
817
|
+
bulkWriter.update(docRef, update.data);
|
|
818
|
+
}
|
|
819
|
+
await bulkWriter.close();
|
|
820
|
+
const result = {
|
|
821
|
+
successCount,
|
|
822
|
+
failureCount,
|
|
823
|
+
totalCount,
|
|
824
|
+
failedDocIds: failedDocIds.length > 0 ? failedDocIds : void 0
|
|
825
|
+
};
|
|
826
|
+
if (logCollector && options.log) {
|
|
827
|
+
result.logFilePath = logCollector.finalize(options.log);
|
|
828
|
+
}
|
|
829
|
+
return result;
|
|
830
|
+
}
|
|
632
831
|
/**
|
|
633
832
|
* Upsert documents matching query conditions
|
|
634
833
|
* Updates existing documents or creates them if they don't exist
|
|
@@ -973,7 +1172,7 @@ var BatchUpdater = class {
|
|
|
973
1172
|
};
|
|
974
1173
|
|
|
975
1174
|
// src/index.ts
|
|
976
|
-
var
|
|
1175
|
+
var import_firestore2 = require("firebase-admin/firestore");
|
|
977
1176
|
// Annotate the CommonJS export names for ESM import in node:
|
|
978
1177
|
0 && (module.exports = {
|
|
979
1178
|
BatchUpdater,
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
// src/core/batch-updater.ts
|
|
2
|
+
import { AggregateField } from "firebase-admin/firestore";
|
|
3
|
+
|
|
1
4
|
// src/utils/logger.ts
|
|
2
5
|
import * as fs from "fs";
|
|
3
6
|
import * as path from "path";
|
|
@@ -284,6 +287,38 @@ var BatchUpdater = class {
|
|
|
284
287
|
data: doc.data()
|
|
285
288
|
}));
|
|
286
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Get a document by its ID directly (faster than findOne with where)
|
|
292
|
+
* @param id - Document ID
|
|
293
|
+
* @returns Document with id and data, or null if not found
|
|
294
|
+
*/
|
|
295
|
+
async getOne(id) {
|
|
296
|
+
this.validateSetup();
|
|
297
|
+
if (this.isCollectionGroup) {
|
|
298
|
+
throw new Error(
|
|
299
|
+
"getOne() cannot be used with collectionGroup(). Use findOne() with where conditions instead."
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
const docRef = this.firestore.collection(this.collectionPath).doc(id);
|
|
303
|
+
let docSnapshot;
|
|
304
|
+
if (this.selectedFields && this.selectedFields.length > 0) {
|
|
305
|
+
const query = this.firestore.collection(this.collectionPath).where("__name__", "==", docRef).select(...this.selectedFields);
|
|
306
|
+
const snapshot = await query.get();
|
|
307
|
+
if (snapshot.empty) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
docSnapshot = snapshot.docs[0];
|
|
311
|
+
} else {
|
|
312
|
+
docSnapshot = await docRef.get();
|
|
313
|
+
if (!docSnapshot.exists) {
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
id: docSnapshot.id,
|
|
319
|
+
data: docSnapshot.data()
|
|
320
|
+
};
|
|
321
|
+
}
|
|
287
322
|
/**
|
|
288
323
|
* Update the first document matching the query conditions
|
|
289
324
|
* @param updateData - Data to update
|
|
@@ -318,6 +353,95 @@ var BatchUpdater = class {
|
|
|
318
353
|
await doc.ref.delete();
|
|
319
354
|
return { success: true, id: doc.id };
|
|
320
355
|
}
|
|
356
|
+
/**
|
|
357
|
+
* Create a single document in the collection
|
|
358
|
+
* @param data - Document data
|
|
359
|
+
* @param id - Optional document ID (auto-generated if not provided)
|
|
360
|
+
* @returns Result with success status and document id
|
|
361
|
+
*/
|
|
362
|
+
async createOne(data, id) {
|
|
363
|
+
this.validateSetup();
|
|
364
|
+
if (this.isCollectionGroup) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
"createOne() cannot be used with collectionGroup(). Use collection() with a specific path instead."
|
|
367
|
+
);
|
|
368
|
+
}
|
|
369
|
+
if (!isValidUpdateData(data)) {
|
|
370
|
+
throw new Error("Document data must be a non-empty object");
|
|
371
|
+
}
|
|
372
|
+
const collection = this.firestore.collection(this.collectionPath);
|
|
373
|
+
const docRef = id ? collection.doc(id) : collection.doc();
|
|
374
|
+
await docRef.set(data);
|
|
375
|
+
return { success: true, id: docRef.id };
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Run aggregate queries (sum, average, count) on matching documents
|
|
379
|
+
* @param spec - Aggregate specification defining operations and fields
|
|
380
|
+
* @returns Object with alias keys and numeric results
|
|
381
|
+
*/
|
|
382
|
+
async aggregate(spec) {
|
|
383
|
+
this.validateSetup();
|
|
384
|
+
if (!spec || Object.keys(spec).length === 0) {
|
|
385
|
+
throw new Error("Aggregate spec must be a non-empty object");
|
|
386
|
+
}
|
|
387
|
+
const query = this.buildQuery();
|
|
388
|
+
const aggregateFields = {};
|
|
389
|
+
for (const [alias, definition] of Object.entries(spec)) {
|
|
390
|
+
switch (definition.op) {
|
|
391
|
+
case "sum":
|
|
392
|
+
if (!definition.field) {
|
|
393
|
+
throw new Error(`Field is required for sum operation (alias: ${alias})`);
|
|
394
|
+
}
|
|
395
|
+
aggregateFields[alias] = AggregateField.sum(definition.field);
|
|
396
|
+
break;
|
|
397
|
+
case "average":
|
|
398
|
+
if (!definition.field) {
|
|
399
|
+
throw new Error(`Field is required for average operation (alias: ${alias})`);
|
|
400
|
+
}
|
|
401
|
+
aggregateFields[alias] = AggregateField.average(definition.field);
|
|
402
|
+
break;
|
|
403
|
+
case "count":
|
|
404
|
+
aggregateFields[alias] = AggregateField.count();
|
|
405
|
+
break;
|
|
406
|
+
default:
|
|
407
|
+
throw new Error(`Unknown aggregate operation: ${definition.op}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
const snapshot = await query.aggregate(aggregateFields).get();
|
|
411
|
+
const data = snapshot.data();
|
|
412
|
+
const result = {};
|
|
413
|
+
for (const alias of Object.keys(spec)) {
|
|
414
|
+
result[alias] = data[alias] ?? null;
|
|
415
|
+
}
|
|
416
|
+
return result;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Get documents with cursor-based pagination
|
|
420
|
+
* @param options - Pagination options (pageSize, startAfter cursor)
|
|
421
|
+
* @returns Page of documents with cursor for next page
|
|
422
|
+
*/
|
|
423
|
+
async paginate(options) {
|
|
424
|
+
this.validateSetup();
|
|
425
|
+
if (!options.pageSize || options.pageSize <= 0) {
|
|
426
|
+
throw new Error("pageSize must be a positive number");
|
|
427
|
+
}
|
|
428
|
+
let query = this.buildQuery().limit(options.pageSize + 1);
|
|
429
|
+
if (options.startAfter) {
|
|
430
|
+
query = query.startAfter(options.startAfter);
|
|
431
|
+
}
|
|
432
|
+
const snapshot = await query.get();
|
|
433
|
+
const hasMore = snapshot.docs.length > options.pageSize;
|
|
434
|
+
const docs = snapshot.docs.slice(0, options.pageSize).map((doc) => ({
|
|
435
|
+
id: doc.id,
|
|
436
|
+
data: doc.data()
|
|
437
|
+
}));
|
|
438
|
+
const lastDoc = snapshot.docs.length > 0 ? snapshot.docs[Math.min(snapshot.docs.length - 1, options.pageSize - 1)] : null;
|
|
439
|
+
return {
|
|
440
|
+
docs,
|
|
441
|
+
nextCursor: hasMore ? lastDoc : null,
|
|
442
|
+
hasMore
|
|
443
|
+
};
|
|
444
|
+
}
|
|
321
445
|
/**
|
|
322
446
|
* Preview changes before executing update
|
|
323
447
|
* @param updateData - Data to update
|
|
@@ -584,6 +708,81 @@ var BatchUpdater = class {
|
|
|
584
708
|
}
|
|
585
709
|
return result;
|
|
586
710
|
}
|
|
711
|
+
/**
|
|
712
|
+
* Update multiple documents with different data for each
|
|
713
|
+
* @param updates - Array of { id, data } objects specifying updates for each document
|
|
714
|
+
* @param options - Bulk update options (e.g., progress callback, log options)
|
|
715
|
+
* @returns Bulk update result with success/failure counts and optional log file path
|
|
716
|
+
*/
|
|
717
|
+
async bulkUpdate(updates, options = {}) {
|
|
718
|
+
this.validateSetup();
|
|
719
|
+
if (this.isCollectionGroup) {
|
|
720
|
+
throw new Error(
|
|
721
|
+
"bulkUpdate() cannot be used with collectionGroup(). Use collection() with a specific path instead."
|
|
722
|
+
);
|
|
723
|
+
}
|
|
724
|
+
if (!Array.isArray(updates) || updates.length === 0) {
|
|
725
|
+
throw new Error("Updates array must be non-empty");
|
|
726
|
+
}
|
|
727
|
+
for (const update of updates) {
|
|
728
|
+
if (!update.id || typeof update.id !== "string") {
|
|
729
|
+
throw new Error("Each update must have a valid id");
|
|
730
|
+
}
|
|
731
|
+
if (!isValidUpdateData(update.data)) {
|
|
732
|
+
throw new Error("Each update must have valid data");
|
|
733
|
+
}
|
|
734
|
+
}
|
|
735
|
+
const totalCount = updates.length;
|
|
736
|
+
let successCount = 0;
|
|
737
|
+
let failureCount = 0;
|
|
738
|
+
const failedDocIds = [];
|
|
739
|
+
const logCollector = options.log?.enabled ? createLogCollector("update", this.collectionPath) : null;
|
|
740
|
+
const bulkWriter = this.firestore.bulkWriter();
|
|
741
|
+
const collection = this.firestore.collection(this.collectionPath);
|
|
742
|
+
let processedCount = 0;
|
|
743
|
+
const docIdMap = /* @__PURE__ */ new Map();
|
|
744
|
+
for (const update of updates) {
|
|
745
|
+
const docRef = collection.doc(update.id);
|
|
746
|
+
docIdMap.set(docRef.path, update.id);
|
|
747
|
+
}
|
|
748
|
+
bulkWriter.onWriteResult((ref) => {
|
|
749
|
+
successCount++;
|
|
750
|
+
processedCount++;
|
|
751
|
+
const docId = docIdMap.get(ref.path) || ref.id;
|
|
752
|
+
logCollector?.addEntry(docId, "success");
|
|
753
|
+
if (options.onProgress) {
|
|
754
|
+
const progress = calculateProgress(processedCount, totalCount);
|
|
755
|
+
options.onProgress(progress);
|
|
756
|
+
}
|
|
757
|
+
});
|
|
758
|
+
bulkWriter.onWriteError((error) => {
|
|
759
|
+
failureCount++;
|
|
760
|
+
processedCount++;
|
|
761
|
+
const docId = error.documentRef?.id || "unknown";
|
|
762
|
+
failedDocIds.push(docId);
|
|
763
|
+
logCollector?.addEntry(docId, "failure", error.message);
|
|
764
|
+
if (options.onProgress) {
|
|
765
|
+
const progress = calculateProgress(processedCount, totalCount);
|
|
766
|
+
options.onProgress(progress);
|
|
767
|
+
}
|
|
768
|
+
return false;
|
|
769
|
+
});
|
|
770
|
+
for (const update of updates) {
|
|
771
|
+
const docRef = collection.doc(update.id);
|
|
772
|
+
bulkWriter.update(docRef, update.data);
|
|
773
|
+
}
|
|
774
|
+
await bulkWriter.close();
|
|
775
|
+
const result = {
|
|
776
|
+
successCount,
|
|
777
|
+
failureCount,
|
|
778
|
+
totalCount,
|
|
779
|
+
failedDocIds: failedDocIds.length > 0 ? failedDocIds : void 0
|
|
780
|
+
};
|
|
781
|
+
if (logCollector && options.log) {
|
|
782
|
+
result.logFilePath = logCollector.finalize(options.log);
|
|
783
|
+
}
|
|
784
|
+
return result;
|
|
785
|
+
}
|
|
587
786
|
/**
|
|
588
787
|
* Upsert documents matching query conditions
|
|
589
788
|
* Updates existing documents or creates them if they don't exist
|