fires2rest 0.0.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/LICENSE +21 -0
- package/README.md +201 -0
- package/dist/index.d.ts +426 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +820 -0
- package/dist/index.js.map +1 -0
- package/package.json +60 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,820 @@
|
|
|
1
|
+
import { SignJWT, importPKCS8 } from "jose";
|
|
2
|
+
|
|
3
|
+
//#region src/auth.ts
|
|
4
|
+
async function createJWT(config) {
|
|
5
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
6
|
+
const payload = {
|
|
7
|
+
iss: config.clientEmail,
|
|
8
|
+
sub: config.clientEmail,
|
|
9
|
+
aud: "https://oauth2.googleapis.com/token",
|
|
10
|
+
iat: now,
|
|
11
|
+
exp: now + 3600,
|
|
12
|
+
scope: "https://www.googleapis.com/auth/datastore"
|
|
13
|
+
};
|
|
14
|
+
try {
|
|
15
|
+
const privateKey = await importPKCS8(config.privateKey, "RS256");
|
|
16
|
+
return await new SignJWT(payload).setProtectedHeader({
|
|
17
|
+
alg: "RS256",
|
|
18
|
+
typ: "JWT"
|
|
19
|
+
}).sign(privateKey);
|
|
20
|
+
} catch (error) {
|
|
21
|
+
console.error("Error creating JWT:", error);
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async function getFirestoreToken(config) {
|
|
26
|
+
const data = await (await fetch("https://oauth2.googleapis.com/token", {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: { "Content-Type": "application/json" },
|
|
29
|
+
body: JSON.stringify({
|
|
30
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
31
|
+
assertion: await createJWT(config)
|
|
32
|
+
})
|
|
33
|
+
})).json();
|
|
34
|
+
if (typeof data.access_token !== "string") throw new Error("Invalid access token");
|
|
35
|
+
return data.access_token;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
//#endregion
|
|
39
|
+
//#region src/utils.ts
|
|
40
|
+
/**
|
|
41
|
+
* Generate a random document ID (20 characters, alphanumeric)
|
|
42
|
+
*/
|
|
43
|
+
function generateDocumentId() {
|
|
44
|
+
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
|
45
|
+
let result = "";
|
|
46
|
+
for (let i = 0; i < 20; i++) result += chars.charAt(Math.floor(Math.random() * 62));
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Parse a document path into collection and document ID
|
|
51
|
+
*/
|
|
52
|
+
function parseDocumentPath(path) {
|
|
53
|
+
const parts = path.split("/");
|
|
54
|
+
if (parts.length < 2 || parts.length % 2 !== 0) throw new Error(`Invalid document path: ${path}`);
|
|
55
|
+
return {
|
|
56
|
+
collection: parts.slice(0, -1).join("/"),
|
|
57
|
+
docId: parts[parts.length - 1]
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Extract document ID from full resource name
|
|
62
|
+
*/
|
|
63
|
+
function extractDocumentId(name) {
|
|
64
|
+
const parts = name.split("/");
|
|
65
|
+
return parts[parts.length - 1];
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get all field paths from an object.
|
|
69
|
+
*/
|
|
70
|
+
function getFieldPaths(obj, prefix = "", isFieldValue$1) {
|
|
71
|
+
const paths = [];
|
|
72
|
+
for (const key of Object.keys(obj)) if (key.includes(".")) paths.push(key);
|
|
73
|
+
else {
|
|
74
|
+
const fullPath = prefix ? `${prefix}.${key}` : key;
|
|
75
|
+
const value = obj[key];
|
|
76
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !isFieldValue$1(value) && Object.keys(value).length > 0 && !("latitude" in value && "longitude" in value)) paths.push(...getFieldPaths(value, fullPath, isFieldValue$1));
|
|
77
|
+
else paths.push(fullPath);
|
|
78
|
+
}
|
|
79
|
+
return paths;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Build Firestore fields from update data, handling dot-notation paths.
|
|
83
|
+
*/
|
|
84
|
+
function buildUpdateFields(data, toFirestoreValue$1, isFieldValue$1) {
|
|
85
|
+
const fields = {};
|
|
86
|
+
for (const [key, value] of Object.entries(data)) {
|
|
87
|
+
if (isFieldValue$1(value)) continue;
|
|
88
|
+
if (key.includes(".")) {
|
|
89
|
+
const parts = key.split(".");
|
|
90
|
+
let current = fields;
|
|
91
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
92
|
+
const part = parts[i];
|
|
93
|
+
if (!current[part]) current[part] = { mapValue: { fields: {} } };
|
|
94
|
+
const mapVal = current[part];
|
|
95
|
+
if (!mapVal.mapValue.fields) mapVal.mapValue.fields = {};
|
|
96
|
+
current = mapVal.mapValue.fields;
|
|
97
|
+
}
|
|
98
|
+
current[parts[parts.length - 1]] = toFirestoreValue$1(value);
|
|
99
|
+
} else fields[key] = toFirestoreValue$1(value);
|
|
100
|
+
}
|
|
101
|
+
return fields;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
//#endregion
|
|
105
|
+
//#region src/field-value.ts
|
|
106
|
+
/**
|
|
107
|
+
* FieldValue Sentinels
|
|
108
|
+
*
|
|
109
|
+
* Sentinel values for special Firestore operations.
|
|
110
|
+
*/
|
|
111
|
+
/** Property name to identify FieldValue instances */
|
|
112
|
+
const FIELD_VALUE_MARKER = "__isFieldValue__";
|
|
113
|
+
/** Base class for FieldValue sentinels */
|
|
114
|
+
var FieldValueBase = class {
|
|
115
|
+
constructor() {
|
|
116
|
+
this.__isFieldValue__ = true;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
/** Server timestamp sentinel */
|
|
120
|
+
var ServerTimestampValue = class extends FieldValueBase {
|
|
121
|
+
constructor(..._args) {
|
|
122
|
+
super(..._args);
|
|
123
|
+
this._type = "serverTimestamp";
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
/** Delete field sentinel */
|
|
127
|
+
var DeleteFieldValue = class extends FieldValueBase {
|
|
128
|
+
constructor(..._args2) {
|
|
129
|
+
super(..._args2);
|
|
130
|
+
this._type = "delete";
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
/** Increment sentinel */
|
|
134
|
+
var IncrementValue = class extends FieldValueBase {
|
|
135
|
+
constructor(amount) {
|
|
136
|
+
super();
|
|
137
|
+
this.amount = amount;
|
|
138
|
+
this._type = "increment";
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
/** Array union sentinel */
|
|
142
|
+
var ArrayUnionValue = class extends FieldValueBase {
|
|
143
|
+
constructor(elements) {
|
|
144
|
+
super();
|
|
145
|
+
this.elements = elements;
|
|
146
|
+
this._type = "arrayUnion";
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
/** Array remove sentinel */
|
|
150
|
+
var ArrayRemoveValue = class extends FieldValueBase {
|
|
151
|
+
constructor(elements) {
|
|
152
|
+
super();
|
|
153
|
+
this.elements = elements;
|
|
154
|
+
this._type = "arrayRemove";
|
|
155
|
+
}
|
|
156
|
+
};
|
|
157
|
+
/**
|
|
158
|
+
* FieldValue factory for creating sentinel values
|
|
159
|
+
*/
|
|
160
|
+
const FieldValue = {
|
|
161
|
+
serverTimestamp() {
|
|
162
|
+
return new ServerTimestampValue();
|
|
163
|
+
},
|
|
164
|
+
delete() {
|
|
165
|
+
return new DeleteFieldValue();
|
|
166
|
+
},
|
|
167
|
+
increment(amount) {
|
|
168
|
+
return new IncrementValue(amount);
|
|
169
|
+
},
|
|
170
|
+
arrayUnion(...elements) {
|
|
171
|
+
return new ArrayUnionValue(elements);
|
|
172
|
+
},
|
|
173
|
+
arrayRemove(...elements) {
|
|
174
|
+
return new ArrayRemoveValue(elements);
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
/**
|
|
178
|
+
* Check if a value is a FieldValue sentinel
|
|
179
|
+
*/
|
|
180
|
+
function isFieldValue(value) {
|
|
181
|
+
return typeof value === "object" && value !== null && FIELD_VALUE_MARKER in value && value[FIELD_VALUE_MARKER] === true;
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Check if a value is a ServerTimestamp sentinel
|
|
185
|
+
*/
|
|
186
|
+
function isServerTimestamp(value) {
|
|
187
|
+
return value instanceof ServerTimestampValue;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Check if a value is a Delete sentinel
|
|
191
|
+
*/
|
|
192
|
+
function isDeleteField(value) {
|
|
193
|
+
return value instanceof DeleteFieldValue;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Check if a value is an Increment sentinel
|
|
197
|
+
*/
|
|
198
|
+
function isIncrement(value) {
|
|
199
|
+
return value instanceof IncrementValue;
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Get increment amount if value is an increment sentinel
|
|
203
|
+
*/
|
|
204
|
+
function getIncrementAmount(value) {
|
|
205
|
+
if (value instanceof IncrementValue) return value.amount;
|
|
206
|
+
}
|
|
207
|
+
/**
|
|
208
|
+
* Check if a value is an ArrayUnion sentinel
|
|
209
|
+
*/
|
|
210
|
+
function isArrayUnion(value) {
|
|
211
|
+
return value instanceof ArrayUnionValue;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Get array union elements if value is an array union sentinel
|
|
215
|
+
*/
|
|
216
|
+
function getArrayUnionElements(value) {
|
|
217
|
+
if (value instanceof ArrayUnionValue) return value.elements;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Check if a value is an ArrayRemove sentinel
|
|
221
|
+
*/
|
|
222
|
+
function isArrayRemove(value) {
|
|
223
|
+
return value instanceof ArrayRemoveValue;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Get array remove elements if value is an array remove sentinel
|
|
227
|
+
*/
|
|
228
|
+
function getArrayRemoveElements(value) {
|
|
229
|
+
if (value instanceof ArrayRemoveValue) return value.elements;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/value.ts
|
|
234
|
+
/**
|
|
235
|
+
* Value Conversion Utilities
|
|
236
|
+
*
|
|
237
|
+
* Bidirectional conversion between JavaScript values and Firestore REST format.
|
|
238
|
+
*/
|
|
239
|
+
/**
|
|
240
|
+
* Represents a geographic point (latitude/longitude).
|
|
241
|
+
*/
|
|
242
|
+
var GeoPoint = class {
|
|
243
|
+
constructor(latitude, longitude) {
|
|
244
|
+
this.latitude = latitude;
|
|
245
|
+
this.longitude = longitude;
|
|
246
|
+
if (latitude < -90 || latitude > 90) throw new Error("Latitude must be between -90 and 90");
|
|
247
|
+
if (longitude < -180 || longitude > 180) throw new Error("Longitude must be between -180 and 180");
|
|
248
|
+
}
|
|
249
|
+
isEqual(other) {
|
|
250
|
+
return this.latitude === other.latitude && this.longitude === other.longitude;
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* Represents a Firestore timestamp with nanosecond precision.
|
|
255
|
+
*/
|
|
256
|
+
var Timestamp = class Timestamp {
|
|
257
|
+
constructor(seconds, nanoseconds) {
|
|
258
|
+
this.seconds = seconds;
|
|
259
|
+
this.nanoseconds = nanoseconds;
|
|
260
|
+
}
|
|
261
|
+
static now() {
|
|
262
|
+
const now = Date.now();
|
|
263
|
+
return new Timestamp(Math.floor(now / 1e3), now % 1e3 * 1e6);
|
|
264
|
+
}
|
|
265
|
+
static fromDate(date) {
|
|
266
|
+
const ms = date.getTime();
|
|
267
|
+
return new Timestamp(Math.floor(ms / 1e3), ms % 1e3 * 1e6);
|
|
268
|
+
}
|
|
269
|
+
static fromMillis(milliseconds) {
|
|
270
|
+
return new Timestamp(Math.floor(milliseconds / 1e3), milliseconds % 1e3 * 1e6);
|
|
271
|
+
}
|
|
272
|
+
toDate() {
|
|
273
|
+
return /* @__PURE__ */ new Date(this.seconds * 1e3 + this.nanoseconds / 1e6);
|
|
274
|
+
}
|
|
275
|
+
toMillis() {
|
|
276
|
+
return this.seconds * 1e3 + this.nanoseconds / 1e6;
|
|
277
|
+
}
|
|
278
|
+
isEqual(other) {
|
|
279
|
+
return this.seconds === other.seconds && this.nanoseconds === other.nanoseconds;
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
/**
|
|
283
|
+
* Convert a JavaScript value to Firestore REST format.
|
|
284
|
+
*/
|
|
285
|
+
function toFirestoreValue(value) {
|
|
286
|
+
if (value === null) return { nullValue: null };
|
|
287
|
+
if (value === void 0) return { nullValue: null };
|
|
288
|
+
if (typeof value === "boolean") return { booleanValue: value };
|
|
289
|
+
if (typeof value === "number") {
|
|
290
|
+
if (Number.isInteger(value)) return { integerValue: String(value) };
|
|
291
|
+
return { doubleValue: value };
|
|
292
|
+
}
|
|
293
|
+
if (typeof value === "string") return { stringValue: value };
|
|
294
|
+
if (value instanceof Date) return { timestampValue: value.toISOString() };
|
|
295
|
+
if (value instanceof Timestamp) return { timestampValue: value.toDate().toISOString() };
|
|
296
|
+
if (value instanceof GeoPoint) return { geoPointValue: {
|
|
297
|
+
latitude: value.latitude,
|
|
298
|
+
longitude: value.longitude
|
|
299
|
+
} };
|
|
300
|
+
if (value instanceof Uint8Array) {
|
|
301
|
+
const binary = Array.from(value, (byte) => String.fromCharCode(byte)).join("");
|
|
302
|
+
return { bytesValue: btoa(binary) };
|
|
303
|
+
}
|
|
304
|
+
if (Array.isArray(value)) return { arrayValue: { values: value.map(toFirestoreValue) } };
|
|
305
|
+
if (typeof value === "object") {
|
|
306
|
+
const fields = {};
|
|
307
|
+
for (const [key, val] of Object.entries(value)) if (!isFieldValue(val)) fields[key] = toFirestoreValue(val);
|
|
308
|
+
return { mapValue: { fields } };
|
|
309
|
+
}
|
|
310
|
+
throw new Error(`Unsupported value type: ${typeof value}`);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Convert document data to Firestore fields format.
|
|
314
|
+
*/
|
|
315
|
+
function toFirestoreFields(data) {
|
|
316
|
+
const fields = {};
|
|
317
|
+
for (const [key, value] of Object.entries(data)) if (!isFieldValue(value)) fields[key] = toFirestoreValue(value);
|
|
318
|
+
return fields;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Extract field transforms from document data.
|
|
322
|
+
*/
|
|
323
|
+
function extractFieldTransforms(data, pathPrefix = "") {
|
|
324
|
+
const transforms = [];
|
|
325
|
+
for (const [key, value] of Object.entries(data)) {
|
|
326
|
+
const fieldPath = pathPrefix ? `${pathPrefix}.${key}` : key;
|
|
327
|
+
if (isFieldValue(value)) {
|
|
328
|
+
const transform = fieldValueToTransform(value, fieldPath);
|
|
329
|
+
if (transform) transforms.push(transform);
|
|
330
|
+
} else if (typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof Timestamp) && !(value instanceof GeoPoint) && !(value instanceof Uint8Array)) transforms.push(...extractFieldTransforms(value, fieldPath));
|
|
331
|
+
}
|
|
332
|
+
return transforms;
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Convert a FieldValue sentinel to a FieldTransform.
|
|
336
|
+
*/
|
|
337
|
+
function fieldValueToTransform(fieldValue, fieldPath) {
|
|
338
|
+
if (isServerTimestamp(fieldValue)) return {
|
|
339
|
+
fieldPath,
|
|
340
|
+
setToServerValue: "REQUEST_TIME"
|
|
341
|
+
};
|
|
342
|
+
if (isDeleteField(fieldValue)) return null;
|
|
343
|
+
if (isIncrement(fieldValue)) return {
|
|
344
|
+
fieldPath,
|
|
345
|
+
increment: toFirestoreValue(getIncrementAmount(fieldValue))
|
|
346
|
+
};
|
|
347
|
+
if (isArrayUnion(fieldValue)) return {
|
|
348
|
+
fieldPath,
|
|
349
|
+
appendMissingElements: { values: (getArrayUnionElements(fieldValue) ?? []).map(toFirestoreValue) }
|
|
350
|
+
};
|
|
351
|
+
if (isArrayRemove(fieldValue)) return {
|
|
352
|
+
fieldPath,
|
|
353
|
+
removeAllFromArray: { values: (getArrayRemoveElements(fieldValue) ?? []).map(toFirestoreValue) }
|
|
354
|
+
};
|
|
355
|
+
return null;
|
|
356
|
+
}
|
|
357
|
+
/**
|
|
358
|
+
* Extract delete field paths from document data.
|
|
359
|
+
*/
|
|
360
|
+
function extractDeleteFields(data, pathPrefix = "") {
|
|
361
|
+
const deletePaths = [];
|
|
362
|
+
for (const [key, value] of Object.entries(data)) {
|
|
363
|
+
const fieldPath = pathPrefix ? `${pathPrefix}.${key}` : key;
|
|
364
|
+
if (isFieldValue(value) && isDeleteField(value)) deletePaths.push(fieldPath);
|
|
365
|
+
else if (typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof Timestamp) && !(value instanceof GeoPoint) && !(value instanceof Uint8Array)) deletePaths.push(...extractDeleteFields(value, fieldPath));
|
|
366
|
+
}
|
|
367
|
+
return deletePaths;
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Extract transform field paths from document data.
|
|
371
|
+
* These are fields with FieldValue sentinels that become transforms (not delete).
|
|
372
|
+
*/
|
|
373
|
+
function extractTransformFields(data, pathPrefix = "") {
|
|
374
|
+
const transformPaths = [];
|
|
375
|
+
for (const [key, value] of Object.entries(data)) {
|
|
376
|
+
const fieldPath = pathPrefix ? `${pathPrefix}.${key}` : key;
|
|
377
|
+
if (isFieldValue(value) && !isDeleteField(value)) transformPaths.push(fieldPath);
|
|
378
|
+
else if (typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Date) && !(value instanceof Timestamp) && !(value instanceof GeoPoint) && !(value instanceof Uint8Array)) transformPaths.push(...extractTransformFields(value, fieldPath));
|
|
379
|
+
}
|
|
380
|
+
return transformPaths;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Convert a Firestore REST value to JavaScript.
|
|
384
|
+
*/
|
|
385
|
+
function fromFirestoreValue(value) {
|
|
386
|
+
if ("nullValue" in value) return null;
|
|
387
|
+
if ("booleanValue" in value) return value.booleanValue;
|
|
388
|
+
if ("integerValue" in value) return parseInt(value.integerValue, 10);
|
|
389
|
+
if ("doubleValue" in value) return value.doubleValue;
|
|
390
|
+
if ("timestampValue" in value) return new Date(value.timestampValue);
|
|
391
|
+
if ("stringValue" in value) return value.stringValue;
|
|
392
|
+
if ("bytesValue" in value) {
|
|
393
|
+
const binary = atob(value.bytesValue);
|
|
394
|
+
const bytes = new Uint8Array(binary.length);
|
|
395
|
+
for (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i);
|
|
396
|
+
return bytes;
|
|
397
|
+
}
|
|
398
|
+
if ("referenceValue" in value) return value.referenceValue;
|
|
399
|
+
if ("geoPointValue" in value) return new GeoPoint(value.geoPointValue.latitude, value.geoPointValue.longitude);
|
|
400
|
+
if ("arrayValue" in value) return (value.arrayValue.values ?? []).map(fromFirestoreValue);
|
|
401
|
+
if ("mapValue" in value) return fromFirestoreFields(value.mapValue.fields ?? {});
|
|
402
|
+
throw new Error("Unknown Firestore value type");
|
|
403
|
+
}
|
|
404
|
+
/**
|
|
405
|
+
* Convert Firestore fields to JavaScript object.
|
|
406
|
+
*/
|
|
407
|
+
function fromFirestoreFields(fields) {
|
|
408
|
+
const result = {};
|
|
409
|
+
for (const [key, value] of Object.entries(fields)) result[key] = fromFirestoreValue(value);
|
|
410
|
+
return result;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
//#endregion
|
|
414
|
+
//#region src/references.ts
|
|
415
|
+
/**
|
|
416
|
+
* Document snapshot implementation.
|
|
417
|
+
*/
|
|
418
|
+
var DocumentSnapshotImpl = class {
|
|
419
|
+
constructor(doc, path) {
|
|
420
|
+
if (doc && doc.fields) {
|
|
421
|
+
this.exists = true;
|
|
422
|
+
this._data = fromFirestoreFields(doc.fields);
|
|
423
|
+
this.createTime = doc.createTime ? new Date(doc.createTime) : void 0;
|
|
424
|
+
this.updateTime = doc.updateTime ? new Date(doc.updateTime) : void 0;
|
|
425
|
+
} else {
|
|
426
|
+
this.exists = false;
|
|
427
|
+
this._data = void 0;
|
|
428
|
+
}
|
|
429
|
+
this.path = path;
|
|
430
|
+
this.id = extractDocumentId(path);
|
|
431
|
+
}
|
|
432
|
+
data() {
|
|
433
|
+
return this._data;
|
|
434
|
+
}
|
|
435
|
+
get(fieldPath) {
|
|
436
|
+
if (!this._data) return void 0;
|
|
437
|
+
const parts = fieldPath.split(".");
|
|
438
|
+
let current = this._data;
|
|
439
|
+
for (const part of parts) {
|
|
440
|
+
if (current === null || current === void 0) return void 0;
|
|
441
|
+
if (typeof current !== "object") return void 0;
|
|
442
|
+
current = current[part];
|
|
443
|
+
}
|
|
444
|
+
return current;
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
/**
|
|
448
|
+
* A reference to a Firestore document.
|
|
449
|
+
*/
|
|
450
|
+
var DocumentReference = class {
|
|
451
|
+
constructor(_firestore, path) {
|
|
452
|
+
this._firestore = _firestore;
|
|
453
|
+
this.path = path;
|
|
454
|
+
this.id = extractDocumentId(path);
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* Get the parent collection reference.
|
|
458
|
+
*/
|
|
459
|
+
get parent() {
|
|
460
|
+
const { collection } = parseDocumentPath(this.path);
|
|
461
|
+
return new CollectionReference(this._firestore, collection);
|
|
462
|
+
}
|
|
463
|
+
/**
|
|
464
|
+
* Get a subcollection of this document.
|
|
465
|
+
*/
|
|
466
|
+
collection(collectionPath) {
|
|
467
|
+
return new CollectionReference(this._firestore, `${this.path}/${collectionPath}`);
|
|
468
|
+
}
|
|
469
|
+
/**
|
|
470
|
+
* Get the document.
|
|
471
|
+
*/
|
|
472
|
+
async get() {
|
|
473
|
+
return new DocumentSnapshotImpl(await this._firestore._getDocument(this.path), this.path);
|
|
474
|
+
}
|
|
475
|
+
/**
|
|
476
|
+
* Set the document data.
|
|
477
|
+
*/
|
|
478
|
+
async set(data, options) {
|
|
479
|
+
return this._firestore._setDocument(this.path, data, options);
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Update the document data.
|
|
483
|
+
*/
|
|
484
|
+
async update(data) {
|
|
485
|
+
return this._firestore._updateDocument(this.path, data);
|
|
486
|
+
}
|
|
487
|
+
/**
|
|
488
|
+
* Delete the document.
|
|
489
|
+
*/
|
|
490
|
+
async delete() {
|
|
491
|
+
await this._firestore._deleteDocument(this.path);
|
|
492
|
+
}
|
|
493
|
+
};
|
|
494
|
+
/**
|
|
495
|
+
* A reference to a Firestore collection.
|
|
496
|
+
*/
|
|
497
|
+
var CollectionReference = class {
|
|
498
|
+
constructor(_firestore, path) {
|
|
499
|
+
this._firestore = _firestore;
|
|
500
|
+
this.path = path;
|
|
501
|
+
const parts = path.split("/");
|
|
502
|
+
this.id = parts[parts.length - 1];
|
|
503
|
+
}
|
|
504
|
+
/**
|
|
505
|
+
* Get a document reference in this collection.
|
|
506
|
+
* If no ID is provided, a random one will be generated.
|
|
507
|
+
*/
|
|
508
|
+
doc(documentId) {
|
|
509
|
+
const docId = documentId ?? generateDocumentId();
|
|
510
|
+
return new DocumentReference(this._firestore, `${this.path}/${docId}`);
|
|
511
|
+
}
|
|
512
|
+
/**
|
|
513
|
+
* Add a new document with an auto-generated ID.
|
|
514
|
+
*/
|
|
515
|
+
async add(data) {
|
|
516
|
+
const ref = this.doc();
|
|
517
|
+
await ref.set(data);
|
|
518
|
+
return ref;
|
|
519
|
+
}
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
//#endregion
|
|
523
|
+
//#region src/transaction.ts
|
|
524
|
+
/**
|
|
525
|
+
* Firestore Transaction
|
|
526
|
+
*/
|
|
527
|
+
/**
|
|
528
|
+
* A Firestore transaction.
|
|
529
|
+
* All reads must happen before any writes.
|
|
530
|
+
*/
|
|
531
|
+
var Transaction = class {
|
|
532
|
+
constructor(_firestore, _transactionId) {
|
|
533
|
+
this._firestore = _firestore;
|
|
534
|
+
this._transactionId = _transactionId;
|
|
535
|
+
this._writes = [];
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* Get a document within this transaction.
|
|
539
|
+
*/
|
|
540
|
+
async get(ref) {
|
|
541
|
+
return new DocumentSnapshotImpl(await this._firestore._getDocument(ref.path, this._transactionId), ref.path);
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Queue a set operation.
|
|
545
|
+
*/
|
|
546
|
+
set(ref, data, options) {
|
|
547
|
+
const docName = this._firestore._getDocumentName(ref.path);
|
|
548
|
+
const fields = toFirestoreFields(data);
|
|
549
|
+
const transforms = extractFieldTransforms(data);
|
|
550
|
+
const write = { update: {
|
|
551
|
+
name: docName,
|
|
552
|
+
fields
|
|
553
|
+
} };
|
|
554
|
+
if (options?.merge) write.updateMask = { fieldPaths: getFieldPaths(data, "", isFieldValue) };
|
|
555
|
+
if (transforms.length > 0) write.updateTransforms = transforms;
|
|
556
|
+
this._writes.push(write);
|
|
557
|
+
return this;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Queue an update operation.
|
|
561
|
+
*/
|
|
562
|
+
update(ref, data) {
|
|
563
|
+
const docName = this._firestore._getDocumentName(ref.path);
|
|
564
|
+
const deleteFields = extractDeleteFields(data);
|
|
565
|
+
const transforms = extractFieldTransforms(data);
|
|
566
|
+
const fieldPaths = getFieldPaths(data, "", isFieldValue).filter((p) => !deleteFields.includes(p));
|
|
567
|
+
const write = {
|
|
568
|
+
update: {
|
|
569
|
+
name: docName,
|
|
570
|
+
fields: toFirestoreFields(data)
|
|
571
|
+
},
|
|
572
|
+
updateMask: { fieldPaths: [...fieldPaths, ...deleteFields] },
|
|
573
|
+
currentDocument: { exists: true }
|
|
574
|
+
};
|
|
575
|
+
if (transforms.length > 0) write.updateTransforms = transforms;
|
|
576
|
+
this._writes.push(write);
|
|
577
|
+
return this;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Queue a delete operation.
|
|
581
|
+
*/
|
|
582
|
+
delete(ref) {
|
|
583
|
+
const docName = this._firestore._getDocumentName(ref.path);
|
|
584
|
+
this._writes.push({ delete: docName });
|
|
585
|
+
return this;
|
|
586
|
+
}
|
|
587
|
+
/** @internal */
|
|
588
|
+
_getWrites() {
|
|
589
|
+
return this._writes;
|
|
590
|
+
}
|
|
591
|
+
/** @internal */
|
|
592
|
+
_getTransactionId() {
|
|
593
|
+
return this._transactionId;
|
|
594
|
+
}
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
//#endregion
|
|
598
|
+
//#region src/client.ts
|
|
599
|
+
/**
|
|
600
|
+
* Firestore REST Client
|
|
601
|
+
*
|
|
602
|
+
* Main client implementation for Firestore REST API with transaction support.
|
|
603
|
+
*/
|
|
604
|
+
const API_BASE = "https://firestore.googleapis.com/v1";
|
|
605
|
+
const DEFAULT_DATABASE = "(default)";
|
|
606
|
+
/**
|
|
607
|
+
* Firestore REST API client.
|
|
608
|
+
*/
|
|
609
|
+
var Firestore = class {
|
|
610
|
+
constructor(config, databaseId = DEFAULT_DATABASE) {
|
|
611
|
+
this._token = null;
|
|
612
|
+
this._tokenExpiry = 0;
|
|
613
|
+
this._config = config;
|
|
614
|
+
this._databaseId = databaseId;
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* Get a collection reference.
|
|
618
|
+
*/
|
|
619
|
+
collection(collectionPath) {
|
|
620
|
+
return new CollectionReference(this, collectionPath);
|
|
621
|
+
}
|
|
622
|
+
/**
|
|
623
|
+
* Get a document reference.
|
|
624
|
+
*/
|
|
625
|
+
doc(documentPath) {
|
|
626
|
+
return new DocumentReference(this, documentPath);
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Run a transaction.
|
|
630
|
+
*/
|
|
631
|
+
async runTransaction(updateFn, options) {
|
|
632
|
+
const maxAttempts = options?.maxAttempts ?? 5;
|
|
633
|
+
let lastError = null;
|
|
634
|
+
let retryTransaction;
|
|
635
|
+
for (let attempt = 0; attempt < maxAttempts; attempt++) try {
|
|
636
|
+
const transactionId = await this._beginTransaction(retryTransaction);
|
|
637
|
+
const transaction = new Transaction(this, transactionId);
|
|
638
|
+
const result = await updateFn(transaction);
|
|
639
|
+
await this._commitTransaction(transactionId, transaction._getWrites());
|
|
640
|
+
return result;
|
|
641
|
+
} catch (error) {
|
|
642
|
+
lastError = error;
|
|
643
|
+
if (error instanceof Error && error.message.includes("ABORTED")) {
|
|
644
|
+
retryTransaction = void 0;
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
throw error;
|
|
648
|
+
}
|
|
649
|
+
throw lastError ?? /* @__PURE__ */ new Error("Transaction failed after max attempts");
|
|
650
|
+
}
|
|
651
|
+
/** @internal */
|
|
652
|
+
async _getToken() {
|
|
653
|
+
if (this._token && Date.now() < this._tokenExpiry - 6e4) return this._token;
|
|
654
|
+
this._token = await getFirestoreToken(this._config);
|
|
655
|
+
this._tokenExpiry = Date.now() + 3600 * 1e3;
|
|
656
|
+
return this._token;
|
|
657
|
+
}
|
|
658
|
+
/** @internal */
|
|
659
|
+
_getDatabasePath() {
|
|
660
|
+
return `projects/${this._config.projectId}/databases/${this._databaseId}`;
|
|
661
|
+
}
|
|
662
|
+
/** @internal */
|
|
663
|
+
_getDocumentName(path) {
|
|
664
|
+
return `${this._getDatabasePath()}/documents/${path}`;
|
|
665
|
+
}
|
|
666
|
+
/** @internal */
|
|
667
|
+
async _getDocument(path, transactionId) {
|
|
668
|
+
const token = await this._getToken();
|
|
669
|
+
let url = `${API_BASE}/${this._getDocumentName(path)}`;
|
|
670
|
+
if (transactionId) url += `?transaction=${encodeURIComponent(transactionId)}`;
|
|
671
|
+
const response = await fetch(url, {
|
|
672
|
+
method: "GET",
|
|
673
|
+
headers: { Authorization: `Bearer ${token}` }
|
|
674
|
+
});
|
|
675
|
+
if (response.status === 404) return null;
|
|
676
|
+
if (!response.ok) {
|
|
677
|
+
const error = await response.json();
|
|
678
|
+
throw new Error(`Failed to get document: ${JSON.stringify(error)}`);
|
|
679
|
+
}
|
|
680
|
+
return response.json();
|
|
681
|
+
}
|
|
682
|
+
/** @internal */
|
|
683
|
+
async _setDocument(path, data, options) {
|
|
684
|
+
const token = await this._getToken();
|
|
685
|
+
const database = this._getDatabasePath();
|
|
686
|
+
const docName = this._getDocumentName(path);
|
|
687
|
+
const fields = toFirestoreFields(data);
|
|
688
|
+
const transforms = extractFieldTransforms(data);
|
|
689
|
+
const writes = [{ update: {
|
|
690
|
+
name: docName,
|
|
691
|
+
fields
|
|
692
|
+
} }];
|
|
693
|
+
if (options?.merge) {
|
|
694
|
+
const fieldPaths = getFieldPaths(data, "", isFieldValue);
|
|
695
|
+
writes[0].updateMask = { fieldPaths };
|
|
696
|
+
}
|
|
697
|
+
if (transforms.length > 0) writes[0].updateTransforms = transforms;
|
|
698
|
+
const response = await fetch(`${API_BASE}/${database}/documents:commit`, {
|
|
699
|
+
method: "POST",
|
|
700
|
+
headers: {
|
|
701
|
+
Authorization: `Bearer ${token}`,
|
|
702
|
+
"Content-Type": "application/json"
|
|
703
|
+
},
|
|
704
|
+
body: JSON.stringify({ writes })
|
|
705
|
+
});
|
|
706
|
+
if (!response.ok) {
|
|
707
|
+
const error = await response.json();
|
|
708
|
+
throw new Error(`Failed to set document: ${JSON.stringify(error)}`);
|
|
709
|
+
}
|
|
710
|
+
return (await response.json()).writeResults?.[0] ?? {};
|
|
711
|
+
}
|
|
712
|
+
/** @internal */
|
|
713
|
+
async _updateDocument(path, data) {
|
|
714
|
+
const token = await this._getToken();
|
|
715
|
+
const database = this._getDatabasePath();
|
|
716
|
+
const docName = this._getDocumentName(path);
|
|
717
|
+
const deleteFields = extractDeleteFields(data);
|
|
718
|
+
const transforms = extractFieldTransforms(data);
|
|
719
|
+
const transformFields = extractTransformFields(data);
|
|
720
|
+
const fieldPaths = getFieldPaths(data, "", isFieldValue).filter((p) => !deleteFields.includes(p) && !transformFields.includes(p));
|
|
721
|
+
const fields = buildUpdateFields(data, toFirestoreValue, isFieldValue);
|
|
722
|
+
const updateMaskPaths = [...fieldPaths, ...deleteFields];
|
|
723
|
+
const writes = [];
|
|
724
|
+
if (updateMaskPaths.length > 0) {
|
|
725
|
+
const write = {
|
|
726
|
+
update: {
|
|
727
|
+
name: docName,
|
|
728
|
+
fields
|
|
729
|
+
},
|
|
730
|
+
updateMask: { fieldPaths: updateMaskPaths },
|
|
731
|
+
currentDocument: { exists: true }
|
|
732
|
+
};
|
|
733
|
+
writes.push(write);
|
|
734
|
+
}
|
|
735
|
+
if (transforms.length > 0) if (writes.length > 0) writes[0].updateTransforms = transforms;
|
|
736
|
+
else writes.push({
|
|
737
|
+
transform: {
|
|
738
|
+
document: docName,
|
|
739
|
+
fieldTransforms: transforms
|
|
740
|
+
},
|
|
741
|
+
currentDocument: { exists: true }
|
|
742
|
+
});
|
|
743
|
+
if (writes.length === 0) return {};
|
|
744
|
+
const response = await fetch(`${API_BASE}/${database}/documents:commit`, {
|
|
745
|
+
method: "POST",
|
|
746
|
+
headers: {
|
|
747
|
+
Authorization: `Bearer ${token}`,
|
|
748
|
+
"Content-Type": "application/json"
|
|
749
|
+
},
|
|
750
|
+
body: JSON.stringify({ writes })
|
|
751
|
+
});
|
|
752
|
+
if (!response.ok) {
|
|
753
|
+
const error = await response.json();
|
|
754
|
+
throw new Error(`Failed to update document: ${JSON.stringify(error)}`);
|
|
755
|
+
}
|
|
756
|
+
return (await response.json()).writeResults?.[0] ?? {};
|
|
757
|
+
}
|
|
758
|
+
/** @internal */
|
|
759
|
+
async _deleteDocument(path) {
|
|
760
|
+
const token = await this._getToken();
|
|
761
|
+
const database = this._getDatabasePath();
|
|
762
|
+
const docName = this._getDocumentName(path);
|
|
763
|
+
const response = await fetch(`${API_BASE}/${database}/documents:commit`, {
|
|
764
|
+
method: "POST",
|
|
765
|
+
headers: {
|
|
766
|
+
Authorization: `Bearer ${token}`,
|
|
767
|
+
"Content-Type": "application/json"
|
|
768
|
+
},
|
|
769
|
+
body: JSON.stringify({ writes: [{ delete: docName }] })
|
|
770
|
+
});
|
|
771
|
+
if (!response.ok) {
|
|
772
|
+
const error = await response.json();
|
|
773
|
+
throw new Error(`Failed to delete document: ${JSON.stringify(error)}`);
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
/** @internal */
|
|
777
|
+
async _beginTransaction(retryTransaction) {
|
|
778
|
+
const token = await this._getToken();
|
|
779
|
+
const database = this._getDatabasePath();
|
|
780
|
+
const options = { readWrite: retryTransaction ? { retryTransaction } : {} };
|
|
781
|
+
const response = await fetch(`${API_BASE}/${database}/documents:beginTransaction`, {
|
|
782
|
+
method: "POST",
|
|
783
|
+
headers: {
|
|
784
|
+
Authorization: `Bearer ${token}`,
|
|
785
|
+
"Content-Type": "application/json"
|
|
786
|
+
},
|
|
787
|
+
body: JSON.stringify({ options })
|
|
788
|
+
});
|
|
789
|
+
if (!response.ok) {
|
|
790
|
+
const error = await response.json();
|
|
791
|
+
throw new Error(`Failed to begin transaction: ${JSON.stringify(error)}`);
|
|
792
|
+
}
|
|
793
|
+
return (await response.json()).transaction;
|
|
794
|
+
}
|
|
795
|
+
/** @internal */
|
|
796
|
+
async _commitTransaction(transactionId, writes) {
|
|
797
|
+
const token = await this._getToken();
|
|
798
|
+
const database = this._getDatabasePath();
|
|
799
|
+
const response = await fetch(`${API_BASE}/${database}/documents:commit`, {
|
|
800
|
+
method: "POST",
|
|
801
|
+
headers: {
|
|
802
|
+
Authorization: `Bearer ${token}`,
|
|
803
|
+
"Content-Type": "application/json"
|
|
804
|
+
},
|
|
805
|
+
body: JSON.stringify({
|
|
806
|
+
writes,
|
|
807
|
+
transaction: transactionId
|
|
808
|
+
})
|
|
809
|
+
});
|
|
810
|
+
if (!response.ok) {
|
|
811
|
+
const error = await response.json();
|
|
812
|
+
throw new Error(`Failed to commit transaction: ${JSON.stringify(error)}`);
|
|
813
|
+
}
|
|
814
|
+
return response.json();
|
|
815
|
+
}
|
|
816
|
+
};
|
|
817
|
+
|
|
818
|
+
//#endregion
|
|
819
|
+
export { CollectionReference, DocumentReference, FieldValue, Firestore, GeoPoint, Timestamp, Transaction, createJWT, fromFirestoreValue, getFirestoreToken, toFirestoreValue };
|
|
820
|
+
//# sourceMappingURL=index.js.map
|