firestore-batch-updater 1.14.0 → 1.16.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,7 +20,7 @@
20
20
  - 비어있는지 확인 - `isEmpty()`로 매칭 문서가 없는지 확인 (`exists()`의 반대)
21
21
  - 전체 문서 조회 - `getAll()`로 매칭되는 모든 문서 데이터 조회
22
22
  - 집계 쿼리 - `aggregate()`로 서버 사이드 `sum`, `average`, `count` 연산
23
- - 간편 집계 - `sum()`과 `avg()`로 단일 필드 간편 집계
23
+ - 간편 집계 - `sum()`, `avg()`, `min()`, `max()`로 단일 필드 간편 집계
24
24
  - 커서 페이지네이션 - `paginate()`로 메모리 효율적인 페이지 단위 조회
25
25
  - ID 직접 조회 - `getOne()`으로 문서 ID로 빠른 조회
26
26
  - 벌크 작업 - `bulkCreate()`, `bulkUpdate()`, `bulkDelete()`로 여러 문서에 각기 다른 데이터로 효율적 처리
@@ -31,6 +31,7 @@
31
31
  - 그룹별 개수 조회 - `countBy()`로 특정 필드 값별 문서 수 집계
32
32
  - 랜덤 샘플링 - `sample()`로 쿼리 결과에서 랜덤 문서 추출
33
33
  - 필드 값 추출 - `pluck()`로 특정 필드 값만 간단하게 배열로 추출
34
+ - 문서 ID 추출 - `pluckIds()`로 매칭 문서의 ID만 배열로 추출
34
35
  - FieldValue 지원 - `increment()`, `arrayUnion()`, `delete()`, `serverTimestamp()` 등 사용 가능
35
36
  - 서브컬렉션 & 컬렉션 그룹 - 서브컬렉션 쿼리 또는 동일 이름의 모든 컬렉션 쿼리
36
37
  - Dry Run 모드 - 실제 변경 없이 작업 시뮬레이션
@@ -118,6 +119,8 @@ console.log(`${result.successCount}개 문서 업데이트 완료`);
118
119
  | `aggregate(spec)` | sum/average/count 집계 쿼리 | `AggregateResult` |
119
120
  | `sum(field)` | 숫자 필드 합계 조회 | `number \| null` |
120
121
  | `avg(field)` | 숫자 필드 평균 조회 | `number \| null` |
122
+ | `min(field)` | 필드 최소값 조회 | `any` |
123
+ | `max(field)` | 필드 최대값 조회 | `any` |
121
124
  | `paginate(options)` | 커서 기반 페이지네이션 | `PaginateResult` |
122
125
  | `bulkCreate(docs, options?)` | 여러 문서를 각기 다른 데이터로 생성 | `BulkCreateResult` |
123
126
  | `bulkUpdate(updates, options?)` | 여러 문서에 각기 다른 데이터 업데이트 | `BulkUpdateResult` |
@@ -127,6 +130,7 @@ console.log(`${result.successCount}개 문서 업데이트 완료`);
127
130
  | `distinct(field)` | 특정 필드의 고유값 조회 | `any[]` |
128
131
  | `sample(n)` | 매칭 문서에서 랜덤 샘플 추출 | `{ id, data }[]` |
129
132
  | `pluck(field)` | 특정 필드 값만 배열로 추출 | `any[]` |
133
+ | `pluckIds()` | 매칭 문서의 ID만 배열로 추출 | `string[]` |
130
134
  | `toJSON(path, options?)` | 문서를 JSON 파일로 내보내기 | `ToJSONResult` |
131
135
  | `fromJSON(path, options?)` | JSON 파일에서 문서 가져오기 | `FromJSONResult` |
132
136
  | `countBy(field)` | 필드 값별 문서 수 집계 | `CountByResult` |
@@ -542,6 +546,29 @@ console.log(`평균 점수: ${avgScore}`);
542
546
  // aggregate({ avg: { op: "average", field: "score" } }) → avg("score")
