firestore-batch-updater 1.11.0 → 1.13.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
@@ -20,6 +20,7 @@
20
20
  - 비어있는지 확인 - `isEmpty()`로 매칭 문서가 없는지 확인 (`exists()`의 반대)
21
21
  - 전체 문서 조회 - `getAll()`로 매칭되는 모든 문서 데이터 조회
22
22
  - 집계 쿼리 - `aggregate()`로 서버 사이드 `sum`, `average`, `count` 연산
23
+ - 간편 집계 - `sum()`과 `avg()`로 단일 필드 간편 집계
23
24
  - 커서 페이지네이션 - `paginate()`로 메모리 효율적인 페이지 단위 조회
24
25
  - ID 직접 조회 - `getOne()`으로 문서 ID로 빠른 조회
25
26
  - 벌크 작업 - `bulkCreate()`, `bulkUpdate()`, `bulkDelete()`로 여러 문서에 각기 다른 데이터로 효율적 처리
@@ -28,6 +29,7 @@
28
29
  - 고유값 조회 - `distinct()`로 특정 필드의 중복 없는 값 목록 조회
29
30
  - JSON 내보내기/가져오기 - `toJSON()` / `fromJSON()`으로 문서 JSON 파일 내보내기/가져오기
30
31
  - 그룹별 개수 조회 - `countBy()`로 특정 필드 값별 문서 수 집계
32
+ - 랜덤 샘플링 - `sample()`로 쿼리 결과에서 랜덤 문서 추출
31
33
  - FieldValue 지원 - `increment()`, `arrayUnion()`, `delete()`, `serverTimestamp()` 등 사용 가능
32
34
  - 서브컬렉션 & 컬렉션 그룹 - 서브컬렉션 쿼리 또는 동일 이름의 모든 컬렉션 쿼리
33
35
  - Dry Run 모드 - 실제 변경 없이 작업 시뮬레이션
@@ -113,6 +115,8 @@ console.log(`${result.successCount}개 문서 업데이트 완료`);
113
115
  | `delete(options?)` | 매칭되는 문서 삭제 | `DeleteResult` |
114
116
  | `deleteOne()` | 첫 번째 매칭 문서 삭제 | `{ success, id }` |
115
117
  | `aggregate(spec)` | sum/average/count 집계 쿼리 | `AggregateResult` |
118
+ | `sum(field)` | 숫자 필드 합계 조회 | `number \| null` |
119
+ | `avg(field)` | 숫자 필드 평균 조회 | `number \| null` |
116
120
  | `paginate(options)` | 커서 기반 페이지네이션 | `PaginateResult` |
117
121
  | `bulkCreate(docs, options?)` | 여러 문서를 각기 다른 데이터로 생성 | `BulkCreateResult` |
118
122
  | `bulkUpdate(updates, options?)` | 여러 문서에 각기 다른 데이터 업데이트 | `BulkUpdateResult` |
@@ -120,6 +124,7 @@ console.log(`${result.successCount}개 문서 업데이트 완료`);
120
124
  | `transform(fn, options?)` | 커스텀 함수로 문서 변환 | `TransformResult` |
121
125
  | `copyTo(target, options?)` | 다른 컬렉션으로 문서 복사/이동 | `CopyToResult` |
122
126
  | `distinct(field)` | 특정 필드의 고유값 조회 | `any[]` |
127
+ | `sample(n)` | 매칭 문서에서 랜덤 샘플 추출 | `{ id, data }[]` |
123
128
  | `toJSON(path, options?)` | 문서를 JSON 파일로 내보내기 | `ToJSONResult` |
124
129
  | `fromJSON(path, options?)` | JSON 파일에서 문서 가져오기 | `FromJSONResult` |
125
130
  | `countBy(field)` | 필드 값별 문서 수 집계 | `CountByResult` |
@@ -511,6 +516,30 @@ console.log(`평균: ${stats.avgAmount}원`);
511
516
  console.log(`주문 수: ${stats.orderCount}건`);
