@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.
- package/package.json +10 -2
- package/src/auth/domain/entities/AnonymousUser.ts +44 -0
- package/src/auth/domain/errors/FirebaseAuthError.ts +18 -0
- package/src/auth/domain/value-objects/FirebaseAuthConfig.ts +45 -0
- package/src/auth/index.ts +146 -0
- package/src/auth/infrastructure/config/FirebaseAuthClient.ts +210 -0
- package/src/auth/infrastructure/config/initializers/FirebaseAuthInitializer.ts +148 -0
- package/src/auth/infrastructure/services/account-deletion.service.ts +250 -0
- package/src/auth/infrastructure/services/anonymous-auth.service.ts +135 -0
- package/src/auth/infrastructure/services/apple-auth.service.ts +146 -0
- package/src/auth/infrastructure/services/auth-guard.service.ts +97 -0
- package/src/auth/infrastructure/services/auth-utils.service.ts +168 -0
- package/src/auth/infrastructure/services/firestore-utils.service.ts +155 -0
- package/src/auth/infrastructure/services/google-auth.service.ts +100 -0
- package/src/auth/infrastructure/services/reauthentication.service.ts +216 -0
- package/src/auth/presentation/hooks/useAnonymousAuth.ts +201 -0
- package/src/auth/presentation/hooks/useFirebaseAuth.ts +84 -0
- package/src/auth/presentation/hooks/useSocialAuth.ts +162 -0
- package/src/firestore/__tests__/BaseRepository.test.ts +133 -0
- package/src/firestore/__tests__/QueryDeduplicationMiddleware.test.ts +147 -0
- package/src/firestore/__tests__/mocks/react-native-firebase.ts +23 -0
- package/src/firestore/__tests__/setup.ts +36 -0
- package/src/firestore/domain/constants/QuotaLimits.ts +97 -0
- package/src/firestore/domain/entities/QuotaMetrics.ts +28 -0
- package/src/firestore/domain/entities/RequestLog.ts +30 -0
- package/src/firestore/domain/errors/FirebaseFirestoreError.ts +52 -0
- package/src/firestore/domain/services/QuotaCalculator.ts +70 -0
- package/src/firestore/index.ts +174 -0
- package/src/firestore/infrastructure/config/FirestoreClient.ts +181 -0
- package/src/firestore/infrastructure/config/initializers/FirebaseFirestoreInitializer.ts +46 -0
- package/src/firestore/infrastructure/middleware/QueryDeduplicationMiddleware.ts +153 -0
- package/src/firestore/infrastructure/middleware/QuotaTrackingMiddleware.ts +165 -0
- package/src/firestore/infrastructure/repositories/BasePaginatedRepository.ts +90 -0
- package/src/firestore/infrastructure/repositories/BaseQueryRepository.ts +80 -0
- package/src/firestore/infrastructure/repositories/BaseRepository.ts +147 -0
- package/src/firestore/infrastructure/services/QuotaMonitorService.ts +108 -0
- package/src/firestore/infrastructure/services/RequestLoggerService.ts +139 -0
- package/src/firestore/types/pagination.types.ts +60 -0
- package/src/firestore/utils/dateUtils.ts +31 -0
- package/src/firestore/utils/document-mapper.helper.ts +145 -0
- package/src/firestore/utils/pagination.helper.ts +93 -0
- package/src/firestore/utils/query-builder.ts +188 -0
- package/src/firestore/utils/quota-error-detector.util.ts +100 -0
- 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
|
+
}
|