@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,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
|
+
}
|
|
@@ -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
|
+
}
|