@seaverse/data-service-sdk 0.10.1 → 0.10.2
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 +297 -1
- package/dist/browser.js +68 -11
- package/dist/browser.js.map +1 -1
- package/dist/browser.umd.js +68 -11
- package/dist/browser.umd.js.map +1 -1
- package/dist/index.cjs +68 -11
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +10 -1
- package/dist/index.js +68 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -632,9 +632,31 @@ initializeWithToken(tokenResponse: FirestoreTokenResponse): Promise<{
|
|
|
632
632
|
db: Firestore;
|
|
633
633
|
userId: string;
|
|
634
634
|
appId: string;
|
|
635
|
+
helper: FirestoreHelper;
|
|
635
636
|
}>
|
|
636
637
|
```
|
|
637
638
|
|
|
639
|
+
**⚠️ IMPORTANT: Safe for Multiple Calls**
|
|
640
|
+
|
|
641
|
+
This function has been updated to safely handle multiple calls:
|
|
642
|
+
- ✅ **Reuses existing Firebase app** if already initialized
|
|
643
|
+
- ✅ **Re-authenticates with new token** for token refresh scenarios
|
|
644
|
+
- ✅ **Handles errors gracefully** with automatic cleanup
|
|
645
|
+
- ✅ **Validates app_id** before initialization
|
|
646
|
+
|
|
647
|
+
**Common Use Cases:**
|
|
648
|
+
```typescript
|
|
649
|
+
// ✅ SAFE: First-time initialization
|
|
650
|
+
const { helper } = await initializeWithToken(tokenResponse);
|
|
651
|
+
|
|
652
|
+
// ✅ SAFE: Token refresh (reuses existing app)
|
|
653
|
+
const newTokenResponse = await client.generateFirestoreToken({ ... });
|
|
654
|
+
const { helper: newHelper } = await initializeWithToken(newTokenResponse);
|
|
655
|
+
|
|
656
|
+
// ✅ SAFE: User re-login (reuses existing app)
|
|
657
|
+
const { helper } = await initializeWithToken(newUserTokenResponse);
|
|
658
|
+
```
|
|
659
|
+
|
|
638
660
|
**Example:**
|
|
639
661
|
|
|
640
662
|
```typescript
|
|
@@ -643,7 +665,7 @@ import { initializeWithToken } from '@seaverse/data-service-sdk';
|
|
|
643
665
|
const tokenResponse = await client.generateGuestFirestoreToken({ app_id: 'my-app' });
|
|
644
666
|
|
|
645
667
|
// One line to get everything!
|
|
646
|
-
const { db, appId, userId } = await initializeWithToken(tokenResponse);
|
|
668
|
+
const { db, appId, userId, helper } = await initializeWithToken(tokenResponse);
|
|
647
669
|
|
|
648
670
|
// Ready to use Firestore
|
|
649
671
|
await addDoc(collection(db, `appData/${appId}/publicData/_data/posts`), { ... });
|
|
@@ -654,6 +676,22 @@ await addDoc(collection(db, `appData/${appId}/publicData/_data/posts`), { ... })
|
|
|
654
676
|
npm install firebase
|
|
655
677
|
```
|
|
656
678
|
|
|
679
|
+
**Error Handling:**
|
|
680
|
+
|
|
681
|
+
```typescript
|
|
682
|
+
try {
|
|
683
|
+
const { helper } = await initializeWithToken(tokenResponse);
|
|
684
|
+
} catch (error) {
|
|
685
|
+
if (error.message.includes('app_id is required')) {
|
|
686
|
+
// Handle missing app_id in token response
|
|
687
|
+
} else if (error.message.includes('Firebase SDK not found')) {
|
|
688
|
+
// Handle missing Firebase installation
|
|
689
|
+
} else if (error.message.includes('Failed to initialize Firebase')) {
|
|
690
|
+
// Handle authentication or initialization errors
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
```
|
|
694
|
+
|
|
657
695
|
## Common Use Cases
|
|
658
696
|
|
|
659
697
|
### Use Case 1: Public Forum with Comments
|
|
@@ -791,6 +829,264 @@ import type {
|
|
|
791
829
|
} from '@seaverse/data-service-sdk';
|
|
792
830
|
```
|
|
793
831
|
|
|
832
|
+
## Production Usage Recommendations
|
|
833
|
+
|
|
834
|
+
### Token Expiration Management
|
|
835
|
+
|
|
836
|
+
Firestore tokens expire after 1 hour (3600 seconds). For production applications, you should implement token refresh logic:
|
|
837
|
+
|
|
838
|
+
```typescript
|
|
839
|
+
import { DataServiceClient, initializeWithToken } from '@seaverse/data-service-sdk';
|
|
840
|
+
|
|
841
|
+
class FirestoreManager {
|
|
842
|
+
private tokenExpiryTime: number | null = null;
|
|
843
|
+
private dataClient = new DataServiceClient();
|
|
844
|
+
private userToken: string | null = null;
|
|
845
|
+
private appId: string;
|
|
846
|
+
|
|
847
|
+
constructor(appId: string) {
|
|
848
|
+
this.appId = appId;
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async initialize(userToken: string) {
|
|
852
|
+
this.userToken = userToken;
|
|
853
|
+
|
|
854
|
+
// Generate Firestore token
|
|
855
|
+
const tokenResponse = await this.dataClient.generateFirestoreToken({
|
|
856
|
+
token: userToken,
|
|
857
|
+
app_id: this.appId
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
// Initialize Firebase (safe to call multiple times)
|
|
861
|
+
const result = await initializeWithToken(tokenResponse);
|
|
862
|
+
|
|
863
|
+
// Track token expiry (expires_in is in seconds)
|
|
864
|
+
this.tokenExpiryTime = Date.now() + (tokenResponse.expires_in * 1000);
|
|
865
|
+
|
|
866
|
+
return result;
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
isTokenExpiringSoon(): boolean {
|
|
870
|
+
if (!this.tokenExpiryTime) return true;
|
|
871
|
+
|
|
872
|
+
// Check if token expires within 5 minutes
|
|
873
|
+
const fiveMinutes = 5 * 60 * 1000;
|
|
874
|
+
return Date.now() + fiveMinutes >= this.tokenExpiryTime;
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
async refreshTokenIfNeeded() {
|
|
878
|
+
if (this.isTokenExpiringSoon() && this.userToken) {
|
|
879
|
+
console.log('Token expiring soon, refreshing...');
|
|
880
|
+
return await this.initialize(this.userToken);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Usage
|
|
886
|
+
const manager = new FirestoreManager('my-app-123');
|
|
887
|
+
|
|
888
|
+
// Initialize
|
|
889
|
+
const { helper } = await manager.initialize(userJwtToken);
|
|
890
|
+
|
|
891
|
+
// Before making Firestore operations, check token
|
|
892
|
+
await manager.refreshTokenIfNeeded();
|
|
893
|
+
await helper.addToPublicData('posts', { title: 'My Post' });
|
|
894
|
+
```
|
|
895
|
+
|
|
896
|
+
### Advanced: Singleton Pattern with React
|
|
897
|
+
|
|
898
|
+
For React applications, you can create a singleton manager with hooks:
|
|
899
|
+
|
|
900
|
+
```typescript
|
|
901
|
+
// firestore-manager.ts
|
|
902
|
+
class FirestoreManager {
|
|
903
|
+
private static instance: FirestoreManager | null = null;
|
|
904
|
+
private helper: FirestoreHelper | null = null;
|
|
905
|
+
private db: Firestore | null = null;
|
|
906
|
+
private appId: string | null = null;
|
|
907
|
+
private userId: string | null = null;
|
|
908
|
+
private tokenExpiryTime: number | null = null;
|
|
909
|
+
|
|
910
|
+
static getInstance(): FirestoreManager {
|
|
911
|
+
if (!FirestoreManager.instance) {
|
|
912
|
+
FirestoreManager.instance = new FirestoreManager();
|
|
913
|
+
}
|
|
914
|
+
return FirestoreManager.instance;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
async initialize(tokenResponse: FirestoreTokenResponse) {
|
|
918
|
+
// Always call initializeWithToken - it's safe for multiple calls
|
|
919
|
+
const result = await initializeWithToken(tokenResponse);
|
|
920
|
+
|
|
921
|
+
this.db = result.db;
|
|
922
|
+
this.appId = result.appId;
|
|
923
|
+
this.userId = result.userId;
|
|
924
|
+
this.helper = result.helper;
|
|
925
|
+
this.tokenExpiryTime = Date.now() + (tokenResponse.expires_in * 1000);
|
|
926
|
+
|
|
927
|
+
return result;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
getHelper(): FirestoreHelper | null {
|
|
931
|
+
return this.helper;
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
isTokenExpiringSoon(): boolean {
|
|
935
|
+
if (!this.tokenExpiryTime) return true;
|
|
936
|
+
const fiveMinutes = 5 * 60 * 1000;
|
|
937
|
+
return Date.now() + fiveMinutes >= this.tokenExpiryTime;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
reset() {
|
|
941
|
+
this.db = null;
|
|
942
|
+
this.appId = null;
|
|
943
|
+
this.userId = null;
|
|
944
|
+
this.helper = null;
|
|
945
|
+
this.tokenExpiryTime = null;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export default FirestoreManager;
|
|
950
|
+
|
|
951
|
+
// useFirestore.ts (React Hook)
|
|
952
|
+
import { useState, useEffect } from 'react';
|
|
953
|
+
import { DataServiceClient } from '@seaverse/data-service-sdk';
|
|
954
|
+
import FirestoreManager from './firestore-manager';
|
|
955
|
+
|
|
956
|
+
export function useFirestore(userToken: string, appId: string) {
|
|
957
|
+
const [helper, setHelper] = useState<FirestoreHelper | null>(null);
|
|
958
|
+
const [loading, setLoading] = useState(true);
|
|
959
|
+
const [error, setError] = useState<Error | null>(null);
|
|
960
|
+
|
|
961
|
+
useEffect(() => {
|
|
962
|
+
let mounted = true;
|
|
963
|
+
|
|
964
|
+
async function init() {
|
|
965
|
+
try {
|
|
966
|
+
const dataClient = new DataServiceClient();
|
|
967
|
+
const tokenResponse = await dataClient.generateFirestoreToken({
|
|
968
|
+
token: userToken,
|
|
969
|
+
app_id: appId
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
const manager = FirestoreManager.getInstance();
|
|
973
|
+
const { helper } = await manager.initialize(tokenResponse);
|
|
974
|
+
|
|
975
|
+
if (mounted) {
|
|
976
|
+
setHelper(helper);
|
|
977
|
+
setLoading(false);
|
|
978
|
+
}
|
|
979
|
+
} catch (err) {
|
|
980
|
+
if (mounted) {
|
|
981
|
+
setError(err as Error);
|
|
982
|
+
setLoading(false);
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
init();
|
|
988
|
+
|
|
989
|
+
return () => {
|
|
990
|
+
mounted = false;
|
|
991
|
+
};
|
|
992
|
+
}, [userToken, appId]);
|
|
993
|
+
|
|
994
|
+
return { helper, loading, error };
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Usage in component
|
|
998
|
+
function MyComponent() {
|
|
999
|
+
const { helper, loading, error } = useFirestore(userToken, 'my-app-123');
|
|
1000
|
+
|
|
1001
|
+
if (loading) return <div>Loading...</div>;
|
|
1002
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
1003
|
+
|
|
1004
|
+
const handleAddPost = async () => {
|
|
1005
|
+
await helper.addToPublicData('posts', {
|
|
1006
|
+
title: 'My Post',
|
|
1007
|
+
content: 'Hello World'
|
|
1008
|
+
});
|
|
1009
|
+
};
|
|
1010
|
+
|
|
1011
|
+
return <button onClick={handleAddPost}>Add Post</button>;
|
|
1012
|
+
}
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
### Background Token Refresh
|
|
1016
|
+
|
|
1017
|
+
For long-running applications, set up automatic background token refresh:
|
|
1018
|
+
|
|
1019
|
+
```typescript
|
|
1020
|
+
class FirestoreManager {
|
|
1021
|
+
private refreshTimer: NodeJS.Timeout | null = null;
|
|
1022
|
+
|
|
1023
|
+
async initialize(userToken: string, appId: string) {
|
|
1024
|
+
const dataClient = new DataServiceClient();
|
|
1025
|
+
const tokenResponse = await dataClient.generateFirestoreToken({
|
|
1026
|
+
token: userToken,
|
|
1027
|
+
app_id: appId
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
const result = await initializeWithToken(tokenResponse);
|
|
1031
|
+
|
|
1032
|
+
// Schedule token refresh 5 minutes before expiry
|
|
1033
|
+
const refreshIn = (tokenResponse.expires_in - 5 * 60) * 1000;
|
|
1034
|
+
this.scheduleRefresh(userToken, appId, refreshIn);
|
|
1035
|
+
|
|
1036
|
+
return result;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
private scheduleRefresh(userToken: string, appId: string, delay: number) {
|
|
1040
|
+
if (this.refreshTimer) {
|
|
1041
|
+
clearTimeout(this.refreshTimer);
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
this.refreshTimer = setTimeout(async () => {
|
|
1045
|
+
console.log('Auto-refreshing Firestore token...');
|
|
1046
|
+
try {
|
|
1047
|
+
await this.initialize(userToken, appId);
|
|
1048
|
+
} catch (error) {
|
|
1049
|
+
console.error('Failed to refresh token:', error);
|
|
1050
|
+
}
|
|
1051
|
+
}, delay);
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
cleanup() {
|
|
1055
|
+
if (this.refreshTimer) {
|
|
1056
|
+
clearTimeout(this.refreshTimer);
|
|
1057
|
+
this.refreshTimer = null;
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
```
|
|
1062
|
+
|
|
1063
|
+
### Error Recovery
|
|
1064
|
+
|
|
1065
|
+
Implement proper error recovery for network failures:
|
|
1066
|
+
|
|
1067
|
+
```typescript
|
|
1068
|
+
async function initializeWithRetry(
|
|
1069
|
+
tokenResponse: FirestoreTokenResponse,
|
|
1070
|
+
maxRetries = 3
|
|
1071
|
+
) {
|
|
1072
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
1073
|
+
try {
|
|
1074
|
+
return await initializeWithToken(tokenResponse);
|
|
1075
|
+
} catch (error) {
|
|
1076
|
+
console.error(`Initialization attempt ${i + 1} failed:`, error);
|
|
1077
|
+
|
|
1078
|
+
if (i === maxRetries - 1) {
|
|
1079
|
+
throw error; // Last attempt failed
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Wait before retry (exponential backoff)
|
|
1083
|
+
const delay = Math.pow(2, i) * 1000;
|
|
1084
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
```
|
|
1089
|
+
|
|
794
1090
|
## Best Practices for LLM
|
|
795
1091
|
|
|
796
1092
|
When using this SDK with LLM-generated code:
|
package/dist/browser.js
CHANGED
|
@@ -5149,7 +5149,7 @@ function getFirebaseConfig(tokenResponse) {
|
|
|
5149
5149
|
*
|
|
5150
5150
|
* This is a convenience function that automatically:
|
|
5151
5151
|
* 1. Creates Firebase config from token response
|
|
5152
|
-
* 2. Initializes Firebase app
|
|
5152
|
+
* 2. Initializes Firebase app (or reuses existing one)
|
|
5153
5153
|
* 3. Signs in with the custom token
|
|
5154
5154
|
* 4. Creates FirestoreHelper for LLM-friendly operations
|
|
5155
5155
|
* 5. Returns authenticated Firebase instances
|
|
@@ -5157,11 +5157,20 @@ function getFirebaseConfig(tokenResponse) {
|
|
|
5157
5157
|
* IMPORTANT: This function requires Firebase SDK to be installed separately:
|
|
5158
5158
|
* npm install firebase
|
|
5159
5159
|
*
|
|
5160
|
+
* ⚠️ IMPORTANT: This function can be called multiple times safely. It will:
|
|
5161
|
+
* - Reuse existing Firebase app if already initialized
|
|
5162
|
+
* - Re-authenticate with new token if needed
|
|
5163
|
+
* - Handle token refresh scenarios
|
|
5164
|
+
*
|
|
5160
5165
|
* 🎯 LLM RECOMMENDED: Use the `helper` object for simplified operations
|
|
5161
5166
|
*
|
|
5162
5167
|
* @param tokenResponse - The Firestore token response from SDK
|
|
5163
5168
|
* @returns Object containing initialized Firebase instances and LLM-friendly helper
|
|
5164
5169
|
*
|
|
5170
|
+
* @throws {Error} If app_id is missing from token response
|
|
5171
|
+
* @throws {Error} If Firebase SDK is not installed
|
|
5172
|
+
* @throws {Error} If authentication fails
|
|
5173
|
+
*
|
|
5165
5174
|
* @example
|
|
5166
5175
|
* ```typescript
|
|
5167
5176
|
* import { initializeWithToken } from '@seaverse/data-service-sdk';
|
|
@@ -5180,17 +5189,25 @@ function getFirebaseConfig(tokenResponse) {
|
|
|
5180
5189
|
* ```
|
|
5181
5190
|
*/
|
|
5182
5191
|
async function initializeWithToken(tokenResponse) {
|
|
5192
|
+
// ✅ FIX #3: Validate appId FIRST before any initialization
|
|
5193
|
+
const appId = tokenResponse.app_id;
|
|
5194
|
+
if (!appId) {
|
|
5195
|
+
throw new Error('app_id is required in token response. ' +
|
|
5196
|
+
'Make sure your token response includes a valid app_id field.');
|
|
5197
|
+
}
|
|
5183
5198
|
// Check if Firebase SDK is available
|
|
5184
5199
|
let initializeApp;
|
|
5185
5200
|
let getAuth;
|
|
5186
5201
|
let signInWithCustomToken;
|
|
5187
5202
|
let getFirestore;
|
|
5203
|
+
let getApps;
|
|
5188
5204
|
try {
|
|
5189
5205
|
// Try to import Firebase modules
|
|
5190
5206
|
const firebaseApp = await import('firebase/app');
|
|
5191
5207
|
const firebaseAuth = await import('firebase/auth');
|
|
5192
5208
|
const firebaseFirestore = await import('firebase/firestore');
|
|
5193
5209
|
initializeApp = firebaseApp.initializeApp;
|
|
5210
|
+
getApps = firebaseApp.getApps;
|
|
5194
5211
|
getAuth = firebaseAuth.getAuth;
|
|
5195
5212
|
signInWithCustomToken = firebaseAuth.signInWithCustomToken;
|
|
5196
5213
|
getFirestore = firebaseFirestore.getFirestore;
|
|
@@ -5199,17 +5216,57 @@ async function initializeWithToken(tokenResponse) {
|
|
|
5199
5216
|
throw new Error('Firebase SDK not found. Please install it: npm install firebase\n' +
|
|
5200
5217
|
'Or import manually and use getFirebaseConfig() helper instead.');
|
|
5201
5218
|
}
|
|
5202
|
-
//
|
|
5203
|
-
const
|
|
5204
|
-
const
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5219
|
+
// ✅ FIX #1: Check if Firebase app already exists
|
|
5220
|
+
const existingApps = getApps();
|
|
5221
|
+
const existingApp = existingApps.find((app) => app.name === '[DEFAULT]');
|
|
5222
|
+
let app;
|
|
5223
|
+
let auth;
|
|
5224
|
+
let db;
|
|
5225
|
+
if (existingApp) {
|
|
5226
|
+
// ✅ FIX #2: Reuse existing app instead of creating new one
|
|
5227
|
+
console.log('[SeaVerse SDK] Reusing existing Firebase app');
|
|
5228
|
+
app = existingApp;
|
|
5229
|
+
auth = getAuth(app);
|
|
5230
|
+
db = getFirestore(app, tokenResponse.database_id);
|
|
5231
|
+
// ✅ FIX #4: Re-authenticate with new token (for token refresh scenarios)
|
|
5232
|
+
try {
|
|
5233
|
+
await signInWithCustomToken(auth, tokenResponse.custom_token);
|
|
5234
|
+
console.log('[SeaVerse SDK] Re-authenticated with new token');
|
|
5235
|
+
}
|
|
5236
|
+
catch (error) {
|
|
5237
|
+
// If already signed in with same token, ignore error
|
|
5238
|
+
// Only throw if it's a real authentication failure
|
|
5239
|
+
if (error?.code !== 'auth/invalid-credential' && error?.code !== 'auth/network-request-failed') {
|
|
5240
|
+
console.warn('[SeaVerse SDK] Re-authentication warning:', error?.message || error);
|
|
5241
|
+
}
|
|
5242
|
+
}
|
|
5243
|
+
}
|
|
5244
|
+
else {
|
|
5245
|
+
// ✅ FIX #4: First-time initialization with proper error handling
|
|
5246
|
+
console.log('[SeaVerse SDK] Initializing new Firebase app');
|
|
5247
|
+
try {
|
|
5248
|
+
const config = getFirebaseConfig(tokenResponse);
|
|
5249
|
+
app = initializeApp(config);
|
|
5250
|
+
auth = getAuth(app);
|
|
5251
|
+
await signInWithCustomToken(auth, tokenResponse.custom_token);
|
|
5252
|
+
// IMPORTANT: Must specify database_id, not just use default!
|
|
5253
|
+
db = getFirestore(app, tokenResponse.database_id);
|
|
5254
|
+
}
|
|
5255
|
+
catch (error) {
|
|
5256
|
+
// ✅ FIX #4: Cleanup on failure to prevent half-initialized state
|
|
5257
|
+
if (app) {
|
|
5258
|
+
try {
|
|
5259
|
+
await app.delete();
|
|
5260
|
+
console.log('[SeaVerse SDK] Cleaned up failed Firebase app initialization');
|
|
5261
|
+
}
|
|
5262
|
+
catch (cleanupError) {
|
|
5263
|
+
console.warn('[SeaVerse SDK] Failed to cleanup app:', cleanupError);
|
|
5264
|
+
}
|
|
5265
|
+
}
|
|
5266
|
+
throw new Error(`Failed to initialize Firebase: ${error instanceof Error ? error.message : String(error)}`);
|
|
5267
|
+
}
|
|
5268
|
+
}
|
|
5211
5269
|
const userId = tokenResponse.user_id;
|
|
5212
|
-
const appId = tokenResponse.app_id || '';
|
|
5213
5270
|
// Create LLM-friendly helper
|
|
5214
5271
|
const helper = new FirestoreHelper(db, appId, userId);
|
|
5215
5272
|
return {
|