@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/dist/index.d.ts
CHANGED
|
@@ -544,22 +544,33 @@ declare class FirestoreHelper {
|
|
|
544
544
|
/**
|
|
545
545
|
* Get all documents from publicData collection
|
|
546
546
|
*
|
|
547
|
+
* By default, this returns only non-deleted documents.
|
|
548
|
+
* Set includeDeleted=true to include soft-deleted documents.
|
|
549
|
+
*
|
|
547
550
|
* @param collectionName - Collection name
|
|
551
|
+
* @param includeDeleted - Include soft-deleted documents (default: false)
|
|
548
552
|
* @returns QuerySnapshot with documents
|
|
549
553
|
*
|
|
550
554
|
* @example
|
|
551
555
|
* ```typescript
|
|
556
|
+
* // Get only active posts (not deleted)
|
|
552
557
|
* const snapshot = await helper.getPublicData('posts');
|
|
553
558
|
* snapshot.forEach(doc => {
|
|
554
559
|
* console.log(doc.id, doc.data());
|
|
555
560
|
* });
|
|
561
|
+
*
|
|
562
|
+
* // Include deleted posts (admin use case)
|
|
563
|
+
* const allPosts = await helper.getPublicData('posts', true);
|
|
556
564
|
* ```
|
|
557
565
|
*/
|
|
558
|
-
getPublicData(collectionName: string): Promise<QuerySnapshot>;
|
|
566
|
+
getPublicData(collectionName: string, includeDeleted?: boolean): Promise<QuerySnapshot>;
|
|
559
567
|
/**
|
|
560
568
|
* Get all documents from userData collection (user's private data)
|
|
561
569
|
*
|
|
570
|
+
* By default, this returns only non-deleted documents.
|
|
571
|
+
*
|
|
562
572
|
* @param collectionName - Collection name
|
|
573
|
+
* @param includeDeleted - Include soft-deleted documents (default: false)
|
|
563
574
|
* @returns QuerySnapshot with documents
|
|
564
575
|
*
|
|
565
576
|
* @example
|
|
@@ -570,7 +581,7 @@ declare class FirestoreHelper {
|
|
|
570
581
|
* });
|
|
571
582
|
* ```
|
|
572
583
|
*/
|
|
573
|
-
getUserData(collectionName: string): Promise<QuerySnapshot>;
|
|
584
|
+
getUserData(collectionName: string, includeDeleted?: boolean): Promise<QuerySnapshot>;
|
|
574
585
|
/**
|
|
575
586
|
* Get all documents from publicRead collection (read-only for users)
|
|
576
587
|
*
|
|
@@ -638,10 +649,43 @@ declare class FirestoreHelper {
|
|
|
638
649
|
*/
|
|
639
650
|
updateDoc(collectionPath: string, docId: string, data: Record<string, any>): Promise<void>;
|
|
640
651
|
/**
|
|
641
|
-
*
|
|
652
|
+
* Soft delete document (mark as deleted without removing)
|
|
653
|
+
*
|
|
654
|
+
* This is the RECOMMENDED way to delete documents. It marks the document
|
|
655
|
+
* as deleted without actually removing it from the database.
|
|
656
|
+
*
|
|
657
|
+
* Automatically sets: _deleted = true, _deletedAt = serverTimestamp()
|
|
642
658
|
*
|
|
643
659
|
* @param collectionPath - Full collection path
|
|
644
660
|
* @param docId - Document ID
|
|
661
|
+
*
|
|
662
|
+
* @example
|
|
663
|
+
* ```typescript
|
|
664
|
+
* // Soft delete a post (recommended)
|
|
665
|
+
* await helper.softDeleteDoc(
|
|
666
|
+
* getPublicDataPath(appId, 'posts'),
|
|
667
|
+
* 'post-123'
|
|
668
|
+
* );
|
|
669
|
+
* ```
|
|
670
|
+
*/
|
|
671
|
+
softDeleteDoc(collectionPath: string, docId: string): Promise<void>;
|
|
672
|
+
/**
|
|
673
|
+
* Hard delete document (permanently remove from database)
|
|
674
|
+
*
|
|
675
|
+
* ⚠️ WARNING: This permanently removes the document.
|
|
676
|
+
* Only admins can hard delete. Regular users should use softDeleteDoc().
|
|
677
|
+
*
|
|
678
|
+
* @param collectionPath - Full collection path
|
|
679
|
+
* @param docId - Document ID
|
|
680
|
+
*
|
|
681
|
+
* @example
|
|
682
|
+
* ```typescript
|
|
683
|
+
* // Hard delete (admin only)
|
|
684
|
+
* await helper.deleteDoc(
|
|
685
|
+
* getPublicDataPath(appId, 'posts'),
|
|
686
|
+
* 'post-123'
|
|
687
|
+
* );
|
|
688
|
+
* ```
|
|
645
689
|
*/
|
|
646
690
|
deleteDoc(collectionPath: string, docId: string): Promise<void>;
|
|
647
691
|
/**
|
|
@@ -668,6 +712,7 @@ declare class FirestoreHelper {
|
|
|
668
712
|
private addDocWithMeta;
|
|
669
713
|
/**
|
|
670
714
|
* Internal: Get all documents from collection
|
|
715
|
+
* Optionally filter out soft-deleted documents
|
|
671
716
|
*/
|
|
672
717
|
private getDocs;
|
|
673
718
|
/**
|
|
@@ -988,5 +1033,149 @@ declare const PATH_PATTERNS: {
|
|
|
988
1033
|
readonly USER_DATA: "userData";
|
|
989
1034
|
};
|
|
990
1035
|
|
|
991
|
-
|
|
992
|
-
|
|
1036
|
+
/**
|
|
1037
|
+
* Validation utilities for Firestore data
|
|
1038
|
+
*
|
|
1039
|
+
* These validators help ensure data follows SeaVerse Firestore security rules.
|
|
1040
|
+
* They provide client-side validation before sending data to Firestore.
|
|
1041
|
+
*
|
|
1042
|
+
* 🚨 IMPORTANT: These are CLIENT-SIDE validations only!
|
|
1043
|
+
* The actual security enforcement happens in Firestore Security Rules.
|
|
1044
|
+
* These validators help catch errors early for better DX.
|
|
1045
|
+
*/
|
|
1046
|
+
/**
|
|
1047
|
+
* Maximum document size in bytes (256 KB)
|
|
1048
|
+
* This matches the Firestore security rule limit
|
|
1049
|
+
*/
|
|
1050
|
+
declare const MAX_DOCUMENT_SIZE = 262144;
|
|
1051
|
+
/**
|
|
1052
|
+
* System reserved field names that users cannot create
|
|
1053
|
+
*
|
|
1054
|
+
* These fields are managed by the system and cannot be set by users:
|
|
1055
|
+
* - _appId: Application ID (auto-injected)
|
|
1056
|
+
* - _createdBy: Creator user ID (auto-injected)
|
|
1057
|
+
* - _createdAt: Creation timestamp (auto-injected)
|
|
1058
|
+
* - _updatedAt: Last update timestamp (auto-managed)
|
|
1059
|
+
* - _deleted: Soft delete flag (auto-managed)
|
|
1060
|
+
* - _deletedAt: Deletion timestamp (auto-managed)
|
|
1061
|
+
*/
|
|
1062
|
+
declare const ALLOWED_RESERVED_FIELDS: string[];
|
|
1063
|
+
/**
|
|
1064
|
+
* Validate that data doesn't contain illegal reserved fields
|
|
1065
|
+
*
|
|
1066
|
+
* Reserved fields (starting with _) are for system use only.
|
|
1067
|
+
* Users can only use allowed system fields.
|
|
1068
|
+
*
|
|
1069
|
+
* @param data - Data object to validate
|
|
1070
|
+
* @throws Error if illegal reserved fields are found
|
|
1071
|
+
*
|
|
1072
|
+
* @example
|
|
1073
|
+
* ```typescript
|
|
1074
|
+
* // ✅ Valid - no reserved fields
|
|
1075
|
+
* validateReservedFields({ title: 'Post', content: 'Hello' });
|
|
1076
|
+
*
|
|
1077
|
+
* // ✅ Valid - allowed system fields
|
|
1078
|
+
* validateReservedFields({ _appId: 'app-1', _createdBy: 'user-1', title: 'Post' });
|
|
1079
|
+
*
|
|
1080
|
+
* // ❌ Invalid - illegal reserved field
|
|
1081
|
+
* validateReservedFields({ _custom: 'value', title: 'Post' });
|
|
1082
|
+
* // Throws: Error: Illegal reserved field "_custom"
|
|
1083
|
+
* ```
|
|
1084
|
+
*/
|
|
1085
|
+
declare function validateReservedFields(data: Record<string, any>): void;
|
|
1086
|
+
/**
|
|
1087
|
+
* Estimate document size in bytes
|
|
1088
|
+
*
|
|
1089
|
+
* This is an approximation based on JSON serialization.
|
|
1090
|
+
* Firestore may calculate size differently, but this gives a good estimate.
|
|
1091
|
+
*
|
|
1092
|
+
* @param data - Data object to measure
|
|
1093
|
+
* @returns Estimated size in bytes
|
|
1094
|
+
*
|
|
1095
|
+
* @example
|
|
1096
|
+
* ```typescript
|
|
1097
|
+
* const data = { title: 'My Post', content: 'Long content...' };
|
|
1098
|
+
* const size = estimateDocumentSize(data);
|
|
1099
|
+
* console.log('Document size:', size, 'bytes');
|
|
1100
|
+
* ```
|
|
1101
|
+
*/
|
|
1102
|
+
declare function estimateDocumentSize(data: Record<string, any>): number;
|
|
1103
|
+
/**
|
|
1104
|
+
* Validate document size doesn't exceed limit
|
|
1105
|
+
*
|
|
1106
|
+
* Firestore has a maximum document size of 1MB, but we enforce 256KB
|
|
1107
|
+
* to match our security rules limit.
|
|
1108
|
+
*
|
|
1109
|
+
* @param data - Data object to validate
|
|
1110
|
+
* @throws Error if document is too large
|
|
1111
|
+
*
|
|
1112
|
+
* @example
|
|
1113
|
+
* ```typescript
|
|
1114
|
+
* const data = { title: 'Post', content: 'Some content' };
|
|
1115
|
+
* validateDocumentSize(data); // OK
|
|
1116
|
+
*
|
|
1117
|
+
* const hugeData = { content: 'x'.repeat(300000) };
|
|
1118
|
+
* validateDocumentSize(hugeData); // Throws error
|
|
1119
|
+
* ```
|
|
1120
|
+
*/
|
|
1121
|
+
declare function validateDocumentSize(data: Record<string, any>): void;
|
|
1122
|
+
/**
|
|
1123
|
+
* Validate data before sending to Firestore
|
|
1124
|
+
*
|
|
1125
|
+
* This runs all validations:
|
|
1126
|
+
* - Reserved fields check
|
|
1127
|
+
* - Document size check
|
|
1128
|
+
*
|
|
1129
|
+
* @param data - Data object to validate
|
|
1130
|
+
* @throws Error if validation fails
|
|
1131
|
+
*
|
|
1132
|
+
* @example
|
|
1133
|
+
* ```typescript
|
|
1134
|
+
* // Use this before adding/updating documents
|
|
1135
|
+
* try {
|
|
1136
|
+
* validateFirestoreData(myData);
|
|
1137
|
+
* await addDoc(collection(db, path), myData);
|
|
1138
|
+
* } catch (error) {
|
|
1139
|
+
* console.error('Validation failed:', error.message);
|
|
1140
|
+
* }
|
|
1141
|
+
* ```
|
|
1142
|
+
*/
|
|
1143
|
+
declare function validateFirestoreData(data: Record<string, any>): void;
|
|
1144
|
+
/**
|
|
1145
|
+
* Check if data contains soft-delete markers
|
|
1146
|
+
*
|
|
1147
|
+
* @param data - Data object to check
|
|
1148
|
+
* @returns True if document is marked as deleted
|
|
1149
|
+
*/
|
|
1150
|
+
declare function isDeleted(data: Record<string, any>): boolean;
|
|
1151
|
+
/**
|
|
1152
|
+
* Validation result for detailed error reporting
|
|
1153
|
+
*/
|
|
1154
|
+
interface ValidationResult {
|
|
1155
|
+
valid: boolean;
|
|
1156
|
+
errors: string[];
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Validate data and return detailed results instead of throwing
|
|
1160
|
+
*
|
|
1161
|
+
* Use this when you want to handle validation errors gracefully
|
|
1162
|
+
* without try/catch blocks.
|
|
1163
|
+
*
|
|
1164
|
+
* @param data - Data object to validate
|
|
1165
|
+
* @returns Validation result with errors
|
|
1166
|
+
*
|
|
1167
|
+
* @example
|
|
1168
|
+
* ```typescript
|
|
1169
|
+
* const result = validateDataDetailed(myData);
|
|
1170
|
+
* if (!result.valid) {
|
|
1171
|
+
* console.error('Validation errors:', result.errors);
|
|
1172
|
+
* // Show errors to user
|
|
1173
|
+
* } else {
|
|
1174
|
+
* // Proceed with save
|
|
1175
|
+
* }
|
|
1176
|
+
* ```
|
|
1177
|
+
*/
|
|
1178
|
+
declare function validateDataDetailed(data: Record<string, any>): ValidationResult;
|
|
1179
|
+
|
|
1180
|
+
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 };
|
|
1181
|
+
export type { ApiError, ApiResponse, DataServiceClientOptions, FirebaseConfig, FirestoreTokenResponse, GenerateFirestoreTokenRequest, GenerateGuestFirestoreTokenRequest, ValidationResult };
|
package/dist/index.js
CHANGED
|
@@ -518,6 +518,260 @@ const PATH_PATTERNS = {
|
|
|
518
518
|
USER_DATA: 'userData',
|
|
519
519
|
};
|
|
520
520
|
|
|
521
|
+
/**
|
|
522
|
+
* Validation utilities for Firestore data
|
|
523
|
+
*
|
|
524
|
+
* These validators help ensure data follows SeaVerse Firestore security rules.
|
|
525
|
+
* They provide client-side validation before sending data to Firestore.
|
|
526
|
+
*
|
|
527
|
+
* 🚨 IMPORTANT: These are CLIENT-SIDE validations only!
|
|
528
|
+
* The actual security enforcement happens in Firestore Security Rules.
|
|
529
|
+
* These validators help catch errors early for better DX.
|
|
530
|
+
*/
|
|
531
|
+
/**
|
|
532
|
+
* Maximum document size in bytes (256 KB)
|
|
533
|
+
* This matches the Firestore security rule limit
|
|
534
|
+
*/
|
|
535
|
+
const MAX_DOCUMENT_SIZE = 262144; // 256 KB
|
|
536
|
+
/**
|
|
537
|
+
* System reserved field names that users cannot create
|
|
538
|
+
*
|
|
539
|
+
* These fields are managed by the system and cannot be set by users:
|
|
540
|
+
* - _appId: Application ID (auto-injected)
|
|
541
|
+
* - _createdBy: Creator user ID (auto-injected)
|
|
542
|
+
* - _createdAt: Creation timestamp (auto-injected)
|
|
543
|
+
* - _updatedAt: Last update timestamp (auto-managed)
|
|
544
|
+
* - _deleted: Soft delete flag (auto-managed)
|
|
545
|
+
* - _deletedAt: Deletion timestamp (auto-managed)
|
|
546
|
+
*/
|
|
547
|
+
const ALLOWED_RESERVED_FIELDS = [
|
|
548
|
+
'_appId',
|
|
549
|
+
'_createdBy',
|
|
550
|
+
'_createdAt',
|
|
551
|
+
'_updatedAt',
|
|
552
|
+
'_deleted',
|
|
553
|
+
'_deletedAt',
|
|
554
|
+
'_updatedBy'
|
|
555
|
+
];
|
|
556
|
+
/**
|
|
557
|
+
* Common illegal reserved field patterns
|
|
558
|
+
* Based on Firestore security rules blacklist
|
|
559
|
+
*/
|
|
560
|
+
const ILLEGAL_RESERVED_FIELDS = [
|
|
561
|
+
// Single letter prefixes
|
|
562
|
+
'_a', '_b', '_c', '_d', '_e', '_f', '_g', '_h', '_i', '_j', '_k', '_l', '_m',
|
|
563
|
+
'_n', '_o', '_p', '_q', '_r', '_s', '_t', '_u', '_v', '_w', '_x', '_y', '_z',
|
|
564
|
+
'_A', '_B', '_C', '_D', '_E', '_F', '_G', '_H', '_I', '_J', '_K', '_L', '_M',
|
|
565
|
+
'_N', '_O', '_P', '_Q', '_R', '_S', '_T', '_U', '_V', '_W', '_X', '_Y', '_Z',
|
|
566
|
+
// Number prefixes
|
|
567
|
+
'_0', '_1', '_2', '_3', '_4', '_5', '_6', '_7', '_8', '_9',
|
|
568
|
+
// Multiple underscores
|
|
569
|
+
'__', '___', '____',
|
|
570
|
+
// Permission related
|
|
571
|
+
'_admin', '_user', '_role', '_permission', '_access', '_auth', '_owner', '_public',
|
|
572
|
+
// Metadata related
|
|
573
|
+
'_custom', '_data', '_meta', '_info', '_config', '_setting', '_value', '_key',
|
|
574
|
+
'_id', '_ID', '_ref', '_timestamp', '_time', '_date', '_status', '_type',
|
|
575
|
+
// Temporary fields
|
|
576
|
+
'_temp', '_tmp', '_test', '_new', '_old', '_bak', '_backup', '_copy',
|
|
577
|
+
// Common business fields
|
|
578
|
+
'_name', '_title', '_description', '_content', '_body', '_text', '_message',
|
|
579
|
+
'_email', '_phone', '_address', '_city', '_country', '_zip', '_code',
|
|
580
|
+
'_price', '_amount', '_quantity', '_total', '_subtotal', '_discount', '_tax',
|
|
581
|
+
'_image', '_avatar', '_photo', '_picture', '_file', '_url', '_link', '_path',
|
|
582
|
+
'_user_id', '_userId', '_username', '_nickname', '_displayName',
|
|
583
|
+
'_password', '_token', '_session', '_apiKey', '_secretKey', '_privateKey',
|
|
584
|
+
// Flag fields
|
|
585
|
+
'_flag', '_enabled', '_disabled', '_active', '_inactive', '_visible', '_hidden',
|
|
586
|
+
'_isAdmin', '_isPublic', '_isPrivate', '_isDeleted', '_isActive', '_isEnabled',
|
|
587
|
+
// State fields
|
|
588
|
+
'_state', '_mode', '_level', '_priority', '_order', '_index', '_count', '_number',
|
|
589
|
+
// System fields
|
|
590
|
+
'_system', '_internal', '_private', '_protected', '_reserved', '_secret', '_hidden'
|
|
591
|
+
];
|
|
592
|
+
/**
|
|
593
|
+
* Validate that data doesn't contain illegal reserved fields
|
|
594
|
+
*
|
|
595
|
+
* Reserved fields (starting with _) are for system use only.
|
|
596
|
+
* Users can only use allowed system fields.
|
|
597
|
+
*
|
|
598
|
+
* @param data - Data object to validate
|
|
599
|
+
* @throws Error if illegal reserved fields are found
|
|
600
|
+
*
|
|
601
|
+
* @example
|
|
602
|
+
* ```typescript
|
|
603
|
+
* // ✅ Valid - no reserved fields
|
|
604
|
+
* validateReservedFields({ title: 'Post', content: 'Hello' });
|
|
605
|
+
*
|
|
606
|
+
* // ✅ Valid - allowed system fields
|
|
607
|
+
* validateReservedFields({ _appId: 'app-1', _createdBy: 'user-1', title: 'Post' });
|
|
608
|
+
*
|
|
609
|
+
* // ❌ Invalid - illegal reserved field
|
|
610
|
+
* validateReservedFields({ _custom: 'value', title: 'Post' });
|
|
611
|
+
* // Throws: Error: Illegal reserved field "_custom"
|
|
612
|
+
* ```
|
|
613
|
+
*/
|
|
614
|
+
function validateReservedFields(data) {
|
|
615
|
+
const keys = Object.keys(data);
|
|
616
|
+
for (const key of keys) {
|
|
617
|
+
// Skip allowed system fields
|
|
618
|
+
if (ALLOWED_RESERVED_FIELDS.includes(key)) {
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
621
|
+
// Check if it's a reserved field (starts with _)
|
|
622
|
+
if (key.startsWith('_')) {
|
|
623
|
+
// Check if it's in the blacklist
|
|
624
|
+
if (ILLEGAL_RESERVED_FIELDS.includes(key)) {
|
|
625
|
+
throw new Error(`Illegal reserved field "${key}". ` +
|
|
626
|
+
`Fields starting with "_" are reserved for system use. ` +
|
|
627
|
+
`Please use a field name without the underscore prefix.`);
|
|
628
|
+
}
|
|
629
|
+
// Even if not in blacklist, warn about unknown _ fields
|
|
630
|
+
throw new Error(`Unknown reserved field "${key}". ` +
|
|
631
|
+
`Fields starting with "_" are reserved for system use. ` +
|
|
632
|
+
`Allowed system fields: ${ALLOWED_RESERVED_FIELDS.join(', ')}. ` +
|
|
633
|
+
`Please use a field name without the underscore prefix.`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
/**
|
|
638
|
+
* Estimate document size in bytes
|
|
639
|
+
*
|
|
640
|
+
* This is an approximation based on JSON serialization.
|
|
641
|
+
* Firestore may calculate size differently, but this gives a good estimate.
|
|
642
|
+
*
|
|
643
|
+
* @param data - Data object to measure
|
|
644
|
+
* @returns Estimated size in bytes
|
|
645
|
+
*
|
|
646
|
+
* @example
|
|
647
|
+
* ```typescript
|
|
648
|
+
* const data = { title: 'My Post', content: 'Long content...' };
|
|
649
|
+
* const size = estimateDocumentSize(data);
|
|
650
|
+
* console.log('Document size:', size, 'bytes');
|
|
651
|
+
* ```
|
|
652
|
+
*/
|
|
653
|
+
function estimateDocumentSize(data) {
|
|
654
|
+
try {
|
|
655
|
+
const json = JSON.stringify(data);
|
|
656
|
+
// Use Blob if available (browser), otherwise estimate from string length
|
|
657
|
+
if (typeof Blob !== 'undefined') {
|
|
658
|
+
return new Blob([json]).size;
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
// Node.js or environments without Blob: estimate from UTF-8 encoded length
|
|
662
|
+
return Buffer.byteLength(json, 'utf8');
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
// Fallback: rough estimate
|
|
667
|
+
return JSON.stringify(data).length * 2; // Assume ~2 bytes per char for safety
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
/**
|
|
671
|
+
* Validate document size doesn't exceed limit
|
|
672
|
+
*
|
|
673
|
+
* Firestore has a maximum document size of 1MB, but we enforce 256KB
|
|
674
|
+
* to match our security rules limit.
|
|
675
|
+
*
|
|
676
|
+
* @param data - Data object to validate
|
|
677
|
+
* @throws Error if document is too large
|
|
678
|
+
*
|
|
679
|
+
* @example
|
|
680
|
+
* ```typescript
|
|
681
|
+
* const data = { title: 'Post', content: 'Some content' };
|
|
682
|
+
* validateDocumentSize(data); // OK
|
|
683
|
+
*
|
|
684
|
+
* const hugeData = { content: 'x'.repeat(300000) };
|
|
685
|
+
* validateDocumentSize(hugeData); // Throws error
|
|
686
|
+
* ```
|
|
687
|
+
*/
|
|
688
|
+
function validateDocumentSize(data) {
|
|
689
|
+
const size = estimateDocumentSize(data);
|
|
690
|
+
if (size > MAX_DOCUMENT_SIZE) {
|
|
691
|
+
throw new Error(`Document size (${size} bytes) exceeds maximum allowed size (${MAX_DOCUMENT_SIZE} bytes / 256 KB). ` +
|
|
692
|
+
`Please reduce the amount of data you're storing in this document.`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
/**
|
|
696
|
+
* Validate data before sending to Firestore
|
|
697
|
+
*
|
|
698
|
+
* This runs all validations:
|
|
699
|
+
* - Reserved fields check
|
|
700
|
+
* - Document size check
|
|
701
|
+
*
|
|
702
|
+
* @param data - Data object to validate
|
|
703
|
+
* @throws Error if validation fails
|
|
704
|
+
*
|
|
705
|
+
* @example
|
|
706
|
+
* ```typescript
|
|
707
|
+
* // Use this before adding/updating documents
|
|
708
|
+
* try {
|
|
709
|
+
* validateFirestoreData(myData);
|
|
710
|
+
* await addDoc(collection(db, path), myData);
|
|
711
|
+
* } catch (error) {
|
|
712
|
+
* console.error('Validation failed:', error.message);
|
|
713
|
+
* }
|
|
714
|
+
* ```
|
|
715
|
+
*/
|
|
716
|
+
function validateFirestoreData(data) {
|
|
717
|
+
validateReservedFields(data);
|
|
718
|
+
validateDocumentSize(data);
|
|
719
|
+
}
|
|
720
|
+
/**
|
|
721
|
+
* Check if data contains soft-delete markers
|
|
722
|
+
*
|
|
723
|
+
* @param data - Data object to check
|
|
724
|
+
* @returns True if document is marked as deleted
|
|
725
|
+
*/
|
|
726
|
+
function isDeleted(data) {
|
|
727
|
+
return data._deleted === true;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Validate data and return detailed results instead of throwing
|
|
731
|
+
*
|
|
732
|
+
* Use this when you want to handle validation errors gracefully
|
|
733
|
+
* without try/catch blocks.
|
|
734
|
+
*
|
|
735
|
+
* @param data - Data object to validate
|
|
736
|
+
* @returns Validation result with errors
|
|
737
|
+
*
|
|
738
|
+
* @example
|
|
739
|
+
* ```typescript
|
|
740
|
+
* const result = validateDataDetailed(myData);
|
|
741
|
+
* if (!result.valid) {
|
|
742
|
+
* console.error('Validation errors:', result.errors);
|
|
743
|
+
* // Show errors to user
|
|
744
|
+
* } else {
|
|
745
|
+
* // Proceed with save
|
|
746
|
+
* }
|
|
747
|
+
* ```
|
|
748
|
+
*/
|
|
749
|
+
function validateDataDetailed(data) {
|
|
750
|
+
const errors = [];
|
|
751
|
+
// Check reserved fields
|
|
752
|
+
try {
|
|
753
|
+
validateReservedFields(data);
|
|
754
|
+
}
|
|
755
|
+
catch (error) {
|
|
756
|
+
if (error instanceof Error) {
|
|
757
|
+
errors.push(error.message);
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// Check document size
|
|
761
|
+
try {
|
|
762
|
+
validateDocumentSize(data);
|
|
763
|
+
}
|
|
764
|
+
catch (error) {
|
|
765
|
+
if (error instanceof Error) {
|
|
766
|
+
errors.push(error.message);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
return {
|
|
770
|
+
valid: errors.length === 0,
|
|
771
|
+
errors
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
|
|
521
775
|
/**
|
|
522
776
|
* Firestore Helper - LLM-Friendly Firestore Operations
|
|
523
777
|
*
|
|
@@ -612,25 +866,36 @@ class FirestoreHelper {
|
|
|
612
866
|
/**
|
|
613
867
|
* Get all documents from publicData collection
|
|
614
868
|
*
|
|
869
|
+
* By default, this returns only non-deleted documents.
|
|
870
|
+
* Set includeDeleted=true to include soft-deleted documents.
|
|
871
|
+
*
|
|
615
872
|
* @param collectionName - Collection name
|
|
873
|
+
* @param includeDeleted - Include soft-deleted documents (default: false)
|
|
616
874
|
* @returns QuerySnapshot with documents
|
|
617
875
|
*
|
|
618
876
|
* @example
|
|
619
877
|
* ```typescript
|
|
878
|
+
* // Get only active posts (not deleted)
|
|
620
879
|
* const snapshot = await helper.getPublicData('posts');
|
|
621
880
|
* snapshot.forEach(doc => {
|
|
622
881
|
* console.log(doc.id, doc.data());
|
|
623
882
|
* });
|
|
883
|
+
*
|
|
884
|
+
* // Include deleted posts (admin use case)
|
|
885
|
+
* const allPosts = await helper.getPublicData('posts', true);
|
|
624
886
|
* ```
|
|
625
887
|
*/
|
|
626
|
-
async getPublicData(collectionName) {
|
|
888
|
+
async getPublicData(collectionName, includeDeleted = false) {
|
|
627
889
|
const path = getPublicDataPath(this.appId, collectionName);
|
|
628
|
-
return this.getDocs(path);
|
|
890
|
+
return this.getDocs(path, includeDeleted);
|
|
629
891
|
}
|
|
630
892
|
/**
|
|
631
893
|
* Get all documents from userData collection (user's private data)
|
|
632
894
|
*
|
|
895
|
+
* By default, this returns only non-deleted documents.
|
|
896
|
+
*
|
|
633
897
|
* @param collectionName - Collection name
|
|
898
|
+
* @param includeDeleted - Include soft-deleted documents (default: false)
|
|
634
899
|
* @returns QuerySnapshot with documents
|
|
635
900
|
*
|
|
636
901
|
* @example
|
|
@@ -641,9 +906,9 @@ class FirestoreHelper {
|
|
|
641
906
|
* });
|
|
642
907
|
* ```
|
|
643
908
|
*/
|
|
644
|
-
async getUserData(collectionName) {
|
|
909
|
+
async getUserData(collectionName, includeDeleted = false) {
|
|
645
910
|
const path = getUserDataPath(this.appId, this.userId, collectionName);
|
|
646
|
-
return this.getDocs(path);
|
|
911
|
+
return this.getDocs(path, includeDeleted);
|
|
647
912
|
}
|
|
648
913
|
/**
|
|
649
914
|
* Get all documents from publicRead collection (read-only for users)
|
|
@@ -723,6 +988,8 @@ class FirestoreHelper {
|
|
|
723
988
|
* ```
|
|
724
989
|
*/
|
|
725
990
|
async updateDoc(collectionPath, docId, data) {
|
|
991
|
+
// Validate user data
|
|
992
|
+
validateFirestoreData(data);
|
|
726
993
|
const { updateDoc, doc, serverTimestamp } = await this.loadFirestore();
|
|
727
994
|
const docRef = doc(this.db, collectionPath, docId);
|
|
728
995
|
return updateDoc(docRef, {
|
|
@@ -732,10 +999,50 @@ class FirestoreHelper {
|
|
|
732
999
|
});
|
|
733
1000
|
}
|
|
734
1001
|
/**
|
|
735
|
-
*
|
|
1002
|
+
* Soft delete document (mark as deleted without removing)
|
|
1003
|
+
*
|
|
1004
|
+
* This is the RECOMMENDED way to delete documents. It marks the document
|
|
1005
|
+
* as deleted without actually removing it from the database.
|
|
1006
|
+
*
|
|
1007
|
+
* Automatically sets: _deleted = true, _deletedAt = serverTimestamp()
|
|
1008
|
+
*
|
|
1009
|
+
* @param collectionPath - Full collection path
|
|
1010
|
+
* @param docId - Document ID
|
|
1011
|
+
*
|
|
1012
|
+
* @example
|
|
1013
|
+
* ```typescript
|
|
1014
|
+
* // Soft delete a post (recommended)
|
|
1015
|
+
* await helper.softDeleteDoc(
|
|
1016
|
+
* getPublicDataPath(appId, 'posts'),
|
|
1017
|
+
* 'post-123'
|
|
1018
|
+
* );
|
|
1019
|
+
* ```
|
|
1020
|
+
*/
|
|
1021
|
+
async softDeleteDoc(collectionPath, docId) {
|
|
1022
|
+
const { updateDoc, doc, serverTimestamp } = await this.loadFirestore();
|
|
1023
|
+
const docRef = doc(this.db, collectionPath, docId);
|
|
1024
|
+
return updateDoc(docRef, {
|
|
1025
|
+
_deleted: true,
|
|
1026
|
+
_deletedAt: serverTimestamp()
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
/**
|
|
1030
|
+
* Hard delete document (permanently remove from database)
|
|
1031
|
+
*
|
|
1032
|
+
* ⚠️ WARNING: This permanently removes the document.
|
|
1033
|
+
* Only admins can hard delete. Regular users should use softDeleteDoc().
|
|
736
1034
|
*
|
|
737
1035
|
* @param collectionPath - Full collection path
|
|
738
1036
|
* @param docId - Document ID
|
|
1037
|
+
*
|
|
1038
|
+
* @example
|
|
1039
|
+
* ```typescript
|
|
1040
|
+
* // Hard delete (admin only)
|
|
1041
|
+
* await helper.deleteDoc(
|
|
1042
|
+
* getPublicDataPath(appId, 'posts'),
|
|
1043
|
+
* 'post-123'
|
|
1044
|
+
* );
|
|
1045
|
+
* ```
|
|
739
1046
|
*/
|
|
740
1047
|
async deleteDoc(collectionPath, docId) {
|
|
741
1048
|
const { deleteDoc, doc } = await this.loadFirestore();
|
|
@@ -771,22 +1078,34 @@ class FirestoreHelper {
|
|
|
771
1078
|
* Internal: Add document with metadata injection
|
|
772
1079
|
*/
|
|
773
1080
|
async addDocWithMeta(collectionPath, data) {
|
|
1081
|
+
// Validate user data before adding system fields
|
|
1082
|
+
validateFirestoreData(data);
|
|
774
1083
|
const { addDoc, collection, serverTimestamp } = await this.loadFirestore();
|
|
775
1084
|
const colRef = collection(this.db, collectionPath);
|
|
776
|
-
|
|
1085
|
+
const docData = {
|
|
777
1086
|
_appId: this.appId,
|
|
778
1087
|
_createdAt: serverTimestamp(),
|
|
779
1088
|
_createdBy: this.userId,
|
|
780
1089
|
...data
|
|
781
|
-
}
|
|
1090
|
+
};
|
|
1091
|
+
return addDoc(colRef, docData);
|
|
782
1092
|
}
|
|
783
1093
|
/**
|
|
784
1094
|
* Internal: Get all documents from collection
|
|
1095
|
+
* Optionally filter out soft-deleted documents
|
|
785
1096
|
*/
|
|
786
|
-
async getDocs(collectionPath) {
|
|
787
|
-
const { getDocs, collection } = await this.loadFirestore();
|
|
1097
|
+
async getDocs(collectionPath, includeDeleted = false) {
|
|
1098
|
+
const { getDocs, collection, query, where } = await this.loadFirestore();
|
|
788
1099
|
const colRef = collection(this.db, collectionPath);
|
|
789
|
-
|
|
1100
|
+
if (includeDeleted) {
|
|
1101
|
+
// Return all documents (including soft-deleted)
|
|
1102
|
+
return getDocs(colRef);
|
|
1103
|
+
}
|
|
1104
|
+
else {
|
|
1105
|
+
// Filter out soft-deleted documents
|
|
1106
|
+
const q = query(colRef, where('_deleted', '==', false));
|
|
1107
|
+
return getDocs(q);
|
|
1108
|
+
}
|
|
790
1109
|
}
|
|
791
1110
|
/**
|
|
792
1111
|
* Internal: Get collection reference
|
|
@@ -990,5 +1309,5 @@ async function initializeWithToken(tokenResponse) {
|
|
|
990
1309
|
};
|
|
991
1310
|
}
|
|
992
1311
|
|
|
993
|
-
export { DEFAULT_BASE_URL, DEFAULT_TIMEOUT, DataServiceClient, ENDPOINTS, FirestoreHelper, PATH_PATTERNS, PathBuilder, addDocWithMeta, getFirebaseConfig, getPublicDataDocPath, getPublicDataPath, getPublicReadDocPath, getPublicReadPath, getUserDataDocPath, getUserDataPath, initializeWithToken, updateDocWithMeta };
|
|
1312
|
+
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 };
|
|
994
1313
|
//# sourceMappingURL=index.js.map
|