@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.
- package/package.json +5 -8
- 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 +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
|
+
}
|