@umituz/react-native-firebase 1.13.3 → 1.13.5

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.
Files changed (27) hide show
  1. package/package.json +5 -8
  2. package/src/firestore/__tests__/BaseRepository.test.ts +133 -0
  3. package/src/firestore/__tests__/QueryDeduplicationMiddleware.test.ts +147 -0
  4. package/src/firestore/__tests__/mocks/react-native-firebase.ts +23 -0
  5. package/src/firestore/__tests__/setup.ts +36 -0
  6. package/src/firestore/domain/constants/QuotaLimits.ts +97 -0
  7. package/src/firestore/domain/entities/QuotaMetrics.ts +28 -0
  8. package/src/firestore/domain/entities/RequestLog.ts +30 -0
  9. package/src/firestore/domain/errors/FirebaseFirestoreError.ts +52 -0
  10. package/src/firestore/domain/services/QuotaCalculator.ts +70 -0
  11. package/src/firestore/index.ts +174 -0
  12. package/src/firestore/infrastructure/config/FirestoreClient.ts +181 -0
  13. package/src/firestore/infrastructure/config/initializers/FirebaseFirestoreInitializer.ts +46 -0
  14. package/src/firestore/infrastructure/middleware/QueryDeduplicationMiddleware.ts +153 -0
  15. package/src/firestore/infrastructure/middleware/QuotaTrackingMiddleware.ts +165 -0
  16. package/src/firestore/infrastructure/repositories/BasePaginatedRepository.ts +90 -0
  17. package/src/firestore/infrastructure/repositories/BaseQueryRepository.ts +80 -0
  18. package/src/firestore/infrastructure/repositories/BaseRepository.ts +147 -0
  19. package/src/firestore/infrastructure/services/QuotaMonitorService.ts +108 -0
  20. package/src/firestore/infrastructure/services/RequestLoggerService.ts +139 -0
  21. package/src/firestore/types/pagination.types.ts +60 -0
  22. package/src/firestore/utils/dateUtils.ts +31 -0
  23. package/src/firestore/utils/document-mapper.helper.ts +145 -0
  24. package/src/firestore/utils/pagination.helper.ts +93 -0
  25. package/src/firestore/utils/query-builder.ts +188 -0
  26. package/src/firestore/utils/quota-error-detector.util.ts +100 -0
  27. package/src/index.ts +8 -0
