@seaverse/data-service-sdk 0.9.0 → 0.10.1

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 CHANGED
@@ -42,6 +42,8 @@ SeaVerse Data Service SDK for accessing Firestore with secure token management a
42
42
  - 📝 TypeScript support with full type definitions
43
43
  - 🤖 LLM-friendly documentation with clear examples
44
44
  - 🛡️ Path helper functions to prevent permission-denied errors
45
+ - ✅ Built-in validation (reserved fields, document size)
46
+ - 🗑️ Soft delete support (mark as deleted without removing)
45
47
 
46
48
  ## Three-Tier Permission Model
47
49
 
@@ -51,7 +53,7 @@ SeaVerse organizes your Firestore data into three permission levels:
51
53
  |-----------------|--------------|-------------|--------------|----------|
52
54
  | **publicRead** | `appData/{app_id}/publicRead/_data/{collection}/{docId}` | All authenticated users | Admin only | System configs, announcements, static content |
53
55
  | **publicData** | `appData/{app_id}/publicData/_data/{collection}/{docId}` | All authenticated users | All authenticated users | User posts, comments, shared content |
54
- | **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 |
55
57
 
56
58
  ### 🚨 CRITICAL: Firestore Path Rules (For LLM)
57
59
 
@@ -86,7 +88,7 @@ const postsRef = collection(db, `appData/${appId}/publicData/_data/posts`);
86
88
  await addDoc(postsRef, { ...data }); // Firestore adds document ID
87
89
 
88
90
  // User Private Data (owner only)
89
- const notesRef = collection(db, `appData/${appId}/userData/${userId}/notes`);
91
+ const notesRef = collection(db, `appData/${appId}/userData/${userId}/_data/notes`);
90
92
  await addDoc(notesRef, { ...data });
91
93
 
92
94
  // Public Read-Only (everyone can read, admin can write)
@@ -95,9 +97,10 @@ await getDocs(configRef);
95
97
  ```
96
98
 
97
99
  **The pattern is always:**
98
- - `appData` → `{app_id}` → `{permission_layer}` → `_data` → `{collection}` → (auto-generated doc ID)
99
- - Collection: 5 segments (odd) ✅
100
- - Document: 6 segments (even) ✅
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
101
104
 
102
105
  ### Required Fields
103
106
 
@@ -113,6 +116,82 @@ All Firestore documents MUST include these three fields:
113
116
 
114
117
  These fields are enforced by Firestore Security Rules and ensure proper data isolation.
115
118
 
119
+ **🎯 Good News for LLM**: When using `FirestoreHelper`, these fields are automatically injected - you don't need to remember them!
120
+
121
+ ### Reserved Fields & Validation
122
+
123
+ **⚠️ IMPORTANT: Fields starting with `_` are reserved for system use!**
124
+
125
+ The SDK automatically validates your data to prevent common mistakes:
126
+
127
+ 1. **Reserved Fields**: You cannot create custom fields starting with `_`
128
+ ```typescript
129
+ // ❌ WRONG - Will throw error
130
+ await helper.addToPublicData('posts', {
131
+ _custom: 'value', // Reserved field!
132
+ title: 'My Post'
133
+ });
134
+
135
+ // ✅ CORRECT
136
+ await helper.addToPublicData('posts', {
137
+ customField: 'value', // No underscore prefix
138
+ title: 'My Post'
139
+ });
140
+ ```
141
+
142
+ 2. **Document Size**: Documents are limited to 256 KB
143
+ ```typescript
144
+ // SDK will automatically check size and throw error if too large
145
+ ```
146
+
147
+ 3. **Automatic Validation**: All `FirestoreHelper` methods validate data automatically
148
+ ```typescript
149
+ // Manual validation if needed
150
+ import { validateFirestoreData, validateDataDetailed } from '@seaverse/data-service-sdk';
151
+
152
+ try {
153
+ validateFirestoreData(myData);
154
+ // Data is valid
155
+ } catch (error) {
156
+ console.error('Validation failed:', error.message);
157
+ }
158
+
159
+ // Or get detailed errors without throwing
160
+ const result = validateDataDetailed(myData);
161
+ if (!result.valid) {
162
+ console.error('Errors:', result.errors);
163
+ }
164
+ ```
165
+
166
+ ### Soft Delete Support
167
+
168
+ **🗑️ Recommended Practice: Use soft delete instead of hard delete**
169
+
170
+ Soft delete marks documents as deleted without removing them from the database:
171
+
172
+ ```typescript
173
+ // ✅ RECOMMENDED: Soft delete (mark as deleted)
174
+ await helper.softDeleteDoc(
175
+ getPublicDataPath(appId, 'posts'),
176
+ 'post-123'
177
+ );
178
+
179
+ // Document is still in database but marked as _deleted = true
180
+ // By default, helper.getPublicData() won't return deleted documents
181
+
182
+ // ❌ Hard delete (only for admins, permanent)
183
+ await helper.deleteDoc(
184
+ getPublicDataPath(appId, 'posts'),
185
+ 'post-123'
186
+ );
187
+ ```
188
+
189
+ **Why soft delete?**
190
+ - ✅ Data recovery possible
191
+ - ✅ Audit trail preserved
192
+ - ✅ Safer for production
193
+ - ✅ Follows industry best practices
194
+
116
195
  ## Installation
117
196
 
118
197
  ```bash
