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