512
517
  ```
513
518
 
519
+ ### 간편 합계 & 평균
520
+
521
+ ```typescript
522
+ // 필드 합계를 바로 조회 (aggregate spec 불필요)
523
+ const totalRevenue = await updater
524
+ .collection("orders")
525
+ .where("status", "==", "completed")
526
+ .sum("amount");
527
+
528
+ console.log(`총 매출: ${totalRevenue}원`);
529
+
530
+ // 필드 평균을 바로 조회
531
+ const avgScore = await updater
532
+ .collection("users")
533
+ .where("status", "==", "active")
534
+ .avg("score");
535
+
536
+ console.log(`평균 점수: ${avgScore}`);
537
+
538
+ // aggregate()와 동일하지만 단일 필드 조회 시 더 간편
539
+ // aggregate({ total: { op: "sum", field: "amount" } }) → sum("amount")
540
+ // aggregate({ avg: { op: "average", field: "score" } }) → avg("score")
541
+ ```
542
+
514
543
  ### 커서 기반 페이지네이션
515
544
 
516
545
  ```typescript
@@ -752,6 +781,26 @@ await updater.collection("users").toJSON("./backup.json");
752
781
  await updater.collection("users_backup").fromJSON("./backup.json");
753
782
  ```
754
783
 
784
+ ### 랜덤 샘플링
785
+
786
+ ```typescript
787
+ // 랜덤으로 5개 문서 추출
788
+ const samples = await updater.collection("users").sample(5);
789
+ samples.forEach(doc => console.log(doc.id, doc.data.name));
790
+
791
+ // 필터된 결과에서 랜덤 샘플
792
+ const activeUsers = await updater
793
+ .collection("users")
794
+ .where("status", "==", "active")
795
+ .sample(3);
796
+
797
+ // select와 함께 사용하여 메모리 효율 극대화
798
+ const randomProducts = await updater
799
+ .collection("products")
800
+ .select("name", "price")
801
+ .sample(10);
802
+ ```
803
+
755
804
  ### Dry Run 모드
756
805
 
757
806
  ```typescript
package/README.md CHANGED
@@ -20,6 +20,7 @@ English | [한국어](./README.ko.md)
20
20
  - Empty check - Use `isEmpty()` to check if no matching documents exist (opposite of `exists()`)
21
21
  - Get all documents - Use `getAll()` to retrieve all matching documents with data
22
22
  - Aggregation - Use `aggregate()` for server-side `sum`, `average`, and `count` operations
23
+ - Quick aggregation - Use `sum()` and `avg()` for simple single-field aggregation
23
24
  - Cursor pagination - Use `paginate()` for memory-efficient page-by-page iteration
24
25
  - Direct ID lookup - Use `getOne()` for fast document retrieval by ID
25
26
  - Bulk operations - Use `bulkCreate()`, `bulkUpdate()`, `bulkDelete()` for efficient multi-document operations with different data each
@@ -28,6 +29,7 @@ English | [한국어](./README.ko.md)
28
29
  - Distinct values - Use `distinct()` to get unique field values from matching documents
29
30
  - JSON export/import - Use `toJSON()` / `fromJSON()` to export/import documents as JSON
30
31
  - Group counting - Use `countBy()` to count documents grouped by field value
32
+ - Random sampling - Use `sample()` to get random documents from query results
31
33
  - FieldValue support - Use `increment()`, `arrayUnion()`, `delete()`, `serverTimestamp()`, etc.
32
34
  - Subcollection & Collection Group - Query subcollections or all collections with the same name
33
35
  - Dry run mode - Simulate operations without making changes
@@ -113,6 +115,8 @@ console.log(`Updated ${result.successCount} documents`);
113
115
  | `delete(options?)` | Delete matching documents | `DeleteResult` |
114
116
  | `deleteOne()` | Delete first matching document | `{ success, id }` |
115
117
  | `aggregate(spec)` | Run sum/average/count queries | `AggregateResult` |
118
+ | `sum(field)` | Get sum of a numeric field | `number \| null` |
119
+ | `avg(field)` | Get average of a numeric field | `number \| null` |
116
120
  | `paginate(options)` | Cursor-based pagination | `PaginateResult` |