543
547
  ```
544
548
 
549
+ ### 최솟값 & 최댓값
550
+
551
+ ```typescript
552
+ // 필드의 최솟값/최댓값 조회
553
+ const cheapest = await updater.collection("products").min("price");
554
+ const mostExpensive = await updater.collection("products").max("price");
555
+ console.log(`가격 범위: ${cheapest}원 - ${mostExpensive}원`);
556
+
557
+ // 날짜/타임스탬프에도 사용 가능
558
+ const earliestOrder = await updater
559
+ .collection("orders")
560
+ .where("status", "==", "completed")
561
+ .min("createdAt");
562
+
563
+ // 매칭 문서가 없으면 null 반환
564
+ const maxScore = await updater
565
+ .collection("users")
566
+ .where("status", "==", "nonexistent")
567
+ .max("score"); // null
568
+ ```
569
+
570
+ > 참고: 한 필드에 `where()`를 걸고 다른 필드에 `min()/max()`를 사용할 경우 Firestore 복합 인덱스가 필요할 수 있습니다. `FAILED_PRECONDITION` 오류가 발생하면 오류 메시지의 링크를 통해 인덱스를 생성하세요.
571
+
545
572
  ### 커서 기반 페이지네이션
546
573
 
547
574
  ```typescript
@@ -802,6 +829,32 @@ const countries = await updater.collection("users").pluck("address.country");
802
829
  // ["US", "KR", "JP", ...]
803
830
  ```
804
831
 
832
+ ### 문서 ID 추출
833
+
834
+ ```typescript
835
+ // 매칭 문서의 ID만 배열로 추출
836
+ const inactiveIds = await updater
837
+ .collection("users")
838
+ .where("status", "==", "inactive")
839
+ .pluckIds();
840
+ console.log(inactiveIds); // ["user-1", "user-3", ...]
841
+
842
+ // bulkDelete/bulkUpdate와 체이닝하여 효율적 처리
843
+ const expiredIds = await updater
844
+ .collection("sessions")
845
+ .where("expiresAt", "<", new Date())
846
+ .pluckIds();
847
+
848
+ await updater.collection("sessions").bulkDelete(expiredIds);
849
+
850
+ // limit, orderBy와 함께 사용 가능
851
+ const topIds = await updater
852
+ .collection("users")
853
+ .orderBy("score", "desc")
854
+ .limit(10)
855
+ .pluckIds();
856
+ ```
857
+
805
858
  ### 랜덤 샘플링
806
859
 
807
860
  ```typescript
package/README.md CHANGED
@@ -20,7 +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
+ - Quick aggregation - Use `sum()`, `avg()`, `min()`, `max()` for simple single-field aggregation
24
24
  - Cursor pagination - Use `paginate()` for memory-efficient page-by-page iteration
25
25
  - Direct ID lookup - Use `getOne()` for fast document retrieval by ID
26
26
  - Bulk operations - Use `bulkCreate()`, `bulkUpdate()`, `bulkDelete()` for efficient multi-document operations with different data each
@@ -31,6 +31,7 @@ English | [한국어](./README.ko.md)
31
31
  - Group counting - Use `countBy()` to count documents grouped by field value
32
32
  - Random sampling - Use `sample()` to get random documents from query results
33
33
  - Field value extraction - Use `pluck()` to get a simple array of field values
34
+ - Document ID extraction - Use `pluckIds()` to get an array of matching document IDs
34
35
  - FieldValue support - Use `increment()`, `arrayUnion()`, `delete()`, `serverTimestamp()`, etc.
35
36
  - Subcollection & Collection Group - Query subcollections or all collections with the same name
36
37
  - Dry run mode - Simulate operations without making changes
@@ -118,6 +119,8 @@ console.log(`Updated ${result.successCount} documents`);
118
119
  | `aggregate(spec)` | Run sum/average/count queries | `AggregateResult` |
119
120
  | `sum(field)` | Get sum of a numeric field | `number \| null` |
120
121
  | `avg(field)` | Get average of a numeric field | `number \| null` |
122
+ | `min(field)` | Get minimum value of a field | `any` |
123
+ | `max(field)` | Get maximum value of a field | `any` |
121
124
  | `paginate(options)` | Cursor-based pagination | `PaginateResult` |
122
125
  | `bulkCreate(docs, options?)` | Create multiple docs with different data | `BulkCreateResult` |
123
126
  | `bulkUpdate(updates, options?)` | Update multiple docs with different data | `BulkUpdateResult` |
