@seaverse/data-service-sdk 0.5.2 → 0.6.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 CHANGED
@@ -10,6 +10,7 @@ SeaVerse Data Service SDK for accessing Firestore with secure token management a
10
10
  - 🔒 Automatic data isolation by app_id
11
11
  - 📝 TypeScript support with full type definitions
12
12
  - 🤖 LLM-friendly documentation with clear examples
13
+ - 🛡️ Path helper functions to prevent permission-denied errors
13
14
 
14
15
  ## Three-Tier Permission Model
15
16
 
@@ -130,12 +131,116 @@ const { DataServiceClient } = require('@seaverse/data-service-sdk');
130
131
  import { DataServiceClient } from '@seaverse/data-service-sdk';
131
132
  ```
132
133
 
134
+ ## Path Helper Functions (🚨 Recommended for LLM)
135
+
136
+ To prevent `permission-denied` errors caused by incorrect paths, we provide helper functions that generate the correct Firestore paths automatically.
137
+
138
+ ### Why Use Path Helpers?
139
+
140
+ **Problem**: LLM or developers might accidentally use wrong paths:
141
+ ```typescript
142
+ // ❌ WRONG - Will cause permission-denied!
143
+ collection(db, `apps/${appId}/publicArticles`) // Not matching security rules!
144
+ collection(db, `apps/${appId}/users/${userId}/articles`) // Not matching security rules!
145
+ ```
146
+
147
+ **Solution**: Use path helper functions:
148
+ ```typescript
149
+ import { getPublicDataPath, getUserDataPath } from '@seaverse/data-service-sdk';
150
+
151
+ // ✅ CORRECT - Guaranteed to match security rules
152
+ collection(db, getPublicDataPath(appId, 'posts')) // → appData/{appId}/publicData/posts
153
+ collection(db, getUserDataPath(appId, userId, 'notes')) // → appData/{appId}/userData/{userId}/notes
154
+ ```
155
+
156
+ ### Available Path Helpers
157
+
158
+ ```typescript
159
+ import {
160
+ getPublicReadPath, // For read-only public data
161
+ getPublicDataPath, // For public read/write data
162
+ getUserDataPath, // For private user data
163
+ getPublicReadDocPath, // For specific public document
164
+ getPublicDataDocPath, // For specific public data document
165
+ getUserDataDocPath, // For specific user document
166
+ PathBuilder // For advanced path building
167
+ } from '@seaverse/data-service-sdk';
168
+ ```
169
+
170
+ ### Basic Usage Examples
171
+
172
+ ```typescript
173
+ // Public data that everyone can read/write
174
+ const postsPath = getPublicDataPath(appId, 'posts');
175
+ await addDoc(collection(db, postsPath), {
176
+ _appId: appId,
177
+ _createdAt: serverTimestamp(),
178
+ _createdBy: userId,
179
+ title: 'My Post'
180
+ });
181
+
182
+ // Private user data
183
+ const notesPath = getUserDataPath(appId, userId, 'notes');
184
+ await addDoc(collection(db, notesPath), {
185
+ _appId: appId,
186
+ _createdAt: serverTimestamp(),
187
+ _createdBy: userId,
188
+ content: 'Private note'
189
+ });
190
+
191
+ // Public read-only data (admin writes only)
192
+ const configPath = getPublicReadPath(appId, 'config');
193
+ const configs = await getDocs(collection(db, configPath));
194
+
195
+ // Access specific document
196
+ const docPath = getPublicDataDocPath(appId, 'posts', 'post-123');
197
+ const docSnap = await getDoc(doc(db, docPath));
198
+ ```
199
+
200
+ ### Advanced: PathBuilder
201
+
202
+ For complex path construction:
203
+
204
+ ```typescript
205
+ import { PathBuilder } from '@seaverse/data-service-sdk';
206
+
207
+ const builder = new PathBuilder(appId);
208
+
209
+ // Build collection path
210
+ const path = builder.publicData('posts').build();
211
+ // Returns: 'appData/my-app/publicData/posts'
212
+
213
+ // Build document path
214
+ const docPath = builder.publicData('posts').doc('post-123').build();
215
+ // Returns: 'appData/my-app/publicData/posts/post-123'
216
+
217
+ // Build user data path
218
+ const userPath = builder.userData(userId, 'notes').build();
219
+ // Returns: 'appData/my-app/userData/user-123/notes'
220
+ ```
221
+
222
+ ### Error Prevention
223
+
224
+ Path helpers validate inputs to prevent common mistakes:
225
+
226
+ ```typescript
227
+ // ❌ These will throw errors:
228
+ getPublicDataPath('my-app', 'posts/comments'); // Error: cannot contain /
229
+ getPublicDataPath('my-app', ''); // Error: must be non-empty string
230
+ getUserDataPath('my-app', '', 'notes'); // Error: userId must be non-empty
231
+ ```
232
+
133
233
  ## Quick Start
134
234
 
135
235
  ### 🚀 Easiest Way (Recommended - Auto Firebase Setup)
136
236
 
137
237
  ```typescript
