@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,90 @@
1
+ /**
2
+ * Base Repository - Pagination Operations
3
+ *
4
+ * Provides pagination operations for Firestore repositories.
5
+ * Extends BaseQueryRepository with pagination-specific functionality.
6
+ */
7
+
8
+ import type { QueryDocumentSnapshot, DocumentData } from "firebase/firestore";
9
+ import { collection, query, orderBy, limit, startAfter, getDoc, doc, getDocs } from "firebase/firestore";
10
+ import { PaginationHelper } from "../../utils/pagination.helper";
11
+ import type { PaginatedResult, PaginationParams } from "../../types/pagination.types";
12
+ import { BaseQueryRepository } from "./BaseQueryRepository";
13
+
14
+ export abstract class BasePaginatedRepository extends BaseQueryRepository {
15
+ /**
16
+ * Execute paginated query with cursor support
17
+ *
18
+ * Generic helper for cursor-based pagination queries.
19
+ * Automatically handles cursor document fetching and result building.
20
+ *
21
+ * @param collectionName - Firestore collection name
22
+ * @param params - Pagination parameters
23
+ * @param orderByField - Field to order by (default: "createdAt")
24
+ * @param orderDirection - Sort direction (default: "desc")
25
+ * @returns QueryDocumentSnapshot array (limit + 1 for hasMore detection)
26
+ */
27
+ protected async executePaginatedQuery(
28
+ collectionName: string,
29
+ params?: PaginationParams,
30
+ orderByField: string = "createdAt",
31
+ orderDirection: "asc" | "desc" = "desc",
32
+ ): Promise<QueryDocumentSnapshot<DocumentData>[]> {
33
+ const db = this.getDbOrThrow();
34
+ const helper = new PaginationHelper();
35
+ const pageLimit = helper.getLimit(params);
36
+ const fetchLimit = helper.getFetchLimit(pageLimit);
37
+
38
+ const collectionRef = collection(db, collectionName);
39
+ let q = query(
40
+ collectionRef,
41
+ orderBy(orderByField, orderDirection),
42
+ limit(fetchLimit),
43
+ );
44
+
45
+ if (helper.hasCursor(params)) {
46
+ const cursorDoc = await getDoc(doc(db, collectionName, params!.cursor!));
47
+ if (cursorDoc.exists()) {
48
+ q = query(
49
+ collectionRef,
50
+ orderBy(orderByField, orderDirection),
51
+ startAfter(cursorDoc),
52
+ limit(fetchLimit),
53
+ );
54
+ }
55
+ }
56
+
57
+ const snapshot = await getDocs(q);
58
+ this.trackRead(collectionName, snapshot.docs.length, snapshot.metadata.fromCache);
59
+ return snapshot.docs;
60
+ }
61
+
62
+ /**
63
+ * Build paginated result from documents
64
+ *
65
+ * Helper to convert raw Firestore documents to paginated result.
66
+ * Works with any document type and cursor extraction logic.
67
+ *
68
+ * @param docs - Firestore document snapshots
69
+ * @param params - Pagination parameters
70
+ * @param extractData - Function to extract data from document
71
+ * @param getCursor - Function to extract cursor from data
72
+ * @returns Paginated result
73
+ */
74
+ protected buildPaginatedResult<T>(
75
+ docs: QueryDocumentSnapshot<DocumentData>[],
76
+ params: PaginationParams | undefined,
77
+ extractData: (doc: QueryDocumentSnapshot<DocumentData>) => T | null,
78
+ getCursor: (item: T) => string,
79
+ ): PaginatedResult<T> {
80
+ const items: T[] = [];
81
+ for (const doc of docs) {
82
+ const data = extractData(doc);
83
+ if (data) items.push(data);
84
+ }
85
+
86
+ const helper = new PaginationHelper<T>();
87
+ const pageLimit = helper.getLimit(params);
88
+ return helper.buildResult(items, pageLimit, getCursor);
89
+ }
90
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Base Repository - Query Operations
3
+ *
4
+ * Provides query and tracking operations for Firestore repositories.
5
+ * Extends BaseRepository with query-specific functionality.
6
+ */
7
+
8
+ import type { Query } from "firebase/firestore";
9
+ import { quotaTrackingMiddleware } from "../middleware/QuotaTrackingMiddleware";
10
+ import { queryDeduplicationMiddleware } from "../middleware/QueryDeduplicationMiddleware";
11
+ import { BaseRepository } from "./BaseRepository";
12
+
13
+ export abstract class BaseQueryRepository extends BaseRepository {
14
+ /**
15
+ * Execute query with deduplication and quota tracking
16
+ * Prevents duplicate queries and tracks quota usage
17
+ *
18
+ * @param collection - Collection name
19
+ * @param query - Firestore query
20
+ * @param queryFn - Function to execute the query
21
+ * @param cached - Whether the result is from cache
22
+ * @returns Query result
23
+ */
24
+ protected async executeQuery<T>(
25
+ collection: string,
26
+ query: Query,
27
+ queryFn: () => Promise<T>,
28
+ cached: boolean = false,
29
+ ): Promise<T> {
30
+ const queryKey = {
31
+ collection,
32
+ filters: query.toString(),
33
+ limit: undefined,
34
+ orderBy: undefined,
35
+ };
36
+
37
+ return queryDeduplicationMiddleware.deduplicate(queryKey, async () => {
38
+ return quotaTrackingMiddleware.trackOperation(
39
+ {
40
+ type: 'read',
41
+ collection,
42
+ count: 1,
43
+ cached,
44
+ },
45
+ queryFn,
46
+ );
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Track read operation
52
+ *
53
+ * @param collection - Collection name
54
+ * @param count - Number of documents read
55
+ * @param cached - Whether the result is from cache
56
+ */
57
+ protected override trackRead(
58
+ collection: string,
59
+ count: number = 1,
60
+ cached: boolean = false,
61
+ ): void {
62
+ quotaTrackingMiddleware.trackRead(collection, count, cached);
63
+ }
64
+
65
+ protected override trackWrite(
66
+ collection: string,
67
+ documentId?: string,
68
+ count: number = 1,
69
+ ): void {
70
+ quotaTrackingMiddleware.trackWrite(collection, documentId, count);
71
+ }
72
+
73
+ protected override trackDelete(
74
+ collection: string,
75
+ documentId?: string,
76
+ count: number = 1,
77
+ ): void {
78
+ quotaTrackingMiddleware.trackDelete(collection, documentId, count);
79
+ }
80
+ }
@@ -0,0 +1,147 @@
1
+ /**
2
+ * Base Repository - Core Operations
3
+ *
4
+ * Provides essential Firestore operations with centralized database access.
5
+ * All Firestore repositories should extend this class.
6
+ *
7
+ * Architecture:
8
+ * - DRY: Centralized database access (getDb)
9
+ * - SOLID: Single Responsibility - Database access only
10
+ * - KISS: Simple base class with protected db property
11
+ * - App-agnostic: Works with any app, no app-specific code
12
+ *
13
+ * This class is designed to be used across hundreds of apps.
14
+ * It provides a consistent interface for Firestore operations.
15
+ */
16
+
17
+ import type { Firestore } from "firebase/firestore";
18
+ import { getFirestore } from "../config/FirestoreClient";
19
+ import {
20
+ isQuotaError as checkQuotaError,
21
+ getQuotaErrorMessage,
22
+ } from "../../utils/quota-error-detector.util";
23
+ import { FirebaseFirestoreQuotaError } from "../../domain/errors/FirebaseFirestoreError";
24
+
25
+ export class BaseRepository {
26
+ private isDestroyed = false;
27
+
28
+ /**
29
+ * Get Firestore database instance
30
+ * Returns null if Firestore is not initialized (offline mode)
31
+ * Use getDbOrThrow() if you need to throw an error instead
32
+ *
33
+ * @returns Firestore instance or null if not initialized
34
+ */
35
+ protected getDb(): Firestore | null {
36
+ if (this.isDestroyed) {
37
+ if (__DEV__) {
38
+ console.warn('[BaseRepository] Attempted to use destroyed repository');
39
+ }
40
+ return null;
41
+ }
42
+ return getFirestore();
43
+ }
44
+
45
+ /**
46
+ * Get Firestore database instance or throw error
47
+ * Throws error if Firestore is not initialized
48
+ * Use this method when Firestore is required for the operation
49
+ *
50
+ * @returns Firestore instance
51
+ * @throws Error if Firestore is not initialized
52
+ */
53
+ protected getDbOrThrow(): Firestore {
54
+ const db = getFirestore();
55
+ if (!db) {
56
+ throw new Error("Firestore is not initialized. Please initialize Firebase App first.");
57
+ }
58
+ return db;
59
+ }
60
+
61
+ /**
62
+ * Check if Firestore is initialized
63
+ * Useful for conditional operations
64
+ *
65
+ * @returns true if Firestore is initialized, false otherwise
66
+ */
67
+ protected isDbInitialized(): boolean {
68
+ try {
69
+ const db = getFirestore();
70
+ return db !== null;
71
+ } catch {
72
+ return false;
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Check if error is a quota error
78
+ * Quota errors indicate daily read/write/delete limits are exceeded
79
+ *
80
+ * @param error - Error to check
81
+ * @returns true if error is a quota error
82
+ */
83
+ protected isQuotaError(error: unknown): boolean {
84
+ return checkQuotaError(error);
85
+ }
86
+
87
+ /**
88
+ * Handle quota error
89
+ * Throws FirebaseFirestoreQuotaError with user-friendly message
90
+ *
91
+ * @param error - Original error
92
+ * @throws FirebaseFirestoreQuotaError
93
+ */
94
+ protected handleQuotaError(error: unknown): never {
95
+ const message = getQuotaErrorMessage();
96
+ throw new FirebaseFirestoreQuotaError(message, error);
97
+ }
98
+
99
+ /**
100
+ * Wrap Firestore operation with quota error handling
101
+ * Automatically detects and handles quota errors
102
+ *
103
+ * @param operation - Firestore operation to execute
104
+ * @returns Result of the operation
105
+ * @throws FirebaseFirestoreQuotaError if quota error occurs
106
+ */
107
+ protected async executeWithQuotaHandling<T>(
108
+ operation: () => Promise<T>,
109
+ ): Promise<T> {
110
+ try {
111
+ return await operation();
112
+ } catch (error) {
113
+ if (this.isQuotaError(error)) {
114
+ this.handleQuotaError(error);
115
+ }
116
+ throw error;
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Track read operations (stub for analytics)
122
+ * @param collection - Collection name
123
+ * @param count - Number of reads
124
+ * @param cached - Whether read was from cache
125
+ */
126
+ protected trackRead(_collection: string, _count: number, _cached: boolean): void {
127
+ // Stub for future analytics implementation
128
+ }
129
+
130
+ protected trackWrite(_collection: string, _docId: string, _count: number): void {
131
+ // Stub for future analytics implementation
132
+ }
133
+
134
+ protected trackDelete(_collection: string, _docId: string, _count: number): void {
135
+ // Stub for future analytics implementation
136
+ }
137
+
138
+ /**
139
+ * Destroy repository and cleanup resources
140
+ */
141
+ destroy(): void {
142
+ this.isDestroyed = true;
143
+ if (__DEV__) {
144
+ console.log('[BaseRepository] Repository destroyed');
145
+ }
146
+ }
147
+ }
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Quota Monitor Service
3
+ * Infrastructure service for monitoring Firestore quota usage
4
+ */
5
+
6
+ import type { QuotaMetrics, QuotaLimits, QuotaStatus } from '../../domain/entities/QuotaMetrics';
7
+ import { QuotaCalculator } from '../../domain/services/QuotaCalculator';
8
+
9
+ export class QuotaMonitorService {
10
+ private metrics: QuotaMetrics = {
11
+ readCount: 0,
12
+ writeCount: 0,
13
+ deleteCount: 0,
14
+ timestamp: Date.now(),
15
+ };
16
+
17
+ private limits: QuotaLimits = QuotaCalculator.getDefaultLimits();
18
+ private listeners: Set<(status: QuotaStatus) => void> = new Set();
19
+
20
+ /**
21
+ * Set quota limits
22
+ */
23
+ setLimits(limits: Partial<QuotaLimits>): void {
24
+ this.limits = { ...this.limits, ...limits };
25
+ }
26
+
27
+ /**
28
+ * Increment read count
29
+ */
30
+ incrementRead(count: number = 1): void {
31
+ this.metrics.readCount += count;
32
+ this.notifyListeners();
33
+ }
34
+
35
+ /**
36
+ * Increment write count
37
+ */
38
+ incrementWrite(count: number = 1): void {
39
+ this.metrics.writeCount += count;
40
+ this.notifyListeners();
41
+ }
42
+
43
+ /**
44
+ * Increment delete count
45
+ */
46
+ incrementDelete(count: number = 1): void {
47
+ this.metrics.deleteCount += count;
48
+ this.notifyListeners();
49
+ }
50
+
51
+ /**
52
+ * Get current metrics
53
+ */
54
+ getMetrics(): QuotaMetrics {
55
+ return { ...this.metrics };
56
+ }
57
+
58
+ /**
59
+ * Get current status
60
+ */
61
+ getStatus(): QuotaStatus {
62
+ return QuotaCalculator.calculateStatus(this.metrics, this.limits);
63
+ }
64
+
65
+ /**
66
+ * Reset metrics
67
+ */
68
+ resetMetrics(): void {
69
+ this.metrics = {
70
+ readCount: 0,
71
+ writeCount: 0,
72
+ deleteCount: 0,
73
+ timestamp: Date.now(),
74
+ };
75
+ this.notifyListeners();
76
+ }
77
+
78
+ /**
79
+ * Add status change listener
80
+ */
81
+ addListener(listener: (status: QuotaStatus) => void): () => void {
82
+ this.listeners.add(listener);
83
+ return () => {
84
+ this.listeners.delete(listener);
85
+ };
86
+ }
87
+
88
+ /**
89
+ * Notify all listeners
90
+ */
91
+ private notifyListeners(): void {
92
+ const status = this.getStatus();
93
+ this.listeners.forEach((listener) => {
94
+ try {
95
+ listener(status);
96
+ } catch (error) {
97
+ /* eslint-disable-next-line no-console */
98
+ if (__DEV__) {
99
+ /* eslint-disable-next-line no-console */
100
+ console.error('[QuotaMonitor] Listener error:', error);
101
+ }
102
+ }
103
+ });
104
+ }
105
+ }
106
+
107
+ export const quotaMonitorService = new QuotaMonitorService();
108
+
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Request Logger Service
3
+ * Infrastructure service for logging Firestore requests
4
+ */
5
+
6
+ import type { RequestLog, RequestStats, RequestType } from '../../domain/entities/RequestLog';
7
+
8
+ export class RequestLoggerService {
9
+ private logs: RequestLog[] = [];
10
+ private readonly MAX_LOGS = 1000;
11
+ private listeners: Set<(log: RequestLog) => void> = new Set();
12
+
13
+ /**
14
+ * Log a request
15
+ */
16
+ logRequest(log: Omit<RequestLog, 'id' | 'timestamp'>): void {
17
+ const fullLog: RequestLog = {
18
+ ...log,
19
+ id: this.generateId(),
20
+ timestamp: Date.now(),
21
+ };
22
+
23
+ this.logs.push(fullLog);
24
+
25
+ if (this.logs.length > this.MAX_LOGS) {
26
+ this.logs.shift();
27
+ }
28
+
29
+ // Log Firestore operations in development mode
30
+ if (__DEV__) {
31
+ const prefix = fullLog.cached ? '[Firestore Cache]' : '[Firestore]';
32
+ const operation = fullLog.type.toUpperCase();
33
+ const status = fullLog.success ? '✓' : '✗';
34
+ const details = fullLog.documentId
35
+ ? `${fullLog.collection}/${fullLog.documentId}`
36
+ : fullLog.collection;
37
+
38
+ if (fullLog.success) {
39
+ // eslint-disable-next-line no-console
40
+ console.log(`${prefix} ${status} ${operation}: ${details}`);
41
+ } else {
42
+ // eslint-disable-next-line no-console
43
+ console.error(`${prefix} ${status} ${operation}: ${details}`, fullLog.error);
44
+ }
45
+ }
46
+
47
+ this.notifyListeners(fullLog);
48
+ }
49
+
50
+ /**
51
+ * Get all logs
52
+ */
53
+ getLogs(): RequestLog[] {
54
+ return [...this.logs];
55
+ }
56
+
57
+ /**
58
+ * Get logs by type
59
+ */
60
+ getLogsByType(type: RequestType): RequestLog[] {
61
+ return this.logs.filter((log) => log.type === type);
62
+ }
63
+
64
+ /**
65
+ * Get request statistics
66
+ */
67
+ getStats(): RequestStats {
68
+ const totalRequests = this.logs.length;
69
+ const readRequests = this.logs.filter((l) => l.type === 'read').length;
70
+ const writeRequests = this.logs.filter((l) => l.type === 'write').length;
71
+ const deleteRequests = this.logs.filter((l) => l.type === 'delete').length;
72
+ const listenerRequests = this.logs.filter((l) => l.type === 'listener').length;
73
+ const cachedRequests = this.logs.filter((l) => l.cached).length;
74
+ const failedRequests = this.logs.filter((l) => !l.success).length;
75
+
76
+ const durations = this.logs
77
+ .filter((l) => l.duration !== undefined)
78
+ .map((l) => l.duration!);
79
+ const averageDuration =
80
+ durations.length > 0
81
+ ? durations.reduce((sum, d) => sum + d, 0) / durations.length
82
+ : 0;
83
+
84
+ return {
85
+ totalRequests,
86
+ readRequests,
87
+ writeRequests,
88
+ deleteRequests,
89
+ listenerRequests,
90
+ cachedRequests,
91
+ failedRequests,
92
+ averageDuration,
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Clear all logs
98
+ */
99
+ clearLogs(): void {
100
+ this.logs = [];
101
+ }
102
+
103
+ /**
104
+ * Add log listener
105
+ */
106
+ addListener(listener: (log: RequestLog) => void): () => void {
107
+ this.listeners.add(listener);
108
+ return () => {
109
+ this.listeners.delete(listener);
110
+ };
111
+ }
112
+
113
+ /**
114
+ * Generate unique ID
115
+ */
116
+ private generateId(): string {
117
+ return `${Date.now()}-${Math.random().toString(36).substring(2, 11)}`;
118
+ }
119
+
120
+ /**
121
+ * Notify all listeners
122
+ */
123
+ private notifyListeners(log: RequestLog): void {
124
+ this.listeners.forEach((listener) => {
125
+ try {
126
+ listener(log);
127
+ } catch (error) {
128
+ /* eslint-disable-next-line no-console */
129
+ if (__DEV__) {
130
+ /* eslint-disable-next-line no-console */
131
+ console.error('[RequestLogger] Listener error:', error);
132
+ }
133
+ }
134
+ });
135
+ }
136
+ }
137
+
138
+ export const requestLoggerService = new RequestLoggerService();
139
+
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Pagination Types
3
+ *
4
+ * Generic types for cursor-based pagination in Firestore.
5
+ * Used across hundreds of apps for consistent pagination interface.
6
+ *
7
+ * @example
8
+ * ```typescript
9
+ * const result: PaginatedResult<Post> = await repository.getPosts({ limit: 10 });
10
+ * console.log(result.items); // Post[]
11
+ * console.log(result.hasMore); // boolean
12
+ * console.log(result.nextCursor); // string | null
13
+ * ```
14
+ */
15
+
16
+ /**
17
+ * Pagination parameters for queries
18
+ */
19
+ export interface PaginationParams {
20
+ /**
21
+ * Maximum number of items to return
22
+ * @default 10
23
+ */
24
+ limit?: number;
25
+
26
+ /**
27
+ * Cursor for pagination (document ID)
28
+ * Use this to fetch next page
29
+ */
30
+ cursor?: string;
31
+ }
32
+
33
+ /**
34
+ * Paginated result structure
35
+ */
36
+ export interface PaginatedResult<T> {
37
+ /**
38
+ * Array of items in current page
39
+ */
40
+ items: T[];
41
+
42
+ /**
43
+ * Cursor for next page (null if no more items)
44
+ */
45
+ nextCursor: string | null;
46
+
47
+ /**
48
+ * Whether there are more items to fetch
49
+ */
50
+ hasMore: boolean;
51
+ }
52
+
53
+ /**
54
+ * Empty paginated result
55
+ */
56
+ export const EMPTY_PAGINATED_RESULT: PaginatedResult<never> = {
57
+ items: [],
58
+ nextCursor: null,
59
+ hasMore: false,
60
+ };
@@ -0,0 +1,31 @@
1
+ import { Timestamp } from 'firebase/firestore';
2
+
3
+ /**
4
+ * Convert ISO string to Firestore Timestamp
5
+ */
6
+ export function isoToTimestamp(isoString: string): Timestamp {
7
+ return Timestamp.fromDate(new Date(isoString));
8
+ }
9
+
10
+ /**
11
+ * Convert Firestore Timestamp to ISO string
12
+ */
13
+ export function timestampToISO(timestamp: Timestamp): string {
14
+ if (!timestamp) return new Date().toISOString();
15
+ return timestamp.toDate().toISOString();
16
+ }
17
+
18
+ /**
19
+ * Convert Firestore Timestamp to Date
20
+ */
21
+ export function timestampToDate(timestamp: Timestamp): Date {
22
+ if (!timestamp) return new Date();
23
+ return timestamp.toDate();
24
+ }
25
+
26
+ /**
27
+ * Get current date as ISO string
28
+ */
29
+ export function getCurrentISOString(): string {
30
+ return new Date().toISOString();
31
+ }