firestore-batch-updater 1.4.0 → 1.5.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
@@ -15,9 +15,11 @@
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()`로 메모리 효율적인 페이지 단위 조회
21
23
  - FieldValue 지원 - `increment()`, `arrayUnion()`, `delete()`, `serverTimestamp()` 등 사용 가능
22
24
  - 서브컬렉션 & 컬렉션 그룹 - 서브컬렉션 쿼리 또는 동일 이름의 모든 컬렉션 쿼리
23
25
  - Dry Run 모드 - 실제 변경 없이 작업 시뮬레이션
@@ -96,9 +98,12 @@ console.log(`${result.successCount}개 문서 업데이트 완료`);
96
98
  | `update(data, options?)` | 매칭되는 문서 업데이트 | `UpdateResult` |
97
99
  | `updateOne(data)` | 첫 번째 매칭 문서 업데이트 | `{ success, id }` |
98
100
  | `create(docs, options?)` | 새 문서 생성 | `CreateResult` |
101
+ | `createOne(data, id?)` | 단일 문서 생성 | `{ success, id }` |
99
102
  | `upsert(data, options?)` | 업데이트 또는 생성 (set with merge) | `UpsertResult` |
100
103
  | `delete(options?)` | 매칭되는 문서 삭제 | `DeleteResult` |
101
104
  | `deleteOne()` | 첫 번째 매칭 문서 삭제 | `{ success, id }` |
105
+ | `aggregate(spec)` | sum/average/count 집계 쿼리 | `AggregateResult` |
106
+ | `paginate(options)` | 커서 기반 페이지네이션 | `PaginateResult` |
102
107
  | `getFields(field)` | 특정 필드 값 조회 | `FieldValueResult[]` |
103
108
 
104
109
  ### 옵션
@@ -146,6 +151,8 @@ console.log(`${result.successCount}개 문서 업데이트 완료`);
146
151
  | `CreateResult` | `successCount`, `failureCount`, `totalCount`, `createdIds[]`, `failedDocIds?`, `logFilePath?` |
147
152
  | `UpsertResult` | `successCount`, `failureCount`, `totalCount`, `failedDocIds?`, `logFilePath?` |
148
153
  | `DeleteResult` | `successCount`, `failureCount`, `totalCount`, `deletedIds[]`, `failedDocIds?`, `logFilePath?` |
154
+ | `AggregateResult` | `{ [alias]: number \| null }` |
155
+ | `PaginateResult` | `docs[]`, `nextCursor`, `hasMore` |
149
156
  | `FieldValueResult` | `id`, `value` |
150
157
 
151
158
  ## 사용 예시
@@ -443,6 +450,67 @@ if (result.success) {
443
450
  }
444
451
  ```
445
452
 
453
+ ### 단일 문서 생성
454
+
455
+ ```typescript
456
+ // 자동 생성 ID로 문서 생성
457
+ const result = await updater
458
+ .collection("users")
459
+ .createOne({ name: "Alice", status: "active", score: 100 });
460
+
461
+ console.log(`문서 생성 완료: ${result.id}`);
462
+
463
+ // 커스텀 ID로 문서 생성
464
+ const result2 = await updater
465
+ .collection("users")
466
+ .createOne({ name: "Bob", status: "active" }, "custom-bob-id");
467
+ ```
468
+
469
+ ### 집계 쿼리
470
+
471
+ ```typescript
472
+ // 매칭 문서에 대해 sum, average, count 집계
473
+ const stats = await updater
474
+ .collection("orders")
475
+ .where("status", "==", "completed")
476
+ .aggregate({
477
+ totalAmount: { op: "sum", field: "amount" },
478
+ avgAmount: { op: "average", field: "amount" },
479
+ orderCount: { op: "count" },
480
+ });
481
+
482
+ console.log(`총액: ${stats.totalAmount}원`);
483
+ console.log(`평균: ${stats.avgAmount}원`);
484
+ console.log(`주문 수: ${stats.orderCount}건`);
485
+ ```
486
+
487
+ ### 커서 기반 페이지네이션
488
+
489
+ ```typescript
490
+ // 페이지 단위로 효율적으로 문서 조회
491
+ let nextCursor = undefined;
492
+
493
+ do {
494
+ const page = await updater
495
+ .collection("users")
496
+ .orderBy("createdAt", "desc")
497
+ .paginate({ pageSize: 20, startAfter: nextCursor });
498
+
499
+ page.docs.forEach((doc) => {
500
+ console.log(`${doc.id}: ${doc.data.name}`);
501
+ });
502
+
503
+ nextCursor = page.nextCursor;
504
+ } while (nextCursor);
505
+
506
+ // select와 함께 사용하여 메모리 효율 극대화
507
+ const page = await updater
508
+ .collection("users")
509
+ .select("name", "email")
510
+ .orderBy("name")
511
+ .paginate({ pageSize: 50 });
512
+ ```
513
+
446
514
  ### Dry Run 모드