@@ -179,17 +258,17 @@ collection(db, `apps/${appId}/users/${userId}/articles`) // Not matching securi
179
258
  import { getPublicDataPath, getUserDataPath } from '@seaverse/data-service-sdk';
180
259
 
181
260
  // ✅ CORRECT - Guaranteed to match security rules
182
- collection(db, getPublicDataPath(appId, 'posts')) // → appData/{appId}/publicData/posts
183
- 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
184
263
  ```
185
264
 
186
265
  ### Available Path Helpers
187
266
 
188
267
  ```typescript
189
268
  import {
190
- getPublicReadPath, // For read-only public data
191
- getPublicDataPath, // For public read/write data
192
- getUserDataPath, // For private user data
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}
193
272
  getPublicReadDocPath, // For specific public document
194
273
  getPublicDataDocPath, // For specific public data document
195
274
  getUserDataDocPath, // For specific user document
@@ -246,7 +325,7 @@ const docPath = builder.publicData('posts').doc('post-123').build();
246
325
 
247
326
  // Build user data path
248
327
  const userPath = builder.userData(userId, 'notes').build();
249
- // Returns: 'appData/my-app/userData/user-123/notes'
328
+ // Returns: 'appData/my-app/userData/user-123/_data/notes'
250
329
  ```
251
330
 
252
331
  ### Error Prevention
@@ -600,7 +679,7 @@ const comments = await getDocs(
600
679
 
601
680
  ```typescript
602
681
  // Only the user can write to their own settings
