@umituz/react-native-firebase 1.13.2 → 1.13.4

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 (44) hide show
  1. package/package.json +10 -2
  2. package/src/auth/domain/entities/AnonymousUser.ts +44 -0
  3. package/src/auth/domain/errors/FirebaseAuthError.ts +18 -0
  4. package/src/auth/domain/value-objects/FirebaseAuthConfig.ts +45 -0
  5. package/src/auth/index.ts +146 -0
  6. package/src/auth/infrastructure/config/FirebaseAuthClient.ts +210 -0
  7. package/src/auth/infrastructure/config/initializers/FirebaseAuthInitializer.ts +148 -0
  8. package/src/auth/infrastructure/services/account-deletion.service.ts +250 -0
  9. package/src/auth/infrastructure/services/anonymous-auth.service.ts +135 -0
  10. package/src/auth/infrastructure/services/apple-auth.service.ts +146 -0
  11. package/src/auth/infrastructure/services/auth-guard.service.ts +97 -0
  12. package/src/auth/infrastructure/services/auth-utils.service.ts +168 -0
  13. package/src/auth/infrastructure/services/firestore-utils.service.ts +155 -0
  14. package/src/auth/infrastructure/services/google-auth.service.ts +100 -0
  15. package/src/auth/infrastructure/services/reauthentication.service.ts +216 -0
  16. package/src/auth/presentation/hooks/useAnonymousAuth.ts +201 -0
  17. package/src/auth/presentation/hooks/useFirebaseAuth.ts +84 -0
  18. package/src/auth/presentation/hooks/useSocialAuth.ts +162 -0
  19. package/src/firestore/__tests__/BaseRepository.test.ts +133 -0
  20. package/src/firestore/__tests__/QueryDeduplicationMiddleware.test.ts +147 -0
  21. package/src/firestore/__tests__/mocks/react-native-firebase.ts +23 -0
  22. package/src/firestore/__tests__/setup.ts +36 -0
  23. package/src/firestore/domain/constants/QuotaLimits.ts +97 -0
  24. package/src/firestore/domain/entities/QuotaMetrics.ts +28 -0
  25. package/src/firestore/domain/entities/RequestLog.ts +30 -0
  26. package/src/firestore/domain/errors/FirebaseFirestoreError.ts +52 -0
  27. package/src/firestore/domain/services/QuotaCalculator.ts +70 -0
  28. package/src/firestore/index.ts +174 -0
  29. package/src/firestore/infrastructure/config/FirestoreClient.ts +181 -0
  30. package/src/firestore/infrastructure/config/initializers/FirebaseFirestoreInitializer.ts +46 -0
  31. package/src/firestore/infrastructure/middleware/QueryDeduplicationMiddleware.ts +153 -0
  32. package/src/firestore/infrastructure/middleware/QuotaTrackingMiddleware.ts +165 -0
  33. package/src/firestore/infrastructure/repositories/BasePaginatedRepository.ts +90 -0
  34. package/src/firestore/infrastructure/repositories/BaseQueryRepository.ts +80 -0
  35. package/src/firestore/infrastructure/repositories/BaseRepository.ts +147 -0
  36. package/src/firestore/infrastructure/services/QuotaMonitorService.ts +108 -0
  37. package/src/firestore/infrastructure/services/RequestLoggerService.ts +139 -0
  38. package/src/firestore/types/pagination.types.ts +60 -0
  39. package/src/firestore/utils/dateUtils.ts +31 -0
  40. package/src/firestore/utils/document-mapper.helper.ts +145 -0
  41. package/src/firestore/utils/pagination.helper.ts +93 -0
  42. package/src/firestore/utils/query-builder.ts +188 -0
  43. package/src/firestore/utils/quota-error-detector.util.ts +100 -0
  44. package/src/index.ts +16 -0