447
515
 
448
516
  ```typescript
package/README.md CHANGED
@@ -15,9 +15,11 @@ 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
21
23
  - FieldValue support - Use `increment()`, `arrayUnion()`, `delete()`, `serverTimestamp()`, etc.
22
24
  - Subcollection & Collection Group - Query subcollections or all collections with the same name
23
25
  - Dry run mode - Simulate operations without making changes
@@ -96,9 +98,12 @@ console.log(`Updated ${result.successCount} documents`);
96
98
  | `update(data, options?)` | Update matching documents | `UpdateResult` |
97
99
  | `updateOne(data)` | Update first matching document | `{ success, id }` |
98
100
  | `create(docs, options?)` | Create new documents | `CreateResult` |
101
+ | `createOne(data, id?)` | Create a single document | `{ success, id }` |
99
102
  | `upsert(data, options?)` | Update or create (set with merge) | `UpsertResult` |
100
103
  | `delete(options?)` | Delete matching documents | `DeleteResult` |
101
104
  | `deleteOne()` | Delete first matching document | `{ success, id }` |
105
+ | `aggregate(spec)` | Run sum/average/count queries | `AggregateResult` |
106
+ | `paginate(options)` | Cursor-based pagination | `PaginateResult` |
102
107
  | `getFields(field)` | Get specific field values | `FieldValueResult[]` |
103
108
 
104
109
  ### Options
@@ -146,6 +151,8 @@ All write operations support an optional `options` parameter:
146
151
  | `CreateResult` | `successCount`, `failureCount`, `totalCount`, `createdIds[]`, `failedDocIds?`, `logFilePath?` |
147
152
  | `UpsertResult` | `successCount`, `failureCount`, `totalCount`, `failedDocIds?`, `logFilePath?` |
148
153
  | `DeleteResult` | `successCount`, `failureCount`, `totalCount`, `deletedIds[]`, `failedDocIds?`, `logFilePath?` |
154
+ | `AggregateResult` | `{ [alias]: number \| null }` |
155
+ | `PaginateResult` | `docs[]`, `nextCursor`, `hasMore` |
149
156
  | `FieldValueResult` | `id`, `value` |
150
157
 
151
158
  ## Usage Examples
@@ -442,6 +449,67 @@ if (result.success) {
442
449
  }
443
450
  ```
444
451
 
452
+ ### Create Single Document
453
+
454
+ ```typescript
455
+ // Create with auto-generated ID
456
+ const result = await updater
457
+ .collection("users")
458
+ .createOne({ name: "Alice", status: "active", score: 100 });
459
+
460
+ console.log(`Created document: ${result.id}`);
461
+
462
+ // Create with custom ID
463
+ const result2 = await updater
464
+ .collection("users")
465
+ .createOne({ name: "Bob", status: "active" }, "custom-bob-id");
466
+ ```
467
+
468
+ ### Aggregate Queries
469
+
470
+ ```typescript
471
+ // Sum, average, count on matching documents
472
+ const stats = await updater
473
+ .collection("orders")
474
+ .where("status", "==", "completed")
475
+ .aggregate({
476
+ totalAmount: { op: "sum", field: "amount" },
477
+ avgAmount: { op: "average", field: "amount" },
478
+ orderCount: { op: "count" },
479
+ });
480
+
481
+ console.log(`Total: $${stats.totalAmount}`);
482
+ console.log(`Average: $${stats.avgAmount}`);
483
+ console.log(`Orders: ${stats.orderCount}`);
484
+ ```
485
+
486
+ ### Cursor-Based Pagination
487
+
488
+ ```typescript
489
+ // Page through documents efficiently
490
+ let nextCursor = undefined;
491
+
492
+ do {
493
+ const page = await updater
494
+ .collection("users")
495
+ .orderBy("createdAt", "desc")
496
+ .paginate({ pageSize: 20, startAfter: nextCursor });
497
+
498
+ page.docs.forEach((doc) => {
499
+ console.log(`${doc.id}: ${doc.data.name}`);
500
+ });
501
+
502
+ nextCursor = page.nextCursor;
503
+ } while (nextCursor);
504
+
505
+ // Works with select for memory efficiency
506
+ const page = await updater
507
+ .collection("users")
508
+ .select("name", "email")
509
+ .orderBy("name")
510
+ .paginate({ pageSize: 50 });
511
+ ```
512
+
445
513
  ### Dry Run Mode
