@prmichaelsen/firebase-admin-sdk-v8 2.0.16 → 2.0.19
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/dist/index.d.mts +26 -26
- package/dist/index.d.ts +26 -26
- package/dist/index.js +210 -140
- package/dist/index.mjs +210 -140
- package/firebase.json +9 -0
- package/firestore.indexes.json +33 -0
- package/firestore.rules +11 -0
- package/jest.e2e.config.js +14 -0
- package/package.json +5 -2
- package/service-account.json +13 -0
package/dist/index.d.mts
CHANGED
|
@@ -287,18 +287,18 @@ declare function getUserFromToken(idToken: string): Promise<UserInfo>;
|
|
|
287
287
|
declare function getAuth(): any;
|
|
288
288
|
|
|
289
289
|
/**
|
|
290
|
-
* Firebase Admin SDK v8 - Firestore
|
|
291
|
-
*
|
|
290
|
+
* Firebase Admin SDK v8 - Firestore CRUD Operations
|
|
291
|
+
* All Firestore document operations using REST API
|
|
292
292
|
*/
|
|
293
293
|
|
|
294
294
|
/**
|
|
295
295
|
* Set a document in Firestore (create or overwrite)
|
|
296
296
|
*
|
|
297
|
-
* @param
|
|
298
|
-
* @param
|
|
299
|
-
* @param
|
|
300
|
-
* @param
|
|
301
|
-
* @returns
|
|
297
|
+
* @param collectionPath - Collection path
|
|
298
|
+
* @param documentId - Document ID
|
|
299
|
+
* @param data - Document data
|
|
300
|
+
* @param options - Set options (merge, mergeFields)
|
|
301
|
+
* @returns Promise that resolves when document is set
|
|
302
302
|
* @throws {Error} If the operation fails
|
|
303
303
|
*
|
|
304
304
|
* @example
|
|
@@ -317,10 +317,10 @@ declare function setDocument(collectionPath: string, documentId: string, data: D
|
|
|
317
317
|
/**
|
|
318
318
|
* Add a document to Firestore collection
|
|
319
319
|
*
|
|
320
|
-
* @param
|
|
321
|
-
* @param
|
|
322
|
-
* @param
|
|
323
|
-
* @returns
|
|
320
|
+
* @param collectionPath - Collection path (e.g., 'users' or 'users/uid/posts')
|
|
321
|
+
* @param data - Document data
|
|
322
|
+
* @param documentId - Optional document ID (auto-generated if not provided)
|
|
323
|
+
* @returns Document reference with id and path
|
|
324
324
|
* @throws {Error} If the operation fails
|
|
325
325
|
*
|
|
326
326
|
* @example
|
|
@@ -337,9 +337,9 @@ declare function addDocument(collectionPath: string, data: DataObject, documentI
|
|
|
337
337
|
/**
|
|
338
338
|
* Get a document from Firestore
|
|
339
339
|
*
|
|
340
|
-
* @param
|
|
341
|
-
* @param
|
|
342
|
-
* @returns
|
|
340
|
+
* @param collectionPath - Collection path
|
|
341
|
+
* @param documentId - Document ID
|
|
342
|
+
* @returns Document data or null if not found
|
|
343
343
|
* @throws {Error} If the operation fails
|
|
344
344
|
*
|
|
345
345
|
* @example
|
|
@@ -354,10 +354,10 @@ declare function getDocument(collectionPath: string, documentId: string): Promis
|
|
|
354
354
|
/**
|
|
355
355
|
* Update a document in Firestore
|
|
356
356
|
*
|
|
357
|
-
* @param
|
|
358
|
-
* @param
|
|
359
|
-
* @param
|
|
360
|
-
* @returns
|
|
357
|
+
* @param collectionPath - Collection path
|
|
358
|
+
* @param documentId - Document ID
|
|
359
|
+
* @param data - Data to update
|
|
360
|
+
* @returns Promise that resolves when document is updated
|
|
361
361
|
* @throws {Error} If the operation fails
|
|
362
362
|
*
|
|
363
363
|
* @example
|
|
@@ -372,9 +372,9 @@ declare function updateDocument(collectionPath: string, documentId: string, data
|
|
|
372
372
|
/**
|
|
373
373
|
* Delete a document from Firestore
|
|
374
374
|
*
|
|
375
|
-
* @param
|
|
376
|
-
* @param
|
|
377
|
-
* @returns
|
|
375
|
+
* @param collectionPath - Collection path
|
|
376
|
+
* @param documentId - Document ID
|
|
377
|
+
* @returns Promise that resolves when document is deleted
|
|
378
378
|
* @throws {Error} If the operation fails
|
|
379
379
|
*
|
|
380
380
|
* @example
|
|
@@ -386,9 +386,9 @@ declare function deleteDocument(collectionPath: string, documentId: string): Pro
|
|
|
386
386
|
/**
|
|
387
387
|
* Query documents in a collection with advanced filtering
|
|
388
388
|
*
|
|
389
|
-
* @param
|
|
390
|
-
* @param
|
|
391
|
-
* @returns
|
|
389
|
+
* @param collectionPath - Collection path
|
|
390
|
+
* @param options - Query options (where, orderBy, limit, etc.)
|
|
391
|
+
* @returns Array of documents with id and data
|
|
392
392
|
* @throws {Error} If the operation fails
|
|
393
393
|
*
|
|
394
394
|
* @example
|
|
@@ -410,8 +410,8 @@ declare function queryDocuments(collectionPath: string, options?: QueryOptions):
|
|
|
410
410
|
/**
|
|
411
411
|
* Perform batch write operations (set, update, delete)
|
|
412
412
|
*
|
|
413
|
-
* @param
|
|
414
|
-
* @returns
|
|
413
|
+
* @param operations - Array of batch operations
|
|
414
|
+
* @returns Batch write result
|
|
415
415
|
* @throws {Error} If the operation fails
|
|
416
416
|
*
|
|
417
417
|
* @example
|
package/dist/index.d.ts
CHANGED
|
@@ -287,18 +287,18 @@ declare function getUserFromToken(idToken: string): Promise<UserInfo>;
|
|
|
287
287
|
declare function getAuth(): any;
|
|
288
288
|
|
|
289
289
|
/**
|
|
290
|
-
* Firebase Admin SDK v8 - Firestore
|
|
291
|
-
*
|
|
290
|
+
* Firebase Admin SDK v8 - Firestore CRUD Operations
|
|
291
|
+
* All Firestore document operations using REST API
|
|
292
292
|
*/
|
|
293
293
|
|
|
294
294
|
/**
|
|
295
295
|
* Set a document in Firestore (create or overwrite)
|
|
296
296
|
*
|
|
297
|
-
* @param
|
|
298
|
-
* @param
|
|
299
|
-
* @param
|
|
300
|
-
* @param
|
|
301
|
-
* @returns
|
|
297
|
+
* @param collectionPath - Collection path
|
|
298
|
+
* @param documentId - Document ID
|
|
299
|
+
* @param data - Document data
|
|
300
|
+
* @param options - Set options (merge, mergeFields)
|
|
301
|
+
* @returns Promise that resolves when document is set
|
|
302
302
|
* @throws {Error} If the operation fails
|
|
303
303
|
*
|
|
304
304
|
* @example
|
|
@@ -317,10 +317,10 @@ declare function setDocument(collectionPath: string, documentId: string, data: D
|
|
|
317
317
|
/**
|
|
318
318
|
* Add a document to Firestore collection
|
|
319
319
|
*
|
|
320
|
-
* @param
|
|
321
|
-
* @param
|
|
322
|
-
* @param
|
|
323
|
-
* @returns
|
|
320
|
+
* @param collectionPath - Collection path (e.g., 'users' or 'users/uid/posts')
|
|
321
|
+
* @param data - Document data
|
|
322
|
+
* @param documentId - Optional document ID (auto-generated if not provided)
|
|
323
|
+
* @returns Document reference with id and path
|
|
324
324
|
* @throws {Error} If the operation fails
|
|
325
325
|
*
|
|
326
326
|
* @example
|
|
@@ -337,9 +337,9 @@ declare function addDocument(collectionPath: string, data: DataObject, documentI
|
|
|
337
337
|
/**
|
|
338
338
|
* Get a document from Firestore
|
|
339
339
|
*
|
|
340
|
-
* @param
|
|
341
|
-
* @param
|
|
342
|
-
* @returns
|
|
340
|
+
* @param collectionPath - Collection path
|
|
341
|
+
* @param documentId - Document ID
|
|
342
|
+
* @returns Document data or null if not found
|
|
343
343
|
* @throws {Error} If the operation fails
|
|
344
344
|
*
|
|
345
345
|
* @example
|
|
@@ -354,10 +354,10 @@ declare function getDocument(collectionPath: string, documentId: string): Promis
|
|
|
354
354
|
/**
|
|
355
355
|
* Update a document in Firestore
|
|
356
356
|
*
|
|
357
|
-
* @param
|
|
358
|
-
* @param
|
|
359
|
-
* @param
|
|
360
|
-
* @returns
|
|
357
|
+
* @param collectionPath - Collection path
|
|
358
|
+
* @param documentId - Document ID
|
|
359
|
+
* @param data - Data to update
|
|
360
|
+
* @returns Promise that resolves when document is updated
|
|
361
361
|
* @throws {Error} If the operation fails
|
|
362
362
|
*
|
|
363
363
|
* @example
|
|
@@ -372,9 +372,9 @@ declare function updateDocument(collectionPath: string, documentId: string, data
|
|
|
372
372
|
/**
|
|
373
373
|
* Delete a document from Firestore
|
|
374
374
|
*
|
|
375
|
-
* @param
|
|
376
|
-
* @param
|
|
377
|
-
* @returns
|
|
375
|
+
* @param collectionPath - Collection path
|
|
376
|
+
* @param documentId - Document ID
|
|
377
|
+
* @returns Promise that resolves when document is deleted
|
|
378
378
|
* @throws {Error} If the operation fails
|
|
379
379
|
*
|
|
380
380
|
* @example
|
|
@@ -386,9 +386,9 @@ declare function deleteDocument(collectionPath: string, documentId: string): Pro
|
|
|
386
386
|
/**
|
|
387
387
|
* Query documents in a collection with advanced filtering
|
|
388
388
|
*
|
|
389
|
-
* @param
|
|
390
|
-
* @param
|
|
391
|
-
* @returns
|
|
389
|
+
* @param collectionPath - Collection path
|
|
390
|
+
* @param options - Query options (where, orderBy, limit, etc.)
|
|
391
|
+
* @returns Array of documents with id and data
|
|
392
392
|
* @throws {Error} If the operation fails
|
|
393
393
|
*
|
|
394
394
|
* @example
|
|
@@ -410,8 +410,8 @@ declare function queryDocuments(collectionPath: string, options?: QueryOptions):
|
|
|
410
410
|
/**
|
|
411
411
|
* Perform batch write operations (set, update, delete)
|
|
412
412
|
*
|
|
413
|
-
* @param
|
|
414
|
-
* @returns
|
|
413
|
+
* @param operations - Array of batch operations
|
|
414
|
+
* @returns Batch write result
|
|
415
415
|
* @throws {Error} If the operation fails
|
|
416
416
|
*
|
|
417
417
|
* @example
|
package/dist/index.js
CHANGED
|
@@ -331,78 +331,6 @@ function getAuth() {
|
|
|
331
331
|
};
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
-
// src/token-generation.ts
|
|
335
|
-
function base64UrlEncode(str) {
|
|
336
|
-
return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
337
|
-
}
|
|
338
|
-
function base64UrlEncodeBuffer(buffer) {
|
|
339
|
-
return btoa(String.fromCharCode(...buffer)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
340
|
-
}
|
|
341
|
-
async function createJWT(serviceAccount) {
|
|
342
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
343
|
-
const expiry = now + 3600;
|
|
344
|
-
const header = {
|
|
345
|
-
alg: "RS256",
|
|
346
|
-
typ: "JWT"
|
|
347
|
-
};
|
|
348
|
-
const payload = {
|
|
349
|
-
iss: serviceAccount.client_email,
|
|
350
|
-
sub: serviceAccount.client_email,
|
|
351
|
-
aud: serviceAccount.token_uri,
|
|
352
|
-
iat: now,
|
|
353
|
-
exp: expiry,
|
|
354
|
-
scope: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/datastore https://www.googleapis.com/auth/firebase"
|
|
355
|
-
};
|
|
356
|
-
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
357
|
-
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
|
358
|
-
const unsignedToken = `${encodedHeader}.${encodedPayload}`;
|
|
359
|
-
const pemContents = serviceAccount.private_key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/\s/g, "");
|
|
360
|
-
const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
|
|
361
|
-
const cryptoKey = await crypto.subtle.importKey(
|
|
362
|
-
"pkcs8",
|
|
363
|
-
binaryDer,
|
|
364
|
-
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
365
|
-
false,
|
|
366
|
-
["sign"]
|
|
367
|
-
);
|
|
368
|
-
const signature = await crypto.subtle.sign(
|
|
369
|
-
"RSASSA-PKCS1-v1_5",
|
|
370
|
-
cryptoKey,
|
|
371
|
-
new TextEncoder().encode(unsignedToken)
|
|
372
|
-
);
|
|
373
|
-
const encodedSignature = base64UrlEncodeBuffer(new Uint8Array(signature));
|
|
374
|
-
return `${unsignedToken}.${encodedSignature}`;
|
|
375
|
-
}
|
|
376
|
-
var cachedAccessToken = null;
|
|
377
|
-
var tokenExpiry = 0;
|
|
378
|
-
async function getAdminAccessToken() {
|
|
379
|
-
if (cachedAccessToken && Date.now() < tokenExpiry) {
|
|
380
|
-
return cachedAccessToken;
|
|
381
|
-
}
|
|
382
|
-
const serviceAccount = getServiceAccount();
|
|
383
|
-
const jwt = await createJWT(serviceAccount);
|
|
384
|
-
const response = await fetch(serviceAccount.token_uri, {
|
|
385
|
-
method: "POST",
|
|
386
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
387
|
-
body: new URLSearchParams({
|
|
388
|
-
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
389
|
-
assertion: jwt
|
|
390
|
-
})
|
|
391
|
-
});
|
|
392
|
-
if (!response.ok) {
|
|
393
|
-
const errorText = await response.text();
|
|
394
|
-
throw new Error(`Failed to get access token: ${errorText}`);
|
|
395
|
-
}
|
|
396
|
-
const data = await response.json();
|
|
397
|
-
cachedAccessToken = data.access_token;
|
|
398
|
-
tokenExpiry = Date.now() + data.expires_in * 1e3 - 6e4;
|
|
399
|
-
return cachedAccessToken;
|
|
400
|
-
}
|
|
401
|
-
function clearTokenCache() {
|
|
402
|
-
cachedAccessToken = null;
|
|
403
|
-
tokenExpiry = 0;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
334
|
// src/field-value.ts
|
|
407
335
|
function serverTimestamp() {
|
|
408
336
|
return {
|
|
@@ -443,8 +371,7 @@ var FieldValue = {
|
|
|
443
371
|
delete: deleteField
|
|
444
372
|
};
|
|
445
373
|
|
|
446
|
-
// src/firestore
|
|
447
|
-
var FIRESTORE_API = "https://firestore.googleapis.com/v1";
|
|
374
|
+
// src/firestore/converters.ts
|
|
448
375
|
function toFirestoreValue(value) {
|
|
449
376
|
if (value === null || value === void 0) {
|
|
450
377
|
return { nullValue: null };
|
|
@@ -504,58 +431,6 @@ function convertToFirestoreFormat(data) {
|
|
|
504
431
|
}
|
|
505
432
|
return result;
|
|
506
433
|
}
|
|
507
|
-
function extractFieldTransforms(data, fieldPrefix = "") {
|
|
508
|
-
const transforms = [];
|
|
509
|
-
for (const [key, value] of Object.entries(data)) {
|
|
510
|
-
const fieldPath = fieldPrefix ? `${fieldPrefix}.${key}` : key;
|
|
511
|
-
if (isFieldValue(value)) {
|
|
512
|
-
switch (value._type) {
|
|
513
|
-
case "serverTimestamp":
|
|
514
|
-
transforms.push({
|
|
515
|
-
fieldPath,
|
|
516
|
-
setToServerValue: "REQUEST_TIME"
|
|
517
|
-
});
|
|
518
|
-
break;
|
|
519
|
-
case "increment":
|
|
520
|
-
transforms.push({
|
|
521
|
-
fieldPath,
|
|
522
|
-
increment: toFirestoreValue(value._value)
|
|
523
|
-
});
|
|
524
|
-
break;
|
|
525
|
-
case "arrayUnion":
|
|
526
|
-
transforms.push({
|
|
527
|
-
fieldPath,
|
|
528
|
-
appendMissingElements: {
|
|
529
|
-
values: value._value.map((v) => toFirestoreValue(v))
|
|
530
|
-
}
|
|
531
|
-
});
|
|
532
|
-
break;
|
|
533
|
-
case "arrayRemove":
|
|
534
|
-
transforms.push({
|
|
535
|
-
fieldPath,
|
|
536
|
-
removeAllFromArray: {
|
|
537
|
-
values: value._value.map((v) => toFirestoreValue(v))
|
|
538
|
-
}
|
|
539
|
-
});
|
|
540
|
-
break;
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
return transforms;
|
|
545
|
-
}
|
|
546
|
-
function removeFieldTransforms(data) {
|
|
547
|
-
const result = {};
|
|
548
|
-
for (const [key, value] of Object.entries(data)) {
|
|
549
|
-
if (isFieldValue(value)) {
|
|
550
|
-
if (value._type === "delete") {
|
|
551
|
-
continue;
|
|
552
|
-
}
|
|
553
|
-
continue;
|
|
554
|
-
}
|
|
555
|
-
result[key] = value;
|
|
556
|
-
}
|
|
557
|
-
return result;
|
|
558
|
-
}
|
|
559
434
|
function fromFirestoreValue(value) {
|
|
560
435
|
if ("stringValue" in value) {
|
|
561
436
|
return value.stringValue;
|
|
@@ -584,12 +459,17 @@ function fromFirestoreValue(value) {
|
|
|
584
459
|
return null;
|
|
585
460
|
}
|
|
586
461
|
function convertFromFirestoreFormat(fields) {
|
|
462
|
+
if (!fields) {
|
|
463
|
+
return {};
|
|
464
|
+
}
|
|
587
465
|
const result = {};
|
|
588
466
|
for (const [key, value] of Object.entries(fields)) {
|
|
589
467
|
result[key] = fromFirestoreValue(value);
|
|
590
468
|
}
|
|
591
469
|
return result;
|
|
592
470
|
}
|
|
471
|
+
|
|
472
|
+
// src/firestore/query-builder.ts
|
|
593
473
|
function buildStructuredQuery(collectionPath, options) {
|
|
594
474
|
const pathSegments = collectionPath.split("/");
|
|
595
475
|
const collectionId = pathSegments[pathSegments.length - 1];
|
|
@@ -672,14 +552,181 @@ function mapWhereOp(op) {
|
|
|
672
552
|
};
|
|
673
553
|
return opMap[op] || "EQUAL";
|
|
674
554
|
}
|
|
675
|
-
|
|
555
|
+
|
|
556
|
+
// src/firestore/transforms.ts
|
|
557
|
+
function extractFieldTransforms(data, fieldPrefix = "") {
|
|
558
|
+
const transforms = [];
|
|
559
|
+
for (const [key, value] of Object.entries(data)) {
|
|
560
|
+
const fieldPath = fieldPrefix ? `${fieldPrefix}.${key}` : key;
|
|
561
|
+
if (isFieldValue(value)) {
|
|
562
|
+
switch (value._type) {
|
|
563
|
+
case "serverTimestamp":
|
|
564
|
+
transforms.push({
|
|
565
|
+
fieldPath,
|
|
566
|
+
setToServerValue: "REQUEST_TIME"
|
|
567
|
+
});
|
|
568
|
+
break;
|
|
569
|
+
case "increment":
|
|
570
|
+
transforms.push({
|
|
571
|
+
fieldPath,
|
|
572
|
+
increment: toFirestoreValue(value._value)
|
|
573
|
+
});
|
|
574
|
+
break;
|
|
575
|
+
case "arrayUnion":
|
|
576
|
+
transforms.push({
|
|
577
|
+
fieldPath,
|
|
578
|
+
appendMissingElements: {
|
|
579
|
+
values: value._value.map((v) => toFirestoreValue(v))
|
|
580
|
+
}
|
|
581
|
+
});
|
|
582
|
+
break;
|
|
583
|
+
case "arrayRemove":
|
|
584
|
+
transforms.push({
|
|
585
|
+
fieldPath,
|
|
586
|
+
removeAllFromArray: {
|
|
587
|
+
values: value._value.map((v) => toFirestoreValue(v))
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
return transforms;
|
|
595
|
+
}
|
|
596
|
+
function removeFieldTransforms(data) {
|
|
597
|
+
const result = {};
|
|
598
|
+
for (const [key, value] of Object.entries(data)) {
|
|
599
|
+
if (isFieldValue(value)) {
|
|
600
|
+
if (value._type === "delete") {
|
|
601
|
+
continue;
|
|
602
|
+
}
|
|
603
|
+
continue;
|
|
604
|
+
}
|
|
605
|
+
result[key] = value;
|
|
606
|
+
}
|
|
607
|
+
return result;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/token-generation.ts
|
|
611
|
+
function base64UrlEncode(str) {
|
|
612
|
+
return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
613
|
+
}
|
|
614
|
+
function base64UrlEncodeBuffer(buffer) {
|
|
615
|
+
return btoa(String.fromCharCode(...buffer)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
616
|
+
}
|
|
617
|
+
async function createJWT(serviceAccount) {
|
|
618
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
619
|
+
const expiry = now + 3600;
|
|
620
|
+
const header = {
|
|
621
|
+
alg: "RS256",
|
|
622
|
+
typ: "JWT"
|
|
623
|
+
};
|
|
624
|
+
const payload = {
|
|
625
|
+
iss: serviceAccount.client_email,
|
|
626
|
+
sub: serviceAccount.client_email,
|
|
627
|
+
aud: serviceAccount.token_uri,
|
|
628
|
+
iat: now,
|
|
629
|
+
exp: expiry,
|
|
630
|
+
scope: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/datastore https://www.googleapis.com/auth/firebase"
|
|
631
|
+
};
|
|
632
|
+
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
633
|
+
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
|
634
|
+
const unsignedToken = `${encodedHeader}.${encodedPayload}`;
|
|
635
|
+
const pemContents = serviceAccount.private_key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/\s/g, "");
|
|
636
|
+
const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
|
|
637
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
638
|
+
"pkcs8",
|
|
639
|
+
binaryDer,
|
|
640
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
641
|
+
false,
|
|
642
|
+
["sign"]
|
|
643
|
+
);
|
|
644
|
+
const signature = await crypto.subtle.sign(
|
|
645
|
+
"RSASSA-PKCS1-v1_5",
|
|
646
|
+
cryptoKey,
|
|
647
|
+
new TextEncoder().encode(unsignedToken)
|
|
648
|
+
);
|
|
649
|
+
const encodedSignature = base64UrlEncodeBuffer(new Uint8Array(signature));
|
|
650
|
+
return `${unsignedToken}.${encodedSignature}`;
|
|
651
|
+
}
|
|
652
|
+
var cachedAccessToken = null;
|
|
653
|
+
var tokenExpiry = 0;
|
|
654
|
+
async function getAdminAccessToken() {
|
|
655
|
+
if (cachedAccessToken && Date.now() < tokenExpiry) {
|
|
656
|
+
return cachedAccessToken;
|
|
657
|
+
}
|
|
658
|
+
const serviceAccount = getServiceAccount();
|
|
659
|
+
const jwt = await createJWT(serviceAccount);
|
|
660
|
+
const response = await fetch(serviceAccount.token_uri, {
|
|
661
|
+
method: "POST",
|
|
662
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
663
|
+
body: new URLSearchParams({
|
|
664
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
665
|
+
assertion: jwt
|
|
666
|
+
})
|
|
667
|
+
});
|
|
668
|
+
if (!response.ok) {
|
|
669
|
+
const errorText = await response.text();
|
|
670
|
+
throw new Error(`Failed to get access token: ${errorText}`);
|
|
671
|
+
}
|
|
672
|
+
const data = await response.json();
|
|
673
|
+
cachedAccessToken = data.access_token;
|
|
674
|
+
tokenExpiry = Date.now() + data.expires_in * 1e3 - 6e4;
|
|
675
|
+
return cachedAccessToken;
|
|
676
|
+
}
|
|
677
|
+
function clearTokenCache() {
|
|
678
|
+
cachedAccessToken = null;
|
|
679
|
+
tokenExpiry = 0;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
// src/firestore/operations.ts
|
|
683
|
+
var FIRESTORE_API = "https://firestore.googleapis.com/v1";
|
|
684
|
+
async function commitWrites(writes) {
|
|
676
685
|
const accessToken = await getAdminAccessToken();
|
|
677
686
|
const projectId = getProjectId();
|
|
678
|
-
const url = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents
|
|
687
|
+
const url = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents:commit`;
|
|
688
|
+
const response = await fetch(url, {
|
|
689
|
+
method: "POST",
|
|
690
|
+
headers: {
|
|
691
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
692
|
+
"Content-Type": "application/json"
|
|
693
|
+
},
|
|
694
|
+
body: JSON.stringify({ writes })
|
|
695
|
+
});
|
|
696
|
+
if (!response.ok) {
|
|
697
|
+
const errorText = await response.text();
|
|
698
|
+
throw new Error(`Failed to commit writes: ${errorText}`);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
async function setDocument(collectionPath, documentId, data, options) {
|
|
702
|
+
const projectId = getProjectId();
|
|
703
|
+
const documentPath = `projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
|
|
679
704
|
const cleanData = removeFieldTransforms(data);
|
|
680
705
|
const firestoreData = convertToFirestoreFormat(cleanData);
|
|
681
706
|
const transforms = extractFieldTransforms(data);
|
|
682
|
-
|
|
707
|
+
if (transforms.length > 0) {
|
|
708
|
+
const updateWrite = {
|
|
709
|
+
update: {
|
|
710
|
+
name: documentPath,
|
|
711
|
+
fields: firestoreData
|
|
712
|
+
},
|
|
713
|
+
updateTransforms: transforms
|
|
714
|
+
};
|
|
715
|
+
const nonTransformFields = Object.keys(cleanData);
|
|
716
|
+
if (nonTransformFields.length > 0) {
|
|
717
|
+
if (options?.merge) {
|
|
718
|
+
updateWrite.updateMask = { fieldPaths: ["*"] };
|
|
719
|
+
} else if (options?.mergeFields && options.mergeFields.length > 0) {
|
|
720
|
+
updateWrite.updateMask = { fieldPaths: options.mergeFields };
|
|
721
|
+
} else {
|
|
722
|
+
updateWrite.updateMask = { fieldPaths: nonTransformFields };
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
await commitWrites([updateWrite]);
|
|
726
|
+
return;
|
|
727
|
+
}
|
|
728
|
+
const accessToken = await getAdminAccessToken();
|
|
729
|
+
const url = `${FIRESTORE_API}/${documentPath}`;
|
|
683
730
|
let queryParams = "";
|
|
684
731
|
if (options?.merge) {
|
|
685
732
|
queryParams = "?updateMask.fieldPaths=*";
|
|
@@ -687,16 +734,13 @@ async function setDocument(collectionPath, documentId, data, options) {
|
|
|
687
734
|
const fieldPaths = options.mergeFields.join("&updateMask.fieldPaths=");
|
|
688
735
|
queryParams = `?updateMask.fieldPaths=${fieldPaths}`;
|
|
689
736
|
}
|
|
690
|
-
if (transforms.length > 0) {
|
|
691
|
-
body.transforms = transforms;
|
|
692
|
-
}
|
|
693
737
|
const response = await fetch(`${url}${queryParams}`, {
|
|
694
738
|
method: "PATCH",
|
|
695
739
|
headers: {
|
|
696
740
|
"Authorization": `Bearer ${accessToken}`,
|
|
697
741
|
"Content-Type": "application/json"
|
|
698
742
|
},
|
|
699
|
-
body: JSON.stringify(
|
|
743
|
+
body: JSON.stringify({ fields: firestoreData })
|
|
700
744
|
});
|
|
701
745
|
if (!response.ok) {
|
|
702
746
|
const errorText = await response.text();
|
|
@@ -754,25 +798,51 @@ async function getDocument(collectionPath, documentId) {
|
|
|
754
798
|
return convertFromFirestoreFormat(result.fields);
|
|
755
799
|
}
|
|
756
800
|
async function updateDocument(collectionPath, documentId, data) {
|
|
757
|
-
const accessToken = await getAdminAccessToken();
|
|
758
801
|
const projectId = getProjectId();
|
|
759
|
-
const
|
|
802
|
+
const documentPath = `projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
|
|
760
803
|
const cleanData = removeFieldTransforms(data);
|
|
761
804
|
const firestoreData = convertToFirestoreFormat(cleanData);
|
|
762
805
|
const transforms = extractFieldTransforms(data);
|
|
763
|
-
const updateMaskFields = Object.keys(data).filter((key) =>
|
|
764
|
-
|
|
765
|
-
|
|
806
|
+
const updateMaskFields = Object.keys(data).filter((key) => {
|
|
807
|
+
if (!isFieldValue(data[key])) {
|
|
808
|
+
return true;
|
|
809
|
+
}
|
|
810
|
+
return data[key]._type === "delete";
|
|
811
|
+
});
|
|
766
812
|
if (transforms.length > 0) {
|
|
767
|
-
|
|
813
|
+
const nonTransformFields = Object.keys(cleanData);
|
|
814
|
+
if (nonTransformFields.length === 0) {
|
|
815
|
+
const transformWrite = {
|
|
816
|
+
transform: {
|
|
817
|
+
document: documentPath,
|
|
818
|
+
fieldTransforms: transforms
|
|
819
|
+
}
|
|
820
|
+
};
|
|
821
|
+
await commitWrites([transformWrite]);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
const updateWrite = {
|
|
825
|
+
update: {
|
|
826
|
+
name: documentPath,
|
|
827
|
+
fields: firestoreData
|
|
828
|
+
},
|
|
829
|
+
updateMask: { fieldPaths: updateMaskFields },
|
|
830
|
+
updateTransforms: transforms,
|
|
831
|
+
currentDocument: { exists: true }
|
|
832
|
+
};
|
|
833
|
+
await commitWrites([updateWrite]);
|
|
834
|
+
return;
|
|
768
835
|
}
|
|
836
|
+
const accessToken = await getAdminAccessToken();
|
|
837
|
+
const url = `${FIRESTORE_API}/${documentPath}`;
|
|
838
|
+
const updateMaskParams = updateMaskFields.map((field) => `updateMask.fieldPaths=${encodeURIComponent(field)}`).join("&");
|
|
769
839
|
const response = await fetch(`${url}?${updateMaskParams}¤tDocument.exists=true`, {
|
|
770
840
|
method: "PATCH",
|
|
771
841
|
headers: {
|
|
772
842
|
"Authorization": `Bearer ${accessToken}`,
|
|
773
843
|
"Content-Type": "application/json"
|
|
774
844
|
},
|
|
775
|
-
body: JSON.stringify(
|
|
845
|
+
body: JSON.stringify({ fields: firestoreData })
|
|
776
846
|
});
|
|
777
847
|
if (!response.ok) {
|
|
778
848
|
const errorText = await response.text();
|
package/dist/index.mjs
CHANGED
|
@@ -288,78 +288,6 @@ function getAuth() {
|
|
|
288
288
|
};
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
-
// src/token-generation.ts
|
|
292
|
-
function base64UrlEncode(str) {
|
|
293
|
-
return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
294
|
-
}
|
|
295
|
-
function base64UrlEncodeBuffer(buffer) {
|
|
296
|
-
return btoa(String.fromCharCode(...buffer)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
297
|
-
}
|
|
298
|
-
async function createJWT(serviceAccount) {
|
|
299
|
-
const now = Math.floor(Date.now() / 1e3);
|
|
300
|
-
const expiry = now + 3600;
|
|
301
|
-
const header = {
|
|
302
|
-
alg: "RS256",
|
|
303
|
-
typ: "JWT"
|
|
304
|
-
};
|
|
305
|
-
const payload = {
|
|
306
|
-
iss: serviceAccount.client_email,
|
|
307
|
-
sub: serviceAccount.client_email,
|
|
308
|
-
aud: serviceAccount.token_uri,
|
|
309
|
-
iat: now,
|
|
310
|
-
exp: expiry,
|
|
311
|
-
scope: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/datastore https://www.googleapis.com/auth/firebase"
|
|
312
|
-
};
|
|
313
|
-
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
314
|
-
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
|
315
|
-
const unsignedToken = `${encodedHeader}.${encodedPayload}`;
|
|
316
|
-
const pemContents = serviceAccount.private_key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/\s/g, "");
|
|
317
|
-
const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
|
|
318
|
-
const cryptoKey = await crypto.subtle.importKey(
|
|
319
|
-
"pkcs8",
|
|
320
|
-
binaryDer,
|
|
321
|
-
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
322
|
-
false,
|
|
323
|
-
["sign"]
|
|
324
|
-
);
|
|
325
|
-
const signature = await crypto.subtle.sign(
|
|
326
|
-
"RSASSA-PKCS1-v1_5",
|
|
327
|
-
cryptoKey,
|
|
328
|
-
new TextEncoder().encode(unsignedToken)
|
|
329
|
-
);
|
|
330
|
-
const encodedSignature = base64UrlEncodeBuffer(new Uint8Array(signature));
|
|
331
|
-
return `${unsignedToken}.${encodedSignature}`;
|
|
332
|
-
}
|
|
333
|
-
var cachedAccessToken = null;
|
|
334
|
-
var tokenExpiry = 0;
|
|
335
|
-
async function getAdminAccessToken() {
|
|
336
|
-
if (cachedAccessToken && Date.now() < tokenExpiry) {
|
|
337
|
-
return cachedAccessToken;
|
|
338
|
-
}
|
|
339
|
-
const serviceAccount = getServiceAccount();
|
|
340
|
-
const jwt = await createJWT(serviceAccount);
|
|
341
|
-
const response = await fetch(serviceAccount.token_uri, {
|
|
342
|
-
method: "POST",
|
|
343
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
344
|
-
body: new URLSearchParams({
|
|
345
|
-
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
346
|
-
assertion: jwt
|
|
347
|
-
})
|
|
348
|
-
});
|
|
349
|
-
if (!response.ok) {
|
|
350
|
-
const errorText = await response.text();
|
|
351
|
-
throw new Error(`Failed to get access token: ${errorText}`);
|
|
352
|
-
}
|
|
353
|
-
const data = await response.json();
|
|
354
|
-
cachedAccessToken = data.access_token;
|
|
355
|
-
tokenExpiry = Date.now() + data.expires_in * 1e3 - 6e4;
|
|
356
|
-
return cachedAccessToken;
|
|
357
|
-
}
|
|
358
|
-
function clearTokenCache() {
|
|
359
|
-
cachedAccessToken = null;
|
|
360
|
-
tokenExpiry = 0;
|
|
361
|
-
}
|
|
362
|
-
|
|
363
291
|
// src/field-value.ts
|
|
364
292
|
function serverTimestamp() {
|
|
365
293
|
return {
|
|
@@ -400,8 +328,7 @@ var FieldValue = {
|
|
|
400
328
|
delete: deleteField
|
|
401
329
|
};
|
|
402
330
|
|
|
403
|
-
// src/firestore
|
|
404
|
-
var FIRESTORE_API = "https://firestore.googleapis.com/v1";
|
|
331
|
+
// src/firestore/converters.ts
|
|
405
332
|
function toFirestoreValue(value) {
|
|
406
333
|
if (value === null || value === void 0) {
|
|
407
334
|
return { nullValue: null };
|
|
@@ -461,58 +388,6 @@ function convertToFirestoreFormat(data) {
|
|
|
461
388
|
}
|
|
462
389
|
return result;
|
|
463
390
|
}
|
|
464
|
-
function extractFieldTransforms(data, fieldPrefix = "") {
|
|
465
|
-
const transforms = [];
|
|
466
|
-
for (const [key, value] of Object.entries(data)) {
|
|
467
|
-
const fieldPath = fieldPrefix ? `${fieldPrefix}.${key}` : key;
|
|
468
|
-
if (isFieldValue(value)) {
|
|
469
|
-
switch (value._type) {
|
|
470
|
-
case "serverTimestamp":
|
|
471
|
-
transforms.push({
|
|
472
|
-
fieldPath,
|
|
473
|
-
setToServerValue: "REQUEST_TIME"
|
|
474
|
-
});
|
|
475
|
-
break;
|
|
476
|
-
case "increment":
|
|
477
|
-
transforms.push({
|
|
478
|
-
fieldPath,
|
|
479
|
-
increment: toFirestoreValue(value._value)
|
|
480
|
-
});
|
|
481
|
-
break;
|
|
482
|
-
case "arrayUnion":
|
|
483
|
-
transforms.push({
|
|
484
|
-
fieldPath,
|
|
485
|
-
appendMissingElements: {
|
|
486
|
-
values: value._value.map((v) => toFirestoreValue(v))
|
|
487
|
-
}
|
|
488
|
-
});
|
|
489
|
-
break;
|
|
490
|
-
case "arrayRemove":
|
|
491
|
-
transforms.push({
|
|
492
|
-
fieldPath,
|
|
493
|
-
removeAllFromArray: {
|
|
494
|
-
values: value._value.map((v) => toFirestoreValue(v))
|
|
495
|
-
}
|
|
496
|
-
});
|
|
497
|
-
break;
|
|
498
|
-
}
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
return transforms;
|
|
502
|
-
}
|
|
503
|
-
function removeFieldTransforms(data) {
|
|
504
|
-
const result = {};
|
|
505
|
-
for (const [key, value] of Object.entries(data)) {
|
|
506
|
-
if (isFieldValue(value)) {
|
|
507
|
-
if (value._type === "delete") {
|
|
508
|
-
continue;
|
|
509
|
-
}
|
|
510
|
-
continue;
|
|
511
|
-
}
|
|
512
|
-
result[key] = value;
|
|
513
|
-
}
|
|
514
|
-
return result;
|
|
515
|
-
}
|
|
516
391
|
function fromFirestoreValue(value) {
|
|
517
392
|
if ("stringValue" in value) {
|
|
518
393
|
return value.stringValue;
|
|
@@ -541,12 +416,17 @@ function fromFirestoreValue(value) {
|
|
|
541
416
|
return null;
|
|
542
417
|
}
|
|
543
418
|
function convertFromFirestoreFormat(fields) {
|
|
419
|
+
if (!fields) {
|
|
420
|
+
return {};
|
|
421
|
+
}
|
|
544
422
|
const result = {};
|
|
545
423
|
for (const [key, value] of Object.entries(fields)) {
|
|
546
424
|
result[key] = fromFirestoreValue(value);
|
|
547
425
|
}
|
|
548
426
|
return result;
|
|
549
427
|
}
|
|
428
|
+
|
|
429
|
+
// src/firestore/query-builder.ts
|
|
550
430
|
function buildStructuredQuery(collectionPath, options) {
|
|
551
431
|
const pathSegments = collectionPath.split("/");
|
|
552
432
|
const collectionId = pathSegments[pathSegments.length - 1];
|
|
@@ -629,14 +509,181 @@ function mapWhereOp(op) {
|
|
|
629
509
|
};
|
|
630
510
|
return opMap[op] || "EQUAL";
|
|
631
511
|
}
|
|
632
|
-
|
|
512
|
+
|
|
513
|
+
// src/firestore/transforms.ts
|
|
514
|
+
function extractFieldTransforms(data, fieldPrefix = "") {
|
|
515
|
+
const transforms = [];
|
|
516
|
+
for (const [key, value] of Object.entries(data)) {
|
|
517
|
+
const fieldPath = fieldPrefix ? `${fieldPrefix}.${key}` : key;
|
|
518
|
+
if (isFieldValue(value)) {
|
|
519
|
+
switch (value._type) {
|
|
520
|
+
case "serverTimestamp":
|
|
521
|
+
transforms.push({
|
|
522
|
+
fieldPath,
|
|
523
|
+
setToServerValue: "REQUEST_TIME"
|
|
524
|
+
});
|
|
525
|
+
break;
|
|
526
|
+
case "increment":
|
|
527
|
+
transforms.push({
|
|
528
|
+
fieldPath,
|
|
529
|
+
increment: toFirestoreValue(value._value)
|
|
530
|
+
});
|
|
531
|
+
break;
|
|
532
|
+
case "arrayUnion":
|
|
533
|
+
transforms.push({
|
|
534
|
+
fieldPath,
|
|
535
|
+
appendMissingElements: {
|
|
536
|
+
values: value._value.map((v) => toFirestoreValue(v))
|
|
537
|
+
}
|
|
538
|
+
});
|
|
539
|
+
break;
|
|
540
|
+
case "arrayRemove":
|
|
541
|
+
transforms.push({
|
|
542
|
+
fieldPath,
|
|
543
|
+
removeAllFromArray: {
|
|
544
|
+
values: value._value.map((v) => toFirestoreValue(v))
|
|
545
|
+
}
|
|
546
|
+
});
|
|
547
|
+
break;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
return transforms;
|
|
552
|
+
}
|
|
553
|
+
function removeFieldTransforms(data) {
|
|
554
|
+
const result = {};
|
|
555
|
+
for (const [key, value] of Object.entries(data)) {
|
|
556
|
+
if (isFieldValue(value)) {
|
|
557
|
+
if (value._type === "delete") {
|
|
558
|
+
continue;
|
|
559
|
+
}
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
result[key] = value;
|
|
563
|
+
}
|
|
564
|
+
return result;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
// src/token-generation.ts
|
|
568
|
+
function base64UrlEncode(str) {
|
|
569
|
+
return btoa(str).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
570
|
+
}
|
|
571
|
+
function base64UrlEncodeBuffer(buffer) {
|
|
572
|
+
return btoa(String.fromCharCode(...buffer)).replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
|
|
573
|
+
}
|
|
574
|
+
async function createJWT(serviceAccount) {
|
|
575
|
+
const now = Math.floor(Date.now() / 1e3);
|
|
576
|
+
const expiry = now + 3600;
|
|
577
|
+
const header = {
|
|
578
|
+
alg: "RS256",
|
|
579
|
+
typ: "JWT"
|
|
580
|
+
};
|
|
581
|
+
const payload = {
|
|
582
|
+
iss: serviceAccount.client_email,
|
|
583
|
+
sub: serviceAccount.client_email,
|
|
584
|
+
aud: serviceAccount.token_uri,
|
|
585
|
+
iat: now,
|
|
586
|
+
exp: expiry,
|
|
587
|
+
scope: "https://www.googleapis.com/auth/cloud-platform https://www.googleapis.com/auth/datastore https://www.googleapis.com/auth/firebase"
|
|
588
|
+
};
|
|
589
|
+
const encodedHeader = base64UrlEncode(JSON.stringify(header));
|
|
590
|
+
const encodedPayload = base64UrlEncode(JSON.stringify(payload));
|
|
591
|
+
const unsignedToken = `${encodedHeader}.${encodedPayload}`;
|
|
592
|
+
const pemContents = serviceAccount.private_key.replace("-----BEGIN PRIVATE KEY-----", "").replace("-----END PRIVATE KEY-----", "").replace(/\s/g, "");
|
|
593
|
+
const binaryDer = Uint8Array.from(atob(pemContents), (c) => c.charCodeAt(0));
|
|
594
|
+
const cryptoKey = await crypto.subtle.importKey(
|
|
595
|
+
"pkcs8",
|
|
596
|
+
binaryDer,
|
|
597
|
+
{ name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" },
|
|
598
|
+
false,
|
|
599
|
+
["sign"]
|
|
600
|
+
);
|
|
601
|
+
const signature = await crypto.subtle.sign(
|
|
602
|
+
"RSASSA-PKCS1-v1_5",
|
|
603
|
+
cryptoKey,
|
|
604
|
+
new TextEncoder().encode(unsignedToken)
|
|
605
|
+
);
|
|
606
|
+
const encodedSignature = base64UrlEncodeBuffer(new Uint8Array(signature));
|
|
607
|
+
return `${unsignedToken}.${encodedSignature}`;
|
|
608
|
+
}
|
|
609
|
+
var cachedAccessToken = null;
|
|
610
|
+
var tokenExpiry = 0;
|
|
611
|
+
async function getAdminAccessToken() {
|
|
612
|
+
if (cachedAccessToken && Date.now() < tokenExpiry) {
|
|
613
|
+
return cachedAccessToken;
|
|
614
|
+
}
|
|
615
|
+
const serviceAccount = getServiceAccount();
|
|
616
|
+
const jwt = await createJWT(serviceAccount);
|
|
617
|
+
const response = await fetch(serviceAccount.token_uri, {
|
|
618
|
+
method: "POST",
|
|
619
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
620
|
+
body: new URLSearchParams({
|
|
621
|
+
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
|
|
622
|
+
assertion: jwt
|
|
623
|
+
})
|
|
624
|
+
});
|
|
625
|
+
if (!response.ok) {
|
|
626
|
+
const errorText = await response.text();
|
|
627
|
+
throw new Error(`Failed to get access token: ${errorText}`);
|
|
628
|
+
}
|
|
629
|
+
const data = await response.json();
|
|
630
|
+
cachedAccessToken = data.access_token;
|
|
631
|
+
tokenExpiry = Date.now() + data.expires_in * 1e3 - 6e4;
|
|
632
|
+
return cachedAccessToken;
|
|
633
|
+
}
|
|
634
|
+
function clearTokenCache() {
|
|
635
|
+
cachedAccessToken = null;
|
|
636
|
+
tokenExpiry = 0;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
// src/firestore/operations.ts
|
|
640
|
+
var FIRESTORE_API = "https://firestore.googleapis.com/v1";
|
|
641
|
+
async function commitWrites(writes) {
|
|
633
642
|
const accessToken = await getAdminAccessToken();
|
|
634
643
|
const projectId = getProjectId();
|
|
635
|
-
const url = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents
|
|
644
|
+
const url = `${FIRESTORE_API}/projects/${projectId}/databases/(default)/documents:commit`;
|
|
645
|
+
const response = await fetch(url, {
|
|
646
|
+
method: "POST",
|
|
647
|
+
headers: {
|
|
648
|
+
"Authorization": `Bearer ${accessToken}`,
|
|
649
|
+
"Content-Type": "application/json"
|
|
650
|
+
},
|
|
651
|
+
body: JSON.stringify({ writes })
|
|
652
|
+
});
|
|
653
|
+
if (!response.ok) {
|
|
654
|
+
const errorText = await response.text();
|
|
655
|
+
throw new Error(`Failed to commit writes: ${errorText}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
async function setDocument(collectionPath, documentId, data, options) {
|
|
659
|
+
const projectId = getProjectId();
|
|
660
|
+
const documentPath = `projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
|
|
636
661
|
const cleanData = removeFieldTransforms(data);
|
|
637
662
|
const firestoreData = convertToFirestoreFormat(cleanData);
|
|
638
663
|
const transforms = extractFieldTransforms(data);
|
|
639
|
-
|
|
664
|
+
if (transforms.length > 0) {
|
|
665
|
+
const updateWrite = {
|
|
666
|
+
update: {
|
|
667
|
+
name: documentPath,
|
|
668
|
+
fields: firestoreData
|
|
669
|
+
},
|
|
670
|
+
updateTransforms: transforms
|
|
671
|
+
};
|
|
672
|
+
const nonTransformFields = Object.keys(cleanData);
|
|
673
|
+
if (nonTransformFields.length > 0) {
|
|
674
|
+
if (options?.merge) {
|
|
675
|
+
updateWrite.updateMask = { fieldPaths: ["*"] };
|
|
676
|
+
} else if (options?.mergeFields && options.mergeFields.length > 0) {
|
|
677
|
+
updateWrite.updateMask = { fieldPaths: options.mergeFields };
|
|
678
|
+
} else {
|
|
679
|
+
updateWrite.updateMask = { fieldPaths: nonTransformFields };
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
await commitWrites([updateWrite]);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
const accessToken = await getAdminAccessToken();
|
|
686
|
+
const url = `${FIRESTORE_API}/${documentPath}`;
|
|
640
687
|
let queryParams = "";
|
|
641
688
|
if (options?.merge) {
|
|
642
689
|
queryParams = "?updateMask.fieldPaths=*";
|
|
@@ -644,16 +691,13 @@ async function setDocument(collectionPath, documentId, data, options) {
|
|
|
644
691
|
const fieldPaths = options.mergeFields.join("&updateMask.fieldPaths=");
|
|
645
692
|
queryParams = `?updateMask.fieldPaths=${fieldPaths}`;
|
|
646
693
|
}
|
|
647
|
-
if (transforms.length > 0) {
|
|
648
|
-
body.transforms = transforms;
|
|
649
|
-
}
|
|
650
694
|
const response = await fetch(`${url}${queryParams}`, {
|
|
651
695
|
method: "PATCH",
|
|
652
696
|
headers: {
|
|
653
697
|
"Authorization": `Bearer ${accessToken}`,
|
|
654
698
|
"Content-Type": "application/json"
|
|
655
699
|
},
|
|
656
|
-
body: JSON.stringify(
|
|
700
|
+
body: JSON.stringify({ fields: firestoreData })
|
|
657
701
|
});
|
|
658
702
|
if (!response.ok) {
|
|
659
703
|
const errorText = await response.text();
|
|
@@ -711,25 +755,51 @@ async function getDocument(collectionPath, documentId) {
|
|
|
711
755
|
return convertFromFirestoreFormat(result.fields);
|
|
712
756
|
}
|
|
713
757
|
async function updateDocument(collectionPath, documentId, data) {
|
|
714
|
-
const accessToken = await getAdminAccessToken();
|
|
715
758
|
const projectId = getProjectId();
|
|
716
|
-
const
|
|
759
|
+
const documentPath = `projects/${projectId}/databases/(default)/documents/${collectionPath}/${documentId}`;
|
|
717
760
|
const cleanData = removeFieldTransforms(data);
|
|
718
761
|
const firestoreData = convertToFirestoreFormat(cleanData);
|
|
719
762
|
const transforms = extractFieldTransforms(data);
|
|
720
|
-
const updateMaskFields = Object.keys(data).filter((key) =>
|
|
721
|
-
|
|
722
|
-
|
|
763
|
+
const updateMaskFields = Object.keys(data).filter((key) => {
|
|
764
|
+
if (!isFieldValue(data[key])) {
|
|
765
|
+
return true;
|
|
766
|
+
}
|
|
767
|
+
return data[key]._type === "delete";
|
|
768
|
+
});
|
|
723
769
|
if (transforms.length > 0) {
|
|
724
|
-
|
|
770
|
+
const nonTransformFields = Object.keys(cleanData);
|
|
771
|
+
if (nonTransformFields.length === 0) {
|
|
772
|
+
const transformWrite = {
|
|
773
|
+
transform: {
|
|
774
|
+
document: documentPath,
|
|
775
|
+
fieldTransforms: transforms
|
|
776
|
+
}
|
|
777
|
+
};
|
|
778
|
+
await commitWrites([transformWrite]);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
const updateWrite = {
|
|
782
|
+
update: {
|
|
783
|
+
name: documentPath,
|
|
784
|
+
fields: firestoreData
|
|
785
|
+
},
|
|
786
|
+
updateMask: { fieldPaths: updateMaskFields },
|
|
787
|
+
updateTransforms: transforms,
|
|
788
|
+
currentDocument: { exists: true }
|
|
789
|
+
};
|
|
790
|
+
await commitWrites([updateWrite]);
|
|
791
|
+
return;
|
|
725
792
|
}
|
|
793
|
+
const accessToken = await getAdminAccessToken();
|
|
794
|
+
const url = `${FIRESTORE_API}/${documentPath}`;
|
|
795
|
+
const updateMaskParams = updateMaskFields.map((field) => `updateMask.fieldPaths=${encodeURIComponent(field)}`).join("&");
|
|
726
796
|
const response = await fetch(`${url}?${updateMaskParams}¤tDocument.exists=true`, {
|
|
727
797
|
method: "PATCH",
|
|
728
798
|
headers: {
|
|
729
799
|
"Authorization": `Bearer ${accessToken}`,
|
|
730
800
|
"Content-Type": "application/json"
|
|
731
801
|
},
|
|
732
|
-
body: JSON.stringify(
|
|
802
|
+
body: JSON.stringify({ fields: firestoreData })
|
|
733
803
|
});
|
|
734
804
|
if (!response.ok) {
|
|
735
805
|
const errorText = await response.text();
|
package/firebase.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"indexes": [
|
|
3
|
+
{
|
|
4
|
+
"collectionGroup": "e2e-tests",
|
|
5
|
+
"queryScope": "COLLECTION",
|
|
6
|
+
"fields": [
|
|
7
|
+
{
|
|
8
|
+
"fieldPath": "_test",
|
|
9
|
+
"order": "ASCENDING"
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
"fieldPath": "age",
|
|
13
|
+
"order": "ASCENDING"
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
"collectionGroup": "messages",
|
|
19
|
+
"queryScope": "COLLECTION",
|
|
20
|
+
"fields": [
|
|
21
|
+
{
|
|
22
|
+
"fieldPath": "_test",
|
|
23
|
+
"order": "ASCENDING"
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"fieldPath": "timestamp",
|
|
27
|
+
"order": "ASCENDING"
|
|
28
|
+
}
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
],
|
|
32
|
+
"fieldOverrides": []
|
|
33
|
+
}
|
package/firestore.rules
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
preset: 'ts-jest',
|
|
3
|
+
testEnvironment: 'node',
|
|
4
|
+
testMatch: ['**/*.e2e.ts'],
|
|
5
|
+
testTimeout: 30000, // 30 seconds for real API calls
|
|
6
|
+
roots: ['<rootDir>/src'],
|
|
7
|
+
collectCoverageFrom: [
|
|
8
|
+
'src/**/*.ts',
|
|
9
|
+
'!src/**/*.spec.ts',
|
|
10
|
+
'!src/**/*.e2e.ts',
|
|
11
|
+
'!src/types.ts',
|
|
12
|
+
'!src/index.ts',
|
|
13
|
+
],
|
|
14
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@prmichaelsen/firebase-admin-sdk-v8",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.19",
|
|
4
4
|
"description": "Firebase Admin SDK for Cloudflare Workers and edge runtimes using REST APIs",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.mjs",
|
|
@@ -26,7 +26,10 @@
|
|
|
26
26
|
"typecheck": "tsc --noEmit",
|
|
27
27
|
"test": "jest",
|
|
28
28
|
"test:watch": "jest --watch",
|
|
29
|
-
"
|
|
29
|
+
"test:e2e": "jest --config jest.e2e.config.js",
|
|
30
|
+
"test:e2e:watch": "jest --config jest.e2e.config.js --watch",
|
|
31
|
+
"test:all": "npm test && npm run test:e2e",
|
|
32
|
+
"prepublishOnly": "npm run build"
|
|
30
33
|
},
|
|
31
34
|
"keywords": [
|
|
32
35
|
"firebase",
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"type": "service_account",
|
|
3
|
+
"project_id": "prmichaelsen-firebase-e2e",
|
|
4
|
+
"private_key_id": "84caabb8515ec5fab4caa53a0f85405c270d5cd4",
|
|
5
|
+
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQC/0n8V7m7vCQ9/\n5FGYL5V20pQxliUMF/ycNBSV/xHrXAvTMxyMEjFK8Tg9sBLdYhuxC488lnwm3/o+\nn5JkckQ2AyzXX6mi+camwxdLXmT97kXqdFLisO4PY7Fv6HvvMgRQqysj7aJkyiz7\nrAs0DpQq1L70sBFdVLiScPH9FFnuV0EeyOTuVgYwGYk1L76/U7bb3WLNCmcmtG+Y\nt4BNzyMqFgtIdXCQFVBQf9HXy3x5xk8a1rrkf7pEuiwK0PXCDIVH7GYX4xCiQ1qs\nYF4IunNFhcwu1hk5XTIDSVrIn/qLPMZvEcBqHxIJd5JvePjx5hPP81b1mrGUVske\nsaRimjxxAgMBAAECggEAAWNKVG7KslkMRH5y5q56yYbMLVsAPp5SehDYZfNtU5jG\nucr2CxS7SDxcOKS0UOdmUDlyBQaztED3mbS5hZeGuHtSZjvaHtnpdDMXe+NIHhw3\nY520LHwKOjtG69/bPFz4x1qjBRoG4Zgi4NlFpXqbhinO4zdTkNYi6xBSd+R0oshP\nSo59lQvs8e3bMD4f4uTl7JxUifjVM64R3gCkH6AbGqU01wg8UzsllCf0fQM04w8i\nq1oVjhBVWYYVE7w2H+EU+LdiRSRET+Vrk5dtBL3vI6yckvGmxTBMnkK2gTFAAR6F\nnN4NKlUsiFol9WVRY3tdRNOnStjlORDyerSEFQaqhwKBgQDfAa5ddxBVbxUS3E2a\nnGJRA44fYlRyZf9KnJdz8uWsCvuG7UYbe2bp7TxlpEXdTG6Vqu/hhYWd/msmhupZ\nc+YFg1HWE1Ee+nQv7iwAD+okfncnKGIC65XjwaSEMRGrmtxBL6Qq4yxn/Yug/8YR\nniGRQKuMtJ2ZA0p41vI6Mkt7kwKBgQDcM7eQQ5482FcXuOfUlPoiWnd+pF5mNrU1\ngKoxdsXMYRKyTGMPLrOKqsf6kisQUlA48vWRCQOa9QBSjHktfOt2EZ3mNQz5k3AR\n/dIjCGf+ATr2P9Sc0uo7sA20XB62wmG+t2A/vxf9WzBAgdDk6nENB4nz6lL9xcak\ngvVt2vXSawKBgQCV6JRk4f/J3oVFC3DjaRKyMPid4kSwLh6B8mfhGrwHfc59cgz5\ntmeFAuPh057fV1zTIXhlmpMqlPdEi9cHUOCkfhVKGewjLetiuPE9DXWxGI5SdVQF\ncIZu9yH3duDRAaXj7/mklteoBAmTrbxg5XLdKKLpUBTM4ihyuNNWCa8yHwKBgCYK\noTnBFMM6NMGaZiKpohTxQBeW2eAar2+QzNZCyKUoWAyJecuTq9zW6Dl3qwzky4sr\nHhVyUzcgAHBCaGTdYehB3t94ZsdvGztgeD8pIp4VJFSKbnaxUVoCbjusdnnoVu6V\ny4D3yHMyn8FlK+uAPQudM835u2CwHEMrhK731uQFAoGBAKO0crVWuY1EP1RLSyLK\nIS3uUgbauBbQlE4ZPBTi84vXCZjN/53obFgkPHJ5tdjxj6C8x58jgHg/Hufyvbib\ngOQQxadM90UBumlzA8lV7A9jvL2ryfop9ketnVO5hPvB4O8iw9Z1R+25FeM9ZFJI\nqmEjiy9Nhmvpvur+FZ85qcRm\n-----END PRIVATE KEY-----\n",
|
|
6
|
+
"client_email": "firebase-adminsdk-fbsvc@prmichaelsen-firebase-e2e.iam.gserviceaccount.com",
|
|
7
|
+
"client_id": "103889460014910902622",
|
|
8
|
+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
|
|
9
|
+
"token_uri": "https://oauth2.googleapis.com/token",
|
|
10
|
+
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
|
|
11
|
+
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-fbsvc%40prmichaelsen-firebase-e2e.iam.gserviceaccount.com",
|
|
12
|
+
"universe_domain": "googleapis.com"
|
|
13
|
+
}
|