@seaverse/data-service-sdk 0.10.1 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +345 -2
- package/dist/browser.js +130 -27
- package/dist/browser.js.map +1 -1
- package/dist/browser.umd.js +130 -27
- package/dist/browser.umd.js.map +1 -1
- package/dist/index.cjs +130 -27
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +55 -6
- package/dist/index.js +130 -27
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ SeaVerse Data Service SDK for accessing Firestore with secure token management a
|
|
|
16
16
|
|---------------------|---------------|--------------|
|
|
17
17
|
| **Write Public Data** | `helper.addToPublicData()` | `await helper.addToPublicData('posts', {title: 'Hello'})` |
|
|
18
18
|
| **Read Public Data** | `helper.getPublicData()` | `const posts = await helper.getPublicData('posts')` |
|
|
19
|
+
| **Read with Pagination** | `helper.getPublicData()` | `const posts = await helper.getPublicData('posts', false, {limit: 10})` |
|
|
19
20
|
| **Write Private Data** | `helper.addToUserData()` | `await helper.addToUserData('notes', {text: 'Secret'})` |
|
|
20
21
|
| **Read Private Data** | `helper.getUserData()` | `const notes = await helper.getUserData('notes')` |
|
|
21
22
|
|
|
@@ -378,6 +379,16 @@ posts.forEach(doc => {
|
|
|
378
379
|
console.log(doc.id, doc.data());
|
|
379
380
|
});
|
|
380
381
|
|
|
382
|
+
// ✅ Pagination support: Get first 10 posts
|
|
383
|
+
const firstPage = await helper.getPublicData('posts', false, { limit: 10 });
|
|
384
|
+
|
|
385
|
+
// Get next 10 posts (start after last document)
|
|
386
|
+
const lastDoc = firstPage.docs[firstPage.docs.length - 1];
|
|
387
|
+
const nextPage = await helper.getPublicData('posts', false, {
|
|
388
|
+
limit: 10,
|
|
389
|
+
startAfter: lastDoc
|
|
390
|
+
});
|
|
391
|
+
|
|
381
392
|
// ✅ LLM-FRIENDLY: Write to userData (private) - auto-isolated!
|
|
382
393
|
await helper.addToUserData('notes', {
|
|
383
394
|
title: 'Private Note',
|
|
@@ -632,9 +643,31 @@ initializeWithToken(tokenResponse: FirestoreTokenResponse): Promise<{
|
|
|
632
643
|
db: Firestore;
|
|
633
644
|
userId: string;
|
|
634
645
|
appId: string;
|
|
646
|
+
helper: FirestoreHelper;
|
|
635
647
|
}>
|
|
636
648
|
```
|
|
637
649
|
|
|
650
|
+
**⚠️ IMPORTANT: Safe for Multiple Calls**
|
|
651
|
+
|
|
652
|
+
This function has been updated to safely handle multiple calls:
|
|
653
|
+
- ✅ **Reuses existing Firebase app** if already initialized
|
|
654
|
+
- ✅ **Re-authenticates with new token** for token refresh scenarios
|
|
655
|
+
- ✅ **Handles errors gracefully** with automatic cleanup
|
|
656
|
+
- ✅ **Validates app_id** before initialization
|
|
657
|
+
|
|
658
|
+
**Common Use Cases:**
|
|
659
|
+
```typescript
|
|
660
|
+
// ✅ SAFE: First-time initialization
|
|
661
|
+
const { helper } = await initializeWithToken(tokenResponse);
|
|
662
|
+
|
|
663
|
+
// ✅ SAFE: Token refresh (reuses existing app)
|
|
664
|
+
const newTokenResponse = await client.generateFirestoreToken({ ... });
|
|
665
|
+
const { helper: newHelper } = await initializeWithToken(newTokenResponse);
|
|
666
|
+
|
|
667
|
+
// ✅ SAFE: User re-login (reuses existing app)
|
|
668
|
+
const { helper } = await initializeWithToken(newUserTokenResponse);
|
|
669
|
+
```
|
|
670
|
+
|
|
638
671
|
**Example:**
|
|
639
672
|
|
|
640
673
|
```typescript
|
|
@@ -643,7 +676,7 @@ import { initializeWithToken } from '@seaverse/data-service-sdk';
|
|
|
643
676
|
const tokenResponse = await client.generateGuestFirestoreToken({ app_id: 'my-app' });
|
|
644
677
|
|
|
645
678
|
// One line to get everything!
|
|
646
|
-
const { db, appId, userId } = await initializeWithToken(tokenResponse);
|
|
679
|
+
const { db, appId, userId, helper } = await initializeWithToken(tokenResponse);
|
|
647
680
|
|
|
648
681
|
// Ready to use Firestore
|
|
649
682
|
await addDoc(collection(db, `appData/${appId}/publicData/_data/posts`), { ... });
|
|
@@ -654,6 +687,22 @@ await addDoc(collection(db, `appData/${appId}/publicData/_data/posts`), { ... })
|
|
|
654
687
|
npm install firebase
|
|
655
688
|
```
|
|
656
689
|
|
|
690
|
+
**Error Handling:**
|
|
691
|
+
|
|
692
|
+
```typescript
|
|
693
|
+
try {
|
|
694
|
+
const { helper } = await initializeWithToken(tokenResponse);
|
|
695
|
+
} catch (error) {
|
|
696
|
+
if (error.message.includes('app_id is required')) {
|
|
697
|
+
// Handle missing app_id in token response
|
|
698
|
+
} else if (error.message.includes('Firebase SDK not found')) {
|
|
699
|
+
// Handle missing Firebase installation
|
|
700
|
+
} else if (error.message.includes('Failed to initialize Firebase')) {
|
|
701
|
+
// Handle authentication or initialization errors
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
```
|
|
705
|
+
|
|
657
706
|
## Common Use Cases
|
|
658
707
|
|
|
659
708
|
### Use Case 1: Public Forum with Comments
|
|
@@ -724,6 +773,42 @@ const userPosts = query(
|
|
|
724
773
|
const snapshot = await getDocs(userPosts);
|
|
725
774
|
```
|
|
726
775
|
|
|
776
|
+
### Use Case 5: Pagination (New!)
|
|
777
|
+
|
|
778
|
+
```typescript
|
|
779
|
+
// ✅ Simple pagination with FirestoreHelper
|
|
780
|
+
|
|
781
|
+
// Get first page (10 posts)
|
|
782
|
+
const firstPage = await helper.getPublicData('posts', false, { limit: 10 });
|
|
783
|
+
|
|
784
|
+
console.log(`Fetched ${firstPage.docs.length} posts`);
|
|
785
|
+
firstPage.forEach(doc => {
|
|
786
|
+
console.log('Post:', doc.id, doc.data());
|
|
787
|
+
});
|
|
788
|
+
|
|
789
|
+
// Check if there are more pages
|
|
790
|
+
if (firstPage.docs.length === 10) {
|
|
791
|
+
// Get next page (start after last document)
|
|
792
|
+
const lastDoc = firstPage.docs[firstPage.docs.length - 1];
|
|
793
|
+
const nextPage = await helper.getPublicData('posts', false, {
|
|
794
|
+
limit: 10,
|
|
795
|
+
startAfter: lastDoc
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
console.log(`Next page: ${nextPage.docs.length} posts`);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// Also works with getUserData and getPublicRead
|
|
802
|
+
const userNotes = await helper.getUserData('notes', false, { limit: 20 });
|
|
803
|
+
const configs = await helper.getPublicRead('config', { limit: 5 });
|
|
804
|
+
```
|
|
805
|
+
|
|
806
|
+
**Pagination Best Practices:**
|
|
807
|
+
- Use a consistent `limit` value across pages for predictable UX
|
|
808
|
+
- Store the last document from the current page to fetch the next page
|
|
809
|
+
- Check if `docs.length` equals your limit to determine if there might be more pages
|
|
810
|
+
- For infinite scroll, keep appending results; for traditional pagination, replace the view
|
|
811
|
+
|
|
727
812
|
## Permission Examples
|
|
728
813
|
|
|
729
814
|
### What Users CAN Do
|
|
@@ -791,6 +876,264 @@ import type {
|
|
|
791
876
|
} from '@seaverse/data-service-sdk';
|
|
792
877
|
```
|
|
793
878
|
|
|
879
|
+
## Production Usage Recommendations
|
|
880
|
+
|
|
881
|
+
### Token Expiration Management
|
|
882
|
+
|
|
883
|
+
Firestore tokens expire after 1 hour (3600 seconds). For production applications, you should implement token refresh logic:
|
|
884
|
+
|
|
885
|
+
```typescript
|
|
886
|
+
import { DataServiceClient, initializeWithToken } from '@seaverse/data-service-sdk';
|
|
887
|
+
|
|
888
|
+
class FirestoreManager {
|
|
889
|
+
private tokenExpiryTime: number | null = null;
|
|
890
|
+
private dataClient = new DataServiceClient();
|
|
891
|
+
private userToken: string | null = null;
|
|
892
|
+
private appId: string;
|
|
893
|
+
|
|
894
|
+
constructor(appId: string) {
|
|
895
|
+
this.appId = appId;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
async initialize(userToken: string) {
|
|
899
|
+
this.userToken = userToken;
|
|
900
|
+
|
|
901
|
+
// Generate Firestore token
|
|
902
|
+
const tokenResponse = await this.dataClient.generateFirestoreToken({
|
|
903
|
+
token: userToken,
|
|
904
|
+
app_id: this.appId
|
|
905
|
+
});
|
|
906
|
+
|
|
907
|
+
// Initialize Firebase (safe to call multiple times)
|
|
908
|
+
const result = await initializeWithToken(tokenResponse);
|
|
909
|
+
|
|
910
|
+
// Track token expiry (expires_in is in seconds)
|
|
911
|
+
this.tokenExpiryTime = Date.now() + (tokenResponse.expires_in * 1000);
|
|
912
|
+
|
|
913
|
+
return result;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
isTokenExpiringSoon(): boolean {
|
|
917
|
+
if (!this.tokenExpiryTime) return true;
|
|
918
|
+
|
|
919
|
+
// Check if token expires within 5 minutes
|
|
920
|
+
const fiveMinutes = 5 * 60 * 1000;
|
|
921
|
+
return Date.now() + fiveMinutes >= this.tokenExpiryTime;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
async refreshTokenIfNeeded() {
|
|
925
|
+
if (this.isTokenExpiringSoon() && this.userToken) {
|
|
926
|
+
console.log('Token expiring soon, refreshing...');
|
|
927
|
+
return await this.initialize(this.userToken);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
// Usage
|
|
933
|
+
const manager = new FirestoreManager('my-app-123');
|
|
934
|
+
|
|
935
|
+
// Initialize
|
|
936
|
+
const { helper } = await manager.initialize(userJwtToken);
|
|
937
|
+
|
|
938
|
+
// Before making Firestore operations, check token
|
|
939
|
+
await manager.refreshTokenIfNeeded();
|
|
940
|
+
await helper.addToPublicData('posts', { title: 'My Post' });
|
|
941
|
+
```
|
|
942
|
+
|
|
943
|
+
### Advanced: Singleton Pattern with React
|
|
944
|
+
|
|
945
|
+
For React applications, you can create a singleton manager with hooks:
|
|
946
|
+
|
|
947
|
+
```typescript
|
|
948
|
+
// firestore-manager.ts
|
|
949
|
+
class FirestoreManager {
|
|
950
|
+
private static instance: FirestoreManager | null = null;
|
|
951
|
+
private helper: FirestoreHelper | null = null;
|
|
952
|
+
private db: Firestore | null = null;
|
|
953
|
+
private appId: string | null = null;
|
|
954
|
+
private userId: string | null = null;
|
|
955
|
+
private tokenExpiryTime: number | null = null;
|
|
956
|
+
|
|
957
|
+
static getInstance(): FirestoreManager {
|
|
958
|
+
if (!FirestoreManager.instance) {
|
|
959
|
+
FirestoreManager.instance = new FirestoreManager();
|
|
960
|
+
}
|
|
961
|
+
return FirestoreManager.instance;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
async initialize(tokenResponse: FirestoreTokenResponse) {
|
|
965
|
+
// Always call initializeWithToken - it's safe for multiple calls
|
|
966
|
+
const result = await initializeWithToken(tokenResponse);
|
|
967
|
+
|
|
968
|
+
this.db = result.db;
|
|
969
|
+
this.appId = result.appId;
|
|
970
|
+
this.userId = result.userId;
|
|
971
|
+
this.helper = result.helper;
|
|
972
|
+
this.tokenExpiryTime = Date.now() + (tokenResponse.expires_in * 1000);
|
|
973
|
+
|
|
974
|
+
return result;
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
getHelper(): FirestoreHelper | null {
|
|
978
|
+
return this.helper;
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
isTokenExpiringSoon(): boolean {
|
|
982
|
+
if (!this.tokenExpiryTime) return true;
|
|
983
|
+
const fiveMinutes = 5 * 60 * 1000;
|
|
984
|
+
return Date.now() + fiveMinutes >= this.tokenExpiryTime;
|
|
985
|
+
}
|
|
986
|
+
|
|
987
|
+
reset() {
|
|
988
|
+
this.db = null;
|
|
989
|
+
this.appId = null;
|
|
990
|
+
this.userId = null;
|
|
991
|
+
this.helper = null;
|
|
992
|
+
this.tokenExpiryTime = null;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
export default FirestoreManager;
|
|
997
|
+
|
|
998
|
+
// useFirestore.ts (React Hook)
|
|
999
|
+
import { useState, useEffect } from 'react';
|
|
1000
|
+
import { DataServiceClient } from '@seaverse/data-service-sdk';
|
|
1001
|
+
import FirestoreManager from './firestore-manager';
|
|
1002
|
+
|
|
1003
|
+
export function useFirestore(userToken: string, appId: string) {
|
|
1004
|
+
const [helper, setHelper] = useState<FirestoreHelper | null>(null);
|
|
1005
|
+
const [loading, setLoading] = useState(true);
|
|
1006
|
+
const [error, setError] = useState<Error | null>(null);
|
|
1007
|
+
|
|
1008
|
+
useEffect(() => {
|
|
1009
|
+
let mounted = true;
|
|
1010
|
+
|
|
1011
|
+
async function init() {
|
|
1012
|
+
try {
|
|
1013
|
+
const dataClient = new DataServiceClient();
|
|
1014
|
+
const tokenResponse = await dataClient.generateFirestoreToken({
|
|
1015
|
+
token: userToken,
|
|
1016
|
+
app_id: appId
|
|
1017
|
+
});
|
|
1018
|
+
|
|
1019
|
+
const manager = FirestoreManager.getInstance();
|
|
1020
|
+
const { helper } = await manager.initialize(tokenResponse);
|
|
1021
|
+
|
|
1022
|
+
if (mounted) {
|
|
1023
|
+
setHelper(helper);
|
|
1024
|
+
setLoading(false);
|
|
1025
|
+
}
|
|
1026
|
+
} catch (err) {
|
|
1027
|
+
if (mounted) {
|
|
1028
|
+
setError(err as Error);
|
|
1029
|
+
setLoading(false);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
|
|
1034
|
+
init();
|
|
1035
|
+
|
|
1036
|
+
return () => {
|
|
1037
|
+
mounted = false;
|
|
1038
|
+
};
|
|
1039
|
+
}, [userToken, appId]);
|
|
1040
|
+
|
|
1041
|
+
return { helper, loading, error };
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
// Usage in component
|
|
1045
|
+
function MyComponent() {
|
|
1046
|
+
const { helper, loading, error } = useFirestore(userToken, 'my-app-123');
|
|
1047
|
+
|
|
1048
|
+
if (loading) return <div>Loading...</div>;
|
|
1049
|
+
if (error) return <div>Error: {error.message}</div>;
|
|
1050
|
+
|
|
1051
|
+
const handleAddPost = async () => {
|
|
1052
|
+
await helper.addToPublicData('posts', {
|
|
1053
|
+
title: 'My Post',
|
|
1054
|
+
content: 'Hello World'
|
|
1055
|
+
});
|
|
1056
|
+
};
|
|
1057
|
+
|
|
1058
|
+
return <button onClick={handleAddPost}>Add Post</button>;
|
|
1059
|
+
}
|
|
1060
|
+
```
|
|
1061
|
+
|
|
1062
|
+
### Background Token Refresh
|
|
1063
|
+
|
|
1064
|
+
For long-running applications, set up automatic background token refresh:
|
|
1065
|
+
|
|
1066
|
+
```typescript
|
|
1067
|
+
class FirestoreManager {
|
|
1068
|
+
private refreshTimer: NodeJS.Timeout | null = null;
|
|
1069
|
+
|
|
1070
|
+
async initialize(userToken: string, appId: string) {
|
|
1071
|
+
const dataClient = new DataServiceClient();
|
|
1072
|
+
const tokenResponse = await dataClient.generateFirestoreToken({
|
|
1073
|
+
token: userToken,
|
|
1074
|
+
app_id: appId
|
|
1075
|
+
});
|
|
1076
|
+
|
|
1077
|
+
const result = await initializeWithToken(tokenResponse);
|
|
1078
|
+
|
|
1079
|
+
// Schedule token refresh 5 minutes before expiry
|
|
1080
|
+
const refreshIn = (tokenResponse.expires_in - 5 * 60) * 1000;
|
|
1081
|
+
this.scheduleRefresh(userToken, appId, refreshIn);
|
|
1082
|
+
|
|
1083
|
+
return result;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
private scheduleRefresh(userToken: string, appId: string, delay: number) {
|
|
1087
|
+
if (this.refreshTimer) {
|
|
1088
|
+
clearTimeout(this.refreshTimer);
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
this.refreshTimer = setTimeout(async () => {
|
|
1092
|
+
console.log('Auto-refreshing Firestore token...');
|
|
1093
|
+
try {
|
|
1094
|
+
await this.initialize(userToken, appId);
|
|
1095
|
+
} catch (error) {
|
|
1096
|
+
console.error('Failed to refresh token:', error);
|
|
1097
|
+
}
|
|
1098
|
+
}, delay);
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
cleanup() {
|
|
1102
|
+
if (this.refreshTimer) {
|
|
1103
|
+
clearTimeout(this.refreshTimer);
|
|
1104
|
+
this.refreshTimer = null;
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
```
|
|
1109
|
+
|
|
1110
|
+
### Error Recovery
|
|
1111
|
+
|
|
1112
|
+
Implement proper error recovery for network failures:
|
|
1113
|
+
|
|
1114
|
+
```typescript
|
|
1115
|
+
async function initializeWithRetry(
|
|
1116
|
+
tokenResponse: FirestoreTokenResponse,
|
|
1117
|
+
maxRetries = 3
|
|
1118
|
+
) {
|
|
1119
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
1120
|
+
try {
|
|
1121
|
+
return await initializeWithToken(tokenResponse);
|
|
1122
|
+
} catch (error) {
|
|
1123
|
+
console.error(`Initialization attempt ${i + 1} failed:`, error);
|
|
1124
|
+
|
|
1125
|
+
if (i === maxRetries - 1) {
|
|
1126
|
+
throw error; // Last attempt failed
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
// Wait before retry (exponential backoff)
|
|
1130
|
+
const delay = Math.pow(2, i) * 1000;
|
|
1131
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
}
|
|
1135
|
+
```
|
|
1136
|
+
|
|
794
1137
|
## Best Practices for LLM
|
|
795
1138
|
|
|
796
1139
|
When using this SDK with LLM-generated code:
|
|
@@ -1020,4 +1363,4 @@ Here's a complete example for using the SDK directly in the browser without any
|
|
|
1020
1363
|
## Related SDKs
|
|
1021
1364
|
|
|
1022
1365
|
- [@seaverse/auth-sdk](../auth-sdk) - User authentication and account management
|
|
1023
|
-
- [@seaverse/
|
|
1366
|
+
- [@seaverse/assets-sdk](../assets-sdk) - Assets and payment processing integration
|
package/dist/browser.js
CHANGED
|
@@ -4782,6 +4782,7 @@ class FirestoreHelper {
|
|
|
4782
4782
|
*
|
|
4783
4783
|
* @param collectionName - Collection name
|
|
4784
4784
|
* @param includeDeleted - Include soft-deleted documents (default: false)
|
|
4785
|
+
* @param options - Pagination options (limit and startAfter)
|
|
4785
4786
|
* @returns QuerySnapshot with documents
|
|
4786
4787
|
*
|
|
4787
4788
|
* @example
|
|
@@ -4794,11 +4795,21 @@ class FirestoreHelper {
|
|
|
4794
4795
|
*
|
|
4795
4796
|
* // Include deleted posts (admin use case)
|
|
4796
4797
|
* const allPosts = await helper.getPublicData('posts', true);
|
|
4798
|
+
*
|
|
4799
|
+
* // Pagination: Get first 10 posts
|
|
4800
|
+
* const firstPage = await helper.getPublicData('posts', false, { limit: 10 });
|
|
4801
|
+
*
|
|
4802
|
+
* // Get next 10 posts (start after last document)
|
|
4803
|
+
* const lastDoc = firstPage.docs[firstPage.docs.length - 1];
|
|
4804
|
+
* const nextPage = await helper.getPublicData('posts', false, {
|
|
4805
|
+
* limit: 10,
|
|
4806
|
+
* startAfter: lastDoc
|
|
4807
|
+
* });
|
|
4797
4808
|
* ```
|
|
4798
4809
|
*/
|
|
4799
|
-
async getPublicData(collectionName, includeDeleted = false) {
|
|
4810
|
+
async getPublicData(collectionName, includeDeleted = false, options) {
|
|
4800
4811
|
const path = getPublicDataPath(this.appId, collectionName);
|
|
4801
|
-
return this.getDocs(path, includeDeleted);
|
|
4812
|
+
return this.getDocs(path, includeDeleted, options);
|
|
4802
4813
|
}
|
|
4803
4814
|
/**
|
|
4804
4815
|
* Get all documents from userData collection (user's private data)
|
|
@@ -4807,6 +4818,7 @@ class FirestoreHelper {
|
|
|
4807
4818
|
*
|
|
4808
4819
|
* @param collectionName - Collection name
|
|
4809
4820
|
* @param includeDeleted - Include soft-deleted documents (default: false)
|
|
4821
|
+
* @param options - Pagination options (limit and startAfter)
|
|
4810
4822
|
* @returns QuerySnapshot with documents
|
|
4811
4823
|
*
|
|
4812
4824
|
* @example
|
|
@@ -4815,26 +4827,45 @@ class FirestoreHelper {
|
|
|
4815
4827
|
* snapshot.forEach(doc => {
|
|
4816
4828
|
* console.log('My note:', doc.data());
|
|
4817
4829
|
* });
|
|
4830
|
+
*
|
|
4831
|
+
* // Pagination: Get first 20 notes
|
|
4832
|
+
* const firstPage = await helper.getUserData('notes', false, { limit: 20 });
|
|
4833
|
+
*
|
|
4834
|
+
* // Get next page
|
|
4835
|
+
* const lastDoc = firstPage.docs[firstPage.docs.length - 1];
|
|
4836
|
+
* const nextPage = await helper.getUserData('notes', false, {
|
|
4837
|
+
* limit: 20,
|
|
4838
|
+
* startAfter: lastDoc
|
|
4839
|
+
* });
|
|
4818
4840
|
* ```
|
|
4819
4841
|
*/
|
|
4820
|
-
async getUserData(collectionName, includeDeleted = false) {
|
|
4842
|
+
async getUserData(collectionName, includeDeleted = false, options) {
|
|
4821
4843
|
const path = getUserDataPath(this.appId, this.userId, collectionName);
|
|
4822
|
-
return this.getDocs(path, includeDeleted);
|
|
4844
|
+
return this.getDocs(path, includeDeleted, options);
|
|
4823
4845
|
}
|
|
4824
4846
|
/**
|
|
4825
4847
|
* Get all documents from publicRead collection (read-only for users)
|
|
4826
4848
|
*
|
|
4827
4849
|
* @param collectionName - Collection name
|
|
4850
|
+
* @param options - Pagination options (limit and startAfter)
|
|
4828
4851
|
* @returns QuerySnapshot with documents
|
|
4829
4852
|
*
|
|
4830
4853
|
* @example
|
|
4831
4854
|
* ```typescript
|
|
4832
4855
|
* const configs = await helper.getPublicRead('config');
|
|
4856
|
+
*
|
|
4857
|
+
* // Pagination
|
|
4858
|
+
* const firstPage = await helper.getPublicRead('config', { limit: 10 });
|
|
4859
|
+
* const lastDoc = firstPage.docs[firstPage.docs.length - 1];
|
|
4860
|
+
* const nextPage = await helper.getPublicRead('config', {
|
|
4861
|
+
* limit: 10,
|
|
4862
|
+
* startAfter: lastDoc
|
|
4863
|
+
* });
|
|
4833
4864
|
* ```
|
|
4834
4865
|
*/
|
|
4835
|
-
async getPublicRead(collectionName) {
|
|
4866
|
+
async getPublicRead(collectionName, options) {
|
|
4836
4867
|
const path = getPublicReadPath(this.appId, collectionName);
|
|
4837
|
-
return this.getDocs(path);
|
|
4868
|
+
return this.getDocs(path, false, options);
|
|
4838
4869
|
}
|
|
4839
4870
|
/**
|
|
4840
4871
|
* Get collection reference for publicData
|
|
@@ -5003,22 +5034,36 @@ class FirestoreHelper {
|
|
|
5003
5034
|
}
|
|
5004
5035
|
/**
|
|
5005
5036
|
* Internal: Get all documents from collection
|
|
5006
|
-
* Optionally filter out soft-deleted documents
|
|
5037
|
+
* Optionally filter out soft-deleted documents and support pagination
|
|
5007
5038
|
*/
|
|
5008
|
-
async getDocs(collectionPath, includeDeleted = false) {
|
|
5009
|
-
const { getDocs, collection, query, where } = await this.loadFirestore();
|
|
5039
|
+
async getDocs(collectionPath, includeDeleted = false, options) {
|
|
5040
|
+
const { getDocs, collection, query, where, limit, startAfter } = await this.loadFirestore();
|
|
5010
5041
|
const colRef = collection(this.db, collectionPath);
|
|
5011
|
-
|
|
5012
|
-
|
|
5013
|
-
|
|
5014
|
-
|
|
5015
|
-
else {
|
|
5042
|
+
// Build query constraints
|
|
5043
|
+
const constraints = [];
|
|
5044
|
+
// Add soft-delete filter if needed
|
|
5045
|
+
if (!includeDeleted) {
|
|
5016
5046
|
// Filter out soft-deleted documents
|
|
5017
5047
|
// Use '!=' to include documents without _deleted field (new documents)
|
|
5018
5048
|
// This will return documents where _deleted is missing, null, undefined, or false
|
|
5019
|
-
|
|
5049
|
+
constraints.push(where('_deleted', '!=', true));
|
|
5050
|
+
}
|
|
5051
|
+
// Add pagination constraints if provided
|
|
5052
|
+
if (options?.limit) {
|
|
5053
|
+
constraints.push(limit(options.limit));
|
|
5054
|
+
}
|
|
5055
|
+
if (options?.startAfter) {
|
|
5056
|
+
constraints.push(startAfter(options.startAfter));
|
|
5057
|
+
}
|
|
5058
|
+
// Execute query
|
|
5059
|
+
if (constraints.length > 0) {
|
|
5060
|
+
const q = query(colRef, ...constraints);
|
|
5020
5061
|
return getDocs(q);
|
|
5021
5062
|
}
|
|
5063
|
+
else {
|
|
5064
|
+
// No constraints, return all documents
|
|
5065
|
+
return getDocs(colRef);
|
|
5066
|
+
}
|
|
5022
5067
|
}
|
|
5023
5068
|
/**
|
|
5024
5069
|
* Internal: Get collection reference
|
|
@@ -5047,7 +5092,8 @@ class FirestoreHelper {
|
|
|
5047
5092
|
query: firestore.query,
|
|
5048
5093
|
where: firestore.where,
|
|
5049
5094
|
orderBy: firestore.orderBy,
|
|
5050
|
-
limit: firestore.limit
|
|
5095
|
+
limit: firestore.limit,
|
|
5096
|
+
startAfter: firestore.startAfter
|
|
5051
5097
|
};
|
|
5052
5098
|
}
|
|
5053
5099
|
}
|
|
@@ -5149,7 +5195,7 @@ function getFirebaseConfig(tokenResponse) {
|
|
|
5149
5195
|
*
|
|
5150
5196
|
* This is a convenience function that automatically:
|
|
5151
5197
|
* 1. Creates Firebase config from token response
|
|
5152
|
-
* 2. Initializes Firebase app
|
|
5198
|
+
* 2. Initializes Firebase app (or reuses existing one)
|
|
5153
5199
|
* 3. Signs in with the custom token
|
|
5154
5200
|
* 4. Creates FirestoreHelper for LLM-friendly operations
|
|
5155
5201
|
* 5. Returns authenticated Firebase instances
|
|
@@ -5157,11 +5203,20 @@ function getFirebaseConfig(tokenResponse) {
|
|
|
5157
5203
|
* IMPORTANT: This function requires Firebase SDK to be installed separately:
|
|
5158
5204
|
* npm install firebase
|
|
5159
5205
|
*
|
|
5206
|
+
* ⚠️ IMPORTANT: This function can be called multiple times safely. It will:
|
|
5207
|
+
* - Reuse existing Firebase app if already initialized
|
|
5208
|
+
* - Re-authenticate with new token if needed
|
|
5209
|
+
* - Handle token refresh scenarios
|
|
5210
|
+
*
|
|
5160
5211
|
* 🎯 LLM RECOMMENDED: Use the `helper` object for simplified operations
|
|
5161
5212
|
*
|
|
5162
5213
|
* @param tokenResponse - The Firestore token response from SDK
|
|
5163
5214
|
* @returns Object containing initialized Firebase instances and LLM-friendly helper
|
|
5164
5215
|
*
|
|
5216
|
+
* @throws {Error} If app_id is missing from token response
|
|
5217
|
+
* @throws {Error} If Firebase SDK is not installed
|
|
5218
|
+
* @throws {Error} If authentication fails
|
|
5219
|
+
*
|
|
5165
5220
|
* @example
|
|
5166
5221
|
* ```typescript
|
|
5167
5222
|
* import { initializeWithToken } from '@seaverse/data-service-sdk';
|
|
@@ -5180,17 +5235,25 @@ function getFirebaseConfig(tokenResponse) {
|
|
|
5180
5235
|
* ```
|
|
5181
5236
|
*/
|
|
5182
5237
|
async function initializeWithToken(tokenResponse) {
|
|
5238
|
+
// ✅ FIX #3: Validate appId FIRST before any initialization
|
|
5239
|
+
const appId = tokenResponse.app_id;
|
|
5240
|
+
if (!appId) {
|
|
5241
|
+
throw new Error('app_id is required in token response. ' +
|
|
5242
|
+
'Make sure your token response includes a valid app_id field.');
|
|
5243
|
+
}
|
|
5183
5244
|
// Check if Firebase SDK is available
|
|
5184
5245
|
let initializeApp;
|
|
5185
5246
|
let getAuth;
|
|
5186
5247
|
let signInWithCustomToken;
|
|
5187
5248
|
let getFirestore;
|
|
5249
|
+
let getApps;
|
|
5188
5250
|
try {
|
|
5189
5251
|
// Try to import Firebase modules
|
|
5190
5252
|
const firebaseApp = await import('firebase/app');
|
|
5191
5253
|
const firebaseAuth = await import('firebase/auth');
|
|
5192
5254
|
const firebaseFirestore = await import('firebase/firestore');
|
|
5193
5255
|
initializeApp = firebaseApp.initializeApp;
|
|
5256
|
+
getApps = firebaseApp.getApps;
|
|
5194
5257
|
getAuth = firebaseAuth.getAuth;
|
|
5195
5258
|
signInWithCustomToken = firebaseAuth.signInWithCustomToken;
|
|
5196
5259
|
getFirestore = firebaseFirestore.getFirestore;
|
|
@@ -5199,17 +5262,57 @@ async function initializeWithToken(tokenResponse) {
|
|
|
5199
5262
|
throw new Error('Firebase SDK not found. Please install it: npm install firebase\n' +
|
|
5200
5263
|
'Or import manually and use getFirebaseConfig() helper instead.');
|
|
5201
5264
|
}
|
|
5202
|
-
//
|
|
5203
|
-
const
|
|
5204
|
-
const
|
|
5205
|
-
|
|
5206
|
-
|
|
5207
|
-
|
|
5208
|
-
|
|
5209
|
-
|
|
5210
|
-
|
|
5265
|
+
// ✅ FIX #1: Check if Firebase app already exists
|
|
5266
|
+
const existingApps = getApps();
|
|
5267
|
+
const existingApp = existingApps.find((app) => app.name === '[DEFAULT]');
|
|
5268
|
+
let app;
|
|
5269
|
+
let auth;
|
|
5270
|
+
let db;
|
|
5271
|
+
if (existingApp) {
|
|
5272
|
+
// ✅ FIX #2: Reuse existing app instead of creating new one
|
|
5273
|
+
console.log('[SeaVerse SDK] Reusing existing Firebase app');
|
|
5274
|
+
app = existingApp;
|
|
5275
|
+
auth = getAuth(app);
|
|
5276
|
+
db = getFirestore(app, tokenResponse.database_id);
|
|
5277
|
+
// ✅ FIX #4: Re-authenticate with new token (for token refresh scenarios)
|
|
5278
|
+
try {
|
|
5279
|
+
await signInWithCustomToken(auth, tokenResponse.custom_token);
|
|
5280
|
+
console.log('[SeaVerse SDK] Re-authenticated with new token');
|
|
5281
|
+
}
|
|
5282
|
+
catch (error) {
|
|
5283
|
+
// If already signed in with same token, ignore error
|
|
5284
|
+
// Only throw if it's a real authentication failure
|
|
5285
|
+
if (error?.code !== 'auth/invalid-credential' && error?.code !== 'auth/network-request-failed') {
|
|
5286
|
+
console.warn('[SeaVerse SDK] Re-authentication warning:', error?.message || error);
|
|
5287
|
+
}
|
|
5288
|
+
}
|
|
5289
|
+
}
|
|
5290
|
+
else {
|
|
5291
|
+
// ✅ FIX #4: First-time initialization with proper error handling
|
|
5292
|
+
console.log('[SeaVerse SDK] Initializing new Firebase app');
|
|
5293
|
+
try {
|
|
5294
|
+
const config = getFirebaseConfig(tokenResponse);
|
|
5295
|
+
app = initializeApp(config);
|
|
5296
|
+
auth = getAuth(app);
|
|
5297
|
+
await signInWithCustomToken(auth, tokenResponse.custom_token);
|
|
5298
|
+
// IMPORTANT: Must specify database_id, not just use default!
|
|
5299
|
+
db = getFirestore(app, tokenResponse.database_id);
|
|
5300
|
+
}
|
|
5301
|
+
catch (error) {
|
|
5302
|
+
// ✅ FIX #4: Cleanup on failure to prevent half-initialized state
|
|
5303
|
+
if (app) {
|
|
5304
|
+
try {
|
|
5305
|
+
await app.delete();
|
|
5306
|
+
console.log('[SeaVerse SDK] Cleaned up failed Firebase app initialization');
|
|
5307
|
+
}
|
|
5308
|
+
catch (cleanupError) {
|
|
5309
|
+
console.warn('[SeaVerse SDK] Failed to cleanup app:', cleanupError);
|
|
5310
|
+
}
|
|
5311
|
+
}
|
|
5312
|
+
throw new Error(`Failed to initialize Firebase: ${error instanceof Error ? error.message : String(error)}`);
|
|
5313
|
+
}
|
|
5314
|
+
}
|
|
5211
5315
|
const userId = tokenResponse.user_id;
|
|
5212
|
-
const appId = tokenResponse.app_id || '';
|
|
5213
5316
|
// Create LLM-friendly helper
|
|
5214
5317
|
const helper = new FirestoreHelper(db, appId, userId);
|
|
5215
5318
|
return {
|