firestore-batch-updater 1.2.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 CHANGED
@@ -14,6 +14,8 @@
14
14
  - 진행 상황 추적 - 실시간 진행률 콜백
15
15
  - 일괄 생성/Upsert/삭제 - 여러 문서를 한 번에 생성, upsert 또는 삭제
16
16
  - 정렬 및 제한 - `orderBy()`와 `limit()`으로 정밀한 제어
17
+ - 필드 선택 - `select()`로 필요한 필드만 로드 (메모리 및 비용 절약)
18
+ - 단일 문서 조회 - `findOne()`으로 효율적인 단일 문서 검색
17
19
  - FieldValue 지원 - `increment()`, `arrayUnion()`, `delete()`, `serverTimestamp()` 등 사용 가능
18
20
  - 서브컬렉션 & 컬렉션 그룹 - 서브컬렉션 쿼리 또는 동일 이름의 모든 컬렉션 쿼리
19
21
  - Dry Run 모드 - 실제 변경 없이 작업 시뮬레이션
@@ -83,7 +85,9 @@ console.log(`${result.successCount}개 문서 업데이트 완료`);
83
85
  | `where(field, op, value)` | 필터 조건 추가 (체이닝 가능) | `this` |
84
86
  | `orderBy(field, direction?)` | 정렬 추가 (체이닝 가능) | `this` |
85
87
  | `limit(count)` | 문서 수 제한 (체이닝 가능) | `this` |
88
+ | `select(...fields)` | 특정 필드만 조회 (체이닝 가능) | `this` |
86
89
  | `count()` | 매칭되는 문서 개수 조회 | `CountResult` |
90
+ | `findOne()` | 첫 번째 매칭 문서 조회 | `{ id, data } \| null` |
87
91
  | `preview(data)` | 업데이트 전 미리보기 | `PreviewResult` |
88
92
  | `update(data, options?)` | 매칭되는 문서 업데이트 | `UpdateResult` |
89
93
  | `create(docs, options?)` | 새 문서 생성 | `CreateResult` |
@@ -316,6 +320,50 @@ const result = await updater
316
320
  console.log(`${result.count}명의 비활성 사용자 발견`);
317
321
  ```
318
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
+
319
367
  ### Dry Run 모드
320
368
 
321
369
  ```typescript
package/README.md CHANGED
@@ -14,6 +14,8 @@ 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
+ - Field selection - Use `select()` to load only needed fields (saves memory and costs)
18
+ - Find single document - Use `findOne()` for efficient single-document retrieval
17
19
  - FieldValue support - Use `increment()`, `arrayUnion()`, `delete()`, `serverTimestamp()`, etc.
18
20
  - Subcollection & Collection Group - Query subcollections or all collections with the same name
19
21
  - Dry run mode - Simulate operations without making changes
@@ -83,7 +85,9 @@ console.log(`Updated ${result.successCount} documents`);
83
85
  | `where(field, op, value)` | Add filter condition (chainable) | `this` |
84
86
  | `orderBy(field, direction?)` | Add sorting (chainable) | `this` |
85
87
  | `limit(count)` | Limit number of documents (chainable) | `this` |
88
+ | `select(...fields)` | Select specific fields to retrieve (chainable) | `this` |
86
89
  | `count()` | Count matching documents | `CountResult` |
90
+ | `findOne()` | Find first matching document | `{ id, data } \| null` |
87
91
  | `preview(data)` | Preview changes before update | `PreviewResult` |
88
92
  | `update(data, options?)` | Update matching documents | `UpdateResult` |
89
93
  | `create(docs, options?)` | Create new documents | `CreateResult` |
@@ -315,6 +319,50 @@ const result = await updater
315
319
  console.log(`Found ${result.count} inactive users`);
316
320
  ```
317
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
+
318
366
  ### Dry Run Mode
319
367
 
320
368
  ```typescript
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
@@ -37,6 +40,14 @@ interface UpdateOptions {
37
40
  * Returns what would happen without making any changes
38
41
  */
39
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;
40
51
  }
41
52
  /**
42
53
  * Result of batch update operation
@@ -140,6 +151,14 @@ interface UpsertOptions {
140
151
  * Returns what would happen without making any changes
141
152
  */
142
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;
143
162
  }
144
163
  /**
145
164
  * Result of batch upsert operation
@@ -174,6 +193,14 @@ interface DeleteOptions {
174
193
  * Returns what would happen without making any changes
175
194
  */
176
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;
177
204
  }
178
205
  /**
179
206
  * Result of batch delete operation
@@ -248,6 +275,7 @@ declare class BatchUpdater {
248
275
  private conditions;
249
276
  private orderByConditions;
250
277
  private limitCount?;
278
+ private selectedFields?;
251
279
  /**
252
280
  * Create a new BatchUpdater instance
253
281
  * @param firestore - Initialized Firestore instance from firebase-admin
@@ -287,11 +315,25 @@ declare class BatchUpdater {
287
315
  * @returns This instance for chaining
288
316
  */
289
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;
290
324
  /**
291
325
  * Count documents matching the query conditions
292
326
  * @returns Count result with number of matching documents
293
327
  */
294
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>;
295
337
  /**
296
338
  * Preview changes before executing update
297
339
  * @param updateData - Data to update
@@ -384,12 +426,13 @@ declare function createLogCollector(operation: "update" | "create" | "upsert" |
384
426
  */
385
427
 
386
428
  /**
387
- * Calculate progress information
429
+ * Calculate progress information with timing details
388
430
  * @param current - Number of documents processed so far
389
431
  * @param total - Total number of documents to process
390
- * @returns Progress information with percentage
432
+ * @param startTime - Start time of the operation (from Date.now())
433
+ * @returns Progress information with percentage, timing, and ETA
391
434
  */
