@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,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pagination Helper
|
|
3
|
+
*
|
|
4
|
+
* Utilities for cursor-based pagination in Firestore.
|
|
5
|
+
* Handles pagination logic, cursor management, and hasMore detection.
|
|
6
|
+
*
|
|
7
|
+
* App-agnostic: Works with any document type and any collection.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* import { PaginationHelper } from '@umituz/react-native-firestore';
|
|
12
|
+
*
|
|
13
|
+
* const helper = new PaginationHelper<Post>();
|
|
14
|
+
* const result = helper.buildResult(posts, 10, post => post.id);
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { PaginatedResult, PaginationParams } from '../types/pagination.types';
|
|
19
|
+
|
|
20
|
+
export class PaginationHelper<T> {
|
|
21
|
+
/**
|
|
22
|
+
* Build paginated result from items
|
|
23
|
+
*
|
|
24
|
+
* @param items - All items fetched (should be limit + 1)
|
|
25
|
+
* @param pageLimit - Requested page size
|
|
26
|
+
* @param getCursor - Function to extract cursor from item
|
|
27
|
+
* @returns Paginated result with hasMore and nextCursor
|
|
28
|
+
*/
|
|
29
|
+
buildResult(
|
|
30
|
+
items: T[],
|
|
31
|
+
pageLimit: number,
|
|
32
|
+
getCursor: (item: T) => string,
|
|
33
|
+
): PaginatedResult<T> {
|
|
34
|
+
const hasMore = items.length > pageLimit;
|
|
35
|
+
const resultItems = hasMore ? items.slice(0, pageLimit) : items;
|
|
36
|
+
const lastItem = resultItems[resultItems.length - 1];
|
|
37
|
+
const nextCursor = hasMore && lastItem
|
|
38
|
+
? getCursor(lastItem)
|
|
39
|
+
: null;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
items: resultItems,
|
|
43
|
+
nextCursor,
|
|
44
|
+
hasMore,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get default limit from params or use default
|
|
50
|
+
*
|
|
51
|
+
* @param params - Pagination params
|
|
52
|
+
* @param defaultLimit - Default limit if not specified
|
|
53
|
+
* @returns Page limit
|
|
54
|
+
*/
|
|
55
|
+
getLimit(params?: PaginationParams, defaultLimit: number = 10): number {
|
|
56
|
+
return params?.limit || defaultLimit;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Calculate fetch limit (page limit + 1 for hasMore detection)
|
|
61
|
+
*
|
|
62
|
+
* @param pageLimit - Requested page size
|
|
63
|
+
* @returns Fetch limit (pageLimit + 1)
|
|
64
|
+
*/
|
|
65
|
+
getFetchLimit(pageLimit: number): number {
|
|
66
|
+
return pageLimit + 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Check if params has cursor
|
|
71
|
+
*
|
|
72
|
+
* @param params - Pagination params
|
|
73
|
+
* @returns true if cursor exists
|
|
74
|
+
*/
|
|
75
|
+
hasCursor(params?: PaginationParams): boolean {
|
|
76
|
+
return !!params?.cursor;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Create pagination helper for a specific type
|
|
82
|
+
*
|
|
83
|
+
* @returns PaginationHelper instance
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* ```typescript
|
|
87
|
+
* const helper = createPaginationHelper<Post>();
|
|
88
|
+
* const result = helper.buildResult(posts, 10, post => post.id);
|
|
89
|
+
* ```
|
|
90
|
+
*/
|
|
91
|
+
export function createPaginationHelper<T>(): PaginationHelper<T> {
|
|
92
|
+
return new PaginationHelper<T>();
|
|
93
|
+
}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Query Builder Utility
|
|
3
|
+
* Single Responsibility: Build Firestore queries with advanced filtering
|
|
4
|
+
*
|
|
5
|
+
* App-agnostic utility for building Firestore queries.
|
|
6
|
+
* Supports:
|
|
7
|
+
* - Firestore 'in' operator (up to 10 values)
|
|
8
|
+
* - Firestore 'or' operator (for >10 values via chunking)
|
|
9
|
+
* - Single value filtering
|
|
10
|
+
* - Multiple field filtering
|
|
11
|
+
* - Date range filtering
|
|
12
|
+
* - Sorting
|
|
13
|
+
* - Limiting
|
|
14
|
+
*
|
|
15
|
+
* This utility is designed to be used across hundreds of apps.
|
|
16
|
+
* It provides a consistent interface for Firestore query building.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import {
|
|
20
|
+
collection,
|
|
21
|
+
query,
|
|
22
|
+
where,
|
|
23
|
+
orderBy,
|
|
24
|
+
limit as limitQuery,
|
|
25
|
+
startAfter,
|
|
26
|
+
or,
|
|
27
|
+
type Firestore,
|
|
28
|
+
type Query,
|
|
29
|
+
Timestamp,
|
|
30
|
+
type WhereFilterOp,
|
|
31
|
+
} from "firebase/firestore";
|
|
32
|
+
|
|
33
|
+
export interface FieldFilter {
|
|
34
|
+
field: string;
|
|
35
|
+
operator: WhereFilterOp;
|
|
36
|
+
value: string | number | boolean | string[] | number[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface QueryBuilderOptions {
|
|
40
|
+
collectionName: string;
|
|
41
|
+
baseFilters?: FieldFilter[];
|
|
42
|
+
dateRange?: {
|
|
43
|
+
field: string;
|
|
44
|
+
startDate?: number;
|
|
45
|
+
endDate?: number;
|
|
46
|
+
};
|
|
47
|
+
sort?: {
|
|
48
|
+
field: string;
|
|
49
|
+
order?: "asc" | "desc";
|
|
50
|
+
};
|
|
51
|
+
limitValue?: number;
|
|
52
|
+
/**
|
|
53
|
+
* Cursor value for pagination (timestamp in milliseconds)
|
|
54
|
+
* Used with startAfter for cursor-based pagination
|
|
55
|
+
*/
|
|
56
|
+
cursorValue?: number;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const MAX_IN_OPERATOR_VALUES = 10;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Build Firestore query with advanced filtering support
|
|
63
|
+
*
|
|
64
|
+
* @param db - Firestore database instance
|
|
65
|
+
* @param options - Query builder options
|
|
66
|
+
* @returns Firestore Query object
|
|
67
|
+
*/
|
|
68
|
+
export function buildQuery(
|
|
69
|
+
db: Firestore,
|
|
70
|
+
options: QueryBuilderOptions,
|
|
71
|
+
): Query {
|
|
72
|
+
const {
|
|
73
|
+
collectionName,
|
|
74
|
+
baseFilters = [],
|
|
75
|
+
dateRange,
|
|
76
|
+
sort,
|
|
77
|
+
limitValue,
|
|
78
|
+
cursorValue,
|
|
79
|
+
} = options;
|
|
80
|
+
|
|
81
|
+
const collectionRef = collection(db, collectionName);
|
|
82
|
+
let q: Query = collectionRef;
|
|
83
|
+
|
|
84
|
+
// Apply base filters
|
|
85
|
+
for (const filter of baseFilters) {
|
|
86
|
+
q = applyFieldFilter(q, filter);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Apply date range filters
|
|
90
|
+
if (dateRange) {
|
|
91
|
+
if (dateRange.startDate) {
|
|
92
|
+
q = query(
|
|
93
|
+
q,
|
|
94
|
+
where(
|
|
95
|
+
dateRange.field,
|
|
96
|
+
">=",
|
|
97
|
+
Timestamp.fromMillis(dateRange.startDate),
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (dateRange.endDate) {
|
|
102
|
+
q = query(
|
|
103
|
+
q,
|
|
104
|
+
where(
|
|
105
|
+
dateRange.field,
|
|
106
|
+
"<=",
|
|
107
|
+
Timestamp.fromMillis(dateRange.endDate),
|
|
108
|
+
),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Apply sorting
|
|
114
|
+
if (sort) {
|
|
115
|
+
const sortOrder = sort.order || "desc";
|
|
116
|
+
q = query(q, orderBy(sort.field, sortOrder));
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Apply cursor for pagination (must come after orderBy)
|
|
120
|
+
if (cursorValue !== undefined) {
|
|
121
|
+
q = query(q, startAfter(Timestamp.fromMillis(cursorValue)));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Apply limit
|
|
125
|
+
if (limitValue !== undefined) {
|
|
126
|
+
q = query(q, limitQuery(limitValue));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return q;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Apply field filter with support for 'in' operator and chunking
|
|
134
|
+
* Handles arrays by using 'in' operator (up to 10 values)
|
|
135
|
+
* For arrays >10 values, splits into chunks and uses 'or' operator
|
|
136
|
+
*/
|
|
137
|
+
function applyFieldFilter(q: Query, filter: FieldFilter): Query {
|
|
138
|
+
const { field, operator, value } = filter;
|
|
139
|
+
|
|
140
|
+
// Handle 'in' operator with array values
|
|
141
|
+
if (operator === "in" && Array.isArray(value)) {
|
|
142
|
+
// Firestore 'in' operator supports up to 10 values
|
|
143
|
+
if (value.length <= MAX_IN_OPERATOR_VALUES) {
|
|
144
|
+
return query(q, where(field, "in", value));
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Split into chunks of 10 and use 'or' operator
|
|
148
|
+
const chunks: (string[] | number[])[] = [];
|
|
149
|
+
for (let i = 0; i < value.length; i += MAX_IN_OPERATOR_VALUES) {
|
|
150
|
+
chunks.push(value.slice(i, i + MAX_IN_OPERATOR_VALUES));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const orConditions = chunks.map((chunk) => where(field, "in", chunk));
|
|
154
|
+
return query(q, or(...orConditions));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Standard filter
|
|
158
|
+
return query(q, where(field, operator, value));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Helper: Create a field filter for 'in' operator
|
|
163
|
+
* Automatically handles chunking if array >10 values
|
|
164
|
+
*/
|
|
165
|
+
export function createInFilter(
|
|
166
|
+
field: string,
|
|
167
|
+
values: string[] | number[],
|
|
168
|
+
): FieldFilter {
|
|
169
|
+
return {
|
|
170
|
+
field,
|
|
171
|
+
operator: "in",
|
|
172
|
+
value: values,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Helper: Create a field filter for equality
|
|
178
|
+
*/
|
|
179
|
+
export function createEqualFilter(
|
|
180
|
+
field: string,
|
|
181
|
+
value: string | number | boolean,
|
|
182
|
+
): FieldFilter {
|
|
183
|
+
return {
|
|
184
|
+
field,
|
|
185
|
+
operator: "==",
|
|
186
|
+
value,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quota Error Detector Utility
|
|
3
|
+
* Single Responsibility: Detect Firebase quota errors
|
|
4
|
+
*
|
|
5
|
+
* Firebase quota limits:
|
|
6
|
+
* - Free tier: 50K reads/day, 20K writes/day, 20K deletes/day
|
|
7
|
+
* - Blaze plan: Pay as you go, higher limits
|
|
8
|
+
*
|
|
9
|
+
* Quota errors are NOT retryable - quota won't increase by retrying
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Check if error is a Firebase quota error
|
|
14
|
+
* Quota errors indicate daily read/write/delete limits are exceeded
|
|
15
|
+
*
|
|
16
|
+
* @param error - Error object to check
|
|
17
|
+
* @returns true if error is a quota error
|
|
18
|
+
*/
|
|
19
|
+
export function isQuotaError(error: unknown): boolean {
|
|
20
|
+
if (!error || typeof error !== "object") {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const firebaseError = error as { code?: string; message?: string; name?: string };
|
|
25
|
+
|
|
26
|
+
// Check error code
|
|
27
|
+
if (firebaseError.code === "resource-exhausted") {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Check error message
|
|
32
|
+
const errorMessage = firebaseError.message?.toLowerCase() || "";
|
|
33
|
+
if (
|
|
34
|
+
errorMessage.includes("quota") ||
|
|
35
|
+
errorMessage.includes("quota exceeded") ||
|
|
36
|
+
errorMessage.includes("resource-exhausted") ||
|
|
37
|
+
errorMessage.includes("daily limit")
|
|
38
|
+
) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check error name
|
|
43
|
+
const errorName = firebaseError.name?.toLowerCase() || "";
|
|
44
|
+
if (errorName.includes("quota") || errorName.includes("resource-exhausted")) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Check if error is retryable
|
|
53
|
+
* Quota errors are NOT retryable
|
|
54
|
+
*
|
|
55
|
+
* @param error - Error object to check
|
|
56
|
+
* @returns true if error is retryable
|
|
57
|
+
*/
|
|
58
|
+
export function isRetryableError(error: unknown): boolean {
|
|
59
|
+
// Quota errors are NOT retryable
|
|
60
|
+
if (isQuotaError(error)) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!error || typeof error !== "object") {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const firebaseError = error as { code?: string; message?: string };
|
|
69
|
+
|
|
70
|
+
// Firestore transaction conflicts are retryable
|
|
71
|
+
if (firebaseError.code === "failed-precondition") {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Network errors are retryable
|
|
76
|
+
if (
|
|
77
|
+
firebaseError.code === "unavailable" ||
|
|
78
|
+
firebaseError.code === "deadline-exceeded"
|
|
79
|
+
) {
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Timeout errors are retryable
|
|
84
|
+
const errorMessage = firebaseError.message?.toLowerCase() || "";
|
|
85
|
+
if (errorMessage.includes("timeout")) {
|
|
86
|
+
return true;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Get user-friendly quota error message
|
|
94
|
+
*
|
|
95
|
+
* @returns User-friendly error message
|
|
96
|
+
*/
|
|
97
|
+
export function getQuotaErrorMessage(): string {
|
|
98
|
+
return "Firebase quota exceeded. Please try again later or upgrade your Firebase plan.";
|
|
99
|
+
}
|
|
100
|
+
|
package/src/index.ts
CHANGED
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
* Domain-Driven Design (DDD) Architecture
|
|
5
5
|
*
|
|
6
6
|
* This package provides Firebase App initialization and core services:
|
|
7
|
+
* - Auth
|
|
8
|
+
* - Firestore
|
|
7
9
|
* - Analytics
|
|
8
10
|
* - Crashlytics
|
|
9
11
|
*
|
|
10
12
|
* Usage:
|
|
11
13
|
* import { initializeFirebase, getFirebaseApp } from '@umituz/react-native-firebase';
|
|
14
|
+
* import { useFirebaseAuth } from '@umituz/react-native-firebase';
|
|
15
|
+
* import { getFirestore, BaseRepository } from '@umituz/react-native-firebase';
|
|
12
16
|
* import { firebaseAnalyticsService } from '@umituz/react-native-firebase';
|
|
13
17
|
*/
|
|
14
18
|
|
|
@@ -43,6 +47,18 @@ export type {
|
|
|
43
47
|
ServiceInitializationResult,
|
|
44
48
|
} from './infrastructure/config/FirebaseClient';
|
|
45
49
|
|
|
50
|
+
// =============================================================================
|
|
51
|
+
// AUTH MODULE
|
|
52
|
+
// =============================================================================
|
|
53
|
+
|
|
54
|
+
export * from './auth';
|
|
55
|
+
|
|
56
|
+
// =============================================================================
|
|
57
|
+
// FIRESTORE MODULE
|
|
58
|
+
// =============================================================================
|
|
59
|
+
|
|
60
|
+
export * from './firestore';
|
|
61
|
+
|
|
46
62
|
// =============================================================================
|
|
47
63
|
// ANALYTICS MODULE
|
|
48
64
|
// =============================================================================
|