138
- import { DataServiceClient, initializeWithToken } from '@seaverse/data-service-sdk';
238
+ import {
239
+ DataServiceClient,
240
+ initializeWithToken,
241
+ getPublicDataPath, // 🛡️ Use path helpers!
242
+ getUserDataPath // 🛡️ Use path helpers!
243
+ } from '@seaverse/data-service-sdk';
139
244
  import { AuthClient } from '@seaverse/auth-sdk';
140
245
  import { collection, addDoc, getDocs, serverTimestamp } from 'firebase/firestore';
141
246
 
@@ -156,10 +261,11 @@ const tokenResponse = await dataClient.generateFirestoreToken({
156
261
  // Step 3: Auto-initialize Firebase (ONE LINE!)
157
262
  const { db, appId, userId } = await initializeWithToken(tokenResponse);
158
263
 
159
- // Step 4: Use Firestore directly!
264
+ // Step 4: Use Firestore directly with path helpers!
160
265
 
161
266
  // Write to publicData (everyone can write)
162
- await addDoc(collection(db, `appData/${appId}/publicData/posts`), {
267
+ const postsPath = getPublicDataPath(appId, 'posts'); // 🛡️ Safe path!
268
+ await addDoc(collection(db, postsPath), {
163
269
  _appId: appId, // REQUIRED
164
270
  _createdAt: serverTimestamp(), // REQUIRED
165
271
  _createdBy: userId, // REQUIRED
@@ -168,13 +274,14 @@ await addDoc(collection(db, `appData/${appId}/publicData/posts`), {
168
274
  });
169
275
 
170
276
  // Read from publicData
171
- const snapshot = await getDocs(collection(db, `appData/${appId}/publicData/posts`));
277
+ const snapshot = await getDocs(collection(db, postsPath));
172
278
  snapshot.forEach(doc => {
173
279
  console.log(doc.id, doc.data());
174
280
  });
175
281
 
176
282
  // Write to userData (private)
177
- await addDoc(collection(db, `appData/${appId}/userData/${userId}/notes`), {
283
+ const notesPath = getUserDataPath(appId, userId, 'notes'); // 🛡️ Safe path!
284
+ await addDoc(collection(db, notesPath), {
178
285
  _appId: appId, // REQUIRED
179
286
  _createdAt: serverTimestamp(), // REQUIRED
180
287
  _createdBy: userId, // REQUIRED
@@ -186,7 +293,11 @@ await addDoc(collection(db, `appData/${appId}/userData/${userId}/notes`), {
186
293
  ### 👤 For Guest Users (Even Simpler!)
187
294
 
188
295
  ```typescript
189
- import { DataServiceClient, initializeWithToken } from '@seaverse/data-service-sdk';
296
+ import {
297
+ DataServiceClient,
298
+ initializeWithToken,
299
+ getPublicDataPath // 🛡️ Use path helpers!
300
+ } from '@seaverse/data-service-sdk';
190
301
  import { collection, addDoc, serverTimestamp } from 'firebase/firestore';
191
302
 
192
303
  // Step 1: Get guest token (no authentication needed!)
@@ -199,7 +310,8 @@ const tokenResponse = await dataClient.generateGuestFirestoreToken({
199
310
  const { db, appId, userId } = await initializeWithToken(tokenResponse);
200
311
 
201
312
  // Step 3: Guest can write to publicData
202
- await addDoc(collection(db, `appData/${appId}/publicData/comments`), {
313
+ const commentsPath = getPublicDataPath(appId, 'comments'); // 🛡️ Safe path!
314
+ await addDoc(collection(db, commentsPath), {
203
315
  _appId: appId,
204
316
  _createdAt: serverTimestamp(),
205
317
  _createdBy: userId, // Guest user ID (e.g., 'guest-abc123')
@@ -566,7 +678,19 @@ import type {
566
678
 
567
679
  When using this SDK with LLM-generated code:
568
680
 
569
- 1. **Always include required fields:**
681
+ 1. **🛡️ MOST IMPORTANT: Use path helper functions to avoid permission-denied errors:**
682
+ ```typescript
683
+ import { getPublicDataPath, getUserDataPath } from '@seaverse/data-service-sdk';
684
+
685
+ // ✅ CORRECT - Use helpers
686
+ const path = getPublicDataPath(appId, 'posts');
687
+ await addDoc(collection(db, path), { ... });
688
+
689
+ // ❌ WRONG - Manual paths may be incorrect
690
+ await addDoc(collection(db, `apps/${appId}/posts`), { ... });
691
+ ```
692
+
693
+ 2. **Always include required fields:**
570
694
  ```typescript
571
695
  {
572
696
  _appId: appId, // From token response
@@ -575,16 +699,26 @@ When using this SDK with LLM-generated code:
575
699
  }
576
700
  ```
577
701
 
578
- 2. **Use correct data paths:**
579
- - publicRead: `appData/${appId}/publicRead/{collection}/{docId}`
580
- - publicData: `appData/${appId}/publicData/{collection}/{docId}`
581
- - userData: `appData/${appId}/userData/${userId}/{collection}/{docId}`
702
+ 3. **Use correct data paths with helpers:**
703
+ ```typescript
704
+ // Use path helpers instead of manual strings!
705
+ import { getPublicReadPath, getPublicDataPath, getUserDataPath } from '@seaverse/data-service-sdk';
706
+
707
+ // publicRead
708
+ const configPath = getPublicReadPath(appId, 'config');
709
+
710
+ // publicData
711
+ const postsPath = getPublicDataPath(appId, 'posts');
712
+
713
+ // userData
714
+ const notesPath = getUserDataPath(appId, userId, 'notes');
715
+ ```
582
716
 
583
- 3. **Handle token expiration:**
717
+ 4. **Handle token expiration:**
584
718
  - Tokens expire after 1 hour (3600 seconds)
585
719
  - Check `expires_in` field and refresh when needed
586
720
 
587
- 4. **Use serverTimestamp() for timestamps:**
721
+ 5. **Use serverTimestamp() for timestamps:**
588
722
  ```typescript
589
723
  import { serverTimestamp } from 'firebase/firestore';
590
724
 
@@ -593,7 +727,7 @@ When using this SDK with LLM-generated code:
593
727
  }
594
728
  ```
595
729
 
596
- 5. **Separate guest and authenticated flows:**
730
+ 6. **Separate guest and authenticated flows:**
597
731
  - Use `generateGuestFirestoreToken()` for anonymous users
598
732
  - Use `generateFirestoreToken()` for logged-in users
599
733
 
package/dist/browser.js CHANGED
@@ -4263,5 +4263,259 @@ async function initializeWithToken(tokenResponse) {
4263
4263
  };
4264
4264
  }
4265
4265
 
4266
- export { DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DataServiceClient, ENDPOINTS, getFirebaseConfig, initializeWithToken };
4266
+ /**
4267
+ * Firestore Path Helper Functions
4268
+ *
4269
+ * These helper functions generate correct Firestore paths that match
4270
+ * the security rules. Use these instead of manually constructing paths
4271
+ * to avoid permission-denied errors.
4272
+ *
4273
+ * 🚨 IMPORTANT: These paths are designed to work with SeaVerse Firestore Rules
4274
+ *
4275
+ * Permission Levels:
4276
+ * - publicRead: Read-only for all users, write for admins only
4277
+ * - publicData: Read/write for all authenticated users
4278
+ * - userData: Read/write only for the data owner
4279
+ */
4280
+ /**
4281
+ * Generate path for publicRead data (read-only for users, write for admins)
4282
+ *
4283
+ * @param appId - Your application ID
4284
+ * @param collection - Collection name (e.g., 'announcements', 'config')
4285
+ * @returns Firestore path string
4286
+ *
4287
+ * @example
4288
+ * ```typescript
4289
+ * // Read system announcements
4290
+ * const path = getPublicReadPath('my-app', 'announcements');
4291
+ * // Returns: 'appData/my-app/publicRead/announcements'
4292
+ *
4293
+ * const snapshot = await getDocs(collection(db, path));
4294
+ * ```
4295
+ */
4296
+ function getPublicReadPath(appId, collectionName) {
4297
+ validateSegment('appId', appId);
4298
+ validateSegment('collectionName', collectionName);
4299
+ return `appData/${appId}/publicRead/${collectionName}`;
4300
+ }
4301
+ /**
4302
+ * Generate path for publicData (read/write for all authenticated users)
4303
+ *
4304
+ * @param appId - Your application ID
4305
+ * @param collection - Collection name (e.g., 'posts', 'comments')
4306
+ * @returns Firestore path string
4307
+ *
4308
+ * @example
4309
+ * ```typescript
4310
+ * // Write a public post
4311
+ * const path = getPublicDataPath('my-app', 'posts');
4312
+ * // Returns: 'appData/my-app/publicData/posts'
4313
+ *
4314
+ * await addDoc(collection(db, path), {
4315
+ * _appId: appId,
4316
+ * _createdAt: serverTimestamp(),
4317
+ * _createdBy: userId,
4318
+ * title: 'My Post'
4319
+ * });
4320
+ * ```
4321
+ */
4322
+ function getPublicDataPath(appId, collectionName) {
4323
+ validateSegment('appId', appId);
4324
+ validateSegment('collectionName', collectionName);
4325
+ return `appData/${appId}/publicData/${collectionName}`;
4326
+ }
4327
+ /**
4328
+ * Generate path for userData (private, read/write only by owner)
4329
+ *
4330
+ * @param appId - Your application ID
4331
+ * @param userId - User ID who owns this data
4332
+ * @param collection - Collection name (e.g., 'notes', 'settings')
4333
+ * @returns Firestore path string
4334
+ *
4335
+ * @example
4336
+ * ```typescript
4337
+ * // Write private user notes
4338
+ * const path = getUserDataPath('my-app', 'user-123', 'notes');
4339
+ * // Returns: 'appData/my-app/userData/user-123/notes'
4340
+ *
4341
+ * await addDoc(collection(db, path), {
4342
+ * _appId: appId,
4343
+ * _createdAt: serverTimestamp(),
4344
+ * _createdBy: userId,
4345
+ * content: 'Private note'
4346
+ * });
4347
+ * ```
4348
+ */
4349
+ function getUserDataPath(appId, userId, collectionName) {
4350
+ validateSegment('appId', appId);
4351
+ validateSegment('userId', userId);
4352
+ validateSegment('collectionName', collectionName);
4353
+ return `appData/${appId}/userData/${userId}/${collectionName}`;
4354
+ }
4355
+ /**
4356
+ * Generate path for a specific document in publicRead
4357
+ *
4358
+ * @param appId - Your application ID
4359
+ * @param collection - Collection name
4360
+ * @param docId - Document ID
4361
+ * @returns Firestore document path string
4362
+ *
4363
+ * @example
4364
+ * ```typescript
4365
+ * const path = getPublicReadDocPath('my-app', 'announcements', 'announcement-1');
4366
+ * // Returns: 'appData/my-app/publicRead/announcements/announcement-1'
4367
+ *
4368
+ * const docSnap = await getDoc(doc(db, path));
4369
+ * ```
4370
+ */
4371
+ function getPublicReadDocPath(appId, collectionName, docId) {
4372
+ validateSegment('appId', appId);
4373
+ validateSegment('collectionName', collectionName);
4374
+ validateSegment('docId', docId);
4375
+ return `appData/${appId}/publicRead/${collectionName}/${docId}`;
4376
+ }
4377
+ /**
4378
+ * Generate path for a specific document in publicData
4379
+ *
4380
+ * @param appId - Your application ID
4381
+ * @param collection - Collection name
4382
+ * @param docId - Document ID
4383
+ * @returns Firestore document path string
4384
+ *
4385
+ * @example
4386
+ * ```typescript
4387
+ * const path = getPublicDataDocPath('my-app', 'posts', 'post-123');
4388
+ * // Returns: 'appData/my-app/publicData/posts/post-123'
4389
+ *
4390
+ * const docSnap = await getDoc(doc(db, path));
4391
+ * ```
4392
+ */
4393
+ function getPublicDataDocPath(appId, collectionName, docId) {
4394
+ validateSegment('appId', appId);
4395
+ validateSegment('collectionName', collectionName);
4396
+ validateSegment('docId', docId);
4397
+ return `appData/${appId}/publicData/${collectionName}/${docId}`;
4398
+ }
4399
+ /**
4400
+ * Generate path for a specific document in userData
4401
+ *
4402
+ * @param appId - Your application ID
4403
+ * @param userId - User ID who owns this data
4404
+ * @param collection - Collection name
4405
+ * @param docId - Document ID
4406
+ * @returns Firestore document path string
4407
+ *
4408
+ * @example
4409
+ * ```typescript
4410
+ * const path = getUserDataDocPath('my-app', 'user-123', 'notes', 'note-456');
4411
+ * // Returns: 'appData/my-app/userData/user-123/notes/note-456'
4412
+ *
4413
+ * const docSnap = await getDoc(doc(db, path));
4414
+ * ```
4415
+ */
4416
+ function getUserDataDocPath(appId, userId, collectionName, docId) {
4417
+ validateSegment('appId', appId);
4418
+ validateSegment('userId', userId);
4419
+ validateSegment('collectionName', collectionName);
4420
+ validateSegment('docId', docId);
4421
+ return `appData/${appId}/userData/${userId}/${collectionName}/${docId}`;
4422
+ }
4423
+ /**
4424
+ * Validate a path segment to ensure it doesn't contain invalid characters
4425
+ *
4426
+ * @param name - Parameter name for error messages
4427
+ * @param value - The segment value to validate
4428
+ * @throws Error if the segment is invalid
4429
+ */
4430
+ function validateSegment(name, value) {
4431
+ if (!value || typeof value !== 'string') {
4432
+ throw new Error(`${name} must be a non-empty string`);
4433
+ }
4434
+ if (value.includes('/')) {
4435
+ throw new Error(`${name} cannot contain forward slashes (/). Got: "${value}"`);
4436
+ }
4437
+ if (value.trim() !== value) {
4438
+ throw new Error(`${name} cannot start or end with whitespace. Got: "${value}"`);
4439
+ }
4440
+ }
4441
+ /**
4442
+ * Path builder for advanced use cases
4443
+ * Provides a fluent interface for building Firestore paths
4444
+ *
4445
+ * @example
4446
+ * ```typescript
4447
+ * // Build a path step by step
4448
+ * const builder = new PathBuilder('my-app');
4449
+ * const path = builder.publicData('posts').build();
4450
+ * // Returns: 'appData/my-app/publicData/posts'
4451
+ *
4452
+ * // With document ID
4453
+ * const docPath = builder.publicData('posts').doc('post-123').build();
4454
+ * // Returns: 'appData/my-app/publicData/posts/post-123'
4455
+ * ```
4456
+ */
4457
+ class PathBuilder {
4458
+ constructor(appId) {
4459
+ this.appId = appId;
4460
+ this.segments = ['appData'];
4461
+ validateSegment('appId', appId);
4462
+ this.segments.push(appId);
4463
+ }
4464
+ /**
4465
+ * Add publicRead collection to path
4466
+ */
4467
+ publicRead(collectionName) {
4468
+ validateSegment('collectionName', collectionName);
4469
+ this.segments.push('publicRead', collectionName);
4470
+ return this;
4471
+ }
4472
+ /**
4473
+ * Add publicData collection to path
4474
+ */
4475
+ publicData(collectionName) {
4476
+ validateSegment('collectionName', collectionName);
4477
+ this.segments.push('publicData', collectionName);
4478
+ return this;
4479
+ }
4480
+ /**
4481
+ * Add userData collection to path
4482
+ */
4483
+ userData(userId, collectionName) {
4484
+ validateSegment('userId', userId);
4485
+ validateSegment('collectionName', collectionName);
4486
+ this.segments.push('userData', userId, collectionName);
4487
+ return this;
4488
+ }
4489
+ /**
4490
+ * Add document ID to path
4491
+ */
4492
+ doc(docId) {
4493
+ validateSegment('docId', docId);
4494
+ this.segments.push(docId);
4495
+ return this;
4496
+ }
4497
+ /**
4498
+ * Build the final path string
4499
+ */
4500
+ build() {
4501
+ return this.segments.join('/');
4502
+ }
4503
+ }
4504
+ /**
4505
+ * Common path patterns as constants for frequently used paths
4506
+ */
4507
+ const PATH_PATTERNS = {
4508
+ /**
4509
+ * Base pattern for all SeaVerse data
4510
+ */
4511
+ APP_DATA: 'appData',
4512
+ /**
4513
+ * Permission layers
4514
+ */
4515
+ PUBLIC_READ: 'publicRead',
4516
+ PUBLIC_DATA: 'publicData',
4517
+ USER_DATA: 'userData',
4518
+ };
4519
+
4520
+ export { DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DataServiceClient, ENDPOINTS, PATH_PATTERNS, PathBuilder, getFirebaseConfig, getPublicDataDocPath, getPublicDataPath, getPublicReadDocPath, getPublicReadPath, getUserDataDocPath, getUserDataPath, initializeWithToken };
4267
4521
  //# sourceMappingURL=browser.js.map