@@ -127,6 +130,7 @@ console.log(`Updated ${result.successCount} documents`);
127
130
  | `distinct(field)` | Get unique values of a field | `any[]` |
128
131
  | `sample(n)` | Get random sample of matching documents | `{ id, data }[]` |
129
132
  | `pluck(field)` | Get array of values for a specific field | `any[]` |
133
+ | `pluckIds()` | Get array of matching document IDs | `string[]` |
130
134
  | `toJSON(path, options?)` | Export documents to JSON file | `ToJSONResult` |
131
135
  | `fromJSON(path, options?)` | Import documents from JSON file | `FromJSONResult` |
132
136
  | `countBy(field)` | Count documents grouped by field value | `CountByResult` |
@@ -566,6 +570,29 @@ console.log(`Average score: ${avgScore}`);
566
570
  // aggregate({ avg: { op: "average", field: "score" } }) → avg("score")
567
571
  ```
568
572
 
573
+ ### Min & Max
574
+
575
+ ```typescript
576
+ // Get the minimum/maximum value of a field
577
+ const cheapest = await updater.collection("products").min("price");
578
+ const mostExpensive = await updater.collection("products").max("price");
579
+ console.log(`Price range: $${cheapest} - $${mostExpensive}`);
580
+
581
+ // Works with dates/timestamps too
582
+ const earliestOrder = await updater
583
+ .collection("orders")
584
+ .where("status", "==", "completed")
585
+ .min("createdAt");
586
+
587
+ // Returns null if no documents match
588
+ const maxScore = await updater
589
+ .collection("users")
590
+ .where("status", "==", "nonexistent")
591
+ .max("score"); // null
592
+ ```
593
+
594
+ > Note: Combining `where()` on one field with `min()/max()` on a different field may require a Firestore composite index. If you see a `FAILED_PRECONDITION` error, follow the link in the error message to create the required index.
595
+
569
596
  ### Cursor-Based Pagination
570
597
 
571
598
  ```typescript
@@ -815,6 +842,32 @@ const countries = await updater.collection("users").pluck("address.country");
815
842
  // ["US", "KR", "JP", ...]
816
843
  ```
817
844
 
845
+ ### Pluck Document IDs
846
+
847
+ ```typescript
848
+ // Get all matching document IDs as an array
849
+ const inactiveIds = await updater
850
+ .collection("users")
851
+ .where("status", "==", "inactive")
852
+ .pluckIds();
853
+ console.log(inactiveIds); // ["user-1", "user-3", ...]
854
+
855
+ // Chain with bulk operations for efficient workflows
856
+ const expiredIds = await updater
857
+ .collection("sessions")
858
+ .where("expiresAt", "<", new Date())
859
+ .pluckIds();
860
+
861
+ await updater.collection("sessions").bulkDelete(expiredIds);
862
+
863
+ // Works with limit() and orderBy()
864
+ const topIds = await updater
865
+ .collection("users")
866
+ .orderBy("score", "desc")
867
+ .limit(10)
868
+ .pluckIds();
869
+ ```
870
+
818
871
  ### Random Sampling
819
872
 
820
873
  ```typescript
