@seaverse/data-service-sdk 0.9.0 → 0.10.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.
@@ -4433,6 +4433,260 @@
4433
4433
  USER_DATA: 'userData',
4434
4434
  };
4435
4435
 
4436
+ /**
4437
+ * Validation utilities for Firestore data
4438
+ *
4439
+ * These validators help ensure data follows SeaVerse Firestore security rules.
4440
+ * They provide client-side validation before sending data to Firestore.
4441
+ *
4442
+ * 🚨 IMPORTANT: These are CLIENT-SIDE validations only!
4443
+ * The actual security enforcement happens in Firestore Security Rules.
4444
+ * These validators help catch errors early for better DX.
4445
+ */
4446
+ /**
4447
+ * Maximum document size in bytes (256 KB)
4448
+ * This matches the Firestore security rule limit
4449
+ */
4450
+ const MAX_DOCUMENT_SIZE = 262144; // 256 KB
4451
+ /**
4452
+ * System reserved field names that users cannot create
4453
+ *
4454
+ * These fields are managed by the system and cannot be set by users:
4455
+ * - _appId: Application ID (auto-injected)
4456
+ * - _createdBy: Creator user ID (auto-injected)
4457
+ * - _createdAt: Creation timestamp (auto-injected)
4458
+ * - _updatedAt: Last update timestamp (auto-managed)
4459
+ * - _deleted: Soft delete flag (auto-managed)
4460
+ * - _deletedAt: Deletion timestamp (auto-managed)
4461
+ */
4462
+ const ALLOWED_RESERVED_FIELDS = [
4463
+ '_appId',
4464
+ '_createdBy',
4465
+ '_createdAt',
4466
+ '_updatedAt',
4467
+ '_deleted',
4468
+ '_deletedAt',
4469
+ '_updatedBy'
4470
+ ];
4471
+ /**
4472
+ * Common illegal reserved field patterns
4473
+ * Based on Firestore security rules blacklist
4474
+ */
4475
+ const ILLEGAL_RESERVED_FIELDS = [
4476
+ // Single letter prefixes
4477
+ '_a', '_b', '_c', '_d', '_e', '_f', '_g', '_h', '_i', '_j', '_k', '_l', '_m',
4478
+ '_n', '_o', '_p', '_q', '_r', '_s', '_t', '_u', '_v', '_w', '_x', '_y', '_z',
4479
+ '_A', '_B', '_C', '_D', '_E', '_F', '_G', '_H', '_I', '_J', '_K', '_L', '_M',
4480
+ '_N', '_O', '_P', '_Q', '_R', '_S', '_T', '_U', '_V', '_W', '_X', '_Y', '_Z',
4481
+ // Number prefixes
4482
+ '_0', '_1', '_2', '_3', '_4', '_5', '_6', '_7', '_8', '_9',
4483
+ // Multiple underscores
4484
+ '__', '___', '____',
4485
+ // Permission related
4486
+ '_admin', '_user', '_role', '_permission', '_access', '_auth', '_owner', '_public',
4487
+ // Metadata related
4488
+ '_custom', '_data', '_meta', '_info', '_config', '_setting', '_value', '_key',
4489
+ '_id', '_ID', '_ref', '_timestamp', '_time', '_date', '_status', '_type',
4490
+ // Temporary fields
4491
+ '_temp', '_tmp', '_test', '_new', '_old', '_bak', '_backup', '_copy',
4492
+ // Common business fields
4493
+ '_name', '_title', '_description', '_content', '_body', '_text', '_message',
4494
+ '_email', '_phone', '_address', '_city', '_country', '_zip', '_code',
4495
+ '_price', '_amount', '_quantity', '_total', '_subtotal', '_discount', '_tax',
4496
+ '_image', '_avatar', '_photo', '_picture', '_file', '_url', '_link', '_path',
4497
+ '_user_id', '_userId', '_username', '_nickname', '_displayName',
4498
+ '_password', '_token', '_session', '_apiKey', '_secretKey', '_privateKey',
4499
+ // Flag fields
4500
+ '_flag', '_enabled', '_disabled', '_active', '_inactive', '_visible', '_hidden',
4501
+ '_isAdmin', '_isPublic', '_isPrivate', '_isDeleted', '_isActive', '_isEnabled',
4502
+ // State fields
4503
+ '_state', '_mode', '_level', '_priority', '_order', '_index', '_count', '_number',
4504
+ // System fields
4505
+ '_system', '_internal', '_private', '_protected', '_reserved', '_secret', '_hidden'
4506
+ ];
4507
+ /**
4508
+ * Validate that data doesn't contain illegal reserved fields
4509
+ *
4510
+ * Reserved fields (starting with _) are for system use only.
4511
+ * Users can only use allowed system fields.
4512
+ *
4513
+ * @param data - Data object to validate
4514
+ * @throws Error if illegal reserved fields are found
4515
+ *
4516
+ * @example
4517
+ * ```typescript
4518
+ * // ✅ Valid - no reserved fields
4519
+ * validateReservedFields({ title: 'Post', content: 'Hello' });
4520
+ *
4521
+ * // ✅ Valid - allowed system fields
4522
+ * validateReservedFields({ _appId: 'app-1', _createdBy: 'user-1', title: 'Post' });
4523
+ *
4524
+ * // ❌ Invalid - illegal reserved field
4525
+ * validateReservedFields({ _custom: 'value', title: 'Post' });
4526
+ * // Throws: Error: Illegal reserved field "_custom"
4527
+ * ```
4528
+ */
4529
+ function validateReservedFields(data) {
4530
+ const keys = Object.keys(data);
4531
+ for (const key of keys) {
4532
+ // Skip allowed system fields
4533
+ if (ALLOWED_RESERVED_FIELDS.includes(key)) {
4534
+ continue;
4535
+ }
4536
+ // Check if it's a reserved field (starts with _)
4537
+ if (key.startsWith('_')) {
4538
+ // Check if it's in the blacklist
4539
+ if (ILLEGAL_RESERVED_FIELDS.includes(key)) {
4540
+ throw new Error(`Illegal reserved field "${key}". ` +
4541
+ `Fields starting with "_" are reserved for system use. ` +
4542
+ `Please use a field name without the underscore prefix.`);
4543
+ }
4544
+ // Even if not in blacklist, warn about unknown _ fields
4545
+ throw new Error(`Unknown reserved field "${key}". ` +
4546
+ `Fields starting with "_" are reserved for system use. ` +
4547
+ `Allowed system fields: ${ALLOWED_RESERVED_FIELDS.join(', ')}. ` +
4548
+ `Please use a field name without the underscore prefix.`);
4549
+ }
4550
+ }
4551
+ }
4552
+ /**
4553
+ * Estimate document size in bytes
4554
+ *
4555
+ * This is an approximation based on JSON serialization.
4556
+ * Firestore may calculate size differently, but this gives a good estimate.
4557
+ *
4558
+ * @param data - Data object to measure
4559
+ * @returns Estimated size in bytes
4560
+ *
4561
+ * @example
4562
+ * ```typescript
4563
+ * const data = { title: 'My Post', content: 'Long content...' };
4564
+ * const size = estimateDocumentSize(data);
4565
+ * console.log('Document size:', size, 'bytes');
4566
+ * ```
4567
+ */
4568
+ function estimateDocumentSize(data) {
4569
+ try {
4570
+ const json = JSON.stringify(data);
4571
+ // Use Blob if available (browser), otherwise estimate from string length
4572
+ if (typeof Blob !== 'undefined') {
4573
+ return new Blob([json]).size;
4574
+ }
4575
+ else {
4576
+ // Node.js or environments without Blob: estimate from UTF-8 encoded length
4577
+ return Buffer.byteLength(json, 'utf8');
4578
+ }
4579
+ }
4580
+ catch (error) {
4581
+ // Fallback: rough estimate
4582
+ return JSON.stringify(data).length * 2; // Assume ~2 bytes per char for safety
4583
+ }
4584
+ }
4585
+ /**
4586
+ * Validate document size doesn't exceed limit
4587
+ *
4588
+ * Firestore has a maximum document size of 1MB, but we enforce 256KB
4589
+ * to match our security rules limit.
4590
+ *
4591
+ * @param data - Data object to validate
4592
+ * @throws Error if document is too large
4593
+ *
4594
+ * @example
4595
+ * ```typescript
4596
+ * const data = { title: 'Post', content: 'Some content' };
4597
+ * validateDocumentSize(data); // OK
4598
+ *
4599
+ * const hugeData = { content: 'x'.repeat(300000) };
4600
+ * validateDocumentSize(hugeData); // Throws error
4601
+ * ```
4602
+ */
4603
+ function validateDocumentSize(data) {
4604
+ const size = estimateDocumentSize(data);
4605
+ if (size > MAX_DOCUMENT_SIZE) {
4606
+ throw new Error(`Document size (${size} bytes) exceeds maximum allowed size (${MAX_DOCUMENT_SIZE} bytes / 256 KB). ` +
4607
+ `Please reduce the amount of data you're storing in this document.`);
4608
+ }
4609
+ }
4610
+ /**
4611
+ * Validate data before sending to Firestore
4612
+ *
4613
+ * This runs all validations:
4614
+ * - Reserved fields check
4615
+ * - Document size check
4616
+ *
4617
+ * @param data - Data object to validate
4618
+ * @throws Error if validation fails
4619
+ *
4620
+ * @example
4621
+ * ```typescript
4622
+ * // Use this before adding/updating documents
4623
+ * try {
4624
+ * validateFirestoreData(myData);
4625
+ * await addDoc(collection(db, path), myData);
4626
+ * } catch (error) {
4627
+ * console.error('Validation failed:', error.message);
4628
+ * }
4629
+ * ```
4630
+ */
4631
+ function validateFirestoreData(data) {
4632
+ validateReservedFields(data);
4633
+ validateDocumentSize(data);
4634
+ }
4635
+ /**
4636
+ * Check if data contains soft-delete markers
4637
+ *
4638
+ * @param data - Data object to check
4639
+ * @returns True if document is marked as deleted
4640
+ */
4641
+ function isDeleted(data) {
4642
+ return data._deleted === true;
4643
+ }
4644
+ /**
4645
+ * Validate data and return detailed results instead of throwing
4646
+ *
4647
+ * Use this when you want to handle validation errors gracefully
4648
+ * without try/catch blocks.
4649
+ *
4650
+ * @param data - Data object to validate
4651
+ * @returns Validation result with errors
4652
+ *
4653
+ * @example
4654
+ * ```typescript
4655
+ * const result = validateDataDetailed(myData);
4656
+ * if (!result.valid) {
4657
+ * console.error('Validation errors:', result.errors);
4658
+ * // Show errors to user
4659
+ * } else {
4660
+ * // Proceed with save
4661
+ * }
4662
+ * ```
4663
+ */
4664
+ function validateDataDetailed(data) {
4665
+ const errors = [];
4666
+ // Check reserved fields
4667
+ try {
4668
+ validateReservedFields(data);
4669
+ }
4670
+ catch (error) {
4671
+ if (error instanceof Error) {
4672
+ errors.push(error.message);
4673
+ }
4674
+ }
4675
+ // Check document size
4676
+ try {
4677
+ validateDocumentSize(data);
4678
+ }
4679
+ catch (error) {
4680
+ if (error instanceof Error) {
4681
+ errors.push(error.message);
4682
+ }
4683
+ }
4684
+ return {
4685
+ valid: errors.length === 0,
4686
+ errors
4687
+ };
4688
+ }
4689
+
4436
4690
  /**
4437
4691
  * Firestore Helper - LLM-Friendly Firestore Operations
4438
4692
  *
@@ -4527,25 +4781,36 @@
4527
4781
  /**
4528
4782
  * Get all documents from publicData collection
4529
4783
  *
4784
+ * By default, this returns only non-deleted documents.
4785
+ * Set includeDeleted=true to include soft-deleted documents.
4786
+ *
4530
4787
  * @param collectionName - Collection name
4788
+ * @param includeDeleted - Include soft-deleted documents (default: false)
4531
4789
  * @returns QuerySnapshot with documents
4532
4790
  *
4533
4791
  * @example
4534
4792
  * ```typescript
4793
+ * // Get only active posts (not deleted)
4535
4794
  * const snapshot = await helper.getPublicData('posts');
4536
4795
  * snapshot.forEach(doc => {
4537
4796
  * console.log(doc.id, doc.data());
4538
4797
  * });
4798
+ *
4799
+ * // Include deleted posts (admin use case)
4800
+ * const allPosts = await helper.getPublicData('posts', true);
4539
4801
  * ```
4540
4802
  */
