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