firestore-batch-updater 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.ko.md +334 -0
- package/README.md +334 -0
- package/dist/index.d.mts +316 -0
- package/dist/index.d.ts +316 -0
- package/dist/index.js +661 -0
- package/dist/index.mjs +616 -0
- package/package.json +55 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 exiivy
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.ko.md
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# Firestore Batch Updater
|
|
2
|
+
|
|
3
|
+
쿼리 기반 필터링과 진행 상황 추적 기능을 제공하는 Firebase Firestore 대량 업데이트 라이브러리입니다.
|
|
4
|
+
|
|
5
|
+
[English](./README.md) | 한국어
|
|
6
|
+
|
|
7
|
+
## 주요 기능
|
|
8
|
+
|
|
9
|
+
- 쿼리 기반 업데이트 - `where()` 조건으로 문서 필터링
|
|
10
|
+
- 500개 제한 없음 - Firebase Admin SDK의 BulkWriter 활용
|
|
11
|
+
- 변경 사항 미리보기 - 업데이트 전 Before/After 비교
|
|
12
|
+
- 진행 상황 추적 - 실시간 진행률 콜백
|
|
13
|
+
- 일괄 생성/Upsert - 여러 문서를 한 번에 생성 또는 upsert
|
|
14
|
+
- 로그 파일 생성 - 감사를 위한 상세 작업 로그 (선택사항)
|
|
15
|
+
|
|
16
|
+
## 설치
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# npm
|
|
20
|
+
npm install firestore-batch-updater
|
|
21
|
+
|
|
22
|
+
# yarn
|
|
23
|
+
yarn add firestore-batch-updater
|
|
24
|
+
|
|
25
|
+
# pnpm
|
|
26
|
+
pnpm add firestore-batch-updater
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**필수 peer dependency:**
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# npm
|
|
33
|
+
npm install firebase-admin
|
|
34
|
+
|
|
35
|
+
# yarn
|
|
36
|
+
yarn add firebase-admin
|
|
37
|
+
|
|
38
|
+
# pnpm
|
|
39
|
+
pnpm add firebase-admin
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 빠른 시작
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { BatchUpdater } from "firestore-batch-updater";
|
|
46
|
+
import { getFirestore } from "firebase-admin/firestore";
|
|
47
|
+
|
|
48
|
+
const firestore = getFirestore();
|
|
49
|
+
const updater = new BatchUpdater(firestore);
|
|
50
|
+
|
|
51
|
+
// 변경 사항 미리보기
|
|
52
|
+
const preview = await updater
|
|
53
|
+
.collection("users")
|
|
54
|
+
.where("status", "==", "inactive")
|
|
55
|
+
.preview({ status: "archived" });
|
|
56
|
+
|
|
57
|
+
console.log(`${preview.affectedCount}개 문서가 영향을 받습니다`);
|
|
58
|
+
|
|
59
|
+
// 업데이트 실행
|
|
60
|
+
const result = await updater
|
|
61
|
+
.collection("users")
|
|
62
|
+
.where("status", "==", "inactive")
|
|
63
|
+
.update({ status: "archived" });
|
|
64
|
+
|
|
65
|
+
console.log(`${result.successCount}개 문서 업데이트 완료`);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## API 레퍼런스
|
|
69
|
+
|
|
70
|
+
### 메서드 개요
|
|
71
|
+
|
|
72
|
+
| 메서드 | 설명 | 반환값 |
|
|
73
|
+
|--------|------|--------|
|
|
74
|
+
| `collection(path)` | 작업할 컬렉션 선택 | `this` |
|
|
75
|
+
| `where(field, op, value)` | 필터 조건 추가 (체이닝 가능) | `this` |
|
|
76
|
+
| `preview(data)` | 업데이트 전 미리보기 | `PreviewResult` |
|
|
77
|
+
| `update(data, options?)` | 매칭되는 문서 업데이트 | `UpdateResult` |
|
|
78
|
+
| `create(docs, options?)` | 새 문서 생성 | `CreateResult` |
|
|
79
|
+
| `upsert(data, options?)` | 업데이트 또는 생성 (set with merge) | `UpsertResult` |
|
|
80
|
+
| `getFields(field)` | 특정 필드 값 조회 | `FieldValue[]` |
|
|
81
|
+
|
|
82
|
+
### 옵션
|
|
83
|
+
|
|
84
|
+
모든 쓰기 작업은 선택적 `options` 매개변수를 지원합니다:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
{
|
|
88
|
+
onProgress?: (progress: ProgressInfo) => void;
|
|
89
|
+
log?: LogOptions;
|
|
90
|
+
batchSize?: number; // update/upsert 전용
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ProgressInfo
|
|
94
|
+
{
|
|
95
|
+
current: number; // 처리된 문서 수
|
|
96
|
+
total: number; // 전체 문서 수
|
|
97
|
+
percentage: number; // 0-100
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// LogOptions
|
|
101
|
+
{
|
|
102
|
+
enabled: boolean; // 로그 파일 생성 여부
|
|
103
|
+
path?: string; // 로그 디렉토리 경로 (기본값: ./logs)
|
|
104
|
+
filename?: string; // 파일명 (기본값: 자동 생성)
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**batchSize 옵션 (대용량 컬렉션용):**
|
|
109
|
+
- 미설정: 모든 문서를 메모리에 한 번에 로드 (소규모 컬렉션에 적합)
|
|
110
|
+
- 설정 시 (예: `batchSize: 1000`): 커서 페이지네이션을 사용하여 배치 단위로 처리 (대규모 컬렉션의 메모리 문제 방지)
|
|
111
|
+
|
|
112
|
+
### 반환 타입
|
|
113
|
+
|
|
114
|
+
| 타입 | 필드 |
|
|
115
|
+
|------|------|
|
|
116
|
+
| `PreviewResult` | `affectedCount`, `samples[]`, `affectedFields[]` |
|
|
117
|
+
| `UpdateResult` | `successCount`, `failureCount`, `totalCount`, `failedDocIds?`, `logFilePath?` |
|
|
118
|
+
| `CreateResult` | `successCount`, `failureCount`, `totalCount`, `createdIds[]`, `failedDocIds?`, `logFilePath?` |
|
|
119
|
+
| `UpsertResult` | `successCount`, `failureCount`, `totalCount`, `failedDocIds?`, `logFilePath?` |
|
|
120
|
+
| `FieldValue` | `id`, `value` |
|
|
121
|
+
|
|
122
|
+
## 사용 예시
|
|
123
|
+
|
|
124
|
+
### 문서 업데이트
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
const result = await updater
|
|
128
|
+
.collection("users")
|
|
129
|
+
.where("status", "==", "inactive")
|
|
130
|
+
.update({ status: "archived" });
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### 문서 생성
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// 자동 생성 ID
|
|
137
|
+
const result = await updater.collection("users").create([
|
|
138
|
+
{ data: { name: "Alice", age: 30 } },
|
|
139
|
+
{ data: { name: "Bob", age: 25 } },
|
|
140
|
+
]);
|
|
141
|
+
console.log("생성된 ID:", result.createdIds);
|
|
142
|
+
|
|
143
|
+
// 지정 ID
|
|
144
|
+
const result2 = await updater.collection("users").create([
|
|
145
|
+
{ id: "user-001", data: { name: "Charlie" } },
|
|
146
|
+
{ id: "user-002", data: { name: "Diana" } },
|
|
147
|
+
]);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### 문서 Upsert
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
const result = await updater
|
|
154
|
+
.collection("users")
|
|
155
|
+
.where("status", "==", "active")
|
|
156
|
+
.upsert({ tier: "premium", updatedAt: new Date() });
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### 업데이트 전 미리보기
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
const preview = await updater
|
|
163
|
+
.collection("orders")
|
|
164
|
+
.where("status", "==", "pending")
|
|
165
|
+
.preview({ status: "cancelled" });
|
|
166
|
+
|
|
167
|
+
if (preview.affectedCount > 1000) {
|
|
168
|
+
console.log("문서가 너무 많습니다. 중단합니다.");
|
|
169
|
+
} else {
|
|
170
|
+
await updater
|
|
171
|
+
.collection("orders")
|
|
172
|
+
.where("status", "==", "pending")
|
|
173
|
+
.update({ status: "cancelled" });
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### 진행 상황 추적
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const result = await updater
|
|
181
|
+
.collection("products")
|
|
182
|
+
.where("inStock", "==", false)
|
|
183
|
+
.update(
|
|
184
|
+
{ status: "discontinued" },
|
|
185
|
+
{
|
|
186
|
+
onProgress: (progress) => {
|
|
187
|
+
console.log(`${progress.percentage}% 완료`);
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 필드 값 조회
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
const emails = await updater
|
|
197
|
+
.collection("users")
|
|
198
|
+
.where("status", "==", "active")
|
|
199
|
+
.getFields("email");
|
|
200
|
+
|
|
201
|
+
// [{ id: 'user1', value: 'user1@example.com' }, ...]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### 다중 조건
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
const ninetyDaysAgo = new Date();
|
|
208
|
+
ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
|
|
209
|
+
|
|
210
|
+
const result = await updater
|
|
211
|
+
.collection("users")
|
|
212
|
+
.where("status", "==", "inactive")
|
|
213
|
+
.where("lastLoginAt", "<", ninetyDaysAgo)
|
|
214
|
+
.where("accountType", "==", "free")
|
|
215
|
+
.update({ status: "archived" });
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
> **참고:** 서로 다른 필드에 여러 `where()` 조건을 사용할 경우, Firestore에서 [복합 인덱스](https://firebase.google.com/docs/firestore/query-data/indexing)가 필요할 수 있습니다. `FAILED_PRECONDITION` 오류가 발생하면 오류 메시지의 링크를 통해 필요한 인덱스를 생성하세요.
|
|
219
|
+
|
|
220
|
+
### 에러 처리
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
const result = await updater
|
|
224
|
+
.collection("users")
|
|
225
|
+
.where("status", "==", "test")
|
|
226
|
+
.update({ status: "verified" });
|
|
227
|
+
|
|
228
|
+
if (result.failureCount > 0) {
|
|
229
|
+
console.log(`${result.failureCount}개 문서 실패`);
|
|
230
|
+
console.log("실패한 ID:", result.failedDocIds);
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### 대용량 컬렉션 페이지네이션
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
// 메모리 문제 방지를 위해 1000개씩 배치 처리
|
|
238
|
+
const result = await updater
|
|
239
|
+
.collection("users")
|
|
240
|
+
.where("status", "==", "inactive")
|
|
241
|
+
.update(
|
|
242
|
+
{ status: "archived" },
|
|
243
|
+
{
|
|
244
|
+
batchSize: 1000,
|
|
245
|
+
onProgress: (progress) => {
|
|
246
|
+
console.log(`${progress.percentage}% 완료`);
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### 로그 파일 생성
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
const result = await updater
|
|
256
|
+
.collection("users")
|
|
257
|
+
.where("status", "==", "inactive")
|
|
258
|
+
.update(
|
|
259
|
+
{ status: "archived" },
|
|
260
|
+
{
|
|
261
|
+
log: {
|
|
262
|
+
enabled: true,
|
|
263
|
+
path: "./logs", // 선택사항
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
if (result.logFilePath) {
|
|
269
|
+
console.log(`로그 저장 경로: ${result.logFilePath}`);
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
로그 파일 예시:
|
|
274
|
+
```
|
|
275
|
+
============================================================
|
|
276
|
+
FIRESTORE BATCH OPERATION LOG
|
|
277
|
+
============================================================
|
|
278
|
+
|
|
279
|
+
Operation: UPDATE
|
|
280
|
+
Collection: users
|
|
281
|
+
Started: 2024-01-15T10:30:00.000Z
|
|
282
|
+
Completed: 2024-01-15T10:30:05.000Z
|
|
283
|
+
|
|
284
|
+
Conditions:
|
|
285
|
+
- status == "inactive"
|
|
286
|
+
|
|
287
|
+
============================================================
|
|
288
|
+
SUMMARY
|
|
289
|
+
============================================================
|
|
290
|
+
Total: 150
|
|
291
|
+
Success: 148
|
|
292
|
+
Failure: 2
|
|
293
|
+
|
|
294
|
+
============================================================
|
|
295
|
+
DETAILS
|
|
296
|
+
============================================================
|
|
297
|
+
|
|
298
|
+
2024-01-15T10:30:01.000Z [SUCCESS] user-001
|
|
299
|
+
2024-01-15T10:30:01.100Z [SUCCESS] user-002
|
|
300
|
+
2024-01-15T10:30:01.200Z [FAILURE] user-003
|
|
301
|
+
Error: Document not found
|
|
302
|
+
...
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## 요구 사항
|
|
306
|
+
|
|
307
|
+
- Node.js 18+
|
|
308
|
+
- Firebase Admin SDK 13.x
|
|
309
|
+
- 서버 사이드 환경 전용 (Admin SDK 필요)
|
|
310
|
+
|
|
311
|
+
## BulkWriter를 사용하는 이유?
|
|
312
|
+
|
|
313
|
+
이 라이브러리는 Firebase의 `BulkWriter`를 사용합니다:
|
|
314
|
+
|
|
315
|
+
- 500개 문서 제한 없음 (배치 쓰기와 달리)
|
|
316
|
+
- 자동 속도 제한
|
|
317
|
+
- 내장 재시도 로직
|
|
318
|
+
- 대규모 작업에 더 나은 성능
|
|
319
|
+
|
|
320
|
+
## 예제
|
|
321
|
+
|
|
322
|
+
더 자세한 예제는 [examples](./examples) 폴더를 확인하세요:
|
|
323
|
+
|
|
324
|
+
- [basic.ts](./examples/basic.ts) - 기본 사용 워크플로우
|
|
325
|
+
- [api-route.ts](./examples/api-route.ts) - API 엔드포인트에서 사용하기
|
|
326
|
+
- [advanced.ts](./examples/advanced.ts) - 고급 기능 및 패턴
|
|
327
|
+
|
|
328
|
+
## 면책 조항
|
|
329
|
+
|
|
330
|
+
이 패키지는 별도의 보증 없이 제공되며, 사용으로 인해 발생하는 데이터 손실, 손상 또는 기타 문제에 대해 작성자는 책임지지 않습니다. 프로덕션 환경에서 사용하기 전에 반드시 개발 환경에서 충분히 테스트하고, 데이터 백업을 확보하시기 바랍니다.
|
|
331
|
+
|
|
332
|
+
## 라이선스
|
|
333
|
+
|
|
334
|
+
MIT
|
package/README.md
ADDED
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
# Firestore Batch Updater
|
|
2
|
+
|
|
3
|
+
Easy batch updates for Firebase Firestore with query-based filtering and progress tracking.
|
|
4
|
+
|
|
5
|
+
English | [한국어](./README.ko.md)
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Query-based updates - Filter documents with `where()` conditions
|
|
10
|
+
- No 500 document limit - Uses Firebase Admin SDK's BulkWriter
|
|
11
|
+
- Preview changes - See before/after comparison before updating
|
|
12
|
+
- Progress tracking - Real-time progress callbacks
|
|
13
|
+
- Batch create/upsert - Create or upsert multiple documents at once
|
|
14
|
+
- Log file generation - Optional detailed operation logs for auditing
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
# npm
|
|
20
|
+
npm install firestore-batch-updater
|
|
21
|
+
|
|
22
|
+
# yarn
|
|
23
|
+
yarn add firestore-batch-updater
|
|
24
|
+
|
|
25
|
+
# pnpm
|
|
26
|
+
pnpm add firestore-batch-updater
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
**Required peer dependency:**
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
# npm
|
|
33
|
+
npm install firebase-admin
|
|
34
|
+
|
|
35
|
+
# yarn
|
|
36
|
+
yarn add firebase-admin
|
|
37
|
+
|
|
38
|
+
# pnpm
|
|
39
|
+
pnpm add firebase-admin
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Quick Start
|
|
43
|
+
|
|
44
|
+
```typescript
|
|
45
|
+
import { BatchUpdater } from "firestore-batch-updater";
|
|
46
|
+
import { getFirestore } from "firebase-admin/firestore";
|
|
47
|
+
|
|
48
|
+
const firestore = getFirestore();
|
|
49
|
+
const updater = new BatchUpdater(firestore);
|
|
50
|
+
|
|
51
|
+
// Preview changes
|
|
52
|
+
const preview = await updater
|
|
53
|
+
.collection("users")
|
|
54
|
+
.where("status", "==", "inactive")
|
|
55
|
+
.preview({ status: "archived" });
|
|
56
|
+
|
|
57
|
+
console.log(`Will affect ${preview.affectedCount} documents`);
|
|
58
|
+
|
|
59
|
+
// Execute update
|
|
60
|
+
const result = await updater
|
|
61
|
+
.collection("users")
|
|
62
|
+
.where("status", "==", "inactive")
|
|
63
|
+
.update({ status: "archived" });
|
|
64
|
+
|
|
65
|
+
console.log(`Updated ${result.successCount} documents`);
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## API Reference
|
|
69
|
+
|
|
70
|
+
### Methods Overview
|
|
71
|
+
|
|
72
|
+
| Method | Description | Returns |
|
|
73
|
+
|--------|-------------|---------|
|
|
74
|
+
| `collection(path)` | Select collection to operate on | `this` |
|
|
75
|
+
| `where(field, op, value)` | Add filter condition (chainable) | `this` |
|
|
76
|
+
| `preview(data)` | Preview changes before update | `PreviewResult` |
|
|
77
|
+
| `update(data, options?)` | Update matching documents | `UpdateResult` |
|
|
78
|
+
| `create(docs, options?)` | Create new documents | `CreateResult` |
|
|
79
|
+
| `upsert(data, options?)` | Update or create (set with merge) | `UpsertResult` |
|
|
80
|
+
| `getFields(field)` | Get specific field values | `FieldValue[]` |
|
|
81
|
+
|
|
82
|
+
### Options
|
|
83
|
+
|
|
84
|
+
All write operations support an optional `options` parameter:
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
{
|
|
88
|
+
onProgress?: (progress: ProgressInfo) => void;
|
|
89
|
+
log?: LogOptions;
|
|
90
|
+
batchSize?: number; // For update/upsert only
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ProgressInfo
|
|
94
|
+
{
|
|
95
|
+
current: number; // Documents processed
|
|
96
|
+
total: number; // Total documents
|
|
97
|
+
percentage: number; // 0-100
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// LogOptions
|
|
101
|
+
{
|
|
102
|
+
enabled: boolean; // Enable log file generation
|
|
103
|
+
path?: string; // Custom log directory (default: ./logs)
|
|
104
|
+
filename?: string; // Custom filename (default: auto-generated)
|
|
105
|
+
}
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**batchSize option (for large collections):**
|
|
109
|
+
- When not set: All documents are loaded into memory at once (suitable for small collections)
|
|
110
|
+
- When set (e.g., `batchSize: 1000`): Documents are processed in batches using cursor pagination (suitable for large collections to prevent memory issues)
|
|
111
|
+
|
|
112
|
+
### Return Types
|
|
113
|
+
|
|
114
|
+
| Type | Fields |
|
|
115
|
+
|------|--------|
|
|
116
|
+
| `PreviewResult` | `affectedCount`, `samples[]`, `affectedFields[]` |
|
|
117
|
+
| `UpdateResult` | `successCount`, `failureCount`, `totalCount`, `failedDocIds?`, `logFilePath?` |
|
|
118
|
+
| `CreateResult` | `successCount`, `failureCount`, `totalCount`, `createdIds[]`, `failedDocIds?`, `logFilePath?` |
|
|
119
|
+
| `UpsertResult` | `successCount`, `failureCount`, `totalCount`, `failedDocIds?`, `logFilePath?` |
|
|
120
|
+
| `FieldValue` | `id`, `value` |
|
|
121
|
+
|
|
122
|
+
## Usage Examples
|
|
123
|
+
|
|
124
|
+
### Update Documents
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
const result = await updater
|
|
128
|
+
.collection("users")
|
|
129
|
+
.where("status", "==", "inactive")
|
|
130
|
+
.update({ status: "archived" });
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Create Documents
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
// Auto-generated IDs
|
|
137
|
+
const result = await updater.collection("users").create([
|
|
138
|
+
{ data: { name: "Alice", age: 30 } },
|
|
139
|
+
{ data: { name: "Bob", age: 25 } },
|
|
140
|
+
]);
|
|
141
|
+
console.log("Created IDs:", result.createdIds);
|
|
142
|
+
|
|
143
|
+
// With specific IDs
|
|
144
|
+
const result2 = await updater.collection("users").create([
|
|
145
|
+
{ id: "user-001", data: { name: "Charlie" } },
|
|
146
|
+
{ id: "user-002", data: { name: "Diana" } },
|
|
147
|
+
]);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Upsert Documents
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
const result = await updater
|
|
154
|
+
.collection("users")
|
|
155
|
+
.where("status", "==", "active")
|
|
156
|
+
.upsert({ tier: "premium", updatedAt: new Date() });
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Preview Before Update
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
const preview = await updater
|
|
163
|
+
.collection("orders")
|
|
164
|
+
.where("status", "==", "pending")
|
|
165
|
+
.preview({ status: "cancelled" });
|
|
166
|
+
|
|
167
|
+
if (preview.affectedCount > 1000) {
|
|
168
|
+
console.log("Too many documents. Aborting.");
|
|
169
|
+
} else {
|
|
170
|
+
await updater
|
|
171
|
+
.collection("orders")
|
|
172
|
+
.where("status", "==", "pending")
|
|
173
|
+
.update({ status: "cancelled" });
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Progress Tracking
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
const result = await updater
|
|
181
|
+
.collection("products")
|
|
182
|
+
.where("inStock", "==", false)
|
|
183
|
+
.update(
|
|
184
|
+
{ status: "discontinued" },
|
|
185
|
+
{
|
|
186
|
+
onProgress: (progress) => {
|
|
187
|
+
console.log(`${progress.percentage}% complete`);
|
|
188
|
+
},
|
|
189
|
+
}
|
|
190
|
+
);
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### Get Field Values
|
|
194
|
+
|
|
195
|
+
```typescript
|
|
196
|
+
const emails = await updater
|
|
197
|
+
.collection("users")
|
|
198
|
+
.where("status", "==", "active")
|
|
199
|
+
.getFields("email");
|
|
200
|
+
|
|
201
|
+
// [{ id: 'user1', value: 'user1@example.com' }, ...]
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Multiple Conditions
|
|
205
|
+
|
|
206
|
+
```typescript
|
|
207
|
+
const ninetyDaysAgo = new Date();
|
|
208
|
+
ninetyDaysAgo.setDate(ninetyDaysAgo.getDate() - 90);
|
|
209
|
+
|
|
210
|
+
const result = await updater
|
|
211
|
+
.collection("users")
|
|
212
|
+
.where("status", "==", "inactive")
|
|
213
|
+
.where("lastLoginAt", "<", ninetyDaysAgo)
|
|
214
|
+
.where("accountType", "==", "free")
|
|
215
|
+
.update({ status: "archived" });
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
> **Note:** When using multiple `where()` conditions on different fields, Firestore may require a [composite index](https://firebase.google.com/docs/firestore/query-data/indexing). If you see a `FAILED_PRECONDITION` error, follow the link in the error message to create the required index.
|
|
219
|
+
|
|
220
|
+
### Error Handling
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
const result = await updater
|
|
224
|
+
.collection("users")
|
|
225
|
+
.where("status", "==", "test")
|
|
226
|
+
.update({ status: "verified" });
|
|
227
|
+
|
|
228
|
+
if (result.failureCount > 0) {
|
|
229
|
+
console.log(`${result.failureCount} documents failed`);
|
|
230
|
+
console.log("Failed IDs:", result.failedDocIds);
|
|
231
|
+
}
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Pagination for Large Collections
|
|
235
|
+
|
|
236
|
+
```typescript
|
|
237
|
+
// Process documents in batches of 1000 to prevent memory issues
|
|
238
|
+
const result = await updater
|
|
239
|
+
.collection("users")
|
|
240
|
+
.where("status", "==", "inactive")
|
|
241
|
+
.update(
|
|
242
|
+
{ status: "archived" },
|
|
243
|
+
{
|
|
244
|
+
batchSize: 1000,
|
|
245
|
+
onProgress: (progress) => {
|
|
246
|
+
console.log(`${progress.percentage}% complete`);
|
|
247
|
+
},
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Log File Generation
|
|
253
|
+
|
|
254
|
+
```typescript
|
|
255
|
+
const result = await updater
|
|
256
|
+
.collection("users")
|
|
257
|
+
.where("status", "==", "inactive")
|
|
258
|
+
.update(
|
|
259
|
+
{ status: "archived" },
|
|
260
|
+
{
|
|
261
|
+
log: {
|
|
262
|
+
enabled: true,
|
|
263
|
+
path: "./logs", // optional
|
|
264
|
+
},
|
|
265
|
+
}
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
if (result.logFilePath) {
|
|
269
|
+
console.log(`Log saved to: ${result.logFilePath}`);
|
|
270
|
+
}
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Log file example:
|
|
274
|
+
```
|
|
275
|
+
============================================================
|
|
276
|
+
FIRESTORE BATCH OPERATION LOG
|
|
277
|
+
============================================================
|
|
278
|
+
|
|
279
|
+
Operation: UPDATE
|
|
280
|
+
Collection: users
|
|
281
|
+
Started: 2024-01-15T10:30:00.000Z
|
|
282
|
+
Completed: 2024-01-15T10:30:05.000Z
|
|
283
|
+
|
|
284
|
+
Conditions:
|
|
285
|
+
- status == "inactive"
|
|
286
|
+
|
|
287
|
+
============================================================
|
|
288
|
+
SUMMARY
|
|
289
|
+
============================================================
|
|
290
|
+
Total: 150
|
|
291
|
+
Success: 148
|
|
292
|
+
Failure: 2
|
|
293
|
+
|
|
294
|
+
============================================================
|
|
295
|
+
DETAILS
|
|
296
|
+
============================================================
|
|
297
|
+
|
|
298
|
+
2024-01-15T10:30:01.000Z [SUCCESS] user-001
|
|
299
|
+
2024-01-15T10:30:01.100Z [SUCCESS] user-002
|
|
300
|
+
2024-01-15T10:30:01.200Z [FAILURE] user-003
|
|
301
|
+
Error: Document not found
|
|
302
|
+
...
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Requirements
|
|
306
|
+
|
|
307
|
+
- Node.js 18+
|
|
308
|
+
- Firebase Admin SDK 13.x
|
|
309
|
+
- Server-side environment only (Admin SDK required)
|
|
310
|
+
|
|
311
|
+
## Why BulkWriter?
|
|
312
|
+
|
|
313
|
+
This library uses Firebase's `BulkWriter` which:
|
|
314
|
+
|
|
315
|
+
- No 500 document limit (unlike batch writes)
|
|
316
|
+
- Automatic rate limiting
|
|
317
|
+
- Built-in retry logic
|
|
318
|
+
- Better performance for large operations
|
|
319
|
+
|
|
320
|
+
## Examples
|
|
321
|
+
|
|
322
|
+
Check out the [examples](./examples) folder:
|
|
323
|
+
|
|
324
|
+
- [basic.ts](./examples/basic.ts) - Basic usage workflow
|
|
325
|
+
- [api-route.ts](./examples/api-route.ts) - Using in API endpoints
|
|
326
|
+
- [advanced.ts](./examples/advanced.ts) - Advanced features and patterns
|
|
327
|
+
|
|
328
|
+
## Disclaimer
|
|
329
|
+
|
|
330
|
+
This package is provided "as is" without warranty of any kind. The author is not responsible for any data loss, corruption, or other issues that may arise from using this package. Always test thoroughly in a development environment before using in production, and ensure you have proper backups of your data.
|
|
331
|
+
|
|
332
|
+
## License
|
|
333
|
+
|
|
334
|
+
MIT
|