package/dist/index.d.mts CHANGED
@@ -655,6 +655,20 @@ declare class BatchUpdater {
655
655
  * @returns Average of the field values, or null if no documents match
656
656
  */
657
657
  avg(field: string): Promise<number | null>;
658
+ /**
659
+ * Get the minimum value of a numeric field from matching documents
660
+ * Uses orderBy + limit(1) since Firestore doesn't support min/max aggregation natively
661
+ * @param field - Field path to find the minimum value of
662
+ * @returns Minimum field value, or null if no documents match
663
+ */
664
+ min(field: string): Promise<any>;
665
+ /**
666
+ * Get the maximum value of a numeric field from matching documents
667
+ * Uses orderBy + limit(1) since Firestore doesn't support min/max aggregation natively
668
+ * @param field - Field path to find the maximum value of
669
+ * @returns Maximum field value, or null if no documents match
670
+ */
671
+ max(field: string): Promise<any>;
658
672
  /**
659
673
  * Get documents with cursor-based pagination
660
674
  * @param options - Pagination options (pageSize, startAfter cursor)
@@ -689,6 +703,12 @@ declare class BatchUpdater {
689
703
  * @returns Array of field values (null/undefined values are excluded)
690
704
  */
691
705
  pluck(field: string): Promise<any[]>;
706
+ /**
707
+ * Get an array of document IDs from matching documents
708
+ * Useful for passing IDs directly to bulkUpdate(), bulkDelete(), etc.
709
+ * @returns Array of document IDs
710
+ */
711
+ pluckIds(): Promise<string[]>;
692
712
  /**
693
713
  * Create multiple documents in batch
694
714
  * Note: This method does not work with collectionGroup()
package/dist/index.d.ts CHANGED
@@ -655,6 +655,20 @@ declare class BatchUpdater {
655
655
  * @returns Average of the field values, or null if no documents match
656
656
  */
657
657
  avg(field: string): Promise<number | null>;
658
+ /**
659
+ * Get the minimum value of a numeric field from matching documents
660
+ * Uses orderBy + limit(1) since Firestore doesn't support min/max aggregation natively
661
+ * @param field - Field path to find the minimum value of
662
+ * @returns Minimum field value, or null if no documents match
663
+ */
664
+ min(field: string): Promise<any>;
665
+ /**
666
+ * Get the maximum value of a numeric field from matching documents
667
+ * Uses orderBy + limit(1) since Firestore doesn't support min/max aggregation natively
668
+ * @param field - Field path to find the maximum value of
669
+ * @returns Maximum field value, or null if no documents match
670
+ */
671
+ max(field: string): Promise<any>;
658
672
  /**
659
673
  * Get documents with cursor-based pagination
660
674
  * @param options - Pagination options (pageSize, startAfter cursor)
@@ -689,6 +703,12 @@ declare class BatchUpdater {
689
703
  * @returns Array of field values (null/undefined values are excluded)
690
704
  */
691
705
  pluck(field: string): Promise<any[]>;
706
+ /**
707
+ * Get an array of document IDs from matching documents
708
+ * Useful for passing IDs directly to bulkUpdate(), bulkDelete(), etc.
709
+ * @returns Array of document IDs
710
+ */
711
+ pluckIds(): Promise<string[]>;
692
712
  /**
693
713
  * Create multiple documents in batch
694
714
  * Note: This method does not work with collectionGroup()
package/dist/index.js CHANGED
@@ -501,6 +501,52 @@ var BatchUpdater = class {
501
501
  });
502
502
  return result._avg;
503
503
  }
504
+ /**
505
+ * Get the minimum value of a numeric field from matching documents
506
+ * Uses orderBy + limit(1) since Firestore doesn't support min/max aggregation natively
507
+ * @param field - Field path to find the minimum value of
508
+ * @returns Minimum field value, or null if no documents match
509
+ */
510
+ async min(field) {
511
+ this.validateSetup();
512
+ if (!field || typeof field !== "string") {
513
+ throw new Error("Field is required for min operation");
514
+ }
515
+ let query = this.isCollectionGroup ? this.firestore.collectionGroup(this.collectionPath) : this.firestore.collection(this.collectionPath);
516
+ for (const condition of this.conditions) {
517
+ query = query.where(condition.field, condition.operator, condition.value);
518
+ }
519
+ query = query.orderBy(field, "asc").limit(1);
520
+ const snapshot = await query.get();
521
+ if (snapshot.empty) {
522
+ return null;
523
+ }
524
+ const value = this.getNestedValue(snapshot.docs[0].data(), field);
525
+ return value ?? null;
526
+ }
527
+ /**
528
+ * Get the maximum value of a numeric field from matching documents
529
+ * Uses orderBy + limit(1) since Firestore doesn't support min/max aggregation natively
530
+ * @param field - Field path to find the maximum value of
531
+ * @returns Maximum field value, or null if no documents match
532
+ */
533
+ async max(field) {
534
+ this.validateSetup();
535
+ if (!field || typeof field !== "string") {
536
+ throw new Error("Field is required for max operation");
537
+ }
538
+ let query = this.isCollectionGroup ? this.firestore.collectionGroup(this.collectionPath) : this.firestore.collection(this.collectionPath);
539
+ for (const condition of this.conditions) {
540
+ query = query.where(condition.field, condition.operator, condition.value);
541
+ }
542
+ query = query.orderBy(field, "desc").limit(1);
543
+ const snapshot = await query.get();
544
+ if (snapshot.empty) {
545
+ return null;
546
+ }
547
+ const value = this.getNestedValue(snapshot.docs[0].data(), field);
548
+ return value ?? null;
549
+ }
504
550
  /**
505
551
  * Get documents with cursor-based pagination
506
552
  * @param options - Pagination options (pageSize, startAfter cursor)
@@ -748,6 +794,17 @@ var BatchUpdater = class {
748
794
  }
749
795
  return values;
750
796
  }
797
+ /**
798
+ * Get an array of document IDs from matching documents
799
+ * Useful for passing IDs directly to bulkUpdate(), bulkDelete(), etc.
800
+ * @returns Array of document IDs
801
+ */
802
+ async pluckIds() {
803
+ this.validateSetup();
804
+ const query = this.buildQuery();
805
+ const snapshot = await query.get();
806
+ return snapshot.docs.map((doc) => doc.id);
807
+ }
751
808
  /**
752
809
  * Create multiple documents in batch
753
810
  * Note: This method does not work with collectionGroup()
package/dist/index.mjs CHANGED
@@ -456,6 +456,52 @@ var BatchUpdater = class {
456
456
  });
457
457
  return result._avg;
458
458
  }
459
+ /**
460
+ * Get the minimum value of a numeric field from matching documents
461
+ * Uses orderBy + limit(1) since Firestore doesn't support min/max aggregation natively
462
+ * @param field - Field path to find the minimum value of
463
+ * @returns Minimum field value, or null if no documents match
464
+ */
465
+ async min(field) {
466
+ this.validateSetup();
467
+ if (!field || typeof field !== "string") {
468
+ throw new Error("Field is required for min operation");
469
+ }
470
+ let query = this.isCollectionGroup ? this.firestore.collectionGroup(this.collectionPath) : this.firestore.collection(this.collectionPath);
471
+ for (const condition of this.conditions) {
472
+ query = query.where(condition.field, condition.operator, condition.value);
473
+ }
474
+ query = query.orderBy(field, "asc").limit(1);
475
+ const snapshot = await query.get();
476
+ if (snapshot.empty) {
477
+ return null;
478
+ }
479
+ const value = this.getNestedValue(snapshot.docs[0].data(), field);
480
+ return value ?? null;
481
+ }
482
+ /**
483
+ * Get the maximum value of a numeric field from matching documents
484
+ * Uses orderBy + limit(1) since Firestore doesn't support min/max aggregation natively
485
+ * @param field - Field path to find the maximum value of
486
+ * @returns Maximum field value, or null if no documents match
487
+ */
488
+ async max(field) {
489
+ this.validateSetup();
490
+ if (!field || typeof field !== "string") {
491
+ throw new Error("Field is required for max operation");
492
+ }
493
+ let query = this.isCollectionGroup ? this.firestore.collectionGroup(this.collectionPath) : this.firestore.collection(this.collectionPath);
494
+ for (const condition of this.conditions) {
495
+ query = query.where(condition.field, condition.operator, condition.value);
496
+ }
497
+ query = query.orderBy(field, "desc").limit(1);
498
+ const snapshot = await query.get();
499
+ if (snapshot.empty) {
500
+ return null;
501
+ }
502
+ const value = this.getNestedValue(snapshot.docs[0].data(), field);
503
+ return value ?? null;
504
+ }
459
505
  /**
460
506
  * Get documents with cursor-based pagination
461
507
  * @param options - Pagination options (pageSize, startAfter cursor)
@@ -703,6 +749,17 @@ var BatchUpdater = class {
703
749
  }
704
750
  return values;
705
751
  }
752
+ /**
753
+ * Get an array of document IDs from matching documents
754
+ * Useful for passing IDs directly to bulkUpdate(), bulkDelete(), etc.
755
+ * @returns Array of document IDs
756
+ */
757
+ async pluckIds() {
758
+ this.validateSetup();
759
+ const query = this.buildQuery();
760
+ const snapshot = await query.get();
761
+ return snapshot.docs.map((doc) => doc.id);
762
+ }
706
763
  /**
707
764
  * Create multiple documents in batch
708
765
  * Note: This method does not work with collectionGroup()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firestore-batch-updater",
3
- "version": "1.14.0",
3
+ "version": "1.16.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",