@cougargrades/firebase-rest-firestore 1.6.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.
@@ -0,0 +1,1078 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WriteResult = exports.DocumentSnapshot = exports.QuerySnapshot = exports.Query = exports.CollectionGroup = exports.DocumentReference = exports.CollectionReference = exports.FirestoreClient = void 0;
4
+ exports.createFirestoreClient = createFirestoreClient;
5
+ const auth_1 = require("./utils/auth");
6
+ const converter_1 = require("./utils/converter");
7
+ const path_1 = require("./utils/path");
8
+ const config_1 = require("./utils/config");
9
+ const path_2 = require("./utils/path");
10
+ /**
11
+ * Firestore client class
12
+ */
13
+ class FirestoreClient {
14
+ /**
15
+ * Constructor
16
+ * @param config Firestore configuration object
17
+ */
18
+ constructor(config) {
19
+ this.token = null;
20
+ this.tokenExpiry = 0;
21
+ this.configChecked = false;
22
+ this.debug = false;
23
+ this.config = config;
24
+ this.pathUtil = (0, path_2.createFirestorePath)(config, config.debug || false);
25
+ this.debug = !!config.debug;
26
+ // Log configuration if debug is enabled
27
+ if (this.debug) {
28
+ console.log("Firestore client initialized with config:", JSON.stringify(this.config, null, 2));
29
+ }
30
+ }
31
+ /**
32
+ * Check configuration parameters
33
+ * @private
34
+ */
35
+ checkConfig() {
36
+ if (this.configChecked) {
37
+ return;
38
+ }
39
+ // 必須パラメータのチェック
40
+ const requiredParams = ["projectId"];
41
+ // Only require auth parameters when not using emulator
42
+ if (!this.config.useEmulator) {
43
+ requiredParams.push("privateKey", "clientEmail");
44
+ }
45
+ const missingParams = requiredParams.filter(param => !this.config[param]);
46
+ if (missingParams.length > 0) {
47
+ throw new Error(`Missing required Firestore configuration parameters: ${missingParams.join(", ")}`);
48
+ }
49
+ this.configChecked = true;
50
+ }
51
+ /**
52
+ * Get authentication token (with caching)
53
+ */
54
+ async getToken() {
55
+ // Check settings before operation
56
+ this.checkConfig();
57
+ // In emulator mode, we don't need a token
58
+ if (this.config.useEmulator) {
59
+ if (this.debug) {
60
+ console.log("Emulator mode: skipping token generation");
61
+ }
62
+ return "emulator-fake-token";
63
+ }
64
+ const now = Date.now();
65
+ // トークンが期限切れか未取得の場合は新しく取得
66
+ if (!this.token || now >= this.tokenExpiry) {
67
+ if (this.debug) {
68
+ console.log("Generating new auth token");
69
+ }
70
+ this.token = await (0, auth_1.getFirestoreToken)(this.config);
71
+ // 50分後に期限切れとする(実際は1時間)
72
+ this.tokenExpiry = now + 50 * 60 * 1000;
73
+ }
74
+ return this.token;
75
+ }
76
+ /**
77
+ * Prepare request headers
78
+ * @param additionalHeaders Additional headers
79
+ * @returns Prepared headers object
80
+ * @private
81
+ */
82
+ async prepareHeaders(additionalHeaders = {}) {
83
+ const headers = {
84
+ "Content-Type": "application/json",
85
+ ...additionalHeaders,
86
+ };
87
+ // Only add auth token for production environment
88
+ if (!this.config.useEmulator) {
89
+ const token = await this.getToken();
90
+ headers["Authorization"] = `Bearer ${token}`;
91
+ }
92
+ else if (this.debug) {
93
+ console.log("Using emulator mode, skipping authorization header");
94
+ }
95
+ return headers;
96
+ }
97
+ /**
98
+ * Get collection reference
99
+ * @param path Collection path
100
+ * @returns CollectionReference instance
101
+ */
102
+ collection(path) {
103
+ // Configuration check is performed at the time of actual operation
104
+ return new CollectionReference(this, path);
105
+ }
106
+ /**
107
+ * Get document reference
108
+ * @param path Document path
109
+ * @returns DocumentReference instance
110
+ */
111
+ doc(path) {
112
+ // Configuration check is performed at the time of actual operation
113
+ const parts = path.split("/");
114
+ if (parts.length % 2 !== 0) {
115
+ throw new Error("Invalid document path. Document path must point to a document, not a collection.");
116
+ }
117
+ const collectionPath = parts.slice(0, parts.length - 1).join("/");
118
+ const docId = parts[parts.length - 1];
119
+ return new DocumentReference(this, collectionPath, docId);
120
+ }
121
+ /**
122
+ * Get collection group reference
123
+ * @param path Collection group ID
124
+ * @returns CollectionGroup instance
125
+ */
126
+ collectionGroup(path) {
127
+ return new CollectionGroup(this, path);
128
+ }
129
+ /**
130
+ * Add document to Firestore
131
+ * @param collectionName Collection name
132
+ * @param data Data to add
133
+ * @returns Added document
134
+ */
135
+ async add(collectionName, data) {
136
+ // Check settings before operation
137
+ this.checkConfig();
138
+ if (this.debug) {
139
+ console.log(`Adding document to collection: ${collectionName}`, data);
140
+ }
141
+ const url = this.pathUtil.getCollectionPath(collectionName);
142
+ const firestoreData = (0, converter_1.convertToFirestoreDocument)(data);
143
+ if (this.debug) {
144
+ console.log(`Making request to: ${url}`, firestoreData);
145
+ }
146
+ const headers = await this.prepareHeaders();
147
+ const response = await fetch(url, {
148
+ method: "POST",
149
+ headers,
150
+ body: JSON.stringify(firestoreData),
151
+ });
152
+ if (this.debug) {
153
+ console.log(`Response status: ${response.status}`);
154
+ }
155
+ if (!response.ok) {
156
+ const errorText = await response.text();
157
+ if (this.debug) {
158
+ console.error(`Error response: ${errorText}`);
159
+ }
160
+ throw new Error(`Firestore API error: ${response.statusText || response.status} - ${errorText}`);
161
+ }
162
+ const result = (await response.json());
163
+ return (0, converter_1.convertFromFirestoreDocument)(result);
164
+ }
165
+ /**
166
+ * Get document
167
+ * @param collectionName Collection name
168
+ * @param documentId Document ID
169
+ * @returns Retrieved document (null if it doesn't exist)
170
+ */
171
+ async get(collectionName, documentId) {
172
+ // Check settings before operation
173
+ this.checkConfig();
174
+ if (this.debug) {
175
+ console.log(`Getting document from collection: ${collectionName}, documentId: ${documentId}`);
176
+ }
177
+ const url = this.pathUtil.getDocumentPath(collectionName, documentId);
178
+ if (this.debug) {
179
+ console.log(`Making request to: ${url}`);
180
+ }
181
+ const headers = await this.prepareHeaders();
182
+ try {
183
+ const response = await fetch(url, {
184
+ method: "GET",
185
+ headers,
186
+ });
187
+ if (this.debug) {
188
+ console.log(`Response status: ${response.status}`);
189
+ }
190
+ // Capture response text for debugging
191
+ const responseText = await response.text();
192
+ if (this.debug) {
193
+ console.log(`Response text: ${responseText.substring(0, 200)}${responseText.length > 200 ? "..." : ""}`);
194
+ }
195
+ if (response.status === 404) {
196
+ return null;
197
+ }
198
+ if (!response.ok) {
199
+ throw new Error(`Firestore API error: ${response.statusText || response.status} - ${responseText}`);
200
+ }
201
+ // Parse the response text
202
+ const result = JSON.parse(responseText);
203
+ return (0, converter_1.convertFromFirestoreDocument)(result);
204
+ }
205
+ catch (error) {
206
+ console.error("Error in get method:", error);
207
+ throw error;
208
+ }
209
+ }
210
+ /**
211
+ * Update document
212
+ * @param collectionName Collection name
213
+ * @param documentId Document ID
214
+ * @param data Data to update
215
+ * @returns Updated document
216
+ */
217
+ async update(collectionName, documentId, data) {
218
+ // Check settings before operation
219
+ this.checkConfig();
220
+ if (this.debug) {
221
+ console.log(`Updating document in collection: ${collectionName}, documentId: ${documentId}`, data);
222
+ }
223
+ const url = this.pathUtil.getDocumentPath(collectionName, documentId);
224
+ if (this.debug) {
225
+ console.log(`Making request to: ${url}`);
226
+ }
227
+ // Get existing document and merge
228
+ const existingDoc = await this.get(collectionName, documentId);
229
+ if (existingDoc) {
230
+ // Check for nested fields
231
+ // Check if data contains dot notation keys (e.g., "favorites.color")
232
+ const updateData = { ...data };
233
+ const dotNotationKeys = Object.keys(data).filter(key => key.includes("."));
234
+ if (dotNotationKeys.length > 0) {
235
+ // スプレッド演算子でコピーして元のオブジェクトを変更しないようにする
236
+ const result = { ...existingDoc };
237
+ // 通常のキーを先に適用
238
+ Object.keys(data)
239
+ .filter(key => !key.includes("."))
240
+ .forEach(key => {
241
+ result[key] = data[key];
242
+ });
243
+ // ドット記法のキーを処理
244
+ dotNotationKeys.forEach(path => {
245
+ const parts = path.split(".");
246
+ let current = result;
247
+ // 最後のパーツ以外をたどってネストしたオブジェクトに到達
248
+ for (let i = 0; i < parts.length - 1; i++) {
249
+ const part = parts[i];
250
+ // パスが存在しない場合は新しいオブジェクトを作成
251
+ if (!current[part] || typeof current[part] !== "object") {
252
+ current[part] = {};
253
+ }
254
+ current = current[part];
255
+ }
256
+ // 最後のパーツに値を設定
257
+ const lastPart = parts[parts.length - 1];
258
+ current[lastPart] = data[path];
259
+ // 元のデータからドット記法のキーを削除
260
+ delete updateData[path];
261
+ });
262
+ data = result;
263
+ }
264
+ else {
265
+ // 通常のマージ
266
+ data = { ...existingDoc, ...data };
267
+ }
268
+ }
269
+ const firestoreData = (0, converter_1.convertToFirestoreDocument)(data);
270
+ const headers = await this.prepareHeaders();
271
+ const response = await fetch(url, {
272
+ method: "PATCH",
273
+ headers,
274
+ body: JSON.stringify(firestoreData),
275
+ });
276
+ if (this.debug) {
277
+ console.log(`Response status: ${response.status}`);
278
+ }
279
+ if (!response.ok) {
280
+ const errorText = await response.text();
281
+ if (this.debug) {
282
+ console.error(`Error response: ${errorText}`);
283
+ }
284
+ throw new Error(`Firestore API error: ${response.statusText || response.status} - ${errorText}`);
285
+ }
286
+ const result = (await response.json());
287
+ return (0, converter_1.convertFromFirestoreDocument)(result);
288
+ }
289
+ /**
290
+ * Delete document
291
+ * @param collectionName Collection name
292
+ * @param documentId Document ID
293
+ * @returns true if deletion successful
294
+ */
295
+ async delete(collectionName, documentId) {
296
+ // Check settings before operation
297
+ this.checkConfig();
298
+ if (this.debug) {
299
+ console.log(`Deleting document from collection: ${collectionName}, documentId: ${documentId}`);
300
+ }
301
+ const url = this.pathUtil.getDocumentPath(collectionName, documentId);
302
+ if (this.debug) {
303
+ console.log(`Making request to: ${url}`);
304
+ }
305
+ // Different header handling for emulator
306
+ const headers = {};
307
+ // Only add auth token for production environment
308
+ if (!this.config.useEmulator) {
309
+ const token = await this.getToken();
310
+ headers["Authorization"] = `Bearer ${token}`;
311
+ }
312
+ const response = await fetch(url, {
313
+ method: "DELETE",
314
+ headers,
315
+ });
316
+ if (this.debug) {
317
+ console.log(`Response status: ${response.status}`);
318
+ }
319
+ if (!response.ok) {
320
+ const errorText = await response.text();
321
+ if (this.debug) {
322
+ console.error(`Error response: ${errorText}`);
323
+ }
324
+ throw new Error(`Firestore API error: ${response.statusText || response.status} - ${errorText}`);
325
+ }
326
+ return true;
327
+ }
328
+ /**
329
+ * Query documents in a collection
330
+ * @param collectionPath Collection path
331
+ * @param options Query options
332
+ * @param allDescendants Whether to include descendant collections
333
+ * @returns Array of documents matching the query
334
+ */
335
+ async query(collectionPath, options = {}, allDescendants = false) {
336
+ // Check settings before operation
337
+ this.checkConfig();
338
+ try {
339
+ // Parse the collection path
340
+ const segments = collectionPath.split("/");
341
+ const collectionId = segments[segments.length - 1];
342
+ // Get the proper runQuery URL from our path helper
343
+ const queryUrl = this.pathUtil.getRunQueryPath(collectionPath);
344
+ if (this.debug) {
345
+ console.log(`Executing query on collection: ${collectionPath}`);
346
+ console.log(`Using runQuery URL: ${queryUrl}`);
347
+ }
348
+ // Create the structured query
349
+ const requestBody = {
350
+ structuredQuery: {
351
+ from: [
352
+ {
353
+ collectionId,
354
+ allDescendants,
355
+ },
356
+ ],
357
+ },
358
+ };
359
+ // Add where filters if present
360
+ if (options.where && options.where.length > 0) {
361
+ // Map our operators to Firestore REST API operators
362
+ const opMap = {
363
+ "==": "EQUAL",
364
+ "!=": "NOT_EQUAL",
365
+ "<": "LESS_THAN",
366
+ "<=": "LESS_THAN_OR_EQUAL",
367
+ ">": "GREATER_THAN",
368
+ ">=": "GREATER_THAN_OR_EQUAL",
369
+ "array-contains": "ARRAY_CONTAINS",
370
+ in: "IN",
371
+ "array-contains-any": "ARRAY_CONTAINS_ANY",
372
+ "not-in": "NOT_IN",
373
+ };
374
+ // Single where clause
375
+ if (options.where.length === 1) {
376
+ const filter = options.where[0];
377
+ const firestoreOp = opMap[filter.op] || filter.op;
378
+ requestBody.structuredQuery.where = {
379
+ fieldFilter: {
380
+ field: { fieldPath: filter.field },
381
+ op: firestoreOp,
382
+ value: (0, converter_1.convertToFirestoreValue)(filter.value),
383
+ },
384
+ };
385
+ }
386
+ // Multiple where clauses (AND)
387
+ else {
388
+ requestBody.structuredQuery.where = {
389
+ compositeFilter: {
390
+ op: "AND",
391
+ filters: options.where.map(filter => {
392
+ const firestoreOp = opMap[filter.op] || filter.op;
393
+ return {
394
+ fieldFilter: {
395
+ field: { fieldPath: filter.field },
396
+ op: firestoreOp,
397
+ value: (0, converter_1.convertToFirestoreValue)(filter.value),
398
+ },
399
+ };
400
+ }),
401
+ },
402
+ };
403
+ }
404
+ }
405
+ // Add order by if present
406
+ if (options.orderBy) {
407
+ requestBody.structuredQuery.orderBy = [
408
+ {
409
+ field: { fieldPath: options.orderBy },
410
+ direction: options.orderDirection || "ASCENDING",
411
+ },
412
+ ];
413
+ }
414
+ // Add limit if present
415
+ if (options.limit) {
416
+ requestBody.structuredQuery.limit = options.limit;
417
+ }
418
+ // Add offset if present
419
+ if (options.offset) {
420
+ requestBody.structuredQuery.offset = options.offset;
421
+ }
422
+ if (this.debug) {
423
+ console.log(`Request payload:`, JSON.stringify(requestBody, null, 2));
424
+ }
425
+ // Use the existing prepareHeaders method for authentication consistency
426
+ const headers = await this.prepareHeaders();
427
+ const response = await fetch(queryUrl, {
428
+ method: "POST",
429
+ headers,
430
+ body: JSON.stringify(requestBody),
431
+ });
432
+ // Collect response for debugging
433
+ const responseText = await response.text();
434
+ if (this.debug) {
435
+ console.log(`API Response:`, responseText);
436
+ }
437
+ if (!response.ok) {
438
+ throw new Error(`Firestore API error: ${response.status} - ${responseText}`);
439
+ }
440
+ // Parse the response
441
+ const results = JSON.parse(responseText);
442
+ if (this.debug) {
443
+ console.log(`Results count: ${results?.length || 0}`);
444
+ }
445
+ // Process the results
446
+ if (!Array.isArray(results)) {
447
+ return [];
448
+ }
449
+ const convertedResults = results
450
+ .filter(item => item.document)
451
+ .map(item => (0, converter_1.convertFromFirestoreDocument)(item.document));
452
+ if (this.debug) {
453
+ console.log(`Converted results:`, convertedResults);
454
+ }
455
+ return convertedResults;
456
+ }
457
+ catch (error) {
458
+ console.error("Query execution error:", error);
459
+ throw error;
460
+ }
461
+ }
462
+ /**
463
+ * ドキュメントを作成または上書き
464
+ * @param collectionName コレクション名
465
+ * @param documentId ドキュメントID
466
+ * @param data ドキュメントデータ
467
+ * @returns 作成されたドキュメントのリファレンス
468
+ */
469
+ async createWithId(collectionName, documentId, data) {
470
+ // 操作前に設定をチェック
471
+ this.checkConfig();
472
+ const url = `${(0, path_1.getFirestoreBasePath)(this.config.projectId, this.config.databaseId, this.config)}/${collectionName}/${documentId}`;
473
+ const firestoreData = (0, converter_1.convertToFirestoreDocument)(data);
474
+ const token = await this.getToken();
475
+ const response = await fetch(url, {
476
+ method: "PATCH",
477
+ headers: {
478
+ "Content-Type": "application/json",
479
+ Authorization: `Bearer ${token}`,
480
+ },
481
+ body: JSON.stringify(firestoreData),
482
+ });
483
+ if (!response.ok) {
484
+ throw new Error(`Firestore API error: ${response.statusText}`);
485
+ }
486
+ const result = (await response.json());
487
+ return (0, converter_1.convertFromFirestoreDocument)(result);
488
+ }
489
+ }
490
+ exports.FirestoreClient = FirestoreClient;
491
+ /**
492
+ * Collection reference class
493
+ */
494
+ class CollectionReference {
495
+ constructor(client, path) {
496
+ this.client = client;
497
+ this._path = path;
498
+ this._queryConstraints = {
499
+ where: [],
500
+ };
501
+ }
502
+ /**
503
+ * Get collection path
504
+ */
505
+ get path() {
506
+ return this._path;
507
+ }
508
+ /**
509
+ * Whether to include all descendant collections
510
+ */
511
+ get allDescendants() {
512
+ return false;
513
+ }
514
+ /**
515
+ * Get document reference
516
+ * @param documentPath Document ID (auto-generated if omitted)
517
+ * @returns DocumentReference instance
518
+ */
519
+ doc(documentPath) {
520
+ const docId = documentPath || this._generateId();
521
+ return new DocumentReference(this.client, this.path, docId);
522
+ }
523
+ /**
524
+ * Add document (ID is auto-generated)
525
+ * @param data Document data
526
+ * @returns Reference to the created document
527
+ */
528
+ async add(data) {
529
+ const result = await this.client.add(this.path, data);
530
+ const docId = result.id;
531
+ return new DocumentReference(this.client, this.path, docId);
532
+ }
533
+ /**
534
+ * Add filter condition
535
+ * @param fieldPath Field path
536
+ * @param opStr Operator
537
+ * @param value Value
538
+ * @returns Query instance
539
+ */
540
+ where(fieldPath, opStr, value) {
541
+ const query = new Query(this.client, this.path, {
542
+ ...this._queryConstraints,
543
+ }, this.allDescendants);
544
+ // Operator conversion
545
+ let firestoreOp;
546
+ switch (opStr) {
547
+ case "==":
548
+ firestoreOp = "EQUAL";
549
+ break;
550
+ case "!=":
551
+ firestoreOp = "NOT_EQUAL";
552
+ break;
553
+ case "<":
554
+ firestoreOp = "LESS_THAN";
555
+ break;
556
+ case "<=":
557
+ firestoreOp = "LESS_THAN_OR_EQUAL";
558
+ break;
559
+ case ">":
560
+ firestoreOp = "GREATER_THAN";
561
+ break;
562
+ case ">=":
563
+ firestoreOp = "GREATER_THAN_OR_EQUAL";
564
+ break;
565
+ case "array-contains":
566
+ firestoreOp = "ARRAY_CONTAINS";
567
+ break;
568
+ case "in":
569
+ firestoreOp = "IN";
570
+ break;
571
+ case "array-contains-any":
572
+ firestoreOp = "ARRAY_CONTAINS_ANY";
573
+ break;
574
+ case "not-in":
575
+ firestoreOp = "NOT_IN";
576
+ break;
577
+ default:
578
+ firestoreOp = opStr;
579
+ }
580
+ query._queryConstraints.where.push({
581
+ field: fieldPath,
582
+ op: firestoreOp,
583
+ value,
584
+ });
585
+ return query;
586
+ }
587
+ /**
588
+ * Add sorting condition
589
+ * @param fieldPath Field path
590
+ * @param directionStr Sort direction ('asc' or 'desc')
591
+ * @returns Query instance
592
+ */
593
+ orderBy(fieldPath, directionStr = "asc") {
594
+ const query = new Query(this.client, this.path, {
595
+ ...this._queryConstraints,
596
+ }, this.allDescendants);
597
+ query._queryConstraints.orderBy = fieldPath;
598
+ query._queryConstraints.orderDirection =
599
+ directionStr === "asc" ? "ASCENDING" : "DESCENDING";
600
+ return query;
601
+ }
602
+ /**
603
+ * Set limit on number of results
604
+ * @param limit Maximum number
605
+ * @returns Query instance
606
+ */
607
+ limit(limit) {
608
+ const query = new Query(this.client, this.path, {
609
+ ...this._queryConstraints,
610
+ }, this.allDescendants);
611
+ query._queryConstraints.limit = limit;
612
+ return query;
613
+ }
614
+ /**
615
+ * Set number of documents to skip
616
+ * @param offset Number to skip
617
+ * @returns Query instance
618
+ */
619
+ offset(offset) {
620
+ const query = new Query(this.client, this.path, {
621
+ ...this._queryConstraints,
622
+ }, this.allDescendants);
623
+ query._queryConstraints.offset = offset;
624
+ return query;
625
+ }
626
+ /**
627
+ * Execute query
628
+ * @returns QuerySnapshot instance
629
+ */
630
+ async get() {
631
+ const results = await this.client.query(this.path, this._queryConstraints, this.allDescendants);
632
+ return new QuerySnapshot(results);
633
+ }
634
+ /**
635
+ * Generate random ID
636
+ * @returns Random ID
637
+ */
638
+ _generateId() {
639
+ // Generate 20-character random ID
640
+ const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
641
+ let id = "";
642
+ for (let i = 0; i < 20; i++) {
643
+ id += chars.charAt(Math.floor(Math.random() * chars.length));
644
+ }
645
+ return id;
646
+ }
647
+ }
648
+ exports.CollectionReference = CollectionReference;
649
+ /**
650
+ * Document reference class
651
+ */
652
+ class DocumentReference {
653
+ constructor(client, collectionPath, docId) {
654
+ this.client = client;
655
+ this.collectionPath = collectionPath;
656
+ this.docId = docId;
657
+ }
658
+ /**
659
+ * Get document ID
660
+ */
661
+ get id() {
662
+ return this.docId;
663
+ }
664
+ /**
665
+ * Get document path
666
+ */
667
+ get path() {
668
+ return `${this.collectionPath}/${this.docId}`;
669
+ }
670
+ /**
671
+ * Get parent collection reference
672
+ */
673
+ get parent() {
674
+ return new CollectionReference(this.client, this.collectionPath);
675
+ }
676
+ /**
677
+ * Get subcollection
678
+ * @param collectionPath Subcollection name
679
+ * @returns CollectionReference instance
680
+ */
681
+ collection(collectionPath) {
682
+ return new CollectionReference(this.client, `${this.path}/${collectionPath}`);
683
+ }
684
+ /**
685
+ * Get document
686
+ * @returns DocumentSnapshot instance
687
+ */
688
+ async get() {
689
+ const data = await this.client.get(this.collectionPath, this.docId);
690
+ return new DocumentSnapshot(this.docId, data);
691
+ }
692
+ /**
693
+ * Create or overwrite document
694
+ * @param data Document data
695
+ * @param options Options (merge is not currently supported)
696
+ * @returns WriteResult instance
697
+ */
698
+ async set(data, options) {
699
+ // Get existing document
700
+ const existingDoc = await this.client.get(this.collectionPath, this.docId);
701
+ if (existingDoc) {
702
+ // If existing document exists, update
703
+ const mergedData = options?.merge ? { ...existingDoc, ...data } : data;
704
+ await this.client.update(this.collectionPath, this.docId, mergedData);
705
+ }
706
+ else {
707
+ // New creation
708
+ await this.client.createWithId(this.collectionPath, this.docId, data);
709
+ }
710
+ return new WriteResult();
711
+ }
712
+ /**
713
+ * Update document
714
+ * @param data Update data
715
+ * @returns WriteResult instance
716
+ */
717
+ async update(data) {
718
+ await this.client.update(this.collectionPath, this.docId, data);
719
+ return new WriteResult();
720
+ }
721
+ /**
722
+ * Delete document
723
+ * @returns WriteResult instance
724
+ */
725
+ async delete() {
726
+ await this.client.delete(this.collectionPath, this.docId);
727
+ return new WriteResult();
728
+ }
729
+ }
730
+ exports.DocumentReference = DocumentReference;
731
+ /**
732
+ * Collection group
733
+ */
734
+ class CollectionGroup {
735
+ constructor(client, path) {
736
+ this.client = client;
737
+ this.path = path;
738
+ this._queryConstraints = {
739
+ where: [],
740
+ };
741
+ }
742
+ /**
743
+ * Whether to include all descendant collections
744
+ */
745
+ get allDescendants() {
746
+ return true;
747
+ }
748
+ /**
749
+ * Add filter condition
750
+ * @param fieldPath Field path
751
+ * @param opStr Operator
752
+ * @param value Value
753
+ * @returns Query instance
754
+ */
755
+ where(fieldPath, opStr, value) {
756
+ const query = new Query(this.client, this.path, {
757
+ ...this._queryConstraints,
758
+ }, this.allDescendants);
759
+ // Operator conversion
760
+ let firestoreOp;
761
+ switch (opStr) {
762
+ case "==":
763
+ firestoreOp = "EQUAL";
764
+ break;
765
+ case "!=":
766
+ firestoreOp = "NOT_EQUAL";
767
+ break;
768
+ case "<":
769
+ firestoreOp = "LESS_THAN";
770
+ break;
771
+ case "<=":
772
+ firestoreOp = "LESS_THAN_OR_EQUAL";
773
+ break;
774
+ case ">":
775
+ firestoreOp = "GREATER_THAN";
776
+ break;
777
+ case ">=":
778
+ firestoreOp = "GREATER_THAN_OR_EQUAL";
779
+ break;
780
+ case "array-contains":
781
+ firestoreOp = "ARRAY_CONTAINS";
782
+ break;
783
+ case "in":
784
+ firestoreOp = "IN";
785
+ break;
786
+ case "array-contains-any":
787
+ firestoreOp = "ARRAY_CONTAINS_ANY";
788
+ break;
789
+ case "not-in":
790
+ firestoreOp = "NOT_IN";
791
+ break;
792
+ default:
793
+ firestoreOp = opStr;
794
+ }
795
+ query._queryConstraints.where.push({
796
+ field: fieldPath,
797
+ op: firestoreOp,
798
+ value,
799
+ });
800
+ return query;
801
+ }
802
+ /**
803
+ * Add sorting condition
804
+ * @param fieldPath Field path
805
+ * @param directionStr Sort direction ('asc' or 'desc')
806
+ * @returns Query instance
807
+ */
808
+ orderBy(fieldPath, directionStr = "asc") {
809
+ const query = new Query(this.client, this.path, {
810
+ ...this._queryConstraints,
811
+ }, this.allDescendants);
812
+ query._queryConstraints.orderBy = fieldPath;
813
+ query._queryConstraints.orderDirection =
814
+ directionStr === "asc" ? "ASCENDING" : "DESCENDING";
815
+ return query;
816
+ }
817
+ /**
818
+ * Set limit on number of results
819
+ * @param limit Maximum number
820
+ * @returns Query instance
821
+ */
822
+ limit(limit) {
823
+ const query = new Query(this.client, this.path, {
824
+ ...this._queryConstraints,
825
+ }, this.allDescendants);
826
+ query._queryConstraints.limit = limit;
827
+ return query;
828
+ }
829
+ /**
830
+ * Set number of documents to skip
831
+ * @param offset Number to skip
832
+ * @returns Query instance
833
+ */
834
+ offset(offset) {
835
+ const query = new Query(this.client, this.path, {
836
+ ...this._queryConstraints,
837
+ }, this.allDescendants);
838
+ query._queryConstraints.offset = offset;
839
+ return query;
840
+ }
841
+ /**
842
+ * Execute query
843
+ * @returns QuerySnapshot instance
844
+ */
845
+ async get() {
846
+ const results = await this.client.query(this.path, this._queryConstraints, this.allDescendants);
847
+ return new QuerySnapshot(results);
848
+ }
849
+ }
850
+ exports.CollectionGroup = CollectionGroup;
851
+ /**
852
+ * Query class
853
+ */
854
+ class Query {
855
+ constructor(client, collectionPath, constraints, allDescendants) {
856
+ this.client = client;
857
+ this.collectionPath = collectionPath;
858
+ this._queryConstraints = constraints;
859
+ this.allDescendants = allDescendants;
860
+ }
861
+ /**
862
+ * Add filter condition
863
+ * @param fieldPath Field path
864
+ * @param opStr Operator
865
+ * @param value Value
866
+ * @returns Query instance
867
+ */
868
+ where(fieldPath, opStr, value) {
869
+ const query = new Query(this.client, this.collectionPath, {
870
+ ...this._queryConstraints,
871
+ }, this.allDescendants);
872
+ // Operator conversion
873
+ let firestoreOp;
874
+ switch (opStr) {
875
+ case "==":
876
+ firestoreOp = "EQUAL";
877
+ break;
878
+ case "!=":
879
+ firestoreOp = "NOT_EQUAL";
880
+ break;
881
+ case "<":
882
+ firestoreOp = "LESS_THAN";
883
+ break;
884
+ case "<=":
885
+ firestoreOp = "LESS_THAN_OR_EQUAL";
886
+ break;
887
+ case ">":
888
+ firestoreOp = "GREATER_THAN";
889
+ break;
890
+ case ">=":
891
+ firestoreOp = "GREATER_THAN_OR_EQUAL";
892
+ break;
893
+ case "array-contains":
894
+ firestoreOp = "ARRAY_CONTAINS";
895
+ break;
896
+ case "in":
897
+ firestoreOp = "IN";
898
+ break;
899
+ case "array-contains-any":
900
+ firestoreOp = "ARRAY_CONTAINS_ANY";
901
+ break;
902
+ case "not-in":
903
+ firestoreOp = "NOT_IN";
904
+ break;
905
+ default:
906
+ firestoreOp = opStr;
907
+ }
908
+ query._queryConstraints.where.push({
909
+ field: fieldPath,
910
+ op: firestoreOp,
911
+ value,
912
+ });
913
+ return query;
914
+ }
915
+ /**
916
+ * Add sorting condition
917
+ * @param fieldPath Field path
918
+ * @param directionStr Sort direction ('asc' or 'desc')
919
+ * @returns Query instance
920
+ */
921
+ orderBy(fieldPath, directionStr = "asc") {
922
+ const query = new Query(this.client, this.collectionPath, {
923
+ ...this._queryConstraints,
924
+ }, this.allDescendants);
925
+ query._queryConstraints.orderBy = fieldPath;
926
+ query._queryConstraints.orderDirection =
927
+ directionStr === "asc" ? "ASCENDING" : "DESCENDING";
928
+ return query;
929
+ }
930
+ /**
931
+ * Set limit on number of results
932
+ * @param limit Maximum number
933
+ * @returns Query instance
934
+ */
935
+ limit(limit) {
936
+ const query = new Query(this.client, this.collectionPath, {
937
+ ...this._queryConstraints,
938
+ }, this.allDescendants);
939
+ query._queryConstraints.limit = limit;
940
+ return query;
941
+ }
942
+ /**
943
+ * Set number of documents to skip
944
+ * @param offset Number to skip
945
+ * @returns Query instance
946
+ */
947
+ offset(offset) {
948
+ const query = new Query(this.client, this.collectionPath, {
949
+ ...this._queryConstraints,
950
+ }, this.allDescendants);
951
+ query._queryConstraints.offset = offset;
952
+ return query;
953
+ }
954
+ /**
955
+ * Execute query
956
+ * @returns QuerySnapshot instance
957
+ */
958
+ async get() {
959
+ const results = await this.client.query(this.collectionPath, this._queryConstraints, this.allDescendants);
960
+ return new QuerySnapshot(results);
961
+ }
962
+ }
963
+ exports.Query = Query;
964
+ /**
965
+ * Query result class
966
+ */
967
+ class QuerySnapshot {
968
+ constructor(results) {
969
+ this._docs = results.map(doc => {
970
+ const { id, ...data } = doc;
971
+ return new DocumentSnapshot(id, data);
972
+ });
973
+ }
974
+ /**
975
+ * Array of documents in the result
976
+ */
977
+ get docs() {
978
+ return this._docs;
979
+ }
980
+ /**
981
+ * Whether the result is empty
982
+ */
983
+ get empty() {
984
+ return this._docs.length === 0;
985
+ }
986
+ /**
987
+ * Number of results
988
+ */
989
+ get size() {
990
+ return this._docs.length;
991
+ }
992
+ /**
993
+ * Execute callback for each document
994
+ * @param callback Callback function to execute for each document
995
+ */
996
+ forEach(callback) {
997
+ this._docs.forEach(callback);
998
+ }
999
+ }
1000
+ exports.QuerySnapshot = QuerySnapshot;
1001
+ /**
1002
+ * Document snapshot class
1003
+ */
1004
+ class DocumentSnapshot {
1005
+ constructor(id, data) {
1006
+ this._id = id;
1007
+ this._data = data;
1008
+ }
1009
+ /**
1010
+ * Document ID
1011
+ */
1012
+ get id() {
1013
+ return this._id;
1014
+ }
1015
+ /**
1016
+ * Whether the document exists
1017
+ */
1018
+ get exists() {
1019
+ return this._data !== null;
1020
+ }
1021
+ /**
1022
+ * Get document data
1023
+ * @returns Document data (undefined if it doesn't exist)
1024
+ */
1025
+ data() {
1026
+ return this._data || undefined;
1027
+ }
1028
+ }
1029
+ exports.DocumentSnapshot = DocumentSnapshot;
1030
+ /**
1031
+ * Write result class
1032
+ */
1033
+ class WriteResult {
1034
+ constructor() {
1035
+ this.writeTime = new Date();
1036
+ }
1037
+ }
1038
+ exports.WriteResult = WriteResult;
1039
+ /**
1040
+ * Create a new Firestore client instance
1041
+ * @param config Firestore configuration object
1042
+ * @returns FirestoreClient instance
1043
+ *
1044
+ * @example
1045
+ * // Connect to default database
1046
+ * const db = createFirestoreClient({
1047
+ * projectId: 'your-project-id',
1048
+ * privateKey: 'your-private-key',
1049
+ * clientEmail: 'your-client-email'
1050
+ * });
1051
+ *
1052
+ * // Connect to a different named database
1053
+ * const customDb = createFirestoreClient({
1054
+ * projectId: 'your-project-id',
1055
+ * privateKey: 'your-private-key',
1056
+ * clientEmail: 'your-client-email',
1057
+ * databaseId: 'your-database-id'
1058
+ * });
1059
+ *
1060
+ * // Connect to local emulator (no auth required)
1061
+ * const emulatorDb = createFirestoreClient({
1062
+ * projectId: 'demo-project',
1063
+ * useEmulator: true,
1064
+ * emulatorHost: '127.0.',
1065
+ * emulatorPort: 8080,
1066
+ * debug: true // Optional: enables detailed logging
1067
+ * });
1068
+ */
1069
+ function createFirestoreClient(config) {
1070
+ // Check private key format
1071
+ if (config.privateKey) {
1072
+ config = {
1073
+ ...config,
1074
+ privateKey: (0, config_1.formatPrivateKey)(config.privateKey),
1075
+ };
1076
+ }
1077
+ return new FirestoreClient(config);
1078
+ }