@@ -0,0 +1,145 @@
1
+ /**
2
+ * Document Mapper Helper
3
+ *
4
+ * Utilities for batch document processing with enrichment.
5
+ * Handles document extraction, validation, and enrichment with related data.
6
+ *
7
+ * App-agnostic: Works with any document type and any enrichment logic.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { DocumentMapperHelper } from '@umituz/react-native-firestore';
12
+ *
13
+ * const mapper = new DocumentMapperHelper<Post, User, EnrichedPost>();
14
+ * const enriched = await mapper.mapWithEnrichment(
15
+ * postDocs,
16
+ * post => extractPost(post),
17
+ * post => post.userId,
18
+ * userId => userRepo.getById(userId),
19
+ * (post, user) => ({ ...post, user })
20
+ * );
21
+ * ```
22
+ */
23
+
24
+ import type { QueryDocumentSnapshot, DocumentData } from 'firebase/firestore';
25
+
26
+ export class DocumentMapperHelper<TSource, TEnrichment, TResult> {
27
+ /**
28
+ * Map documents with enrichment from related data
29
+ *
30
+ * Process flow:
31
+ * 1. Extract source data from document
32
+ * 2. Skip if extraction fails or source is invalid
33
+ * 3. Get enrichment key from source
34
+ * 4. Fetch enrichment data using the key
35
+ * 5. Skip if enrichment data not found
36
+ * 6. Combine source and enrichment into result
37
+ *
38
+ * @param docs - Firestore document snapshots
39
+ * @param extractSource - Extract source data from document
40
+ * @param getEnrichmentKey - Get enrichment key from source
41
+ * @param fetchEnrichment - Fetch enrichment data by key
42
+ * @param combineData - Combine source and enrichment into result
43
+ * @returns Array of enriched results
44
+ */
45
+ async mapWithEnrichment(
46
+ docs: QueryDocumentSnapshot<DocumentData>[],
47
+ extractSource: (doc: QueryDocumentSnapshot<DocumentData>) => TSource | null,
48
+ getEnrichmentKey: (source: TSource) => string,
49
+ fetchEnrichment: (key: string) => Promise<TEnrichment | null>,
50
+ combineData: (source: TSource, enrichment: TEnrichment) => TResult,
51
+ ): Promise<TResult[]> {
52
+ const results: TResult[] = [];
53
+
54
+ for (const doc of docs) {
55
+ const source = extractSource(doc);
56
+ if (!source) continue;
57
+
58
+ const enrichmentKey = getEnrichmentKey(source);
59
+ const enrichment = await fetchEnrichment(enrichmentKey);
60
+ if (!enrichment) continue;
61
+
62
+ results.push(combineData(source, enrichment));
63
+ }
64
+
65
+ return results;
66
+ }
67
+
68
+ /**
69
+ * Map documents with multiple enrichments
70
+ *
71
+ * Similar to mapWithEnrichment but supports multiple enrichment sources.
72
+ * Useful when result needs data from multiple related collections.
73
+ *
74
+ * @param docs - Firestore document snapshots
75
+ * @param extractSource - Extract source data from document
76
+ * @param getEnrichmentKeys - Get all enrichment keys from source
77
+ * @param fetchEnrichments - Fetch all enrichment data by keys
78
+ * @param combineData - Combine source and enrichments into result
79
+ * @returns Array of enriched results
80
+ */
81
+ async mapWithMultipleEnrichments<TEnrichments extends Record<string, unknown>>(
82
+ docs: QueryDocumentSnapshot<DocumentData>[],
83
+ extractSource: (doc: QueryDocumentSnapshot<DocumentData>) => TSource | null,
84
+ getEnrichmentKeys: (source: TSource) => Record<string, string>,
85
+ fetchEnrichments: (keys: Record<string, string>) => Promise<TEnrichments | null>,
86
+ combineData: (source: TSource, enrichments: TEnrichments) => TResult,
87
+ ): Promise<TResult[]> {
88
+ const results: TResult[] = [];
89
+
90
+ for (const doc of docs) {
91
+ const source = extractSource(doc);
92
+ if (!source) continue;
93
+
94
+ const enrichmentKeys = getEnrichmentKeys(source);
95
+ const enrichments = await fetchEnrichments(enrichmentKeys);
96
+ if (!enrichments) continue;
97
+
98
+ results.push(combineData(source, enrichments));
99
+ }
100
+
101
+ return results;
102
+ }
103
+
104
+ /**
105
+ * Simple document mapping without enrichment
106
+ *
107
+ * @param docs - Firestore document snapshots
108
+ * @param extractData - Extract data from document
109
+ * @returns Array of extracted data (nulls filtered out)
110
+ */
111
+ map(
112
+ docs: QueryDocumentSnapshot<DocumentData>[],
113
+ extractData: (doc: QueryDocumentSnapshot<DocumentData>) => TResult | null,
114
+ ): TResult[] {
115
+ const results: TResult[] = [];
116
+
117
+ for (const doc of docs) {
118
+ const data = extractData(doc);
119
+ if (data) {
120
+ results.push(data);
121
+ }
122
+ }
123
+
124
+ return results;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Create document mapper helper
130
+ *
131
+ * @returns DocumentMapperHelper instance
132
+ *
133
+ * @example
134
+ * ```typescript
135
+ * const mapper = createDocumentMapper<Post, User, EnrichedPost>();
136
+ * const enriched = await mapper.mapWithEnrichment(...);
137
+ * ```
138
+ */
139
+ export function createDocumentMapper<
140
+ TSource,
141
+ TEnrichment,
142
+ TResult,
143
+ >(): DocumentMapperHelper<TSource, TEnrichment, TResult> {
144
+ return new DocumentMapperHelper<TSource, TEnrichment, TResult>();
145
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * Pagination Helper
3
+ *
4
+ * Utilities for cursor-based pagination in Firestore.
5
+ * Handles pagination logic, cursor management, and hasMore detection.
6
+ *
7
+ * App-agnostic: Works with any document type and any collection.
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { PaginationHelper } from '@umituz/react-native-firestore';
12
+ *
13
+ * const helper = new PaginationHelper<Post>();
14
+ * const result = helper.buildResult(posts, 10, post => post.id);
15
+ * ```
16
+ */
17
+
18
+ import type { PaginatedResult, PaginationParams } from '../types/pagination.types';
19
+
20
+ export class PaginationHelper<T> {
21
+ /**
22
+ * Build paginated result from items
23
+ *
24
+ * @param items - All items fetched (should be limit + 1)
25
+ * @param pageLimit - Requested page size
26
+ * @param getCursor - Function to extract cursor from item
27
+ * @returns Paginated result with hasMore and nextCursor
28
+ */
29
+ buildResult(
30
+ items: T[],
31
+ pageLimit: number,
32
+ getCursor: (item: T) => string,
33
+ ): PaginatedResult<T> {
34
+ const hasMore = items.length > pageLimit;
35
+ const resultItems = hasMore ? items.slice(0, pageLimit) : items;
36
+ const lastItem = resultItems[resultItems.length - 1];
37
+ const nextCursor = hasMore && lastItem
38
+ ? getCursor(lastItem)
39
+ : null;
40
+
41
+ return {
42
+ items: resultItems,
43
+ nextCursor,
44
+ hasMore,
45
+ };
46
+ }
47
+
48
+ /**
49
+ * Get default limit from params or use default
50
+ *
51
+ * @param params - Pagination params
52
+ * @param defaultLimit - Default limit if not specified
53
+ * @returns Page limit
54
+ */
55
+ getLimit(params?: PaginationParams, defaultLimit: number = 10): number {
56
+ return params?.limit || defaultLimit;
57
+ }
58
+
59
+ /**
60
+ * Calculate fetch limit (page limit + 1 for hasMore detection)
61
+ *
62
+ * @param pageLimit - Requested page size
63
+ * @returns Fetch limit (pageLimit + 1)
64
+ */
65
+ getFetchLimit(pageLimit: number): number {
66
+ return pageLimit + 1;
67
+ }
68
+
69
+ /**
70
+ * Check if params has cursor
71
+ *
72
+ * @param params - Pagination params
73
+ * @returns true if cursor exists
74
+ */
75
+ hasCursor(params?: PaginationParams): boolean {
76
+ return !!params?.cursor;
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Create pagination helper for a specific type
82
+ *
83
+ * @returns PaginationHelper instance
84
+ *
85
+ * @example
86
+ * ```typescript
87
+ * const helper = createPaginationHelper<Post>();
88
+ * const result = helper.buildResult(posts, 10, post => post.id);
89
+ * ```
90
+ */
91
+ export function createPaginationHelper<T>(): PaginationHelper<T> {
92
+ return new PaginationHelper<T>();
93
+ }
@@ -0,0 +1,188 @@
1
+ /**
2
+ * Query Builder Utility
3
+ * Single Responsibility: Build Firestore queries with advanced filtering
4
+ *
5
+ * App-agnostic utility for building Firestore queries.
6
+ * Supports:
7
+ * - Firestore 'in' operator (up to 10 values)
8
+ * - Firestore 'or' operator (for >10 values via chunking)
9
+ * - Single value filtering
10
+ * - Multiple field filtering
11
+ * - Date range filtering
12
+ * - Sorting
13
+ * - Limiting
14
+ *
15
+ * This utility is designed to be used across hundreds of apps.
16
+ * It provides a consistent interface for Firestore query building.
17
+ */
18
+
19
+ import {
20
+ collection,
21
+ query,
22
+ where,
23
+ orderBy,
24
+ limit as limitQuery,
25
+ startAfter,
26
+ or,
27
+ type Firestore,
28
+ type Query,
29
+ Timestamp,
30
+ type WhereFilterOp,
31
+ } from "firebase/firestore";
32
+
33
+ export interface FieldFilter {
34
+ field: string;
35
+ operator: WhereFilterOp;
36
+ value: string | number | boolean | string[] | number[];
37
+ }
38
+
39
+ export interface QueryBuilderOptions {
40
+ collectionName: string;
41
+ baseFilters?: FieldFilter[];
42
+ dateRange?: {
43
+ field: string;
44
+ startDate?: number;
45
+ endDate?: number;
46
+ };
47
+ sort?: {
48
+ field: string;
49
+ order?: "asc" | "desc";
50
+ };
51
+ limitValue?: number;
52
+ /**
53
+ * Cursor value for pagination (timestamp in milliseconds)
54
+ * Used with startAfter for cursor-based pagination
55
+ */
56
+ cursorValue?: number;
57
+ }
58
+
59
+ const MAX_IN_OPERATOR_VALUES = 10;
60
+
61
+ /**
62
+ * Build Firestore query with advanced filtering support
63
+ *
64
+ * @param db - Firestore database instance
65
+ * @param options - Query builder options
66
+ * @returns Firestore Query object
67
+ */
68
+ export function buildQuery(
69
+ db: Firestore,
70
+ options: QueryBuilderOptions,
71
+ ): Query {
72
+ const {
73
+ collectionName,
74
+ baseFilters = [],
75
+ dateRange,
76
+ sort,
77
+ limitValue,
78
+ cursorValue,
79
+ } = options;
80
+
81
+ const collectionRef = collection(db, collectionName);
82
+ let q: Query = collectionRef;
83
+
84
+ // Apply base filters
85
+ for (const filter of baseFilters) {
86
+ q = applyFieldFilter(q, filter);
87
+ }
88
+
89
+ // Apply date range filters
90
+ if (dateRange) {
91
+ if (dateRange.startDate) {
92
+ q = query(
93
+ q,
94
+ where(
95
+ dateRange.field,
96
+ ">=",
97
+ Timestamp.fromMillis(dateRange.startDate),
98
+ ),
99
+ );
100
+ }
101
+ if (dateRange.endDate) {
102
+ q = query(
103
+ q,
104
+ where(
105
+ dateRange.field,
106
+ "<=",
107
+ Timestamp.fromMillis(dateRange.endDate),
108
+ ),
109
+ );
110
+ }
111
+ }
112
+
113
+ // Apply sorting
114
+ if (sort) {
115
+ const sortOrder = sort.order || "desc";
116
+ q = query(q, orderBy(sort.field, sortOrder));
117
+ }
118
+
119
+ // Apply cursor for pagination (must come after orderBy)
120
+ if (cursorValue !== undefined) {
121
+ q = query(q, startAfter(Timestamp.fromMillis(cursorValue)));
122
+ }
123
+
124
+ // Apply limit
125
+ if (limitValue !== undefined) {
126
+ q = query(q, limitQuery(limitValue));
127
+ }
128
+
129
+ return q;
130
+ }
131
+
132
+ /**
133
+ * Apply field filter with support for 'in' operator and chunking
134
+ * Handles arrays by using 'in' operator (up to 10 values)
135
+ * For arrays >10 values, splits into chunks and uses 'or' operator
136
+ */
137
+ function applyFieldFilter(q: Query, filter: FieldFilter): Query {
138
+ const { field, operator, value } = filter;
139
+
140
+ // Handle 'in' operator with array values
141
+ if (operator === "in" && Array.isArray(value)) {
142
+ // Firestore 'in' operator supports up to 10 values
143
+ if (value.length <= MAX_IN_OPERATOR_VALUES) {
144
+ return query(q, where(field, "in", value));
145
+ }
146
+
147
+ // Split into chunks of 10 and use 'or' operator
148
+ const chunks: (string[] | number[])[] = [];
149
+ for (let i = 0; i < value.length; i += MAX_IN_OPERATOR_VALUES) {
150
+ chunks.push(value.slice(i, i + MAX_IN_OPERATOR_VALUES));
151
+ }
152
+
153
+ const orConditions = chunks.map((chunk) => where(field, "in", chunk));
154
+ return query(q, or(...orConditions));
155
+ }
156
+
157
+ // Standard filter
158
+ return query(q, where(field, operator, value));
159
+ }
160
+
161
+ /**
162
+ * Helper: Create a field filter for 'in' operator
163
+ * Automatically handles chunking if array >10 values
164
+ */
165
+ export function createInFilter(
166
+ field: string,
167
+ values: string[] | number[],
168
+ ): FieldFilter {
169
+ return {
170
+ field,
171
+ operator: "in",
172
+ value: values,
173
+ };
174
+ }
175
+
176
+ /**
177
+ * Helper: Create a field filter for equality
178
+ */
179
+ export function createEqualFilter(
180
+ field: string,
181
+ value: string | number | boolean,
182
+ ): FieldFilter {
183
+ return {
184
+ field,
185
+ operator: "==",
186
+ value,
187
+ };
188
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Quota Error Detector Utility
3
+ * Single Responsibility: Detect Firebase quota errors
4
+ *
5
+ * Firebase quota limits:
6
+ * - Free tier: 50K reads/day, 20K writes/day, 20K deletes/day
7
+ * - Blaze plan: Pay as you go, higher limits
8
+ *
9
+ * Quota errors are NOT retryable - quota won't increase by retrying
10
+ */
11
+
12
+ /**
13
+ * Check if error is a Firebase quota error
14
+ * Quota errors indicate daily read/write/delete limits are exceeded
15
+ *
16
+ * @param error - Error object to check
17
+ * @returns true if error is a quota error
18
+ */
19
+ export function isQuotaError(error: unknown): boolean {
20
+ if (!error || typeof error !== "object") {
21
+ return false;
22
+ }
23
+
24
+ const firebaseError = error as { code?: string; message?: string; name?: string };
25
+
26
+ // Check error code
27
+ if (firebaseError.code === "resource-exhausted") {
28
+ return true;
29
+ }
30
+
31
+ // Check error message
32
+ const errorMessage = firebaseError.message?.toLowerCase() || "";
33
+ if (
34
+ errorMessage.includes("quota") ||
35
+ errorMessage.includes("quota exceeded") ||
36
+ errorMessage.includes("resource-exhausted") ||
37
+ errorMessage.includes("daily limit")
38
+ ) {
39
+ return true;
40
+ }
41
+
42
+ // Check error name
43
+ const errorName = firebaseError.name?.toLowerCase() || "";
44
+ if (errorName.includes("quota") || errorName.includes("resource-exhausted")) {
45
+ return true;
46
+ }
47
+
48
+ return false;
49
+ }
50
+
51
+ /**
52
+ * Check if error is retryable
53
+ * Quota errors are NOT retryable
54
+ *
55
+ * @param error - Error object to check
56
+ * @returns true if error is retryable
57
+ */
58
+ export function isRetryableError(error: unknown): boolean {
59
+ // Quota errors are NOT retryable
60
+ if (isQuotaError(error)) {
61
+ return false;
62
+ }
63
+
64
+ if (!error || typeof error !== "object") {
65
+ return false;
66
+ }
67
+
68
+ const firebaseError = error as { code?: string; message?: string };
69
+
70
+ // Firestore transaction conflicts are retryable
71
+ if (firebaseError.code === "failed-precondition") {
72
+ return true;
73
+ }
74
+
75
+ // Network errors are retryable
76
+ if (
77
+ firebaseError.code === "unavailable" ||
78
+ firebaseError.code === "deadline-exceeded"
79
+ ) {
80
+ return true;
81
+ }
82
+
83
+ // Timeout errors are retryable
84
+ const errorMessage = firebaseError.message?.toLowerCase() || "";
85
+ if (errorMessage.includes("timeout")) {
86
+ return true;
87
+ }
88
+
89
+ return false;
90
+ }
91
+
92
+ /**
93
+ * Get user-friendly quota error message
94
+ *
95
+ * @returns User-friendly error message
96
+ */
97
+ export function getQuotaErrorMessage(): string {
98
+ return "Firebase quota exceeded. Please try again later or upgrade your Firebase plan.";
99
+ }
100
+
package/src/index.ts CHANGED
@@ -5,12 +5,14 @@
5
5
  *
6
6
  * This package provides Firebase App initialization and core services:
7
7
  * - Auth
8
+ * - Firestore
8
9
  * - Analytics
9
10
  * - Crashlytics
10
11
  *
11
12
  * Usage:
12
13
  * import { initializeFirebase, getFirebaseApp } from '@umituz/react-native-firebase';
13
14
  * import { useFirebaseAuth } from '@umituz/react-native-firebase';
15
+ * import { getFirestore, BaseRepository } from '@umituz/react-native-firebase';
14
16
  * import { firebaseAnalyticsService } from '@umituz/react-native-firebase';
15
17
  */
16
18
 
@@ -51,6 +53,12 @@ export type {
51
53
 
52
54
  export * from './auth';
53
55
 
56
+ // =============================================================================
57
+ // FIRESTORE MODULE
58
+ // =============================================================================
59
+
60
+ export * from './firestore';
61
+
54
62
  // =============================================================================
55
63
  // ANALYTICS MODULE
56
64
  // =============================================================================