@seaverse/data-service-sdk 0.5.2 → 0.7.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/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import axios from 'axios';
2
+ export { addDoc, arrayRemove, arrayUnion, collection, collectionGroup, deleteDoc, deleteField, doc, endAt, endBefore, getDoc, getDocs, getFirestore, increment, limit, limitToLast, onSnapshot, orderBy, query, runTransaction, serverTimestamp, setDoc, startAfter, startAt, updateDoc, where, writeBatch } from 'firebase/firestore';
2
3
 
3
4
  /**
4
5
  * Data Service SDK configuration
@@ -263,6 +264,628 @@ class DataServiceClient {
263
264
  }
264
265
  }
265
266
 
267
+ /**
268
+ * Firestore Path Helper Functions
269
+ *
270
+ * These helper functions generate correct Firestore paths that match
271
+ * the security rules. Use these instead of manually constructing paths
272
+ * to avoid permission-denied errors.
273
+ *
274
+ * 🚨 IMPORTANT: These paths are designed to work with SeaVerse Firestore Rules
275
+ *
276
+ * Permission Levels:
277
+ * - publicRead: Read-only for all users, write for admins only
278
+ * - publicData: Read/write for all authenticated users
279
+ * - userData: Read/write only for the data owner
280
+ */
281
+ /**
282
+ * Generate path for publicRead data (read-only for users, write for admins)
283
+ *
284
+ * @param appId - Your application ID
285
+ * @param collection - Collection name (e.g., 'announcements', 'config')
286
+ * @returns Firestore path string
287
+ *
288
+ * @example
289
+ * ```typescript
290
+ * // Read system announcements
291
+ * const path = getPublicReadPath('my-app', 'announcements');
292
+ * // Returns: 'appData/my-app/publicRead/announcements'
293
+ *
294
+ * const snapshot = await getDocs(collection(db, path));
295
+ * ```
296
+ */
297
+ function getPublicReadPath(appId, collectionName) {
298
+ validateSegment('appId', appId);
299
+ validateSegment('collectionName', collectionName);
300
+ return `appData/${appId}/publicRead/${collectionName}`;
301
+ }
302
+ /**
303
+ * Generate path for publicData (read/write for all authenticated users)
304
+ *
305
+ * @param appId - Your application ID
306
+ * @param collection - Collection name (e.g., 'posts', 'comments')
307
+ * @returns Firestore path string
308
+ *
309
+ * @example
310
+ * ```typescript
311
+ * // Write a public post
312
+ * const path = getPublicDataPath('my-app', 'posts');
313
+ * // Returns: 'appData/my-app/publicData/posts'
314
+ *
315
+ * await addDoc(collection(db, path), {
316
+ * _appId: appId,
317
+ * _createdAt: serverTimestamp(),
318
+ * _createdBy: userId,
319
+ * title: 'My Post'
320
+ * });
321
+ * ```
322
+ */
323
+ function getPublicDataPath(appId, collectionName) {
324
+ validateSegment('appId', appId);
325
+ validateSegment('collectionName', collectionName);
326
+ return `appData/${appId}/publicData/${collectionName}`;
327
+ }
328
+ /**
329
+ * Generate path for userData (private, read/write only by owner)
330
+ *
331
+ * @param appId - Your application ID
332
+ * @param userId - User ID who owns this data
333
+ * @param collection - Collection name (e.g., 'notes', 'settings')
334
+ * @returns Firestore path string
335
+ *
336
+ * @example
337
+ * ```typescript
338
+ * // Write private user notes
339
+ * const path = getUserDataPath('my-app', 'user-123', 'notes');
340
+ * // Returns: 'appData/my-app/userData/user-123/notes'
341
+ *
342
+ * await addDoc(collection(db, path), {
343
+ * _appId: appId,
344
+ * _createdAt: serverTimestamp(),
345
+ * _createdBy: userId,
346
+ * content: 'Private note'
347
+ * });
348
+ * ```
349
+ */
350
+ function getUserDataPath(appId, userId, collectionName) {
351
+ validateSegment('appId', appId);
352
+ validateSegment('userId', userId);
353
+ validateSegment('collectionName', collectionName);
354
+ return `appData/${appId}/userData/${userId}/${collectionName}`;
355
+ }
356
+ /**
357
+ * Generate path for a specific document in publicRead
358
+ *
359
+ * @param appId - Your application ID
360
+ * @param collection - Collection name
361
+ * @param docId - Document ID
362
+ * @returns Firestore document path string
363
+ *
364
+ * @example
365
+ * ```typescript
366
+ * const path = getPublicReadDocPath('my-app', 'announcements', 'announcement-1');
367
+ * // Returns: 'appData/my-app/publicRead/announcements/announcement-1'
368
+ *
369
+ * const docSnap = await getDoc(doc(db, path));
370
+ * ```
371
+ */
372
+ function getPublicReadDocPath(appId, collectionName, docId) {
373
+ validateSegment('appId', appId);
374
+ validateSegment('collectionName', collectionName);
375
+ validateSegment('docId', docId);
376
+ return `appData/${appId}/publicRead/${collectionName}/${docId}`;
377
+ }
378
+ /**
379
+ * Generate path for a specific document in publicData
380
+ *
381
+ * @param appId - Your application ID
382
+ * @param collection - Collection name
383
+ * @param docId - Document ID
384
+ * @returns Firestore document path string
385
+ *
386
+ * @example
387
+ * ```typescript
388
+ * const path = getPublicDataDocPath('my-app', 'posts', 'post-123');
389
+ * // Returns: 'appData/my-app/publicData/posts/post-123'
390
+ *
391
+ * const docSnap = await getDoc(doc(db, path));
392
+ * ```
393
+ */
394
+ function getPublicDataDocPath(appId, collectionName, docId) {
395
+ validateSegment('appId', appId);
396
+ validateSegment('collectionName', collectionName);
397
+ validateSegment('docId', docId);
398
+ return `appData/${appId}/publicData/${collectionName}/${docId}`;
399
+ }
400
+ /**
401
+ * Generate path for a specific document in userData
402
+ *
403
+ * @param appId - Your application ID
404
+ * @param userId - User ID who owns this data
405
+ * @param collection - Collection name
406
+ * @param docId - Document ID
407
+ * @returns Firestore document path string
408
+ *
409
+ * @example
410
+ * ```typescript
411
+ * const path = getUserDataDocPath('my-app', 'user-123', 'notes', 'note-456');
412
+ * // Returns: 'appData/my-app/userData/user-123/notes/note-456'
413
+ *
414
+ * const docSnap = await getDoc(doc(db, path));
415
+ * ```
416
+ */
417
+ function getUserDataDocPath(appId, userId, collectionName, docId) {
418
+ validateSegment('appId', appId);
419
+ validateSegment('userId', userId);
420
+ validateSegment('collectionName', collectionName);
421
+ validateSegment('docId', docId);
422
+ return `appData/${appId}/userData/${userId}/${collectionName}/${docId}`;
423
+ }
424
+ /**
425
+ * Validate a path segment to ensure it doesn't contain invalid characters
426
+ *
427
+ * @param name - Parameter name for error messages
428
+ * @param value - The segment value to validate
429
+ * @throws Error if the segment is invalid
430
+ */
431
+ function validateSegment(name, value) {
432
+ if (!value || typeof value !== 'string') {
433
+ throw new Error(`${name} must be a non-empty string`);
434
+ }
435
+ if (value.includes('/')) {
436
+ throw new Error(`${name} cannot contain forward slashes (/). Got: "${value}"`);
437
+ }
438
+ if (value.trim() !== value) {
439
+ throw new Error(`${name} cannot start or end with whitespace. Got: "${value}"`);
440
+ }
441
+ }
442
+ /**
443
+ * Path builder for advanced use cases
444
+ * Provides a fluent interface for building Firestore paths
445
+ *
446
+ * @example
447
+ * ```typescript
448
+ * // Build a path step by step
449
+ * const builder = new PathBuilder('my-app');
450
+ * const path = builder.publicData('posts').build();
451
+ * // Returns: 'appData/my-app/publicData/posts'
452
+ *
453
+ * // With document ID
454
+ * const docPath = builder.publicData('posts').doc('post-123').build();
455
+ * // Returns: 'appData/my-app/publicData/posts/post-123'
456
+ * ```
457
+ */
458
+ class PathBuilder {
459
+ constructor(appId) {
460
+ this.appId = appId;
461
+ this.segments = ['appData'];
462
+ validateSegment('appId', appId);
463
+ this.segments.push(appId);
464
+ }
465
+ /**
466
+ * Add publicRead collection to path
467
+ */
468
+ publicRead(collectionName) {
469
+ validateSegment('collectionName', collectionName);
470
+ this.segments.push('publicRead', collectionName);
471
+ return this;
472
+ }
473
+ /**
474
+ * Add publicData collection to path
475
+ */
476
+ publicData(collectionName) {
477
+ validateSegment('collectionName', collectionName);
478
+ this.segments.push('publicData', collectionName);
479
+ return this;
480
+ }
481
+ /**
482
+ * Add userData collection to path
483
+ */
484
+ userData(userId, collectionName) {
485
+ validateSegment('userId', userId);
486
+ validateSegment('collectionName', collectionName);
487
+ this.segments.push('userData', userId, collectionName);
488
+ return this;
489
+ }
490
+ /**
491
+ * Add document ID to path
492
+ */
493
+ doc(docId) {
494
+ validateSegment('docId', docId);
495
+ this.segments.push(docId);
496
+ return this;
497
+ }
498
+ /**
499
+ * Build the final path string
500
+ */
501
+ build() {
502
+ return this.segments.join('/');
503
+ }
504
+ }
505
+ /**
506
+ * Common path patterns as constants for frequently used paths
507
+ */
508
+ const PATH_PATTERNS = {
509
+ /**
510
+ * Base pattern for all SeaVerse data
511
+ */
512
+ APP_DATA: 'appData',
513
+ /**
514
+ * Permission layers
515
+ */
516
+ PUBLIC_READ: 'publicRead',
517
+ PUBLIC_DATA: 'publicData',
518
+ USER_DATA: 'userData',
519
+ };
520
+
521
+ /**
522
+ * Firestore Helper - LLM-Friendly Firestore Operations
523
+ *
524
+ * This helper class automatically handles required fields (_appId, _createdAt, _createdBy)
525
+ * so LLM doesn't need to remember them. It provides a simple, high-level API.
526
+ *
527
+ * 🎯 LLM-FIRST DESIGN:
528
+ * - No need to remember required fields
529
+ * - No need to construct paths manually
530
+ * - No need to import serverTimestamp
531
+ * - One method does everything
532
+ */
533
+ /**
534
+ * Firestore operations helper with automatic metadata injection
535
+ *
536
+ * This class wraps Firestore operations and automatically adds required fields:
537
+ * - _appId: Application ID (for data isolation)
538
+ * - _createdAt: Server timestamp (for creation time)
539
+ * - _createdBy: User ID (for ownership tracking)
540
+ *
541
+ * @example
542
+ * ```typescript
543
+ * const helper = new FirestoreHelper(db, appId, userId);
544
+ *
545
+ * // ✅ LLM-friendly: Just pass your data, required fields auto-injected
546
+ * await helper.addToPublicData('posts', {
547
+ * title: 'My Post',
548
+ * content: 'Hello world'
549
+ * });
550
+ *
551
+ * // ❌ OLD WAY: LLM needs to remember 3 required fields
552
+ * await addDoc(collection(db, path), {
553
+ * _appId: appId,
554
+ * _createdAt: serverTimestamp(),
555
+ * _createdBy: userId,
556
+ * title: 'My Post',
557
+ * content: 'Hello world'
558
+ * });
559
+ * ```
560
+ */
561
+ class FirestoreHelper {
562
+ constructor(db, appId, userId) {
563
+ this.db = db;
564
+ this.appId = appId;
565
+ this.userId = userId;
566
+ }
567
+ /**
568
+ * Add document to publicData (shared, read/write for all users)
569
+ *
570
+ * Automatically injects: _appId, _createdAt, _createdBy
571
+ *
572
+ * @param collectionName - Collection name (e.g., 'posts', 'comments')
573
+ * @param data - Your business data
574
+ * @returns Document reference
575
+ *
576
+ * @example
577
+ * ```typescript
578
+ * // ✅ LLM-friendly: Simple and clean
579
+ * const docRef = await helper.addToPublicData('posts', {
580
+ * title: 'My First Post',
581
+ * content: 'Hello world'
582
+ * });
583
+ * console.log('Created post:', docRef.id);
584
+ * ```
585
+ */
586
+ async addToPublicData(collectionName, data) {
587
+ const path = getPublicDataPath(this.appId, collectionName);
588
+ return this.addDocWithMeta(path, data);
589
+ }
590
+ /**
591
+ * Add document to userData (private, only owner can read/write)
592
+ *
593
+ * Automatically injects: _appId, _createdAt, _createdBy
594
+ *
595
+ * @param collectionName - Collection name (e.g., 'notes', 'settings')
596
+ * @param data - Your business data
597
+ * @returns Document reference
598
+ *
599
+ * @example
600
+ * ```typescript
601
+ * // ✅ LLM-friendly: Private data, auto-isolated
602
+ * await helper.addToUserData('notes', {
603
+ * title: 'Private Note',
604
+ * content: 'Only I can see this'
605
+ * });
606
+ * ```
607
+ */
608
+ async addToUserData(collectionName, data) {
609
+ const path = getUserDataPath(this.appId, this.userId, collectionName);
610
+ return this.addDocWithMeta(path, data);
611
+ }
612
+ /**
613
+ * Get all documents from publicData collection
614
+ *
615
+ * @param collectionName - Collection name
616
+ * @returns QuerySnapshot with documents
617
+ *
618
+ * @example
619
+ * ```typescript
620
+ * const snapshot = await helper.getPublicData('posts');
621
+ * snapshot.forEach(doc => {
622
+ * console.log(doc.id, doc.data());
623
+ * });
624
+ * ```
625
+ */
626
+ async getPublicData(collectionName) {
627
+ const path = getPublicDataPath(this.appId, collectionName);
628
+ return this.getDocs(path);
629
+ }
630
+ /**
631
+ * Get all documents from userData collection (user's private data)
632
+ *
633
+ * @param collectionName - Collection name
634
+ * @returns QuerySnapshot with documents
635
+ *
636
+ * @example
637
+ * ```typescript
638
+ * const snapshot = await helper.getUserData('notes');
639
+ * snapshot.forEach(doc => {
640
+ * console.log('My note:', doc.data());
641
+ * });
642
+ * ```
643
+ */
644
+ async getUserData(collectionName) {
645
+ const path = getUserDataPath(this.appId, this.userId, collectionName);
646
+ return this.getDocs(path);
647
+ }
648
+ /**
649
+ * Get all documents from publicRead collection (read-only for users)
650
+ *
651
+ * @param collectionName - Collection name
652
+ * @returns QuerySnapshot with documents
653
+ *
654
+ * @example
655
+ * ```typescript
656
+ * const configs = await helper.getPublicRead('config');
657
+ * ```
658
+ */
659
+ async getPublicRead(collectionName) {
660
+ const path = getPublicReadPath(this.appId, collectionName);
661
+ return this.getDocs(path);
662
+ }
663
+ /**
664
+ * Get collection reference for publicData
665
+ * Use this for advanced queries with where(), orderBy(), limit()
666
+ *
667
+ * @param collectionName - Collection name
668
+ * @returns Collection reference
669
+ *
670
+ * @example
671
+ * ```typescript
672
+ * import { query, where, orderBy } from 'firebase/firestore';
673
+ *
674
+ * const postsRef = helper.publicDataCollection('posts');
675
+ * const q = query(
676
+ * postsRef,
677
+ * where('_createdBy', '==', userId),
678
+ * orderBy('_createdAt', 'desc')
679
+ * );
680
+ * const snapshot = await getDocs(q);
681
+ * ```
682
+ */
683
+ publicDataCollection(collectionName) {
684
+ const path = getPublicDataPath(this.appId, collectionName);
685
+ return this.getCollection(path);
686
+ }
687
+ /**
688
+ * Get collection reference for userData
689
+ *
690
+ * @param collectionName - Collection name
691
+ * @returns Collection reference
692
+ */
693
+ userDataCollection(collectionName) {
694
+ const path = getUserDataPath(this.appId, this.userId, collectionName);
695
+ return this.getCollection(path);
696
+ }
697
+ /**
698
+ * Get collection reference for publicRead
699
+ *
700
+ * @param collectionName - Collection name
701
+ * @returns Collection reference
702
+ */
703
+ publicReadCollection(collectionName) {
704
+ const path = getPublicReadPath(this.appId, collectionName);
705
+ return this.getCollection(path);
706
+ }
707
+ /**
708
+ * Update document with automatic metadata update
709
+ *
710
+ * Automatically sets: _updatedAt, _updatedBy
711
+ *
712
+ * @param collectionPath - Full collection path
713
+ * @param docId - Document ID
714
+ * @param data - Data to update
715
+ *
716
+ * @example
717
+ * ```typescript
718
+ * await helper.updateDoc(
719
+ * getPublicDataPath(appId, 'posts'),
720
+ * 'post-123',
721
+ * { title: 'Updated Title' }
722
+ * );
723
+ * ```
724
+ */
725
+ async updateDoc(collectionPath, docId, data) {
726
+ const { updateDoc, doc, serverTimestamp } = await this.loadFirestore();
727
+ const docRef = doc(this.db, collectionPath, docId);
728
+ return updateDoc(docRef, {
729
+ ...data,
730
+ _updatedAt: serverTimestamp(),
731
+ _updatedBy: this.userId
732
+ });
733
+ }
734
+ /**
735
+ * Delete document
736
+ *
737
+ * @param collectionPath - Full collection path
738
+ * @param docId - Document ID
739
+ */
740
+ async deleteDoc(collectionPath, docId) {
741
+ const { deleteDoc, doc } = await this.loadFirestore();
742
+ const docRef = doc(this.db, collectionPath, docId);
743
+ return deleteDoc(docRef);
744
+ }
745
+ /**
746
+ * Get single document by ID
747
+ *
748
+ * @param collectionPath - Full collection path
749
+ * @param docId - Document ID
750
+ *
751
+ * @example
752
+ * ```typescript
753
+ * const docSnap = await helper.getDoc(
754
+ * getPublicDataPath(appId, 'posts'),
755
+ * 'post-123'
756
+ * );
757
+ * if (docSnap.exists()) {
758
+ * console.log(docSnap.data());
759
+ * }
760
+ * ```
761
+ */
762
+ async getDoc(collectionPath, docId) {
763
+ const { getDoc, doc } = await this.loadFirestore();
764
+ const docRef = doc(this.db, collectionPath, docId);
765
+ return getDoc(docRef);
766
+ }
767
+ // ============================================================================
768
+ // Private Helper Methods
769
+ // ============================================================================
770
+ /**
771
+ * Internal: Add document with metadata injection
772
+ */
773
+ async addDocWithMeta(collectionPath, data) {
774
+ const { addDoc, collection, serverTimestamp } = await this.loadFirestore();
775
+ const colRef = collection(this.db, collectionPath);
776
+ return addDoc(colRef, {
777
+ _appId: this.appId,
778
+ _createdAt: serverTimestamp(),
779
+ _createdBy: this.userId,
780
+ ...data
781
+ });
782
+ }
783
+ /**
784
+ * Internal: Get all documents from collection
785
+ */
786
+ async getDocs(collectionPath) {
787
+ const { getDocs, collection } = await this.loadFirestore();
788
+ const colRef = collection(this.db, collectionPath);
789
+ return getDocs(colRef);
790
+ }
791
+ /**
792
+ * Internal: Get collection reference
793
+ */
794
+ getCollection(collectionPath) {
795
+ // Note: This is sync, so we can't use dynamic import
796
+ // We assume firebase/firestore is already loaded by initializeWithToken
797
+ const { collection } = require('firebase/firestore');
798
+ return collection(this.db, collectionPath);
799
+ }
800
+ /**
801
+ * Internal: Lazy load Firebase Firestore functions
802
+ */
803
+ async loadFirestore() {
804
+ const firestore = await import('firebase/firestore');
805
+ return {
806
+ collection: firestore.collection,
807
+ doc: firestore.doc,
808
+ addDoc: firestore.addDoc,
809
+ setDoc: firestore.setDoc,
810
+ getDoc: firestore.getDoc,
811
+ getDocs: firestore.getDocs,
812
+ updateDoc: firestore.updateDoc,
813
+ deleteDoc: firestore.deleteDoc,
814
+ serverTimestamp: firestore.serverTimestamp,
815
+ query: firestore.query,
816
+ where: firestore.where,
817
+ orderBy: firestore.orderBy,
818
+ limit: firestore.limit
819
+ };
820
+ }
821
+ }
822
+ /**
823
+ * Standalone helper function: Add document with automatic metadata injection
824
+ *
825
+ * Use this if you don't want to create a FirestoreHelper instance.
826
+ *
827
+ * @param db - Firestore instance
828
+ * @param collectionPath - Full collection path
829
+ * @param appId - Application ID
830
+ * @param userId - User ID
831
+ * @param data - Your business data
832
+ * @returns Document reference
833
+ *
834
+ * @example
835
+ * ```typescript
836
+ * import { addDocWithMeta, getPublicDataPath } from '@seaverse/data-service-sdk';
837
+ *
838
+ * const docRef = await addDocWithMeta(
839
+ * db,
840
+ * getPublicDataPath(appId, 'posts'),
841
+ * appId,
842
+ * userId,
843
+ * { title: 'Post', content: 'Hello' }
844
+ * );
845
+ * ```
846
+ */
847
+ async function addDocWithMeta(db, collectionPath, appId, userId, data) {
848
+ const { addDoc, collection, serverTimestamp } = await import('firebase/firestore');
849
+ const colRef = collection(db, collectionPath);
850
+ return addDoc(colRef, {
851
+ _appId: appId,
852
+ _createdAt: serverTimestamp(),
853
+ _createdBy: userId,
854
+ ...data
855
+ });
856
+ }
857
+ /**
858
+ * Standalone helper function: Update document with automatic metadata
859
+ *
860
+ * @param db - Firestore instance
861
+ * @param collectionPath - Full collection path
862
+ * @param docId - Document ID
863
+ * @param userId - User ID (for _updatedBy field)
864
+ * @param data - Data to update
865
+ *
866
+ * @example
867
+ * ```typescript
868
+ * import { updateDocWithMeta, getPublicDataPath } from '@seaverse/data-service-sdk';
869
+ *
870
+ * await updateDocWithMeta(
871
+ * db,
872
+ * getPublicDataPath(appId, 'posts'),
873
+ * 'post-123',
874
+ * userId,
875
+ * { title: 'Updated Title' }
876
+ * );
877
+ * ```
878
+ */
879
+ async function updateDocWithMeta(db, collectionPath, docId, userId, data) {
880
+ const { updateDoc, doc, serverTimestamp } = await import('firebase/firestore');
881
+ const docRef = doc(db, collectionPath, docId);
882
+ return updateDoc(docRef, {
883
+ ...data,
884
+ _updatedAt: serverTimestamp(),
885
+ _updatedBy: userId
886
+ });
887
+ }
888
+
266
889
  /**
267
890
  * Create Firebase configuration from Firestore token response
268
891
  *
@@ -296,23 +919,32 @@ function getFirebaseConfig(tokenResponse) {
296
919
  * 1. Creates Firebase config from token response
297
920
  * 2. Initializes Firebase app
298
921
  * 3. Signs in with the custom token
299
- * 4. Returns authenticated Firebase instances
922
+ * 4. Creates FirestoreHelper for LLM-friendly operations
923
+ * 5. Returns authenticated Firebase instances
300
924
  *
301
925
  * IMPORTANT: This function requires Firebase SDK to be installed separately:
302
926
  * npm install firebase
303
927
  *
928
+ * 🎯 LLM RECOMMENDED: Use the `helper` object for simplified operations
929
+ *
304
930
  * @param tokenResponse - The Firestore token response from SDK
305
- * @returns Object containing initialized Firebase app, auth, and db instances
931
+ * @returns Object containing initialized Firebase instances and LLM-friendly helper
306
932
  *
307
933
  * @example
308
934
  * ```typescript
309
935
  * import { initializeWithToken } from '@seaverse/data-service-sdk';
310
936
  *
311
937
  * const tokenResponse = await client.generateGuestFirestoreToken({ app_id: 'my-app' });
312
- * const { app, auth, db, userId, appId } = await initializeWithToken(tokenResponse);
938
+ * const { db, userId, appId, helper } = await initializeWithToken(tokenResponse);
313
939
  *
314
- * // Ready to use Firestore!
315
- * const snapshot = await getDocs(collection(db, `appData/${appId}/publicData/posts`));
940
+ * // LLM-FRIENDLY: Use helper (automatic required fields)
941
+ * await helper.addToPublicData('posts', {
942
+ * title: 'My Post',
943
+ * content: 'Hello'
944
+ * });
945
+ *
946
+ * // ❌ OLD WAY: Manual (need to remember required fields)
947
+ * // await addDoc(collection(db, path), { _appId, _createdAt, _createdBy, ...data });
316
948
  * ```
317
949
  */
