@seaverse/data-service-sdk 0.10.0 → 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 +313 -16
- package/dist/browser.js +80 -21
- package/dist/browser.js.map +1 -1
- package/dist/browser.umd.js +80 -21
- package/dist/browser.umd.js.map +1 -1
- package/dist/index.cjs +80 -21
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +20 -11
- package/dist/index.js +80 -21
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ SeaVerse organizes your Firestore data into three permission levels:
|
|
|
53
53
|
|-----------------|--------------|-------------|--------------|----------|
|
|
54
54
|
| **publicRead** | `appData/{app_id}/publicRead/_data/{collection}/{docId}` | All authenticated users | Admin only | System configs, announcements, static content |
|
|
55
55
|
| **publicData** | `appData/{app_id}/publicData/_data/{collection}/{docId}` | All authenticated users | All authenticated users | User posts, comments, shared content |
|
|
56
|
-
| **userData** | `appData/{app_id}/userData/{user_id}/{collection}/{docId}` | Owner only | Owner only | User settings, private notes, personal data |
|
|
56
|
+
| **userData** | `appData/{app_id}/userData/{user_id}/_data/{collection}/{docId}` | Owner only | Owner only | User settings, private notes, personal data |
|
|
57
57
|
|
|
58
58
|
### 🚨 CRITICAL: Firestore Path Rules (For LLM)
|
|
59
59
|
|
|
@@ -88,7 +88,7 @@ const postsRef = collection(db, `appData/${appId}/publicData/_data/posts`);
|
|
|
88
88
|
await addDoc(postsRef, { ...data }); // Firestore adds document ID
|
|
89
89
|
|
|
90
90
|
// User Private Data (owner only)
|
|
91
|
-
const notesRef = collection(db, `appData/${appId}/userData/${userId}/notes`);
|
|
91
|
+
const notesRef = collection(db, `appData/${appId}/userData/${userId}/_data/notes`);
|
|
92
92
|
await addDoc(notesRef, { ...data });
|
|
93
93
|
|
|
94
94
|
// Public Read-Only (everyone can read, admin can write)
|
|
@@ -97,9 +97,10 @@ await getDocs(configRef);
|
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
**The pattern is always:**
|
|
100
|
-
- `appData` → `{app_id}` → `{permission_layer}` → `_data` → `{collection}` → (auto-generated doc ID)
|
|
101
|
-
- Collection: 5 segments (odd) ✅
|
|
102
|
-
-
|
|
100
|
+
- `appData` → `{app_id}` → `{permission_layer}` → (`{user_id}` for userData) → `_data` → `{collection}` → (auto-generated doc ID)
|
|
101
|
+
- publicRead/publicData Collection: 5 segments (odd) ✅
|
|
102
|
+
- userData Collection: 6 segments (even) ✅ (includes userId)
|
|
103
|
+
- Document paths: add 1 more segment for docId
|
|
103
104
|
|
|
104
105
|
### Required Fields
|
|
105
106
|
|
|
@@ -257,17 +258,17 @@ collection(db, `apps/${appId}/users/${userId}/articles`) // Not matching securi
|
|
|
257
258
|
import { getPublicDataPath, getUserDataPath } from '@seaverse/data-service-sdk';
|
|
258
259
|
|
|
259
260
|
// ✅ CORRECT - Guaranteed to match security rules
|
|
260
|
-
collection(db, getPublicDataPath(appId, 'posts')) // → appData/{appId}/publicData/posts
|
|
261
|
-
collection(db, getUserDataPath(appId, userId, 'notes')) // → appData/{appId}/userData/{userId}/notes
|
|
261
|
+
collection(db, getPublicDataPath(appId, 'posts')) // → appData/{appId}/publicData/_data/posts
|
|
262
|
+
collection(db, getUserDataPath(appId, userId, 'notes')) // → appData/{appId}/userData/{userId}/_data/notes
|
|
262
263
|
```
|
|
263
264
|
|
|
264
265
|
### Available Path Helpers
|
|
265
266
|
|
|
266
267
|
```typescript
|
|
267
268
|
import {
|
|
268
|
-
getPublicReadPath, //
|
|
269
|
-
getPublicDataPath, //
|
|
270
|
-
getUserDataPath, //
|
|
269
|
+
getPublicReadPath, // Returns: appData/{appId}/publicRead/_data/{collection}
|
|
270
|
+
getPublicDataPath, // Returns: appData/{appId}/publicData/_data/{collection}
|
|
271
|
+
getUserDataPath, // Returns: appData/{appId}/userData/{userId}/_data/{collection}
|
|
271
272
|
getPublicReadDocPath, // For specific public document
|
|
272
273
|
getPublicDataDocPath, // For specific public data document
|
|
273
274
|
getUserDataDocPath, // For specific user document
|
|
@@ -324,7 +325,7 @@ const docPath = builder.publicData('posts').doc('post-123').build();
|
|
|
324
325
|
|
|
325
326
|
// Build user data path
|
|
326
327
|
const userPath = builder.userData(userId, 'notes').build();
|
|
327
|
-
// Returns: 'appData/my-app/userData/user-123/notes'
|
|
328
|
+
// Returns: 'appData/my-app/userData/user-123/_data/notes'
|
|
328
329
|
```
|
|
329
330
|
|
|
330
331
|
### Error Prevention
|
|
@@ -631,9 +632,31 @@ initializeWithToken(tokenResponse: FirestoreTokenResponse): Promise<{
|
|
|
631
632
|
db: Firestore;
|
|
632
633
|
userId: string;
|
|
633
634
|
appId: string;
|
|
635
|
+
helper: FirestoreHelper;
|
|
634
636
|
}>
|
|
635
637
|
```
|
|
636
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
|
+
|
|
637
660
|
**Example:**
|
|
638
661
|
|
|
639
662
|
```typescript
|
|
@@ -642,7 +665,7 @@ import { initializeWithToken } from '@seaverse/data-service-sdk';
|
|
|
642
665
|
const tokenResponse = await client.generateGuestFirestoreToken({ app_id: 'my-app' });
|
|
643
666
|
|
|
644
667
|
// One line to get everything!
|
|
645
|
-
const { db, appId, userId } = await initializeWithToken(tokenResponse);
|
|
668
|
+
const { db, appId, userId, helper } = await initializeWithToken(tokenResponse);
|
|
646
669
|
|
|
647
670
|
// Ready to use Firestore
|
|
648
671
|
await addDoc(collection(db, `appData/${appId}/publicData/_data/posts`), { ... });
|
|
@@ -653,6 +676,22 @@ await addDoc(collection(db, `appData/${appId}/publicData/_data/posts`), { ... })
|
|
|
653
676
|
npm install firebase
|
|
654
677
|
```
|
|
655
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
|
+
|
|
656
695
|
## Common Use Cases
|
|
657
696
|
|
|
658
697
|
### Use Case 1: Public Forum with Comments
|
|
@@ -678,7 +717,7 @@ const comments = await getDocs(
|
|
|
678
717
|
|
|
679
718
|
```typescript
|
|
680
719
|
// Only the user can write to their own settings
|
|
681
|
-
await setDoc(doc(db, `appData/${appId}/userData/${userId}/settings/preferences`), {
|
|
720
|
+
await setDoc(doc(db, `appData/${appId}/userData/${userId}/_data/settings/preferences`), {
|
|
682
721
|
_appId: appId,
|
|
683
722
|
_createdAt: serverTimestamp(),
|
|
684
723
|
_createdBy: userId,
|
|
@@ -689,7 +728,7 @@ await setDoc(doc(db, `appData/${appId}/userData/${userId}/settings/preferences`)
|
|
|
689
728
|
|
|
690
729
|
// Only the user can read their own settings
|
|
691
730
|
const settings = await getDoc(
|
|
692
|
-
doc(db, `appData/${appId}/userData/${userId}/settings/preferences`)
|
|
731
|
+
doc(db, `appData/${appId}/userData/${userId}/_data/settings/preferences`)
|
|
693
732
|
);
|
|
694
733
|
```
|
|
695
734
|
|
|
@@ -730,7 +769,7 @@ const snapshot = await getDocs(userPosts);
|
|
|
730
769
|
✅ **Authenticated Users:**
|
|
731
770
|
- Read `publicRead` data
|
|
732
771
|
- Read and write `publicData` (all users' data)
|
|
733
|
-
- Read and write their own `userData/{userId}
|
|
772
|
+
- Read and write their own `userData/{userId}/_data/` only
|
|
734
773
|
- Update/delete documents where `_createdBy == userId`
|
|
735
774
|
|
|
736
775
|
✅ **Guest Users:**
|
|
@@ -790,6 +829,264 @@ import type {
|
|
|
790
829
|
} from '@seaverse/data-service-sdk';
|
|
791
830
|
```
|
|
792
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
|
+
|
|
793
1090
|
## Best Practices for LLM
|
|
794
1091
|
|
|
795
1092
|
When using this SDK with LLM-generated code:
|
|
@@ -933,7 +1230,7 @@ async function completeExample() {
|
|
|
933
1230
|
|
|
934
1231
|
// 7. Save user preferences (private)
|
|
935
1232
|
await addDoc(
|
|
936
|
-
collection(db, `appData/${appId}/userData/${userId}/preferences`),
|
|
1233
|
+
collection(db, `appData/${appId}/userData/${userId}/_data/preferences`),
|
|
937
1234
|
{
|
|
938
1235
|
_appId: appId,
|
|
939
1236
|
_createdAt: serverTimestamp(),
|
package/dist/browser.js
CHANGED
|
@@ -3941,9 +3941,9 @@ const ENDPOINTS = {
|
|
|
3941
3941
|
* ---------------------------
|
|
3942
3942
|
* Your Firestore data is organized in three permission levels:
|
|
3943
3943
|
*
|
|
3944
|
-
* 1. publicRead/ - System configs, announcements (Read: Everyone, Write: Admin only)
|
|
3945
|
-
* 2. publicData/ - User posts, shared content (Read: Everyone, Write: Everyone)
|
|
3946
|
-
* 3. userData/{userId}/ - Private user data (Read/Write: Owner only)
|
|
3944
|
+
* 1. publicRead/_data/ - System configs, announcements (Read: Everyone, Write: Admin only)
|
|
3945
|
+
* 2. publicData/_data/ - User posts, shared content (Read: Everyone, Write: Everyone)
|
|
3946
|
+
* 3. userData/{userId}/_data/ - Private user data (Read/Write: Owner only)
|
|
3947
3947
|
*
|
|
3948
3948
|
* QUICK START FOR LLM:
|
|
3949
3949
|
* -------------------
|
|
@@ -3995,7 +3995,7 @@ const ENDPOINTS = {
|
|
|
3995
3995
|
* const snapshot = await getDocs(collection(db, `appData/${appId}/publicData/posts`));
|
|
3996
3996
|
*
|
|
3997
3997
|
* // Write to userData (only owner can write)
|
|
3998
|
-
* await addDoc(collection(db, `appData/${appId}/userData/${userId}/notes`), {
|
|
3998
|
+
* await addDoc(collection(db, `appData/${appId}/userData/${userId}/_data/notes`), {
|
|
3999
3999
|
* _appId: appId, // REQUIRED
|
|
4000
4000
|
* _createdAt: serverTimestamp(), // REQUIRED
|
|
4001
4001
|
* _createdBy: userId, // REQUIRED
|
|
@@ -4248,7 +4248,7 @@ function getPublicDataPath(appId, collectionName) {
|
|
|
4248
4248
|
* ```typescript
|
|
4249
4249
|
* // Write private user notes
|
|
4250
4250
|
* const path = getUserDataPath('my-app', 'user-123', 'notes');
|
|
4251
|
-
* // Returns: 'appData/my-app/userData/user-123/notes'
|
|
4251
|
+
* // Returns: 'appData/my-app/userData/user-123/_data/notes'
|
|
4252
4252
|
*
|
|
4253
4253
|
* await addDoc(collection(db, path), {
|
|
4254
4254
|
* _appId: appId,
|
|
@@ -4262,7 +4262,7 @@ function getUserDataPath(appId, userId, collectionName) {
|
|
|
4262
4262
|
validateSegment('appId', appId);
|
|
4263
4263
|
validateSegment('userId', userId);
|
|
4264
4264
|
validateSegment('collectionName', collectionName);
|
|
4265
|
-
return `appData/${appId}/userData/${userId}/${collectionName}`;
|
|
4265
|
+
return `appData/${appId}/userData/${userId}/_data/${collectionName}`;
|
|
4266
4266
|
}
|
|
4267
4267
|
/**
|
|
4268
4268
|
* Generate path for a specific document in publicRead
|
|
@@ -4320,7 +4320,7 @@ function getPublicDataDocPath(appId, collectionName, docId) {
|
|
|
4320
4320
|
* @example
|
|
4321
4321
|
* ```typescript
|
|
4322
4322
|
* const path = getUserDataDocPath('my-app', 'user-123', 'notes', 'note-456');
|
|
4323
|
-
* // Returns: 'appData/my-app/userData/user-123/notes/note-456'
|
|
4323
|
+
* // Returns: 'appData/my-app/userData/user-123/_data/notes/note-456'
|
|
4324
4324
|
*
|
|
4325
4325
|
* const docSnap = await getDoc(doc(db, path));
|
|
4326
4326
|
* ```
|
|
@@ -4330,7 +4330,7 @@ function getUserDataDocPath(appId, userId, collectionName, docId) {
|
|
|
4330
4330
|
validateSegment('userId', userId);
|
|
4331
4331
|
validateSegment('collectionName', collectionName);
|
|
4332
4332
|
validateSegment('docId', docId);
|
|
4333
|
-
return `appData/${appId}/userData/${userId}/${collectionName}/${docId}`;
|
|
4333
|
+
return `appData/${appId}/userData/${userId}/_data/${collectionName}/${docId}`;
|
|
4334
4334
|
}
|
|
4335
4335
|
/**
|
|
4336
4336
|
* Validate a path segment to ensure it doesn't contain invalid characters
|
|
@@ -4395,7 +4395,7 @@ class PathBuilder {
|
|
|
4395
4395
|
userData(userId, collectionName) {
|
|
4396
4396
|
validateSegment('userId', userId);
|
|
4397
4397
|
validateSegment('collectionName', collectionName);
|
|
4398
|
-
this.segments.push('userData', userId, collectionName);
|
|
4398
|
+
this.segments.push('userData', userId, '_data', collectionName);
|
|
4399
4399
|
return this;
|
|
4400
4400
|
}
|
|
4401
4401
|
/**
|
|
@@ -5014,7 +5014,9 @@ class FirestoreHelper {
|
|
|
5014
5014
|
}
|
|
5015
5015
|
else {
|
|
5016
5016
|
// Filter out soft-deleted documents
|
|
5017
|
-
|
|
5017
|
+
// Use '!=' to include documents without _deleted field (new documents)
|
|
5018
|
+
// This will return documents where _deleted is missing, null, undefined, or false
|
|
5019
|
+
const q = query(colRef, where('_deleted', '!=', true));
|
|
5018
5020
|
return getDocs(q);
|
|
5019
5021
|
}
|
|
5020
5022
|
}
|
|
@@ -5147,7 +5149,7 @@ function getFirebaseConfig(tokenResponse) {
|
|
|
5147
5149
|
*
|
|
5148
5150
|
* This is a convenience function that automatically:
|
|
5149
5151
|
* 1. Creates Firebase config from token response
|
|
5150
|
-
* 2. Initializes Firebase app
|
|
5152
|
+
* 2. Initializes Firebase app (or reuses existing one)
|
|
5151
5153
|
* 3. Signs in with the custom token
|
|
5152
5154
|
* 4. Creates FirestoreHelper for LLM-friendly operations
|
|
5153
5155
|
* 5. Returns authenticated Firebase instances
|
|
@@ -5155,11 +5157,20 @@ function getFirebaseConfig(tokenResponse) {
|
|
|
5155
5157
|
* IMPORTANT: This function requires Firebase SDK to be installed separately:
|
|
5156
5158
|
* npm install firebase
|
|
5157
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
|
+
*
|
|
5158
5165
|
* 🎯 LLM RECOMMENDED: Use the `helper` object for simplified operations
|
|
5159
5166
|
*
|
|
5160
5167
|
* @param tokenResponse - The Firestore token response from SDK
|
|
5161
5168
|
* @returns Object containing initialized Firebase instances and LLM-friendly helper
|
|
5162
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
|
+
*
|
|
5163
5174
|
* @example
|
|
5164
5175
|
* ```typescript
|
|
5165
5176
|
* import { initializeWithToken } from '@seaverse/data-service-sdk';
|
|
@@ -5178,17 +5189,25 @@ function getFirebaseConfig(tokenResponse) {
|
|
|
5178
5189
|
* ```
|
|
5179
5190
|
*/
|
|
5180
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
|
+
}
|
|
5181
5198
|
// Check if Firebase SDK is available
|
|
5182
5199
|
let initializeApp;
|
|
5183
5200
|
let getAuth;
|
|
5184
5201
|
let signInWithCustomToken;
|
|
5185
5202
|
let getFirestore;
|
|
5203
|
+
let getApps;
|
|
5186
5204
|
try {
|
|
5187
5205
|
// Try to import Firebase modules
|
|
5188
5206
|
const firebaseApp = await import('firebase/app');
|
|
5189
5207
|
const firebaseAuth = await import('firebase/auth');
|
|
5190
5208
|
const firebaseFirestore = await import('firebase/firestore');
|
|
5191
5209
|
initializeApp = firebaseApp.initializeApp;
|
|
5210
|
+
getApps = firebaseApp.getApps;
|
|
5192
5211
|
getAuth = firebaseAuth.getAuth;
|
|
5193
5212
|
signInWithCustomToken = firebaseAuth.signInWithCustomToken;
|
|
5194
5213
|
getFirestore = firebaseFirestore.getFirestore;
|
|
@@ -5197,17 +5216,57 @@ async function initializeWithToken(tokenResponse) {
|
|
|
5197
5216
|
throw new Error('Firebase SDK not found. Please install it: npm install firebase\n' +
|
|
5198
5217
|
'Or import manually and use getFirebaseConfig() helper instead.');
|
|
5199
5218
|
}
|
|
5200
|
-
//
|
|
5201
|
-
const
|
|
5202
|
-
const
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
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
|
+
}
|
|
5209
5269
|
const userId = tokenResponse.user_id;
|
|
5210
|
-
const appId = tokenResponse.app_id || '';
|
|
5211
5270
|
// Create LLM-friendly helper
|
|
5212
5271
|
const helper = new FirestoreHelper(db, appId, userId);
|
|
5213
5272
|
return {
|