@@ -0,0 +1,181 @@
1
+ /**
2
+ * Firestore Client - Infrastructure Layer
3
+ *
4
+ * Domain-Driven Design: Infrastructure implementation of Firestore client
5
+ * Singleton pattern for managing Firestore instance
6
+ *
7
+ * IMPORTANT: This package requires Firebase App to be initialized first.
8
+ * Use @umituz/react-native-firebase to initialize Firebase App.
9
+ *
10
+ * SOLID Principles:
11
+ * - Single Responsibility: Only manages Firestore initialization
12
+ * - Open/Closed: Extensible through configuration, closed for modification
13
+ * - Dependency Inversion: Depends on Firebase App from @umituz/react-native-firebase
14
+ */
15
+
16
+ import type { Firestore } from 'firebase/firestore';
17
+ import { getFirebaseApp } from '../../../infrastructure/config/FirebaseClient';
18
+ import { FirebaseFirestoreInitializer } from './initializers/FirebaseFirestoreInitializer';
19
+
20
+ /**
21
+ * Firestore Client Singleton
22
+ * Manages Firestore initialization
23
+ */
24
+ class FirestoreClientSingleton {
25
+ private static instance: FirestoreClientSingleton | null = null;
26
+ private firestore: Firestore | null = null;
27
+ private initializationError: string | null = null;
28
+
29
+ private constructor() {
30
+ // Private constructor to enforce singleton pattern
31
+ }
32
+
33
+ /**
34
+ * Get singleton instance
35
+ */
36
+ static getInstance(): FirestoreClientSingleton {
37
+ if (!FirestoreClientSingleton.instance) {
38
+ FirestoreClientSingleton.instance = new FirestoreClientSingleton();
39
+ }
40
+ return FirestoreClientSingleton.instance;
41
+ }
42
+
43
+ /**
44
+ * Initialize Firestore
45
+ * Requires Firebase App to be initialized first via @umituz/react-native-firebase
46
+ *
47
+ * @returns Firestore instance or null if initialization fails
48
+ */
49
+ initialize(): Firestore | null {
50
+ if (this.firestore) {
51
+ return this.firestore;
52
+ }
53
+ if (this.initializationError) {
54
+ return null;
55
+ }
56
+ try {
57
+ const app = getFirebaseApp(); // Get the core Firebase App
58
+
59
+ // Return null if Firebase App is not available (offline mode)
60
+ if (!app) {
61
+ return null;
62
+ }
63
+
64
+ this.firestore = FirebaseFirestoreInitializer.initialize(app);
65
+ return this.firestore;
66
+ } catch (error) {
67
+ this.initializationError =
68
+ error instanceof Error
69
+ ? error.message
70
+ : 'Failed to initialize Firestore client';
71
+ return null;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Get Firestore instance
77
+ * Auto-initializes if Firebase App is available
78
+ * Returns null if config is not available (offline mode - no error)
79
+ * @returns Firestore instance or null if not initialized
80
+ */
81
+ getFirestore(): Firestore | null {
82
+ // Auto-initialize if not already initialized
83
+ if (!this.firestore && !this.initializationError) {
84
+ try {
85
+ // Try to get Firebase App (will auto-initialize if config is available)
86
+ const app = getFirebaseApp();
87
+ if (app) {
88
+ this.initialize();
89
+ }
90
+ } catch {
91
+ // Firebase App not available, return null (offline mode)
92
+ return null;
93
+ }
94
+ }
95
+
96
+ // Return null if not initialized (offline mode - no error)
97
+ return this.firestore || null;
98
+ }
99
+
100
+ /**
101
+ * Check if Firestore is initialized
102
+ */
103
+ isInitialized(): boolean {
104
+ return this.firestore !== null;
105
+ }
106
+
107
+ /**
108
+ * Get initialization error if any
109
+ */
110
+ getInitializationError(): string | null {
111
+ return this.initializationError;
112
+ }
113
+
114
+ /**
115
+ * Reset Firestore client instance
116
+ * Useful for testing
117
+ */
118
+ reset(): void {
119
+ this.firestore = null;
120
+ this.initializationError = null;
121
+ }
122
+ }
123
+
124
+ export const firestoreClient = FirestoreClientSingleton.getInstance();
125
+
126
+ /**
127
+ * Initialize Firestore
128
+ * Requires Firebase App to be initialized first via @umituz/react-native-firebase
129
+ *
130
+ * @returns Firestore instance or null if initialization fails
131
+ *
132
+ * @example
133
+ * ```typescript
134
+ * import { initializeFirebase } from '@umituz/react-native-firebase';
135
+ * import { initializeFirestore } from '@umituz/react-native-firestore';
136
+ *
137
+ * // Initialize Firebase App first
138
+ * const app = initializeFirebase(config);
139
+ *
140
+ * // Then initialize Firestore
141
+ * const db = initializeFirestore();
142
+ * ```
143
+ */
144
+ export function initializeFirestore(): Firestore | null {
145
+ return firestoreClient.initialize();
146
+ }
147
+
148
+ /**
149
+ * Get Firestore instance
150
+ * Auto-initializes if Firebase App is available
151
+ * Returns null if config is not available (offline mode - no error)
152
+ * @returns Firestore instance or null if not initialized
153
+ */
154
+ export function getFirestore(): Firestore | null {
155
+ return firestoreClient.getFirestore();
156
+ }
157
+
158
+ /**
159
+ * Check if Firestore is initialized
160
+ */
161
+ export function isFirestoreInitialized(): boolean {
162
+ return firestoreClient.isInitialized();
163
+ }
164
+
165
+ /**
166
+ * Get Firestore initialization error if any
167
+ */
168
+ export function getFirestoreInitializationError(): string | null {
169
+ return firestoreClient.getInitializationError();
170
+ }
171
+
172
+ /**
173
+ * Reset Firestore client instance
174
+ * Useful for testing
175
+ */
176
+ export function resetFirestoreClient(): void {
177
+ firestoreClient.reset();
178
+ }
179
+
180
+ export type { Firestore } from 'firebase/firestore';
181
+
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Firebase Firestore Initializer
3
+ *
4
+ * Single Responsibility: Initialize Firestore instance
5
+ *
6
+ * NOTE: React Native does not support IndexedDB (browser API), so we use
7
+ * memoryLocalCache instead of persistentLocalCache. For client-side caching,
8
+ * use TanStack Query which works on all platforms.
9
+ */
10
+
11
+ import {
12
+ getFirestore,
13
+ initializeFirestore,
14
+ memoryLocalCache,
15
+ } from 'firebase/firestore';
16
+ import type { Firestore } from 'firebase/firestore';
17
+ import type { FirebaseApp } from 'firebase/app';
18
+
19
+ /**
20
+ * Initializes Firestore
21
+ * Platform-agnostic: Works on all platforms (Web, iOS, Android)
22
+ */
23
+ export class FirebaseFirestoreInitializer {
24
+ /**
25
+ * Initialize Firestore with memory cache configuration
26
+ * React Native does not support IndexedDB, so we use memory cache
27
+ * For offline persistence, use TanStack Query with AsyncStorage
28
+ */
29
+ static initialize(app: FirebaseApp): Firestore {
30
+ try {
31
+ // Use memory cache for React Native compatibility
32
+ // IndexedDB (persistentLocalCache) is not available in React Native
33
+ return initializeFirestore(app, {
34
+ localCache: memoryLocalCache(),
35
+ });
36
+ } catch (error: any) {
37
+ // If already initialized, get existing instance
38
+ if (error.code === 'failed-precondition') {
39
+ return getFirestore(app);
40
+ }
41
+
42
+ return getFirestore(app);
43
+ }
44
+ }
45
+ }
46
+
@@ -0,0 +1,153 @@
1
+ /**
2
+ * Query Deduplication Middleware
3
+ * Prevents duplicate Firestore queries within a short time window
4
+ */
5
+
6
+ interface PendingQuery {
7
+ promise: Promise<unknown>;
8
+ timestamp: number;
9
+ }
10
+
11
+ interface QueryKey {
12
+ collection: string;
13
+ filters: string;
14
+ limit?: number;
15
+ orderBy?: string;
16
+ }
17
+
18
+ export class QueryDeduplicationMiddleware {
19
+ private pendingQueries = new Map<string, PendingQuery>();
20
+ private readonly DEDUPLICATION_WINDOW_MS = 1000; // 1 second
21
+ private readonly CLEANUP_INTERVAL_MS = 5000; // 5 seconds
22
+ private cleanupTimer: NodeJS.Timeout | null = null;
23
+
24
+ constructor() {
25
+ this.startCleanupTimer();
26
+ }
27
+
28
+ /**
29
+ * Start cleanup timer to prevent memory leaks
30
+ */
31
+ private startCleanupTimer(): void {
32
+ if (this.cleanupTimer) {
33
+ clearInterval(this.cleanupTimer);
34
+ }
35
+
36
+ this.cleanupTimer = setInterval(() => {
37
+ this.cleanupExpiredQueries();
38
+ }, this.CLEANUP_INTERVAL_MS);
39
+ }
40
+
41
+ /**
42
+ * Clean up expired queries to prevent memory leaks
43
+ */
44
+ private cleanupExpiredQueries(): void {
45
+ const now = Date.now();
46
+ for (const [key, query] of this.pendingQueries.entries()) {
47
+ if (now - query.timestamp > this.DEDUPLICATION_WINDOW_MS) {
48
+ this.pendingQueries.delete(key);
49
+ }
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Generate query key from query parameters
55
+ */
56
+ private generateQueryKey(key: QueryKey): string {
57
+ const parts = [
58
+ key.collection,
59
+ key.filters,
60
+ key.limit?.toString() || '',
61
+ key.orderBy || '',
62
+ ];
63
+ return parts.join('|');
64
+ }
65
+
66
+ /**
67
+ * Check if query is already pending
68
+ */
69
+ private isQueryPending(key: string): boolean {
70
+ const pending = this.pendingQueries.get(key);
71
+ if (!pending) return false;
72
+
73
+ const age = Date.now() - pending.timestamp;
74
+ if (age > this.DEDUPLICATION_WINDOW_MS) {
75
+ this.pendingQueries.delete(key);
76
+ return false;
77
+ }
78
+
79
+ return true;
80
+ }
81
+
82
+ /**
83
+ * Get pending query promise
84
+ */
85
+ private getPendingQuery(key: string): Promise<unknown> | null {
86
+ const pending = this.pendingQueries.get(key);
87
+ return pending ? pending.promise : null;
88
+ }
89
+
90
+ /**
91
+ * Add query to pending list
92
+ */
93
+ private addPendingQuery(key: string, promise: Promise<unknown>): void {
94
+ this.pendingQueries.set(key, {
95
+ promise,
96
+ timestamp: Date.now(),
97
+ });
98
+
99
+ promise.finally(() => {
100
+ this.pendingQueries.delete(key);
101
+ });
102
+ }
103
+
104
+ /**
105
+ * Deduplicate a query
106
+ */
107
+ async deduplicate<T>(
108
+ queryKey: QueryKey,
109
+ queryFn: () => Promise<T>,
110
+ ): Promise<T> {
111
+ const key = this.generateQueryKey(queryKey);
112
+
113
+ if (this.isQueryPending(key)) {
114
+ const pendingPromise = this.getPendingQuery(key);
115
+ if (pendingPromise) {
116
+ return pendingPromise as Promise<T>;
117
+ }
118
+ }
119
+
120
+ const promise = queryFn();
121
+ this.addPendingQuery(key, promise);
122
+
123
+ return promise;
124
+ }
125
+
126
+ /**
127
+ * Clear all pending queries
128
+ */
129
+ clear(): void {
130
+ this.pendingQueries.clear();
131
+ }
132
+
133
+ /**
134
+ * Destroy middleware and cleanup resources
135
+ */
136
+ destroy(): void {
137
+ if (this.cleanupTimer) {
138
+ clearInterval(this.cleanupTimer);
139
+ this.cleanupTimer = null;
140
+ }
141
+ this.pendingQueries.clear();
142
+ }
143
+
144
+ /**
145
+ * Get pending queries count
146
+ */
147
+ getPendingCount(): number {
148
+ return this.pendingQueries.size;
149
+ }
150
+ }
151
+
152
+ export const queryDeduplicationMiddleware = new QueryDeduplicationMiddleware();
153
+
@@ -0,0 +1,165 @@
1
+ /**
2
+ * Quota Tracking Middleware
3
+ * Tracks Firestore operations for quota monitoring
4
+ */
5
+
6
+ import { quotaMonitorService } from '../services/QuotaMonitorService';
7
+ import { requestLoggerService } from '../services/RequestLoggerService';
8
+ import type { RequestType } from '../../domain/entities/RequestLog';
9
+
10
+ interface TrackedOperation {
11
+ type: RequestType;
12
+ collection: string;
13
+ documentId?: string;
14
+ count: number;
15
+ cached?: boolean;
16
+ }
17
+
18
+ export class QuotaTrackingMiddleware {
19
+ /**
20
+ * Track a read operation
21
+ */
22
+ trackRead(collection: string, count: number = 1, cached: boolean = false): void {
23
+ quotaMonitorService.incrementRead(count);
24
+ requestLoggerService.logRequest({
25
+ type: 'read',
26
+ collection,
27
+ success: true,
28
+ cached,
29
+ });
30
+ }
31
+
32
+ /**
33
+ * Track a write operation
34
+ */
35
+ trackWrite(
36
+ collection: string,
37
+ documentId?: string,
38
+ count: number = 1,
39
+ ): void {
40
+ quotaMonitorService.incrementWrite(count);
41
+ requestLoggerService.logRequest({
42
+ type: 'write',
43
+ collection,
44
+ documentId,
45
+ success: true,
46
+ cached: false,
47
+ });
48
+ }
49
+
50
+ /**
51
+ * Track a delete operation
52
+ */
53
+ trackDelete(
54
+ collection: string,
55
+ documentId?: string,
56
+ count: number = 1,
57
+ ): void {
58
+ quotaMonitorService.incrementDelete(count);
59
+ requestLoggerService.logRequest({
60
+ type: 'delete',
61
+ collection,
62
+ documentId,
63
+ success: true,
64
+ cached: false,
65
+ });
66
+ }
67
+
68
+ /**
69
+ * Track a listener operation
70
+ */
71
+ trackListener(collection: string, documentId?: string): void {
72
+ requestLoggerService.logRequest({
73
+ type: 'listener',
74
+ collection,
75
+ documentId,
76
+ success: true,
77
+ cached: false,
78
+ });
79
+ }
80
+
81
+ /**
82
+ * Track a failed operation
83
+ */
84
+ trackError(
85
+ type: RequestType,
86
+ collection: string,
87
+ error: string,
88
+ documentId?: string,
89
+ ): void {
90
+ requestLoggerService.logRequest({
91
+ type,
92
+ collection,
93
+ documentId,
94
+ success: false,
95
+ error,
96
+ cached: false,
97
+ });
98
+ }
99
+
100
+ /**
101
+ * Track operation with timing
102
+ */
103
+ async trackOperation<T>(
104
+ operation: TrackedOperation,
105
+ operationFn: () => Promise<T>,
106
+ ): Promise<T> {
107
+ const startTime = Date.now();
108
+
109
+ try {
110
+ const result = await operationFn();
111
+ const duration = Date.now() - startTime;
112
+
113
+ if (operation.type === 'read') {
114
+ quotaMonitorService.incrementRead(operation.count);
115
+ requestLoggerService.logRequest({
116
+ type: 'read',
117
+ collection: operation.collection,
118
+ documentId: operation.documentId,
119
+ success: true,
120
+ cached: operation.cached || false,
121
+ duration,
122
+ });
123
+ } else if (operation.type === 'write') {
124
+ quotaMonitorService.incrementWrite(operation.count);
125
+ requestLoggerService.logRequest({
126
+ type: 'write',
127
+ collection: operation.collection,
128
+ documentId: operation.documentId,
129
+ success: true,
130
+ cached: false,
131
+ duration,
132
+ });
133
+ } else if (operation.type === 'delete') {
134
+ quotaMonitorService.incrementDelete(operation.count);
135
+ requestLoggerService.logRequest({
136
+ type: 'delete',
137
+ collection: operation.collection,
138
+ documentId: operation.documentId,
139
+ success: true,
140
+ cached: false,
141
+ duration,
142
+ });
143
+ }
144
+
145
+ return result;
146
+ } catch (error) {
147
+ const duration = Date.now() - startTime;
148
+ const errorMessage = error instanceof Error ? error.message : 'Unknown error';
149
+ requestLoggerService.logRequest({
150
+ type: operation.type,
151
+ collection: operation.collection,
152
+ documentId: operation.documentId,
153
+ success: false,
154
+ error: errorMessage,
155
+ cached: false,
156
+ duration,
157
+ });
158
+
159
+ throw error;
160
+ }
161
+ }
162
+ }
163
+
164
+ export const quotaTrackingMiddleware = new QuotaTrackingMiddleware();
165
+
@@ -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
+ }