603
- await setDoc(doc(db, `appData/${appId}/userData/${userId}/settings/preferences`), {
682
+ await setDoc(doc(db, `appData/${appId}/userData/${userId}/_data/settings/preferences`), {
604
683
  _appId: appId,
605
684
  _createdAt: serverTimestamp(),
606
685
  _createdBy: userId,
@@ -611,7 +690,7 @@ await setDoc(doc(db, `appData/${appId}/userData/${userId}/settings/preferences`)
611
690
 
612
691
  // Only the user can read their own settings
613
692
  const settings = await getDoc(
614
- doc(db, `appData/${appId}/userData/${userId}/settings/preferences`)
693
+ doc(db, `appData/${appId}/userData/${userId}/_data/settings/preferences`)
615
694
  );
616
695
  ```
617
696
 
@@ -652,7 +731,7 @@ const snapshot = await getDocs(userPosts);
652
731
  ✅ **Authenticated Users:**
653
732
  - Read `publicRead` data
654
733
  - Read and write `publicData` (all users' data)
655
- - Read and write their own `userData/{userId}` only
734
+ - Read and write their own `userData/{userId}/_data/` only
656
735
  - Update/delete documents where `_createdBy == userId`
657
736
 
658
737
  ✅ **Guest Users:**
@@ -716,7 +795,20 @@ import type {
716
795
 
717
796
  When using this SDK with LLM-generated code:
718
797
 
719
- 1. **🛡️ MOST IMPORTANT: Use path helper functions to avoid permission-denied errors:**
798
+ 1. **🎯 EASIEST: Use `FirestoreHelper` for everything!**
799
+ ```typescript
800
+ // ✅ BEST PRACTICE - Let helper handle everything
801
+ const { helper } = await initializeWithToken(tokenResponse);
802
+ await helper.addToPublicData('posts', { title: 'Post', content: 'Hello' });
803
+
804
+ // No need to remember:
805
+ // - Required fields (_appId, _createdAt, _createdBy)
806
+ // - Path construction
807
+ // - Data validation
808
+ // - Soft delete logic
809
+ ```
810
+
811
+ 2. **🛡️ Use path helper functions to avoid permission-denied errors:**
720
812
  ```typescript
721
813
  import { getPublicDataPath, getUserDataPath } from '@seaverse/data-service-sdk';
722
814
 
@@ -728,35 +820,40 @@ When using this SDK with LLM-generated code:
728
820
  await addDoc(collection(db, `apps/${appId}/posts`), { ... });
729
821
  ```
730
822
 
731
- 2. **Always include required fields:**
823
+ 3. **⚠️ Never use field names starting with `_`:**
732
824
  ```typescript
733
- {
734
- _appId: appId, // From token response
735
- _createdAt: serverTimestamp(), // Use serverTimestamp()
736
- _createdBy: userId // From token response
737
- }
825
+ // ❌ WRONG - Reserved field
826
+ { _myField: 'value' }
827
+
828
+ // CORRECT
829
+ { myField: 'value' }
738
830
  ```
739
831
 
740
- 3. **Use correct data paths with helpers:**
832
+ 4. **🗑️ Use soft delete instead of hard delete:**
741
833
  ```typescript
742
- // Use path helpers instead of manual strings!
743
- import { getPublicReadPath, getPublicDataPath, getUserDataPath } from '@seaverse/data-service-sdk';
834
+ // RECOMMENDED
835
+ await helper.softDeleteDoc(path, docId);
744
836
 
745
- // publicRead
746
- const configPath = getPublicReadPath(appId, 'config');
837
+ // ❌ Only for admins
838
+ await helper.deleteDoc(path, docId);
839
+ ```
747
840
 
748
- // publicData
749
- const postsPath = getPublicDataPath(appId, 'posts');
841
+ 5. **✅ Validation is automatic, but you can validate manually if needed:**
842
+ ```typescript
843
+ import { validateFirestoreData } from '@seaverse/data-service-sdk';
750
844
 
751
- // userData
752
- const notesPath = getUserDataPath(appId, userId, 'notes');
845
+ try {
846
+ validateFirestoreData(myData);
847
+ } catch (error) {
848
+ console.error('Invalid data:', error.message);
849
+ }
753
850
  ```
754
851
 
755
- 4. **Handle token expiration:**
852
+ 6. **Handle token expiration:**
756
853
  - Tokens expire after 1 hour (3600 seconds)
757
854
  - Check `expires_in` field and refresh when needed
758
855
 
759
- 5. **Use serverTimestamp() for timestamps:**
856
+ 7. **Use serverTimestamp() for timestamps:**
760
857
  ```typescript
761
858
  import { serverTimestamp } from 'firebase/firestore';
762
859
 
@@ -765,7 +862,7 @@ When using this SDK with LLM-generated code:
765
862
  }
766
863
  ```
767
864
 
768
- 6. **Separate guest and authenticated flows:**
865
+ 8. **Separate guest and authenticated flows:**
769
866
  - Use `generateGuestFirestoreToken()` for anonymous users
770
867
  - Use `generateFirestoreToken()` for logged-in users
771
868
 
@@ -837,7 +934,7 @@ async function completeExample() {
837
934
 
838
935
  // 7. Save user preferences (private)
839
936
  await addDoc(
840
- collection(db, `appData/${appId}/userData/${userId}/preferences`),
937
+ collection(db, `appData/${appId}/userData/${userId}/_data/preferences`),
841
938
  {
842
939
  _appId: appId,
843
940
  _createdAt: serverTimestamp(),