446
514
 
447
515
  ```typescript
package/dist/index.d.mts CHANGED
@@ -260,6 +260,48 @@ 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
+ }
263
305
 
264
306
  /**
265
307
  * BatchUpdater - Core class for batch operations on Firestore
@@ -364,6 +406,28 @@ declare class BatchUpdater {
364
406
  success: boolean;
365
407
  id: string | null;
366
408
  }>;
409
+ /**
410
+ * Create a single document in the collection
411
+ * @param data - Document data
412
+ * @param id - Optional document ID (auto-generated if not provided)
413
+ * @returns Result with success status and document id
414
+ */
415
+ createOne(data: Record<string, any>, id?: string): Promise<{
416
+ success: boolean;
417
+ id: string;
418
+ }>;
419
+ /**
420
+ * Run aggregate queries (sum, average, count) on matching documents
421
+ * @param spec - Aggregate specification defining operations and fields
422
+ * @returns Object with alias keys and numeric results
423
+ */
424
+ aggregate(spec: AggregateSpec): Promise<AggregateResult>;
425
+ /**
426
+ * Get documents with cursor-based pagination
427
+ * @param options - Pagination options (pageSize, startAfter cursor)
428
+ * @returns Page of documents with cursor for next page
429
+ */
430
+ paginate(options: PaginateOptions): Promise<PaginateResult>;
367
431
  /**
368
432
  * Preview changes before executing update
369
433
  * @param updateData - Data to update
@@ -490,4 +554,4 @@ declare function isValidUpdateData(value: any): value is Record<string, any>;
490
554
  */
491
555
  declare function formatError(error: unknown, context?: string): string;
492
556
 
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 };
557
+ export { type AggregateResult, type AggregateSpec, BatchUpdater, 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,48 @@ 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
+ }
263
305
 
264
306
  /**
265
307
  * BatchUpdater - Core class for batch operations on Firestore
@@ -364,6 +406,28 @@ declare class BatchUpdater {
364
406
  success: boolean;
365
407
  id: string | null;
366
408
  }>;
409
+ /**
410
+ * Create a single document in the collection
411
+ * @param data - Document data
412
+ * @param id - Optional document ID (auto-generated if not provided)
413
+ * @returns Result with success status and document id
414
+ */
415
+ createOne(data: Record<string, any>, id?: string): Promise<{
416
+ success: boolean;
417
+ id: string;
418
+ }>;
419
+ /**
420
+ * Run aggregate queries (sum, average, count) on matching documents
421
+ * @param spec - Aggregate specification defining operations and fields
422
+ * @returns Object with alias keys and numeric results
423
+ */
424
+ aggregate(spec: AggregateSpec): Promise<AggregateResult>;
425
+ /**
426
+ * Get documents with cursor-based pagination
427
+ * @param options - Pagination options (pageSize, startAfter cursor)
428
+ * @returns Page of documents with cursor for next page
429
+ */
430
+ paginate(options: PaginateOptions): Promise<PaginateResult>;
367
431
  /**
368
432
  * Preview changes before executing update
369
433
  * @param updateData - Data to update
@@ -490,4 +554,4 @@ declare function isValidUpdateData(value: any): value is Record<string, any>;
490
554
  */
491
555
  declare function formatError(error: unknown, context?: string): string;
492
556
 
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 };
557
+ export { type AggregateResult, type AggregateSpec, BatchUpdater, 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: () => import_firestore.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"));
@@ -363,6 +366,95 @@ var BatchUpdater = class {
363
366
  await doc.ref.delete();
364
367
  return { success: true, id: doc.id };
365
368
  }
369
+ /**
370
+ * Create a single document in the collection
371
+ * @param data - Document data
372
+ * @param id - Optional document ID (auto-generated if not provided)
373
+ * @returns Result with success status and document id
374
+ */
375
+ async createOne(data, id) {
376
+ this.validateSetup();
377
+ if (this.isCollectionGroup) {
378
+ throw new Error(
379
+ "createOne() cannot be used with collectionGroup(). Use collection() with a specific path instead."
380
+ );
381
+ }
382
+ if (!isValidUpdateData(data)) {
383
+ throw new Error("Document data must be a non-empty object");
384
+ }
385
+ const collection = this.firestore.collection(this.collectionPath);
386
+ const docRef = id ? collection.doc(id) : collection.doc();
387
+ await docRef.set(data);
388
+ return { success: true, id: docRef.id };
389
+ }
390
+ /**
391
+ * Run aggregate queries (sum, average, count) on matching documents
392
+ * @param spec - Aggregate specification defining operations and fields
393
+ * @returns Object with alias keys and numeric results
394
+ */
395
+ async aggregate(spec) {
396
+ this.validateSetup();
397
+ if (!spec || Object.keys(spec).length === 0) {
398
+ throw new Error("Aggregate spec must be a non-empty object");
399
+ }
400
+ const query = this.buildQuery();
401
+ const aggregateFields = {};
402
+ for (const [alias, definition] of Object.entries(spec)) {
403
+ switch (definition.op) {
404
+ case "sum":
405
+ if (!definition.field) {
406
+ throw new Error(`Field is required for sum operation (alias: ${alias})`);
407
+ }
408
+ aggregateFields[alias] = import_firestore.AggregateField.sum(definition.field);
409
+ break;
410
+ case "average":
411
+ if (!definition.field) {
412
+ throw new Error(`Field is required for average operation (alias: ${alias})`);
413
+ }
414
+ aggregateFields[alias] = import_firestore.AggregateField.average(definition.field);
415
+ break;
416
+ case "count":
417
+ aggregateFields[alias] = import_firestore.AggregateField.count();
418
+ break;
419
+ default:
420
+ throw new Error(`Unknown aggregate operation: ${definition.op}`);
421
+ }
422
+ }
423
+ const snapshot = await query.aggregate(aggregateFields).get();
424
+ const data = snapshot.data();
425
+ const result = {};
426
+ for (const alias of Object.keys(spec)) {
427
+ result[alias] = data[alias] ?? null;
428
+ }
429
+ return result;
430
+ }
431
+ /**
432
+ * Get documents with cursor-based pagination
433
+ * @param options - Pagination options (pageSize, startAfter cursor)
434
+ * @returns Page of documents with cursor for next page
435
+ */
436
+ async paginate(options) {
437
+ this.validateSetup();
438
+ if (!options.pageSize || options.pageSize <= 0) {
439
+ throw new Error("pageSize must be a positive number");
440
+ }
441
+ let query = this.buildQuery().limit(options.pageSize + 1);
442
+ if (options.startAfter) {
443
+ query = query.startAfter(options.startAfter);
444
+ }
445
+ const snapshot = await query.get();
446
+ const hasMore = snapshot.docs.length > options.pageSize;
447
+ const docs = snapshot.docs.slice(0, options.pageSize).map((doc) => ({
448
+ id: doc.id,
449
+ data: doc.data()
450
+ }));
451
+ const lastDoc = snapshot.docs.length > 0 ? snapshot.docs[Math.min(snapshot.docs.length - 1, options.pageSize - 1)] : null;
452
+ return {
453
+ docs,
454
+ nextCursor: hasMore ? lastDoc : null,
455
+ hasMore
456
+ };
457
+ }
366
458
  /**
367
459
  * Preview changes before executing update
368
460
  * @param updateData - Data to update
@@ -973,7 +1065,7 @@ var BatchUpdater = class {
973
1065
  };
974
1066
 
975
1067
  // src/index.ts
976
- var import_firestore = require("firebase-admin/firestore");
1068
+ var import_firestore2 = require("firebase-admin/firestore");
977
1069
  // Annotate the CommonJS export names for ESM import in node:
978
1070
  0 && (module.exports = {
979
1071
  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";
@@ -318,6 +321,95 @@ var BatchUpdater = class {
318
321
  await doc.ref.delete();
319
322
  return { success: true, id: doc.id };
320
323
  }
324
+ /**
325
+ * Create a single document in the collection
326
+ * @param data - Document data
327
+ * @param id - Optional document ID (auto-generated if not provided)
328
+ * @returns Result with success status and document id
329
+ */
330
+ async createOne(data, id) {
331
+ this.validateSetup();
332
+ if (this.isCollectionGroup) {
333
+ throw new Error(
334
+ "createOne() cannot be used with collectionGroup(). Use collection() with a specific path instead."
335
+ );
336
+ }
337
+ if (!isValidUpdateData(data)) {
338
+ throw new Error("Document data must be a non-empty object");
339
+ }
340
+ const collection = this.firestore.collection(this.collectionPath);
341
+ const docRef = id ? collection.doc(id) : collection.doc();
342
+ await docRef.set(data);
343
+ return { success: true, id: docRef.id };
344
+ }
345
+ /**
346
+ * Run aggregate queries (sum, average, count) on matching documents
347
+ * @param spec - Aggregate specification defining operations and fields
348
+ * @returns Object with alias keys and numeric results
349
+ */
350
+ async aggregate(spec) {
351
+ this.validateSetup();
352
+ if (!spec || Object.keys(spec).length === 0) {
353
+ throw new Error("Aggregate spec must be a non-empty object");
354
+ }
355
+ const query = this.buildQuery();
356
+ const aggregateFields = {};
357
+ for (const [alias, definition] of Object.entries(spec)) {
358
+ switch (definition.op) {
359
+ case "sum":
360
+ if (!definition.field) {
361
+ throw new Error(`Field is required for sum operation (alias: ${alias})`);
362
+ }
363
+ aggregateFields[alias] = AggregateField.sum(definition.field);
364
+ break;
365
+ case "average":
366
+ if (!definition.field) {
367
+ throw new Error(`Field is required for average operation (alias: ${alias})`);
368
+ }
369
+ aggregateFields[alias] = AggregateField.average(definition.field);
370
+ break;
371
+ case "count":
372
+ aggregateFields[alias] = AggregateField.count();
373
+ break;
374
+ default:
375
+ throw new Error(`Unknown aggregate operation: ${definition.op}`);
376
+ }
377
+ }
378
+ const snapshot = await query.aggregate(aggregateFields).get();
379
+ const data = snapshot.data();
380
+ const result = {};
381
+ for (const alias of Object.keys(spec)) {
382
+ result[alias] = data[alias] ?? null;
383
+ }
384
+ return result;
385
+ }
386
+ /**
387
+ * Get documents with cursor-based pagination
388
+ * @param options - Pagination options (pageSize, startAfter cursor)
389
+ * @returns Page of documents with cursor for next page
390
+ */
391
+ async paginate(options) {
392
+ this.validateSetup();
393
+ if (!options.pageSize || options.pageSize <= 0) {
394
+ throw new Error("pageSize must be a positive number");
395
+ }
396
+ let query = this.buildQuery().limit(options.pageSize + 1);
397
+ if (options.startAfter) {
398
+ query = query.startAfter(options.startAfter);
399
+ }
400
+ const snapshot = await query.get();
401
+ const hasMore = snapshot.docs.length > options.pageSize;
402
+ const docs = snapshot.docs.slice(0, options.pageSize).map((doc) => ({
403
+ id: doc.id,
404
+ data: doc.data()
405
+ }));
406
+ const lastDoc = snapshot.docs.length > 0 ? snapshot.docs[Math.min(snapshot.docs.length - 1, options.pageSize - 1)] : null;
407
+ return {
408
+ docs,
409
+ nextCursor: hasMore ? lastDoc : null,
410
+ hasMore
411
+ };
412
+ }
321
413
  /**
322
414
  * Preview changes before executing update
323
415
  * @param updateData - Data to update
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "firestore-batch-updater",
3
- "version": "1.4.0",
3
+ "version": "1.5.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",