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.
- package/README.md +37 -0
- package/dist/cjs/client.d.ts +454 -0
- package/dist/cjs/client.js +1192 -0
- package/dist/cjs/field-value.d.ts +22 -0
- package/dist/cjs/field-value.js +31 -0
- package/dist/{index.d.ts → cjs/index.d.ts} +2 -1
- package/dist/{index.js → cjs/index.js} +4 -2
- package/dist/cjs/types.d.ts +146 -0
- package/dist/cjs/types.js +75 -0
- package/dist/cjs/utils/auth.d.ts +13 -0
- package/dist/{utils → cjs/utils}/auth.js +14 -10
- package/dist/cjs/utils/config.d.ts +6 -0
- package/dist/{utils → cjs/utils}/config.js +0 -12
- package/dist/cjs/utils/converter.d.ts +57 -0
- package/dist/cjs/utils/converter.js +224 -0
- package/dist/cjs/utils/path.d.ts +73 -0
- package/dist/cjs/utils/path.js +176 -0
- package/dist/esm/client.d.ts +454 -0
- package/dist/esm/client.js +1180 -0
- package/dist/esm/field-value.d.ts +22 -0
- package/dist/esm/field-value.js +27 -0
- package/dist/esm/index.d.ts +8 -0
- package/dist/esm/index.js +13 -0
- package/dist/esm/types.d.ts +146 -0
- package/dist/esm/types.js +70 -0
- package/dist/esm/utils/auth.d.ts +13 -0
- package/dist/esm/utils/auth.js +57 -0
- package/dist/esm/utils/config.d.ts +6 -0
- package/dist/esm/utils/config.js +11 -0
- package/dist/esm/utils/converter.d.ts +57 -0
- package/dist/esm/utils/converter.js +216 -0
- package/dist/esm/utils/path.d.ts +73 -0
- package/dist/esm/utils/path.js +169 -0
- package/dist/types/client.d.ts +454 -0
- package/dist/types/field-value.d.ts +22 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/types.d.ts +146 -0
- package/dist/types/utils/auth.d.ts +13 -0
- package/dist/types/utils/config.d.ts +6 -0
- package/dist/types/utils/converter.d.ts +57 -0
- package/dist/types/utils/path.d.ts +73 -0
- package/package.json +19 -5
- package/dist/client.d.ts +0 -381
- package/dist/client.js +0 -915
- package/dist/types.d.ts +0 -65
- package/dist/types.js +0 -2
- package/dist/utils/auth.d.ts +0 -13
- package/dist/utils/config.d.ts +0 -13
- package/dist/utils/converter.d.ts +0 -27
- package/dist/utils/converter.js +0 -113
- package/dist/utils/path.d.ts +0 -14
- 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
|
+
}
|