318
950
  async function initializeWithToken(tokenResponse) {
@@ -344,14 +976,19 @@ async function initializeWithToken(tokenResponse) {
344
976
  // Get Firestore instance with correct database ID
345
977
  // IMPORTANT: Must specify database_id, not just use default!
346
978
  const db = getFirestore(app, tokenResponse.database_id);
979
+ const userId = tokenResponse.user_id;
980
+ const appId = tokenResponse.app_id || '';
981
+ // Create LLM-friendly helper
982
+ const helper = new FirestoreHelper(db, appId, userId);
347
983
  return {
348
984
  app,
349
985
  auth,
350
986
  db,
351
- userId: tokenResponse.user_id,
352
- appId: tokenResponse.app_id || '',
987
+ userId,
988
+ appId,
989
+ helper,
353
990
  };
354
991
  }
355
992
 
356
- export { DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DataServiceClient, ENDPOINTS, getFirebaseConfig, initializeWithToken };
993
+ export { DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DataServiceClient, ENDPOINTS, FirestoreHelper, PATH_PATTERNS, PathBuilder, addDocWithMeta, getFirebaseConfig, getPublicDataDocPath, getPublicDataPath, getPublicReadDocPath, getPublicReadPath, getUserDataDocPath, getUserDataPath, initializeWithToken, updateDocWithMeta };
357
994
  //# sourceMappingURL=index.js.map