@seaverse/data-service-sdk 0.8.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.
- package/README.md +142 -27
- package/dist/browser.js +330 -11
- package/dist/browser.js.map +1 -1
- package/dist/browser.umd.js +337 -10
- package/dist/browser.umd.js.map +1 -1
- package/dist/index.cjs +337 -10
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +194 -5
- package/dist/index.js +330 -11
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,17 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
SeaVerse Data Service SDK for accessing Firestore with secure token management and three-tier permission model.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
---
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
## 🤖 **For LLM: START HERE!**
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
- ✅ Required fields (`_appId`, `_createdAt`, `_createdBy`)
|
|
11
|
-
- ✅ Correct Firestore paths
|
|
12
|
-
- ✅ Server timestamps
|
|
13
|
-
- ✅ Single import source
|
|
9
|
+
### 🚀 5-Minute Quick Start
|
|
14
10
|
|
|
15
|
-
|
|
11
|
+
**👉 Simplest Way**: Check **[LLM-FIRST.md](./LLM-FIRST.md)** - Complete HTML page examples (copy & paste ready)
|
|
12
|
+
|
|
13
|
+
**⚡ Core API Quick Reference** (Only 4 methods to remember):
|
|
14
|
+
|
|
15
|
+
| What You Want to Do | Use Which API | Code Example |
|
|
16
|
+
|---------------------|---------------|--------------|
|
|
17
|
+
| **Write Public Data** | `helper.addToPublicData()` | `await helper.addToPublicData('posts', {title: 'Hello'})` |
|
|
18
|
+
| **Read Public Data** | `helper.getPublicData()` | `const posts = await helper.getPublicData('posts')` |
|
|
19
|
+
| **Write Private Data** | `helper.addToUserData()` | `await helper.addToUserData('notes', {text: 'Secret'})` |
|
|
20
|
+
| **Read Private Data** | `helper.getUserData()` | `const notes = await helper.getUserData('notes')` |
|
|
21
|
+
|
|
22
|
+
**✨ Key Benefits**:
|
|
23
|
+
- ✅ `helper` automatically handles required fields (`_appId`, `_createdAt`, `_createdBy`)
|
|
24
|
+
- ✅ Automatically uses correct Firestore paths
|
|
25
|
+
- ✅ Automatically uses server timestamps
|
|
26
|
+
- ✅ Single import source (no need to import from multiple packages)
|
|
27
|
+
|
|
28
|
+
**🎯 Quick Links**:
|
|
29
|
+
- **[LLM-FIRST.md](./LLM-FIRST.md)** - Complete HTML examples (todo list, message board)
|
|
30
|
+
- **[LLM-QUICK-START.md](./LLM-QUICK-START.md)** - Code snippets and common mistakes
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 📖 For Developers
|
|
16
35
|
|
|
17
36
|
## Features
|
|
18
37
|
|
|
@@ -23,6 +42,8 @@ The Quick Start guide shows you the **recommended LLM-friendly API** that automa
|
|
|
23
42
|
- 📝 TypeScript support with full type definitions
|
|
24
43
|
- 🤖 LLM-friendly documentation with clear examples
|
|
25
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)
|
|
26
47
|
|
|
27
48
|
## Three-Tier Permission Model
|
|
28
49
|
|
|
@@ -94,6 +115,82 @@ All Firestore documents MUST include these three fields:
|
|
|
94
115
|
|
|
95
116
|
These fields are enforced by Firestore Security Rules and ensure proper data isolation.
|
|
96
117
|
|
|
118
|
+
**🎯 Good News for LLM**: When using `FirestoreHelper`, these fields are automatically injected - you don't need to remember them!
|
|
119
|
+
|
|
120
|
+
### Reserved Fields & Validation
|
|
121
|
+
|
|
122
|
+
**⚠️ IMPORTANT: Fields starting with `_` are reserved for system use!**
|
|
123
|
+
|
|
124
|
+
The SDK automatically validates your data to prevent common mistakes:
|
|
125
|
+
|
|
126
|
+
1. **Reserved Fields**: You cannot create custom fields starting with `_`
|
|
127
|
+
```typescript
|
|
128
|
+
// ❌ WRONG - Will throw error
|
|
129
|
+
await helper.addToPublicData('posts', {
|
|
130
|
+
_custom: 'value', // Reserved field!
|
|
131
|
+
title: 'My Post'
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ✅ CORRECT
|
|
135
|
+
await helper.addToPublicData('posts', {
|
|
136
|
+
customField: 'value', // No underscore prefix
|
|
137
|
+
title: 'My Post'
|
|
138
|
+
});
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
2. **Document Size**: Documents are limited to 256 KB
|
|
142
|
+
```typescript
|
|
143
|
+
// SDK will automatically check size and throw error if too large
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
3. **Automatic Validation**: All `FirestoreHelper` methods validate data automatically
|
|
147
|
+
```typescript
|
|
148
|
+
// Manual validation if needed
|
|
149
|
+
import { validateFirestoreData, validateDataDetailed } from '@seaverse/data-service-sdk';
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
validateFirestoreData(myData);
|
|
153
|
+
// Data is valid
|
|
154
|
+
} catch (error) {
|
|
155
|
+
console.error('Validation failed:', error.message);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Or get detailed errors without throwing
|
|
159
|
+
const result = validateDataDetailed(myData);
|
|
160
|
+
if (!result.valid) {
|
|
161
|
+
console.error('Errors:', result.errors);
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Soft Delete Support
|
|
166
|
+
|
|
167
|
+
**🗑️ Recommended Practice: Use soft delete instead of hard delete**
|
|
168
|
+
|
|
169
|
+
Soft delete marks documents as deleted without removing them from the database:
|
|
170
|
+
|
|
171
|
+
```typescript
|
|
172
|
+
// ✅ RECOMMENDED: Soft delete (mark as deleted)
|
|
173
|
+
await helper.softDeleteDoc(
|
|
174
|
+
getPublicDataPath(appId, 'posts'),
|
|
175
|
+
'post-123'
|
|
176
|
+
);
|
|
177
|
+
|
|
178
|
+
// Document is still in database but marked as _deleted = true
|
|
179
|
+
// By default, helper.getPublicData() won't return deleted documents
|
|
180
|
+
|
|
181
|
+
// ❌ Hard delete (only for admins, permanent)
|
|
182
|
+
await helper.deleteDoc(
|
|
183
|
+
getPublicDataPath(appId, 'posts'),
|
|
184
|
+
'post-123'
|
|
185
|
+
);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
**Why soft delete?**
|
|
189
|
+
- ✅ Data recovery possible
|
|
190
|
+
- ✅ Audit trail preserved
|
|
191
|
+
- ✅ Safer for production
|
|
192
|
+
- ✅ Follows industry best practices
|
|
193
|
+
|
|
97
194
|
## Installation
|
|
98
195
|
|
|
99
196
|
```bash
|
|
@@ -697,7 +794,20 @@ import type {
|
|
|
697
794
|
|
|
698
795
|
When using this SDK with LLM-generated code:
|
|
699
796
|
|
|
700
|
-
1.
|
|
797
|
+
1. **🎯 EASIEST: Use `FirestoreHelper` for everything!**
|
|
798
|
+
```typescript
|
|
799
|
+
// ✅ BEST PRACTICE - Let helper handle everything
|
|
800
|
+
const { helper } = await initializeWithToken(tokenResponse);
|
|
801
|
+
await helper.addToPublicData('posts', { title: 'Post', content: 'Hello' });
|
|
802
|
+
|
|
803
|
+
// No need to remember:
|
|
804
|
+
// - Required fields (_appId, _createdAt, _createdBy)
|
|
805
|
+
// - Path construction
|
|
806
|
+
// - Data validation
|
|
807
|
+
// - Soft delete logic
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
2. **🛡️ Use path helper functions to avoid permission-denied errors:**
|
|
701
811
|
```typescript
|
|
702
812
|
import { getPublicDataPath, getUserDataPath } from '@seaverse/data-service-sdk';
|
|
703
813
|
|
|
@@ -709,35 +819,40 @@ When using this SDK with LLM-generated code:
|
|
|
709
819
|
await addDoc(collection(db, `apps/${appId}/posts`), { ... });
|
|
710
820
|
```
|
|
711
821
|
|
|
712
|
-
|
|
822
|
+
3. **⚠️ Never use field names starting with `_`:**
|
|
713
823
|
```typescript
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
}
|
|
824
|
+
// ❌ WRONG - Reserved field
|
|
825
|
+
{ _myField: 'value' }
|
|
826
|
+
|
|
827
|
+
// ✅ CORRECT
|
|
828
|
+
{ myField: 'value' }
|
|
719
829
|
```
|
|
720
830
|
|
|
721
|
-
|
|
831
|
+
4. **🗑️ Use soft delete instead of hard delete:**
|
|
722
832
|
```typescript
|
|
723
|
-
//
|
|
724
|
-
|
|
833
|
+
// ✅ RECOMMENDED
|
|
834
|
+
await helper.softDeleteDoc(path, docId);
|
|
725
835
|
|
|
726
|
-
//
|
|
727
|
-
|
|
836
|
+
// ❌ Only for admins
|
|
837
|
+
await helper.deleteDoc(path, docId);
|
|
838
|
+
```
|
|
728
839
|
|
|
729
|
-
|
|
730
|
-
|
|
840
|
+
5. **✅ Validation is automatic, but you can validate manually if needed:**
|
|
841
|
+
```typescript
|
|
842
|
+
import { validateFirestoreData } from '@seaverse/data-service-sdk';
|
|
731
843
|
|
|
732
|
-
|
|
733
|
-
|
|
844
|
+
try {
|
|
845
|
+
validateFirestoreData(myData);
|
|
846
|
+
} catch (error) {
|
|
847
|
+
console.error('Invalid data:', error.message);
|
|
848
|
+
}
|
|
734
849
|
```
|
|
735
850
|
|
|
736
|
-
|
|
851
|
+
6. **Handle token expiration:**
|
|
737
852
|
- Tokens expire after 1 hour (3600 seconds)
|
|
738
853
|
- Check `expires_in` field and refresh when needed
|
|
739
854
|
|
|
740
|
-
|
|
855
|
+
7. **Use serverTimestamp() for timestamps:**
|
|
741
856
|
```typescript
|
|
742
857
|
import { serverTimestamp } from 'firebase/firestore';
|
|
743
858
|
|
|
@@ -746,7 +861,7 @@ When using this SDK with LLM-generated code:
|
|
|
746
861
|
}
|
|
747
862
|
```
|
|
748
863
|
|
|
749
|
-
|
|
864
|
+
8. **Separate guest and authenticated flows:**
|
|
750
865
|
- Use `generateGuestFirestoreToken()` for anonymous users
|
|
751
866
|
- Use `generateFirestoreToken()` for logged-in users
|
|
752
867
|
|
package/dist/browser.js
CHANGED
|
@@ -4429,6 +4429,260 @@ const PATH_PATTERNS = {
|
|
|
4429
4429
|
USER_DATA: 'userData',
|
|
4430
4430
|
};
|
|
4431
4431
|
|
|
4432
|
+
/**
|
|
4433
|
+
* Validation utilities for Firestore data
|
|
4434
|
+
*
|
|
4435
|
+
* These validators help ensure data follows SeaVerse Firestore security rules.
|
|
4436
|
+
* They provide client-side validation before sending data to Firestore.
|
|
4437
|
+
*
|
|
4438
|
+
* 🚨 IMPORTANT: These are CLIENT-SIDE validations only!
|
|
4439
|
+
* The actual security enforcement happens in Firestore Security Rules.
|
|
4440
|
+
* These validators help catch errors early for better DX.
|
|
4441
|
+
*/
|
|
4442
|
+
/**
|
|
4443
|
+
* Maximum document size in bytes (256 KB)
|
|
4444
|
+
* This matches the Firestore security rule limit
|
|
4445
|
+
*/
|
|
4446
|
+
const MAX_DOCUMENT_SIZE = 262144; // 256 KB
|
|
4447
|
+
/**
|
|
4448
|
+
* System reserved field names that users cannot create
|
|
4449
|
+
*
|
|
4450
|
+
* These fields are managed by the system and cannot be set by users:
|
|
4451
|
+
* - _appId: Application ID (auto-injected)
|
|
4452
|
+
* - _createdBy: Creator user ID (auto-injected)
|
|
4453
|
+
* - _createdAt: Creation timestamp (auto-injected)
|
|
4454
|
+
* - _updatedAt: Last update timestamp (auto-managed)
|
|
4455
|
+
* - _deleted: Soft delete flag (auto-managed)
|
|
4456
|
+
* - _deletedAt: Deletion timestamp (auto-managed)
|
|
4457
|
+
*/
|
|
4458
|
+
const ALLOWED_RESERVED_FIELDS = [
|
|
4459
|
+
'_appId',
|
|
4460
|
+
'_createdBy',
|
|
4461
|
+
'_createdAt',
|
|
4462
|
+
'_updatedAt',
|
|
4463
|
+
'_deleted',
|
|
4464
|
+
'_deletedAt',
|
|
4465
|
+
'_updatedBy'
|
|
4466
|
+
];
|
|
4467
|
+
/**
|
|
4468
|
+
* Common illegal reserved field patterns
|
|
4469
|
+
* Based on Firestore security rules blacklist
|
|
4470
|
+
*/
|
|
4471
|
+
const ILLEGAL_RESERVED_FIELDS = [
|
|
4472
|
+
// Single letter prefixes
|
|
4473
|
+
'_a', '_b', '_c', '_d', '_e', '_f', '_g', '_h', '_i', '_j', '_k', '_l', '_m',
|
|
4474
|
+
'_n', '_o', '_p', '_q', '_r', '_s', '_t', '_u', '_v', '_w', '_x', '_y', '_z',
|
|
4475
|
+
'_A', '_B', '_C', '_D', '_E', '_F', '_G', '_H', '_I', '_J', '_K', '_L', '_M',
|
|
4476
|
+
'_N', '_O', '_P', '_Q', '_R', '_S', '_T', '_U', '_V', '_W', '_X', '_Y', '_Z',
|
|
4477
|
+
// Number prefixes
|
|
4478
|
+
'_0', '_1', '_2', '_3', '_4', '_5', '_6', '_7', '_8', '_9',
|
|
4479
|
+
// Multiple underscores
|
|
4480
|
+
'__', '___', '____',
|
|
4481
|
+
// Permission related
|
|
4482
|
+
'_admin', '_user', '_role', '_permission', '_access', '_auth', '_owner', '_public',
|
|
4483
|
+
// Metadata related
|
|
4484
|
+
'_custom', '_data', '_meta', '_info', '_config', '_setting', '_value', '_key',
|
|
4485
|
+
'_id', '_ID', '_ref', '_timestamp', '_time', '_date', '_status', '_type',
|
|
4486
|
+
// Temporary fields
|
|
4487
|
+
'_temp', '_tmp', '_test', '_new', '_old', '_bak', '_backup', '_copy',
|
|
4488
|
+
// Common business fields
|
|
4489
|
+
'_name', '_title', '_description', '_content', '_body', '_text', '_message',
|
|
4490
|
+
'_email', '_phone', '_address', '_city', '_country', '_zip', '_code',
|
|
4491
|
+
'_price', '_amount', '_quantity', '_total', '_subtotal', '_discount', '_tax',
|
|
4492
|
+
'_image', '_avatar', '_photo', '_picture', '_file', '_url', '_link', '_path',
|
|
4493
|
+
'_user_id', '_userId', '_username', '_nickname', '_displayName',
|
|
4494
|
+
'_password', '_token', '_session', '_apiKey', '_secretKey', '_privateKey',
|
|
4495
|
+
// Flag fields
|
|
4496
|
+
'_flag', '_enabled', '_disabled', '_active', '_inactive', '_visible', '_hidden',
|
|
4497
|
+
'_isAdmin', '_isPublic', '_isPrivate', '_isDeleted', '_isActive', '_isEnabled',
|
|
4498
|
+
// State fields
|
|
4499
|
+
'_state', '_mode', '_level', '_priority', '_order', '_index', '_count', '_number',
|
|
4500
|
+
// System fields
|
|
4501
|
+
'_system', '_internal', '_private', '_protected', '_reserved', '_secret', '_hidden'
|
|
4502
|
+
];
|
|
4503
|
+
/**
|
|
4504
|
+
* Validate that data doesn't contain illegal reserved fields
|
|
4505
|
+
*
|
|
4506
|
+
* Reserved fields (starting with _) are for system use only.
|
|
4507
|
+
* Users can only use allowed system fields.
|
|
4508
|
+
*
|
|
4509
|
+
* @param data - Data object to validate
|
|
4510
|
+
* @throws Error if illegal reserved fields are found
|
|
4511
|
+
*
|
|
4512
|
+
* @example
|
|
4513
|
+
* ```typescript
|
|
4514
|
+
* // ✅ Valid - no reserved fields
|
|
4515
|
+
* validateReservedFields({ title: 'Post', content: 'Hello' });
|
|
4516
|
+
*
|
|
4517
|
+
* // ✅ Valid - allowed system fields
|
|
4518
|
+
* validateReservedFields({ _appId: 'app-1', _createdBy: 'user-1', title: 'Post' });
|
|
4519
|
+
*
|
|
4520
|
+
* // ❌ Invalid - illegal reserved field
|
|
4521
|
+
* validateReservedFields({ _custom: 'value', title: 'Post' });
|
|
4522
|
+
* // Throws: Error: Illegal reserved field "_custom"
|
|
4523
|
+
* ```
|
|
4524
|
+
*/
|
|
4525
|
+
function validateReservedFields(data) {
|
|
4526
|
+
const keys = Object.keys(data);
|
|
4527
|
+
for (const key of keys) {
|
|
4528
|
+
// Skip allowed system fields
|
|
4529
|
+
if (ALLOWED_RESERVED_FIELDS.includes(key)) {
|
|
4530
|
+
continue;
|
|
4531
|
+
}
|
|
4532
|
+
// Check if it's a reserved field (starts with _)
|
|
4533
|
+
if (key.startsWith('_')) {
|
|
4534
|
+
// Check if it's in the blacklist
|
|
4535
|
+
if (ILLEGAL_RESERVED_FIELDS.includes(key)) {
|
|
4536
|
+
throw new Error(`Illegal reserved field "${key}". ` +
|
|
4537
|
+
`Fields starting with "_" are reserved for system use. ` +
|
|
4538
|
+
`Please use a field name without the underscore prefix.`);
|
|
4539
|
+
}
|
|
4540
|
+
// Even if not in blacklist, warn about unknown _ fields
|
|
4541
|
+
throw new Error(`Unknown reserved field "${key}". ` +
|
|
4542
|
+
`Fields starting with "_" are reserved for system use. ` +
|
|
4543
|
+
`Allowed system fields: ${ALLOWED_RESERVED_FIELDS.join(', ')}. ` +
|
|
4544
|
+
`Please use a field name without the underscore prefix.`);
|
|
4545
|
+
}
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
/**
|
|
4549
|
+
* Estimate document size in bytes
|
|
4550
|
+
*
|
|
4551
|
+
* This is an approximation based on JSON serialization.
|
|
4552
|
+
* Firestore may calculate size differently, but this gives a good estimate.
|
|
4553
|
+
*
|
|
4554
|
+
* @param data - Data object to measure
|
|
4555
|
+
* @returns Estimated size in bytes
|
|
4556
|
+
*
|
|
4557
|
+
* @example
|
|
4558
|
+
* ```typescript
|
|
4559
|
+
* const data = { title: 'My Post', content: 'Long content...' };
|
|
4560
|
+
* const size = estimateDocumentSize(data);
|
|
4561
|
+
* console.log('Document size:', size, 'bytes');
|
|
4562
|
+
* ```
|
|
4563
|
+
*/
|
|
4564
|
+
function estimateDocumentSize(data) {
|
|
4565
|
+
try {
|
|
4566
|
+
const json = JSON.stringify(data);
|
|
4567
|
+
// Use Blob if available (browser), otherwise estimate from string length
|
|
4568
|
+
if (typeof Blob !== 'undefined') {
|
|
4569
|
+
return new Blob([json]).size;
|
|
4570
|
+
}
|
|
4571
|
+
else {
|
|
4572
|
+
// Node.js or environments without Blob: estimate from UTF-8 encoded length
|
|
4573
|
+
return Buffer.byteLength(json, 'utf8');
|
|
4574
|
+
}
|
|
4575
|
+
}
|
|
4576
|
+
catch (error) {
|
|
4577
|
+
// Fallback: rough estimate
|
|
4578
|
+
return JSON.stringify(data).length * 2; // Assume ~2 bytes per char for safety
|
|
4579
|
+
}
|
|
4580
|
+
}
|
|
4581
|
+
/**
|
|
4582
|
+
* Validate document size doesn't exceed limit
|
|
4583
|
+
*
|
|
4584
|
+
* Firestore has a maximum document size of 1MB, but we enforce 256KB
|
|
4585
|
+
* to match our security rules limit.
|
|
4586
|
+
*
|
|
4587
|
+
* @param data - Data object to validate
|
|
4588
|
+
* @throws Error if document is too large
|
|
4589
|
+
*
|
|
4590
|
+
* @example
|
|
4591
|
+
* ```typescript
|
|
4592
|
+
* const data = { title: 'Post', content: 'Some content' };
|
|
4593
|
+
* validateDocumentSize(data); // OK
|
|
4594
|
+
*
|
|
4595
|
+
* const hugeData = { content: 'x'.repeat(300000) };
|
|
4596
|
+
* validateDocumentSize(hugeData); // Throws error
|
|
4597
|
+
* ```
|
|
4598
|
+
*/
|
|
4599
|
+
function validateDocumentSize(data) {
|
|
4600
|
+
const size = estimateDocumentSize(data);
|
|
4601
|
+
if (size > MAX_DOCUMENT_SIZE) {
|
|
4602
|
+
throw new Error(`Document size (${size} bytes) exceeds maximum allowed size (${MAX_DOCUMENT_SIZE} bytes / 256 KB). ` +
|
|
4603
|
+
`Please reduce the amount of data you're storing in this document.`);
|
|
4604
|
+
}
|
|
4605
|
+
}
|
|
4606
|
+
/**
|
|
4607
|
+
* Validate data before sending to Firestore
|
|
4608
|
+
*
|
|
4609
|
+
* This runs all validations:
|
|
4610
|
+
* - Reserved fields check
|
|
4611
|
+
* - Document size check
|
|
4612
|
+
*
|
|
4613
|
+
* @param data - Data object to validate
|
|
4614
|
+
* @throws Error if validation fails
|
|
4615
|
+
*
|
|
4616
|
+
* @example
|
|
4617
|
+
* ```typescript
|
|
4618
|
+
* // Use this before adding/updating documents
|
|
4619
|
+
* try {
|
|
4620
|
+
* validateFirestoreData(myData);
|
|
4621
|
+
* await addDoc(collection(db, path), myData);
|
|
4622
|
+
* } catch (error) {
|
|
4623
|
+
* console.error('Validation failed:', error.message);
|
|
4624
|
+
* }
|
|
4625
|
+
* ```
|
|
4626
|
+
*/
|
|
4627
|
+
function validateFirestoreData(data) {
|
|
4628
|
+
validateReservedFields(data);
|
|
4629
|
+
validateDocumentSize(data);
|
|
4630
|
+
}
|
|
4631
|
+
/**
|
|
4632
|
+
* Check if data contains soft-delete markers
|
|
4633
|
+
*
|
|
4634
|
+
* @param data - Data object to check
|
|
4635
|
+
* @returns True if document is marked as deleted
|
|
4636
|
+
*/
|
|
4637
|
+
function isDeleted(data) {
|
|
4638
|
+
return data._deleted === true;
|
|
4639
|
+
}
|
|
4640
|
+
/**
|
|
4641
|
+
* Validate data and return detailed results instead of throwing
|
|
4642
|
+
*
|
|
4643
|
+
* Use this when you want to handle validation errors gracefully
|
|
4644
|
+
* without try/catch blocks.
|
|
4645
|
+
*
|
|
4646
|
+
* @param data - Data object to validate
|
|
4647
|
+
* @returns Validation result with errors
|
|
4648
|
+
*
|
|
4649
|
+
* @example
|
|
4650
|
+
* ```typescript
|
|
4651
|
+
* const result = validateDataDetailed(myData);
|
|
4652
|
+
* if (!result.valid) {
|
|
4653
|
+
* console.error('Validation errors:', result.errors);
|
|
4654
|
+
* // Show errors to user
|
|
4655
|
+
* } else {
|
|
4656
|
+
* // Proceed with save
|
|
4657
|
+
* }
|
|
4658
|
+
* ```
|
|
4659
|
+
*/
|
|
4660
|
+
function validateDataDetailed(data) {
|
|
4661
|
+
const errors = [];
|
|
4662
|
+
// Check reserved fields
|
|
4663
|
+
try {
|
|
4664
|
+
validateReservedFields(data);
|
|
4665
|
+
}
|
|
4666
|
+
catch (error) {
|
|
4667
|
+
if (error instanceof Error) {
|
|
4668
|
+
errors.push(error.message);
|
|
4669
|
+
}
|
|
4670
|
+
}
|
|
4671
|
+
// Check document size
|
|
4672
|
+
try {
|
|
4673
|
+
validateDocumentSize(data);
|
|
4674
|
+
}
|
|
4675
|
+
catch (error) {
|
|
4676
|
+
if (error instanceof Error) {
|
|
4677
|
+
errors.push(error.message);
|
|
4678
|
+
}
|
|
4679
|
+
}
|
|
4680
|
+
return {
|
|
4681
|
+
valid: errors.length === 0,
|
|
4682
|
+
errors
|
|
4683
|
+
};
|
|
4684
|
+
}
|
|
4685
|
+
|
|
4432
4686
|
/**
|
|
4433
4687
|
* Firestore Helper - LLM-Friendly Firestore Operations
|
|
4434
4688
|
*
|
|
@@ -4523,25 +4777,36 @@ class FirestoreHelper {
|
|
|
4523
4777
|
/**
|
|
4524
4778
|
* Get all documents from publicData collection
|
|
4525
4779
|
*
|
|
4780
|
+
* By default, this returns only non-deleted documents.
|
|
4781
|
+
* Set includeDeleted=true to include soft-deleted documents.
|
|
4782
|
+
*
|
|
4526
4783
|
* @param collectionName - Collection name
|
|
4784
|
+
* @param includeDeleted - Include soft-deleted documents (default: false)
|
|
4527
4785
|
* @returns QuerySnapshot with documents
|
|
4528
4786
|
*
|
|
4529
4787
|
* @example
|
|
4530
4788
|
* ```typescript
|
|
4789
|
+
* // Get only active posts (not deleted)
|
|
4531
4790
|
* const snapshot = await helper.getPublicData('posts');
|
|
4532
4791
|
* snapshot.forEach(doc => {
|
|
4533
4792
|
* console.log(doc.id, doc.data());
|
|
4534
4793
|
* });
|
|
4794
|
+
*
|
|
4795
|
+
* // Include deleted posts (admin use case)
|
|
4796
|
+
* const allPosts = await helper.getPublicData('posts', true);
|
|
4535
4797
|
* ```
|
|
4536
4798
|
*/
|
|
4537
|
-
async getPublicData(collectionName) {
|
|
4799
|
+
async getPublicData(collectionName, includeDeleted = false) {
|
|
4538
4800
|
const path = getPublicDataPath(this.appId, collectionName);
|
|
4539
|
-
return this.getDocs(path);
|
|
4801
|
+
return this.getDocs(path, includeDeleted);
|
|
4540
4802
|
}
|
|
4541
4803
|
/**
|
|
4542
4804
|
* Get all documents from userData collection (user's private data)
|
|
4543
4805
|
*
|
|
4806
|
+
* By default, this returns only non-deleted documents.
|
|
4807
|
+
*
|
|
4544
4808
|
* @param collectionName - Collection name
|
|
4809
|
+
* @param includeDeleted - Include soft-deleted documents (default: false)
|
|
4545
4810
|
* @returns QuerySnapshot with documents
|
|
4546
4811
|
*
|
|
4547
4812
|
* @example
|
|
@@ -4552,9 +4817,9 @@ class FirestoreHelper {
|
|
|
4552
4817
|
* });
|
|
4553
4818
|
* ```
|
|
4554
4819
|
*/
|
|
4555
|
-
async getUserData(collectionName) {
|
|
4820
|
+
async getUserData(collectionName, includeDeleted = false) {
|
|
4556
4821
|
const path = getUserDataPath(this.appId, this.userId, collectionName);
|
|
4557
|
-
return this.getDocs(path);
|
|
4822
|
+
return this.getDocs(path, includeDeleted);
|
|
4558
4823
|
}
|
|
4559
4824
|
/**
|
|
4560
4825
|
* Get all documents from publicRead collection (read-only for users)
|
|
@@ -4634,6 +4899,8 @@ class FirestoreHelper {
|
|
|
4634
4899
|
* ```
|
|
4635
4900
|
*/
|
|
4636
4901
|
async updateDoc(collectionPath, docId, data) {
|
|
4902
|
+
// Validate user data
|
|
4903
|
+
validateFirestoreData(data);
|
|
4637
4904
|
const { updateDoc, doc, serverTimestamp } = await this.loadFirestore();
|
|
4638
4905
|
const docRef = doc(this.db, collectionPath, docId);
|
|
4639
4906
|
return updateDoc(docRef, {
|
|
@@ -4643,10 +4910,50 @@ class FirestoreHelper {
|
|
|
4643
4910
|
});
|
|
4644
4911
|
}
|
|
4645
4912
|
/**
|
|
4646
|
-
*
|
|
4913
|
+
* Soft delete document (mark as deleted without removing)
|
|
4914
|
+
*
|
|
4915
|
+
* This is the RECOMMENDED way to delete documents. It marks the document
|
|
4916
|
+
* as deleted without actually removing it from the database.
|
|
4917
|
+
*
|
|
4918
|
+
* Automatically sets: _deleted = true, _deletedAt = serverTimestamp()
|
|
4919
|
+
*
|
|
4920
|
+
* @param collectionPath - Full collection path
|
|
4921
|
+
* @param docId - Document ID
|
|
4922
|
+
*
|
|
4923
|
+
* @example
|
|
4924
|
+
* ```typescript
|
|
4925
|
+
* // Soft delete a post (recommended)
|
|
4926
|
+
* await helper.softDeleteDoc(
|
|
4927
|
+
* getPublicDataPath(appId, 'posts'),
|
|
4928
|
+
* 'post-123'
|
|
4929
|
+
* );
|
|
4930
|
+
* ```
|
|
4931
|
+
*/
|
|
4932
|
+
async softDeleteDoc(collectionPath, docId) {
|
|
4933
|
+
const { updateDoc, doc, serverTimestamp } = await this.loadFirestore();
|
|
4934
|
+
const docRef = doc(this.db, collectionPath, docId);
|
|
4935
|
+
return updateDoc(docRef, {
|
|
4936
|
+
_deleted: true,
|
|
4937
|
+
_deletedAt: serverTimestamp()
|
|
4938
|
+
});
|
|
4939
|
+
}
|
|
4940
|
+
/**
|
|
4941
|
+
* Hard delete document (permanently remove from database)
|
|
4942
|
+
*
|
|
4943
|
+
* ⚠️ WARNING: This permanently removes the document.
|
|
4944
|
+
* Only admins can hard delete. Regular users should use softDeleteDoc().
|
|
4647
4945
|
*
|
|
4648
4946
|
* @param collectionPath - Full collection path
|
|
4649
4947
|
* @param docId - Document ID
|
|
4948
|
+
*
|
|
4949
|
+
* @example
|
|
4950
|
+
* ```typescript
|
|
4951
|
+
* // Hard delete (admin only)
|
|
4952
|
+
* await helper.deleteDoc(
|
|
4953
|
+
* getPublicDataPath(appId, 'posts'),
|
|
4954
|
+
* 'post-123'
|
|
4955
|
+
* );
|
|
4956
|
+
* ```
|
|
4650
4957
|
*/
|
|
4651
4958
|
async deleteDoc(collectionPath, docId) {
|
|
4652
4959
|
const { deleteDoc, doc } = await this.loadFirestore();
|
|
@@ -4682,22 +4989,34 @@ class FirestoreHelper {
|
|
|
4682
4989
|
* Internal: Add document with metadata injection
|
|
4683
4990
|
*/
|
|
4684
4991
|
async addDocWithMeta(collectionPath, data) {
|
|
4992
|
+
// Validate user data before adding system fields
|
|
4993
|
+
validateFirestoreData(data);
|
|
4685
4994
|
const { addDoc, collection, serverTimestamp } = await this.loadFirestore();
|
|
4686
4995
|
const colRef = collection(this.db, collectionPath);
|
|
4687
|
-
|
|
4996
|
+
const docData = {
|
|
4688
4997
|
_appId: this.appId,
|
|
4689
4998
|
_createdAt: serverTimestamp(),
|
|
4690
4999
|
_createdBy: this.userId,
|
|
4691
5000
|
...data
|
|
4692
|
-
}
|
|
5001
|
+
};
|
|
5002
|
+
return addDoc(colRef, docData);
|
|
4693
5003
|
}
|
|
4694
5004
|
/**
|
|
4695
5005
|
* Internal: Get all documents from collection
|
|
5006
|
+
* Optionally filter out soft-deleted documents
|
|
4696
5007
|
*/
|
|
4697
|
-
async getDocs(collectionPath) {
|
|
4698
|
-
const { getDocs, collection } = await this.loadFirestore();
|
|
5008
|
+
async getDocs(collectionPath, includeDeleted = false) {
|
|
5009
|
+
const { getDocs, collection, query, where } = await this.loadFirestore();
|
|
4699
5010
|
const colRef = collection(this.db, collectionPath);
|
|
4700
|
-
|
|
5011
|
+
if (includeDeleted) {
|
|
5012
|
+
// Return all documents (including soft-deleted)
|
|
5013
|
+
return getDocs(colRef);
|
|
5014
|
+
}
|
|
5015
|
+
else {
|
|
5016
|
+
// Filter out soft-deleted documents
|
|
5017
|
+
const q = query(colRef, where('_deleted', '==', false));
|
|
5018
|
+
return getDocs(q);
|
|
5019
|
+
}
|
|
4701
5020
|
}
|
|
4702
5021
|
/**
|
|
4703
5022
|
* Internal: Get collection reference
|
|
@@ -4901,5 +5220,5 @@ async function initializeWithToken(tokenResponse) {
|
|
|
4901
5220
|
};
|
|
4902
5221
|
}
|
|
4903
5222
|
|
|
4904
|
-
export { DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DataServiceClient, ENDPOINTS, FirestoreHelper, PATH_PATTERNS, PathBuilder, addDocWithMeta, getFirebaseConfig, getPublicDataDocPath, getPublicDataPath, getPublicReadDocPath, getPublicReadPath, getUserDataDocPath, getUserDataPath, initializeWithToken, updateDocWithMeta };
|
|
5223
|
+
export { ALLOWED_RESERVED_FIELDS, DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DataServiceClient, ENDPOINTS, FirestoreHelper, MAX_DOCUMENT_SIZE, PATH_PATTERNS, PathBuilder, addDocWithMeta, estimateDocumentSize, getFirebaseConfig, getPublicDataDocPath, getPublicDataPath, getPublicReadDocPath, getPublicReadPath, getUserDataDocPath, getUserDataPath, initializeWithToken, isDeleted, updateDocWithMeta, validateDataDetailed, validateDocumentSize, validateFirestoreData, validateReservedFields };
|
|
4905
5224
|
//# sourceMappingURL=browser.js.map
|