392
- declare function calculateProgress(current: number, total: number): ProgressInfo;
435
+ declare function calculateProgress(current: number, total: number, startTime?: number): ProgressInfo;
393
436
  /**
394
437
  * Extract field names from update data
395
438
  * @param updateData - Data to be updated
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
@@ -37,6 +40,14 @@ interface UpdateOptions {
37
40
  * Returns what would happen without making any changes
38
41
  */
39
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;
40
51
  }
41
52
  /**
42
53
  * Result of batch update operation
@@ -140,6 +151,14 @@ interface UpsertOptions {
140
151
  * Returns what would happen without making any changes
141
152
  */
142
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;
143
162
  }
144
163
  /**
145
164
  * Result of batch upsert operation
@@ -174,6 +193,14 @@ interface DeleteOptions {
174
193
  * Returns what would happen without making any changes
175
194
  */
176
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;
177
204
  }
178
205
  /**
179
206
  * Result of batch delete operation
@@ -248,6 +275,7 @@ declare class BatchUpdater {
248
275
  private conditions;
249
276
  private orderByConditions;
250
277
  private limitCount?;
278
+ private selectedFields?;
251
279
  /**
252
280
  * Create a new BatchUpdater instance
253
281
  * @param firestore - Initialized Firestore instance from firebase-admin
@@ -287,11 +315,25 @@ declare class BatchUpdater {
287
315
  * @returns This instance for chaining
288
316
  */
289
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;
290
324
  /**
291
325
  * Count documents matching the query conditions
292
326
  * @returns Count result with number of matching documents
293
327
  */
294
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>;
295
337
  /**
296
338
  * Preview changes before executing update
297
339
  * @param updateData - Data to update
@@ -384,12 +426,13 @@ declare function createLogCollector(operation: "update" | "create" | "upsert" |
384
426
  */
385
427
 
386
428
  /**
387
- * Calculate progress information
429
+ * Calculate progress information with timing details
388
430
  * @param current - Number of documents processed so far
389
431
  * @param total - Total number of documents to process
390
- * @returns Progress information with percentage
432
+ * @param startTime - Start time of the operation (from Date.now())
433
+ * @returns Progress information with percentage, timing, and ETA
391
434
  */
392
- declare function calculateProgress(current: number, total: number): ProgressInfo;
435
+ declare function calculateProgress(current: number, total: number, startTime?: number): ProgressInfo;
393
436
  /**
394
437
  * Extract field names from update data
395
438
  * @param updateData - Data to be updated
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) {
@@ -213,6 +221,7 @@ var BatchUpdater = class {
213
221
  this.conditions = [];
214
222
  this.orderByConditions = [];
215
223
  this.limitCount = void 0;
224
+ this.selectedFields = void 0;
216
225
  return this;
217
226
  }
218
227
  /**
@@ -226,6 +235,7 @@ var BatchUpdater = class {
226
235
  this.conditions = [];
227
236
  this.orderByConditions = [];
228
237
  this.limitCount = void 0;
238
+ this.selectedFields = void 0;
229
239
  return this;
230
240
  }
231
241
  /**
@@ -258,6 +268,15 @@ var BatchUpdater = class {
258
268
  this.limitCount = count;
259
269
  return this;
260
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
+ }
261
280
  /**
262
281
  * Count documents matching the query conditions
263
282
  * @returns Count result with number of matching documents
@@ -270,6 +289,23 @@ var BatchUpdater = class {
270
289
  count: snapshot.data().count
271
290
  };
272
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
+ }
273
309
  /**
274
310
  * Preview changes before executing update
275
311
  * @param updateData - Data to update
@@ -857,6 +893,9 @@ var BatchUpdater = class {
857
893
  if (this.limitCount !== void 0 && this.limitCount > 0) {
858
894
  query = query.limit(this.limitCount);
859
895
  }
896
+ if (this.selectedFields && this.selectedFields.length > 0) {
897
+ query = query.select(...this.selectedFields);
898
+ }
860
899
  return query;
861
900
  }
862
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) {
@@ -168,6 +176,7 @@ var BatchUpdater = class {
168
176
  this.conditions = [];
169
177
  this.orderByConditions = [];
170
178
  this.limitCount = void 0;
179
+ this.selectedFields = void 0;
171
180
  return this;
172
181
  }
173
182
  /**
@@ -181,6 +190,7 @@ var BatchUpdater = class {
181
190
  this.conditions = [];
182
191
  this.orderByConditions = [];
183
192
  this.limitCount = void 0;
193
+ this.selectedFields = void 0;
184
194
  return this;
185
195
  }
186
196
  /**
@@ -213,6 +223,15 @@ var BatchUpdater = class {
213
223
  this.limitCount = count;
214
224
  return this;
215
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
+ }
216
235
  /**
217
236
  * Count documents matching the query conditions
218
237
  * @returns Count result with number of matching documents
@@ -225,6 +244,23 @@ var BatchUpdater = class {
225
244
  count: snapshot.data().count
226
245
  };
227
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
+ }
228
264
  /**
229
265
  * Preview changes before executing update
230
266
  * @param updateData - Data to update
@@ -812,6 +848,9 @@ var BatchUpdater = class {
812
848
  if (this.limitCount !== void 0 && this.limitCount > 0) {
813
849
  query = query.limit(this.limitCount);
814
850
  }
851
+ if (this.selectedFields && this.selectedFields.length > 0) {
852
+ query = query.select(...this.selectedFields);
853
+ }
815
854
  return query;
816
855
  }
817
856
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firestore-batch-updater",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
4
  "description": "Batch update Firestore documents with query-based filtering and preview",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",