4541
- async getPublicData(collectionName) {
4803
+ async getPublicData(collectionName, includeDeleted = false) {
4542
4804
  const path = getPublicDataPath(this.appId, collectionName);
4543
- return this.getDocs(path);
4805
+ return this.getDocs(path, includeDeleted);
4544
4806
  }
4545
4807
  /**
4546
4808
  * Get all documents from userData collection (user's private data)
4547
4809
  *
4810
+ * By default, this returns only non-deleted documents.
4811
+ *
4548
4812
  * @param collectionName - Collection name
4813
+ * @param includeDeleted - Include soft-deleted documents (default: false)
4549
4814
  * @returns QuerySnapshot with documents
4550
4815
  *
4551
4816
  * @example
@@ -4556,9 +4821,9 @@
4556
4821
  * });
4557
4822
  * ```
4558
4823
  */
4559
- async getUserData(collectionName) {
4824
+ async getUserData(collectionName, includeDeleted = false) {
4560
4825
  const path = getUserDataPath(this.appId, this.userId, collectionName);
4561
- return this.getDocs(path);
4826
+ return this.getDocs(path, includeDeleted);
4562
4827
  }
4563
4828
  /**
4564
4829
  * Get all documents from publicRead collection (read-only for users)
@@ -4638,6 +4903,8 @@
4638
4903
  * ```
4639
4904
  */
4640
4905
  async updateDoc(collectionPath, docId, data) {
4906
+ // Validate user data
4907
+ validateFirestoreData(data);
4641
4908
  const { updateDoc, doc, serverTimestamp } = await this.loadFirestore();
4642
4909
  const docRef = doc(this.db, collectionPath, docId);
4643
4910
  return updateDoc(docRef, {
@@ -4647,10 +4914,50 @@
4647
4914
  });
4648
4915
  }
4649
4916
  /**
4650
- * Delete document
4917
+ * Soft delete document (mark as deleted without removing)
4918
+ *
4919
+ * This is the RECOMMENDED way to delete documents. It marks the document
4920
+ * as deleted without actually removing it from the database.
4921
+ *
4922
+ * Automatically sets: _deleted = true, _deletedAt = serverTimestamp()
4923
+ *
4924
+ * @param collectionPath - Full collection path
4925
+ * @param docId - Document ID
4926
+ *
4927
+ * @example
4928
+ * ```typescript
4929
+ * // Soft delete a post (recommended)
4930
+ * await helper.softDeleteDoc(
4931
+ * getPublicDataPath(appId, 'posts'),
4932
+ * 'post-123'
4933
+ * );
4934
+ * ```
4935
+ */
4936
+ async softDeleteDoc(collectionPath, docId) {
4937
+ const { updateDoc, doc, serverTimestamp } = await this.loadFirestore();
4938
+ const docRef = doc(this.db, collectionPath, docId);
4939
+ return updateDoc(docRef, {
4940
+ _deleted: true,
4941
+ _deletedAt: serverTimestamp()
4942
+ });
4943
+ }
4944
+ /**
4945
+ * Hard delete document (permanently remove from database)
4946
+ *
4947
+ * ⚠️ WARNING: This permanently removes the document.
4948
+ * Only admins can hard delete. Regular users should use softDeleteDoc().
4651
4949
  *
4652
4950
  * @param collectionPath - Full collection path
4653
4951
  * @param docId - Document ID
4952
+ *
4953
+ * @example
4954
+ * ```typescript
4955
+ * // Hard delete (admin only)
4956
+ * await helper.deleteDoc(
4957
+ * getPublicDataPath(appId, 'posts'),
4958
+ * 'post-123'
4959
+ * );
4960
+ * ```
4654
4961
  */
4655
4962
  async deleteDoc(collectionPath, docId) {
4656
4963
  const { deleteDoc, doc } = await this.loadFirestore();
@@ -4686,22 +4993,34 @@
4686
4993
  * Internal: Add document with metadata injection
4687
4994
  */
4688
4995
  async addDocWithMeta(collectionPath, data) {
4996
+ // Validate user data before adding system fields
4997
+ validateFirestoreData(data);
4689
4998
  const { addDoc, collection, serverTimestamp } = await this.loadFirestore();
4690
4999
  const colRef = collection(this.db, collectionPath);
4691
- return addDoc(colRef, {
5000
+ const docData = {
4692
5001
  _appId: this.appId,
4693
5002
  _createdAt: serverTimestamp(),
4694
5003
  _createdBy: this.userId,
4695
5004
  ...data
4696
- });
5005
+ };
5006
+ return addDoc(colRef, docData);
4697
5007
  }
4698
5008
  /**
4699
5009
  * Internal: Get all documents from collection
5010
+ * Optionally filter out soft-deleted documents
4700
5011
  */
4701
- async getDocs(collectionPath) {
4702
- const { getDocs, collection } = await this.loadFirestore();
5012
+ async getDocs(collectionPath, includeDeleted = false) {
5013
+ const { getDocs, collection, query, where } = await this.loadFirestore();
4703
5014
  const colRef = collection(this.db, collectionPath);
4704
- return getDocs(colRef);
5015
+ if (includeDeleted) {
5016
+ // Return all documents (including soft-deleted)
5017
+ return getDocs(colRef);
5018
+ }
5019
+ else {
5020
+ // Filter out soft-deleted documents
5021
+ const q = query(colRef, where('_deleted', '==', false));
5022
+ return getDocs(q);
5023
+ }
4705
5024
  }
4706
5025
  /**
4707
5026
  * Internal: Get collection reference
@@ -5013,14 +5332,17 @@
5013
5332
  enumerable: true,
5014
5333
  get: function () { return firestore.writeBatch; }
5015
5334
  });
5335
+ exports.ALLOWED_RESERVED_FIELDS = ALLOWED_RESERVED_FIELDS;
5016
5336
  exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
5017
5337
  exports.DEFAULT_TIMEOUT = DEFAULT_TIMEOUT;
5018
5338
  exports.DataServiceClient = DataServiceClient;
5019
5339
  exports.ENDPOINTS = ENDPOINTS;
5020
5340
  exports.FirestoreHelper = FirestoreHelper;
5341
+ exports.MAX_DOCUMENT_SIZE = MAX_DOCUMENT_SIZE;
5021
5342
  exports.PATH_PATTERNS = PATH_PATTERNS;
5022
5343
  exports.PathBuilder = PathBuilder;
5023
5344
  exports.addDocWithMeta = addDocWithMeta;
5345
+ exports.estimateDocumentSize = estimateDocumentSize;
5024
5346
  exports.getFirebaseConfig = getFirebaseConfig;
5025
5347
  exports.getPublicDataDocPath = getPublicDataDocPath;
5026
5348
  exports.getPublicDataPath = getPublicDataPath;
@@ -5029,7 +5351,12 @@
5029
5351
  exports.getUserDataDocPath = getUserDataDocPath;
5030
5352
  exports.getUserDataPath = getUserDataPath;
5031
5353
  exports.initializeWithToken = initializeWithToken;
5354
+ exports.isDeleted = isDeleted;
5032
5355
  exports.updateDocWithMeta = updateDocWithMeta;
5356
+ exports.validateDataDetailed = validateDataDetailed;
5357
+ exports.validateDocumentSize = validateDocumentSize;
5358
+ exports.validateFirestoreData = validateFirestoreData;
5359
+ exports.validateReservedFields = validateReservedFields;
5033
5360
 
5034
5361
  }));
5035
5362
  //# sourceMappingURL=browser.umd.js.map