117
121
  | `bulkCreate(docs, options?)` | Create multiple docs with different data | `BulkCreateResult` |
118
122
  | `bulkUpdate(updates, options?)` | Update multiple docs with different data | `BulkUpdateResult` |
@@ -120,6 +124,7 @@ console.log(`Updated ${result.successCount} documents`);
120
124
  | `transform(fn, options?)` | Transform docs with custom function | `TransformResult` |
121
125
  | `copyTo(target, options?)` | Copy/move docs to another collection | `CopyToResult` |
122
126
  | `distinct(field)` | Get unique values of a field | `any[]` |
127
+ | `sample(n)` | Get random sample of matching documents | `{ id, data }[]` |
123
128
  | `toJSON(path, options?)` | Export documents to JSON file | `ToJSONResult` |
124
129
  | `fromJSON(path, options?)` | Import documents from JSON file | `FromJSONResult` |
125
130
  | `countBy(field)` | Count documents grouped by field value | `CountByResult` |
@@ -535,6 +540,30 @@ console.log(`Average: $${stats.avgAmount}`);
535
540
  console.log(`Orders: ${stats.orderCount}`);
536
541
  ```
537
542
 
543
+ ### Quick Sum & Average
544
+
545
+ ```typescript
546
+ // Get sum of a field directly (no need for aggregate spec)
547
+ const totalRevenue = await updater
548
+ .collection("orders")
549
+ .where("status", "==", "completed")
550
+ .sum("amount");
551
+
552
+ console.log(`Total revenue: $${totalRevenue}`);
553
+
554
+ // Get average of a field directly
555
+ const avgScore = await updater
556
+ .collection("users")
557
+ .where("status", "==", "active")
558
+ .avg("score");
559
+
560
+ console.log(`Average score: ${avgScore}`);
561
+
562
+ // Equivalent to aggregate(), but simpler for single-field queries
563
+ // aggregate({ total: { op: "sum", field: "amount" } }) → sum("amount")
564
+ // aggregate({ avg: { op: "average", field: "score" } }) → avg("score")
565
+ ```
566
+
538
567
  ### Cursor-Based Pagination
539
568
 
540
569
  ```typescript
@@ -765,6 +794,26 @@ await updater.collection("users").toJSON("./backup.json");
765
794
  await updater.collection("users_backup").fromJSON("./backup.json");
766
795
  ```
767
796
 
797
+ ### Random Sampling
798
+
799
+ ```typescript
800
+ // Get 5 random documents
801
+ const samples = await updater.collection("users").sample(5);
802
+ samples.forEach(doc => console.log(doc.id, doc.data.name));
803
+
804
+ // Random sample from filtered results
805
+ const activeUsers = await updater
806
+ .collection("users")
807
+ .where("status", "==", "active")
808
+ .sample(3);
809
+
810
+ // With select for memory efficiency
811
+ const randomProducts = await updater
812
+ .collection("products")
813
+ .select("name", "price")
814
+ .sample(10);
815
+ ```
816
+
768
817
  ### Dry Run Mode
769
818
 
770
819
  ```typescript
