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