package/dist/index.d.mts CHANGED
@@ -641,6 +641,20 @@ declare class BatchUpdater {
641
641
  * @returns Object with alias keys and numeric results
642
642
  */
643
643
  aggregate(spec: AggregateSpec): Promise<AggregateResult>;
644
+ /**
645
+ * Get the sum of a numeric field from matching documents
646
+ * Convenience wrapper around aggregate() for simple sum queries
647
+ * @param field - Field path to sum
648
+ * @returns Sum of the field values, or null if no documents match
649
+ */
650
+ sum(field: string): Promise<number | null>;
651
+ /**
652
+ * Get the average of a numeric field from matching documents
653
+ * Convenience wrapper around aggregate() for simple average queries
654
+ * @param field - Field path to average
655
+ * @returns Average of the field values, or null if no documents match
656
+ */
657
+ avg(field: string): Promise<number | null>;
644
658
  /**
645
659
  * Get documents with cursor-based pagination
646
660
  * @param options - Pagination options (pageSize, startAfter cursor)
@@ -730,6 +744,15 @@ declare class BatchUpdater {
730
744
  * @returns Array of unique values
731
745
  */
732
746
  distinct(field: string): Promise<any[]>;
747
+ /**
748
+ * Get a random sample of matching documents
749
+ * @param n - Number of documents to sample
750
+ * @returns Array of randomly selected documents with { id, data }
751
+ */
752
+ sample(n: number): Promise<{
753
+ id: string;
754
+ data: Record<string, any>;
755
+ }[]>;
733
756
  /**
734
757
  * Export matching documents to a JSON file
735
758
  * @param filePath - Path for the output JSON file
package/dist/index.d.ts CHANGED
@@ -641,6 +641,20 @@ declare class BatchUpdater {
641
641
  * @returns Object with alias keys and numeric results
642
642
  */
643
643
  aggregate(spec: AggregateSpec): Promise<AggregateResult>;
644
+ /**
645
+ * Get the sum of a numeric field from matching documents
646
+ * Convenience wrapper around aggregate() for simple sum queries
647
+ * @param field - Field path to sum
648
+ * @returns Sum of the field values, or null if no documents match
649
+ */
650
+ sum(field: string): Promise<number | null>;
651
+ /**
652
+ * Get the average of a numeric field from matching documents
653
+ * Convenience wrapper around aggregate() for simple average queries
654
+ * @param field - Field path to average
655
+ * @returns Average of the field values, or null if no documents match
656
+ */
657
+ avg(field: string): Promise<number | null>;
644
658
  /**
645
659
  * Get documents with cursor-based pagination
646
660
  * @param options - Pagination options (pageSize, startAfter cursor)
@@ -730,6 +744,15 @@ declare class BatchUpdater {
730
744
  * @returns Array of unique values
731
745
  */
732
746
  distinct(field: string): Promise<any[]>;
747
+ /**
748
+ * Get a random sample of matching documents
749
+ * @param n - Number of documents to sample
750
+ * @returns Array of randomly selected documents with { id, data }
751
+ */
752
+ sample(n: number): Promise<{
753
+ id: string;
754
+ data: Record<string, any>;
755
+ }[]>;
733
756
  /**
734
757
  * Export matching documents to a JSON file
735
758
  * @param filePath - Path for the output JSON file
package/dist/index.js CHANGED
@@ -469,6 +469,38 @@ var BatchUpdater = class {
469
469
  }
470
470
  return result;
471
471
  }
472
+ /**
473
+ * Get the sum of a numeric field from matching documents
474
+ * Convenience wrapper around aggregate() for simple sum queries
475
+ * @param field - Field path to sum
476
+ * @returns Sum of the field values, or null if no documents match
477
+ */
478
+ async sum(field) {
479
+ this.validateSetup();
480
+ if (!field) {
481
+ throw new Error("Field is required for sum operation");
482
+ }
483
+ const result = await this.aggregate({
484
+ _sum: { op: "sum", field }
485
+ });
486
+ return result._sum;
487
+ }
488
+ /**
489
+ * Get the average of a numeric field from matching documents
490
+ * Convenience wrapper around aggregate() for simple average queries
491
+ * @param field - Field path to average
492
+ * @returns Average of the field values, or null if no documents match
493
+ */
494
+ async avg(field) {
495
+ this.validateSetup();
496
+ if (!field) {
497
+ throw new Error("Field is required for average operation");
498
+ }
499
+ const result = await this.aggregate({
500
+ _avg: { op: "average", field }
501
+ });
502
+ return result._avg;
503
+ }
472
504
  /**
473
505
  * Get documents with cursor-based pagination
474
506
  * @param options - Pagination options (pageSize, startAfter cursor)
@@ -1231,6 +1263,34 @@ var BatchUpdater = class {
1231
1263
  }
1232
1264
  return values;
1233
1265
  }
1266
+ /**
1267
+ * Get a random sample of matching documents
1268
+ * @param n - Number of documents to sample
1269
+ * @returns Array of randomly selected documents with { id, data }
1270
+ */
1271
+ async sample(n) {
1272
+ this.validateSetup();
1273
+ if (!Number.isInteger(n) || n < 1) {
1274
+ throw new Error("Sample size must be a positive integer");
1275
+ }
1276
+ const query = this.buildQuery();
1277
+ const snapshot = await query.get();
1278
+ if (snapshot.empty) {
1279
+ return [];
1280
+ }
1281
+ const docs = snapshot.docs.map((doc) => ({
1282
+ id: doc.id,
1283
+ data: doc.data()
1284
+ }));
1285
+ if (docs.length <= n) {
1286
+ return docs;
1287
+ }
1288
+ for (let i = docs.length - 1; i > docs.length - 1 - n; i--) {
1289
+ const j = Math.floor(Math.random() * (i + 1));
1290
+ [docs[i], docs[j]] = [docs[j], docs[i]];
1291
+ }
1292
+ return docs.slice(docs.length - n);
1293
+ }
1234
1294
  /**
1235
1295
  * Export matching documents to a JSON file
1236
1296
  * @param filePath - Path for the output JSON file
package/dist/index.mjs CHANGED
@@ -424,6 +424,38 @@ var BatchUpdater = class {
424
424
  }
425
425
  return result;
426
426
  }
427
+ /**
428
+ * Get the sum of a numeric field from matching documents
429
+ * Convenience wrapper around aggregate() for simple sum queries
430
+ * @param field - Field path to sum
431
+ * @returns Sum of the field values, or null if no documents match
432
+ */
433
+ async sum(field) {
434
+ this.validateSetup();
435
+ if (!field) {
436
+ throw new Error("Field is required for sum operation");
437
+ }
438
+ const result = await this.aggregate({
439
+ _sum: { op: "sum", field }
440
+ });
441
+ return result._sum;
442
+ }
443
+ /**
444
+ * Get the average of a numeric field from matching documents
445
+ * Convenience wrapper around aggregate() for simple average queries
446
+ * @param field - Field path to average
447
+ * @returns Average of the field values, or null if no documents match
448
+ */
449
+ async avg(field) {
450
+ this.validateSetup();
451
+ if (!field) {
452
+ throw new Error("Field is required for average operation");
453
+ }
454
+ const result = await this.aggregate({
455
+ _avg: { op: "average", field }
456
+ });
457
+ return result._avg;
458
+ }
427
459
  /**
428
460
  * Get documents with cursor-based pagination
429
461
  * @param options - Pagination options (pageSize, startAfter cursor)
@@ -1186,6 +1218,34 @@ var BatchUpdater = class {
1186
1218
  }
1187
1219
  return values;
1188
1220
  }
1221
+ /**
1222
+ * Get a random sample of matching documents
1223
+ * @param n - Number of documents to sample
1224
+ * @returns Array of randomly selected documents with { id, data }
1225
+ */
1226
+ async sample(n) {
1227
+ this.validateSetup();
1228
+ if (!Number.isInteger(n) || n < 1) {
1229
+ throw new Error("Sample size must be a positive integer");
1230
+ }
1231
+ const query = this.buildQuery();
1232
+ const snapshot = await query.get();
1233
+ if (snapshot.empty) {
1234
+ return [];
1235
+ }
1236
+ const docs = snapshot.docs.map((doc) => ({
1237
+ id: doc.id,
1238
+ data: doc.data()
1239
+ }));
1240
+ if (docs.length <= n) {
1241
+ return docs;
1242
+ }
1243
+ for (let i = docs.length - 1; i > docs.length - 1 - n; i--) {
1244
+ const j = Math.floor(Math.random() * (i + 1));
1245
+ [docs[i], docs[j]] = [docs[j], docs[i]];
1246
+ }
1247
+ return docs.slice(docs.length - n);
1248
+ }
1189
1249
  /**
1190
1250
  * Export matching documents to a JSON file
1191
1251
  * @param filePath - Path for the output JSON file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firestore-batch-updater",
3
- "version": "1.11.0",
3
+ "version": "1.13.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",