@umituz/react-native-firebase 1.13.53 → 1.13.55
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-firebase",
|
|
3
|
-
"version": "1.13.
|
|
3
|
+
"version": "1.13.55",
|
|
4
4
|
"description": "Unified Firebase package for React Native apps - Auth and Firestore services using Firebase JS SDK (no native modules).",
|
|
5
5
|
"main": "./src/index.ts",
|
|
6
6
|
"types": "./src/index.ts",
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { useState, useEffect, useCallback, useRef } from "react";
|
|
7
|
-
import { onAuthStateChanged, type Auth } from "firebase/auth";
|
|
8
|
-
import {
|
|
7
|
+
import { onAuthStateChanged, type Auth, type User } from "firebase/auth";
|
|
8
|
+
import type { AuthCheckResult } from "../../infrastructure/services/auth-utils.service";
|
|
9
9
|
import { anonymousAuthService, type AnonymousAuthResult } from "../../infrastructure/services/anonymous-auth.service";
|
|
10
|
+
import { createAuthStateChangeHandler, userToAuthCheckResult } from "./utils/auth-state-change.handler";
|
|
10
11
|
|
|
11
12
|
declare const __DEV__: boolean;
|
|
12
13
|
|
|
@@ -36,61 +37,32 @@ export interface UseAnonymousAuthResult extends AuthCheckResult {
|
|
|
36
37
|
* Hook for anonymous authentication
|
|
37
38
|
*/
|
|
38
39
|
export function useAnonymousAuth(auth: Auth | null): UseAnonymousAuthResult {
|
|
39
|
-
const [authState, setAuthState] = useState<AuthCheckResult>(
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
const [authState, setAuthState] = useState<AuthCheckResult>({
|
|
41
|
+
isAuthenticated: false,
|
|
42
|
+
isAnonymous: false,
|
|
43
|
+
currentUser: null,
|
|
44
|
+
userId: null,
|
|
45
|
+
});
|
|
42
46
|
const [loading, setLoading] = useState(true);
|
|
43
47
|
const [error, setError] = useState<Error | null>(null);
|
|
44
|
-
const authRef = useRef(auth);
|
|
45
48
|
const unsubscribeRef = useRef<(() => void) | null>(null);
|
|
46
49
|
|
|
47
|
-
// Update ref when auth changes
|
|
48
|
-
useEffect(() => {
|
|
49
|
-
authRef.current = auth;
|
|
50
|
-
}, [auth]);
|
|
51
|
-
|
|
52
50
|
// Clear error helper
|
|
53
51
|
const clearError = useCallback(() => {
|
|
54
52
|
setError(null);
|
|
55
53
|
}, []);
|
|
56
54
|
|
|
57
|
-
// Auth state change handler
|
|
58
|
-
const handleAuthStateChange = useCallback((user:
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
currentUser: null,
|
|
67
|
-
userId: null,
|
|
68
|
-
});
|
|
69
|
-
} else {
|
|
70
|
-
const anonymous = user.isAnonymous === true;
|
|
71
|
-
setAuthState({
|
|
72
|
-
isAuthenticated: true,
|
|
73
|
-
isAnonymous: anonymous,
|
|
74
|
-
currentUser: user,
|
|
75
|
-
userId: user.uid,
|
|
76
|
-
});
|
|
77
|
-
}
|
|
78
|
-
setError(null);
|
|
79
|
-
} catch (err) {
|
|
80
|
-
const authError = err instanceof Error ? err : new Error('Auth state check failed');
|
|
81
|
-
setError(authError);
|
|
82
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
83
|
-
// eslint-disable-next-line no-console
|
|
84
|
-
console.error("[useAnonymousAuth] Auth state change error", authError);
|
|
85
|
-
}
|
|
86
|
-
} finally {
|
|
87
|
-
setLoading(false);
|
|
88
|
-
}
|
|
89
|
-
}, []);
|
|
55
|
+
// Auth state change handler
|
|
56
|
+
const handleAuthStateChange = useCallback((user: User | null) => {
|
|
57
|
+
const handler = createAuthStateChangeHandler({
|
|
58
|
+
setAuthState,
|
|
59
|
+
setLoading,
|
|
60
|
+
setError,
|
|
61
|
+
});
|
|
62
|
+
handler(user);
|
|
63
|
+
}, [setAuthState, setLoading, setError]);
|
|
90
64
|
|
|
91
65
|
// Setup auth state listener
|
|
92
|
-
// IMPORTANT: Do NOT call handleAuthStateChange() immediately!
|
|
93
|
-
// Wait for onAuthStateChanged to fire - it will have the correct user from persistence
|
|
94
66
|
useEffect(() => {
|
|
95
67
|
// Cleanup previous listener
|
|
96
68
|
if (unsubscribeRef.current) {
|
|
@@ -99,12 +71,7 @@ export function useAnonymousAuth(auth: Auth | null): UseAnonymousAuthResult {
|
|
|
99
71
|
}
|
|
100
72
|
|
|
101
73
|
if (!auth) {
|
|
102
|
-
setAuthState(
|
|
103
|
-
isAuthenticated: false,
|
|
104
|
-
isAnonymous: false,
|
|
105
|
-
currentUser: null,
|
|
106
|
-
userId: null,
|
|
107
|
-
});
|
|
74
|
+
setAuthState(userToAuthCheckResult(null));
|
|
108
75
|
setLoading(false);
|
|
109
76
|
setError(null);
|
|
110
77
|
return;
|
|
@@ -114,10 +81,9 @@ export function useAnonymousAuth(auth: Auth | null): UseAnonymousAuthResult {
|
|
|
114
81
|
setLoading(true);
|
|
115
82
|
|
|
116
83
|
try {
|
|
117
|
-
// Listen to auth state changes
|
|
118
|
-
// The first callback will have the user restored from persistence (or null)
|
|
84
|
+
// Listen to auth state changes
|
|
119
85
|
unsubscribeRef.current = onAuthStateChanged(auth, (user) => {
|
|
120
|
-
if (
|
|
86
|
+
if (__DEV__) {
|
|
121
87
|
// eslint-disable-next-line no-console
|
|
122
88
|
console.log("[useAnonymousAuth] onAuthStateChanged fired", {
|
|
123
89
|
hasUser: !!user,
|
|
@@ -126,14 +92,13 @@ export function useAnonymousAuth(auth: Auth | null): UseAnonymousAuthResult {
|
|
|
126
92
|
email: user?.email,
|
|
127
93
|
});
|
|
128
94
|
}
|
|
129
|
-
// IMPORTANT: Pass the user from the callback, not auth.currentUser!
|
|
130
95
|
handleAuthStateChange(user);
|
|
131
96
|
});
|
|
132
97
|
} catch (err) {
|
|
133
98
|
const authError = err instanceof Error ? err : new Error('Auth listener setup failed');
|
|
134
99
|
setError(authError);
|
|
135
100
|
setLoading(false);
|
|
136
|
-
if (
|
|
101
|
+
if (__DEV__) {
|
|
137
102
|
// eslint-disable-next-line no-console
|
|
138
103
|
console.error("[useAnonymousAuth] Auth listener setup error", authError);
|
|
139
104
|
}
|
|
@@ -161,12 +126,9 @@ export function useAnonymousAuth(auth: Auth | null): UseAnonymousAuthResult {
|
|
|
161
126
|
|
|
162
127
|
try {
|
|
163
128
|
const result = await anonymousAuthService.signInAnonymously(auth);
|
|
164
|
-
|
|
165
|
-
// Update auth state after successful sign in
|
|
166
|
-
// Pass the user from the result, not from auth.currentUser
|
|
167
129
|
handleAuthStateChange(result.user);
|
|
168
130
|
|
|
169
|
-
if (
|
|
131
|
+
if (__DEV__) {
|
|
170
132
|
// eslint-disable-next-line no-console
|
|
171
133
|
console.log("[useAnonymousAuth] Successfully signed in anonymously", {
|
|
172
134
|
uid: result.anonymousUser.uid,
|
|
@@ -178,7 +140,7 @@ export function useAnonymousAuth(auth: Auth | null): UseAnonymousAuthResult {
|
|
|
178
140
|
} catch (err) {
|
|
179
141
|
const authError = err instanceof Error ? err : new Error('Anonymous sign in failed');
|
|
180
142
|
setError(authError);
|
|
181
|
-
if (
|
|
143
|
+
if (__DEV__) {
|
|
182
144
|
// eslint-disable-next-line no-console
|
|
183
145
|
console.error("[useAnonymousAuth] Sign in error", authError);
|
|
184
146
|
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auth State Change Handler
|
|
3
|
+
* Single Responsibility: Handle authentication state changes
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { User } from 'firebase/auth';
|
|
7
|
+
import type { AuthCheckResult } from '../../../infrastructure/services/auth-utils.service';
|
|
8
|
+
|
|
9
|
+
declare const __DEV__: boolean;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Convert Firebase User to AuthCheckResult
|
|
13
|
+
* @param user - Firebase user or null
|
|
14
|
+
* @returns AuthCheckResult
|
|
15
|
+
*/
|
|
16
|
+
export function userToAuthCheckResult(user: User | null): AuthCheckResult {
|
|
17
|
+
if (!user) {
|
|
18
|
+
return {
|
|
19
|
+
isAuthenticated: false,
|
|
20
|
+
isAnonymous: false,
|
|
21
|
+
currentUser: null,
|
|
22
|
+
userId: null,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
isAuthenticated: true,
|
|
28
|
+
isAnonymous: user.isAnonymous === true,
|
|
29
|
+
currentUser: user,
|
|
30
|
+
userId: user.uid,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create auth state change handler callback
|
|
36
|
+
* Returns a function that can be used with onAuthStateChanged
|
|
37
|
+
*
|
|
38
|
+
* @param setAuthState - State setter for auth state
|
|
39
|
+
* @param setLoading - State setter for loading
|
|
40
|
+
* @param setError - State setter for errors
|
|
41
|
+
* @returns Callback function for auth state changes
|
|
42
|
+
*/
|
|
43
|
+
export type AuthStateChangeHandler = (user: User | null) => void;
|
|
44
|
+
|
|
45
|
+
export interface CreateAuthStateChangeHandlerParams {
|
|
46
|
+
setAuthState: (state: AuthCheckResult) => void;
|
|
47
|
+
setLoading: (loading: boolean) => void;
|
|
48
|
+
setError: (error: Error | null) => void;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function createAuthStateChangeHandler(
|
|
52
|
+
params: CreateAuthStateChangeHandlerParams
|
|
53
|
+
): AuthStateChangeHandler {
|
|
54
|
+
const { setAuthState, setLoading, setError } = params;
|
|
55
|
+
|
|
56
|
+
return (user: User | null) => {
|
|
57
|
+
try {
|
|
58
|
+
const authState = userToAuthCheckResult(user);
|
|
59
|
+
setAuthState(authState);
|
|
60
|
+
setError(null);
|
|
61
|
+
} catch (err) {
|
|
62
|
+
const authError =
|
|
63
|
+
err instanceof Error ? err : new Error('Auth state check failed');
|
|
64
|
+
setError(authError);
|
|
65
|
+
|
|
66
|
+
if (__DEV__) {
|
|
67
|
+
// eslint-disable-next-line no-console
|
|
68
|
+
console.error('[AuthStateHandler] Auth state change error', authError);
|
|
69
|
+
}
|
|
70
|
+
} finally {
|
|
71
|
+
setLoading(false);
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -6,11 +6,6 @@
|
|
|
6
6
|
*
|
|
7
7
|
* IMPORTANT: This package requires Firebase App to be initialized first.
|
|
8
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
9
|
*/
|
|
15
10
|
|
|
16
11
|
// eslint-disable-next-line no-console
|
|
@@ -29,13 +24,8 @@ class FirestoreClientSingleton {
|
|
|
29
24
|
private firestore: Firestore | null = null;
|
|
30
25
|
private initializationError: string | null = null;
|
|
31
26
|
|
|
32
|
-
private constructor() {
|
|
33
|
-
// Private constructor to enforce singleton pattern
|
|
34
|
-
}
|
|
27
|
+
private constructor() {}
|
|
35
28
|
|
|
36
|
-
/**
|
|
37
|
-
* Get singleton instance
|
|
38
|
-
*/
|
|
39
29
|
static getInstance(): FirestoreClientSingleton {
|
|
40
30
|
if (!FirestoreClientSingleton.instance) {
|
|
41
31
|
FirestoreClientSingleton.instance = new FirestoreClientSingleton();
|
|
@@ -43,26 +33,13 @@ class FirestoreClientSingleton {
|
|
|
43
33
|
return FirestoreClientSingleton.instance;
|
|
44
34
|
}
|
|
45
35
|
|
|
46
|
-
/**
|
|
47
|
-
* Initialize Firestore
|
|
48
|
-
* Requires Firebase App to be initialized first via @umituz/react-native-firebase
|
|
49
|
-
*
|
|
50
|
-
* @returns Firestore instance or null if initialization fails
|
|
51
|
-
*/
|
|
52
36
|
initialize(): Firestore | null {
|
|
53
|
-
if (this.firestore)
|
|
54
|
-
|
|
55
|
-
}
|
|
56
|
-
if (this.initializationError) {
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
try {
|
|
60
|
-
const app = getFirebaseApp(); // Get the core Firebase App
|
|
37
|
+
if (this.firestore) return this.firestore;
|
|
38
|
+
if (this.initializationError) return null;
|
|
61
39
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
40
|
+
try {
|
|
41
|
+
const app = getFirebaseApp();
|
|
42
|
+
if (!app) return null;
|
|
66
43
|
|
|
67
44
|
this.firestore = FirebaseFirestoreInitializer.initialize(app);
|
|
68
45
|
return this.firestore;
|
|
@@ -75,61 +52,22 @@ class FirestoreClientSingleton {
|
|
|
75
52
|
}
|
|
76
53
|
}
|
|
77
54
|
|
|
78
|
-
/**
|
|
79
|
-
* Get Firestore instance
|
|
80
|
-
* Auto-initializes if Firebase App is available
|
|
81
|
-
* Returns null if config is not available (offline mode - no error)
|
|
82
|
-
* @returns Firestore instance or null if not initialized
|
|
83
|
-
*/
|
|
84
55
|
getFirestore(): Firestore | null {
|
|
85
|
-
// Auto-initialize if not already initialized
|
|
86
56
|
if (!this.firestore && !this.initializationError) {
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
const app = getFirebaseApp();
|
|
90
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
91
|
-
// eslint-disable-next-line no-console
|
|
92
|
-
console.log("[FirestoreClient] getFirestore - app:", !!app);
|
|
93
|
-
}
|
|
94
|
-
if (app) {
|
|
95
|
-
this.initialize();
|
|
96
|
-
}
|
|
97
|
-
} catch (e) {
|
|
98
|
-
// Firebase App not available, return null (offline mode)
|
|
99
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
100
|
-
// eslint-disable-next-line no-console
|
|
101
|
-
console.log("[FirestoreClient] getFirestore error:", e);
|
|
102
|
-
}
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
108
|
-
// eslint-disable-next-line no-console
|
|
109
|
-
console.log("[FirestoreClient] returning firestore:", !!this.firestore, "constructor:", this.firestore?.constructor?.name);
|
|
57
|
+
const app = getFirebaseApp();
|
|
58
|
+
if (app) this.initialize();
|
|
110
59
|
}
|
|
111
|
-
// Return null if not initialized (offline mode - no error)
|
|
112
60
|
return this.firestore || null;
|
|
113
61
|
}
|
|
114
62
|
|
|
115
|
-
/**
|
|
116
|
-
* Check if Firestore is initialized
|
|
117
|
-
*/
|
|
118
63
|
isInitialized(): boolean {
|
|
119
64
|
return this.firestore !== null;
|
|
120
65
|
}
|
|
121
66
|
|
|
122
|
-
/**
|
|
123
|
-
* Get initialization error if any
|
|
124
|
-
*/
|
|
125
67
|
getInitializationError(): string | null {
|
|
126
68
|
return this.initializationError;
|
|
127
69
|
}
|
|
128
70
|
|
|
129
|
-
/**
|
|
130
|
-
* Reset Firestore client instance
|
|
131
|
-
* Useful for testing
|
|
132
|
-
*/
|
|
133
71
|
reset(): void {
|
|
134
72
|
this.firestore = null;
|
|
135
73
|
this.initializationError = null;
|
|
@@ -138,56 +76,22 @@ class FirestoreClientSingleton {
|
|
|
138
76
|
|
|
139
77
|
export const firestoreClient = FirestoreClientSingleton.getInstance();
|
|
140
78
|
|
|
141
|
-
/**
|
|
142
|
-
* Initialize Firestore
|
|
143
|
-
* Requires Firebase App to be initialized first via @umituz/react-native-firebase
|
|
144
|
-
*
|
|
145
|
-
* @returns Firestore instance or null if initialization fails
|
|
146
|
-
*
|
|
147
|
-
* @example
|
|
148
|
-
* ```typescript
|
|
149
|
-
* import { initializeFirebase } from '@umituz/react-native-firebase';
|
|
150
|
-
* import { initializeFirestore } from '@umituz/react-native-firestore';
|
|
151
|
-
*
|
|
152
|
-
* // Initialize Firebase App first
|
|
153
|
-
* const app = initializeFirebase(config);
|
|
154
|
-
*
|
|
155
|
-
* // Then initialize Firestore
|
|
156
|
-
* const db = initializeFirestore();
|
|
157
|
-
* ```
|
|
158
|
-
*/
|
|
159
79
|
export function initializeFirestore(): Firestore | null {
|
|
160
80
|
return firestoreClient.initialize();
|
|
161
81
|
}
|
|
162
82
|
|
|
163
|
-
/**
|
|
164
|
-
* Get Firestore instance
|
|
165
|
-
* Auto-initializes if Firebase App is available
|
|
166
|
-
* Returns null if config is not available (offline mode - no error)
|
|
167
|
-
* @returns Firestore instance or null if not initialized
|
|
168
|
-
*/
|
|
169
83
|
export function getFirestore(): Firestore | null {
|
|
170
84
|
return firestoreClient.getFirestore();
|
|
171
85
|
}
|
|
172
86
|
|
|
173
|
-
/**
|
|
174
|
-
* Check if Firestore is initialized
|
|
175
|
-
*/
|
|
176
87
|
export function isFirestoreInitialized(): boolean {
|
|
177
88
|
return firestoreClient.isInitialized();
|
|
178
89
|
}
|
|
179
90
|
|
|
180
|
-
/**
|
|
181
|
-
* Get Firestore initialization error if any
|
|
182
|
-
*/
|
|
183
91
|
export function getFirestoreInitializationError(): string | null {
|
|
184
92
|
return firestoreClient.getInitializationError();
|
|
185
93
|
}
|
|
186
94
|
|
|
187
|
-
/**
|
|
188
|
-
* Reset Firestore client instance
|
|
189
|
-
* Useful for testing
|
|
190
|
-
*/
|
|
191
95
|
export function resetFirestoreClient(): void {
|
|
192
96
|
firestoreClient.reset();
|
|
193
97
|
}
|
|
@@ -1,19 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Query Builder Utility
|
|
3
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
4
|
*/
|
|
18
5
|
|
|
19
6
|
import {
|
|
@@ -49,97 +36,58 @@ export interface QueryBuilderOptions {
|
|
|
49
36
|
order?: "asc" | "desc";
|
|
50
37
|
};
|
|
51
38
|
limitValue?: number;
|
|
52
|
-
/**
|
|
53
|
-
* Cursor value for pagination (timestamp in milliseconds)
|
|
54
|
-
* Used with startAfter for cursor-based pagination
|
|
55
|
-
*/
|
|
56
39
|
cursorValue?: number;
|
|
57
40
|
}
|
|
58
41
|
|
|
59
42
|
const MAX_IN_OPERATOR_VALUES = 10;
|
|
60
43
|
|
|
61
44
|
/**
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
* @param db - Firestore database instance
|
|
65
|
-
* @param options - Query builder options
|
|
66
|
-
* @returns Firestore Query object
|
|
45
|
+
* Apply date range filters to query
|
|
67
46
|
*/
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
}
|
|
47
|
+
function applyDateRange(q: Query, dateRange: QueryBuilderOptions['dateRange']): Query {
|
|
48
|
+
if (!dateRange) return q;
|
|
88
49
|
|
|
89
|
-
|
|
90
|
-
|
|
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
|
-
}
|
|
50
|
+
if (dateRange.startDate) {
|
|
51
|
+
q = query(q, where(dateRange.field, ">=", Timestamp.fromMillis(dateRange.startDate)));
|
|
111
52
|
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (sort) {
|
|
115
|
-
const sortOrder = sort.order || "desc";
|
|
116
|
-
q = query(q, orderBy(sort.field, sortOrder));
|
|
53
|
+
if (dateRange.endDate) {
|
|
54
|
+
q = query(q, where(dateRange.field, "<=", Timestamp.fromMillis(dateRange.endDate)));
|
|
117
55
|
}
|
|
56
|
+
return q;
|
|
57
|
+
}
|
|
118
58
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
59
|
+
/**
|
|
60
|
+
* Apply sorting to query
|
|
61
|
+
*/
|
|
62
|
+
function applySort(q: Query, sort?: QueryBuilderOptions['sort']): Query {
|
|
63
|
+
if (!sort) return q;
|
|
64
|
+
const sortOrder = sort.order || "desc";
|
|
65
|
+
return query(q, orderBy(sort.field, sortOrder));
|
|
66
|
+
}
|
|
123
67
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
68
|
+
/**
|
|
69
|
+
* Apply cursor for pagination
|
|
70
|
+
*/
|
|
71
|
+
function applyCursor(q: Query, cursorValue?: number): Query {
|
|
72
|
+
if (cursorValue === undefined) return q;
|
|
73
|
+
return query(q, startAfter(Timestamp.fromMillis(cursorValue)));
|
|
74
|
+
}
|
|
128
75
|
|
|
129
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Apply limit to query
|
|
78
|
+
*/
|
|
79
|
+
function applyLimit(q: Query, limitValue?: number): Query {
|
|
80
|
+
if (limitValue === undefined) return q;
|
|
81
|
+
return query(q, limitQuery(limitValue));
|
|
130
82
|
}
|
|
131
83
|
|
|
132
84
|
/**
|
|
133
|
-
* Apply field filter with
|
|
134
|
-
* Handles arrays by using 'in' operator (up to 10 values)
|
|
135
|
-
* For arrays >10 values, splits into chunks and uses 'or' operator
|
|
85
|
+
* Apply field filter with 'in' operator and chunking support
|
|
136
86
|
*/
|
|
137
87
|
function applyFieldFilter(q: Query, filter: FieldFilter): Query {
|
|
138
88
|
const { field, operator, value } = filter;
|
|
139
89
|
|
|
140
|
-
// Handle 'in' operator with array values
|
|
141
90
|
if (operator === "in" && Array.isArray(value)) {
|
|
142
|
-
// Firestore 'in' operator supports up to 10 values
|
|
143
91
|
if (value.length <= MAX_IN_OPERATOR_VALUES) {
|
|
144
92
|
return query(q, where(field, "in", value));
|
|
145
93
|
}
|
|
@@ -154,35 +102,58 @@ function applyFieldFilter(q: Query, filter: FieldFilter): Query {
|
|
|
154
102
|
return query(q, or(...orConditions));
|
|
155
103
|
}
|
|
156
104
|
|
|
157
|
-
// Standard filter
|
|
158
105
|
return query(q, where(field, operator, value));
|
|
159
106
|
}
|
|
160
107
|
|
|
161
108
|
/**
|
|
162
|
-
*
|
|
163
|
-
|
|
109
|
+
* Build Firestore query with advanced filtering support
|
|
110
|
+
*/
|
|
111
|
+
export function buildQuery(
|
|
112
|
+
db: Firestore,
|
|
113
|
+
options: QueryBuilderOptions,
|
|
114
|
+
): Query {
|
|
115
|
+
const {
|
|
116
|
+
collectionName,
|
|
117
|
+
baseFilters = [],
|
|
118
|
+
dateRange,
|
|
119
|
+
sort,
|
|
120
|
+
limitValue,
|
|
121
|
+
cursorValue,
|
|
122
|
+
} = options;
|
|
123
|
+
|
|
124
|
+
const collectionRef = collection(db, collectionName);
|
|
125
|
+
let q: Query = collectionRef;
|
|
126
|
+
|
|
127
|
+
// Apply base filters
|
|
128
|
+
for (const filter of baseFilters) {
|
|
129
|
+
q = applyFieldFilter(q, filter);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Apply modifiers in correct order
|
|
133
|
+
q = applyDateRange(q, dateRange);
|
|
134
|
+
q = applySort(q, sort);
|
|
135
|
+
q = applyCursor(q, cursorValue);
|
|
136
|
+
q = applyLimit(q, limitValue);
|
|
137
|
+
|
|
138
|
+
return q;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Create a field filter for 'in' operator
|
|
164
143
|
*/
|
|
165
144
|
export function createInFilter(
|
|
166
145
|
field: string,
|
|
167
146
|
values: string[] | number[],
|
|
168
147
|
): FieldFilter {
|
|
169
|
-
return {
|
|
170
|
-
field,
|
|
171
|
-
operator: "in",
|
|
172
|
-
value: values,
|
|
173
|
-
};
|
|
148
|
+
return { field, operator: "in", value: values };
|
|
174
149
|
}
|
|
175
150
|
|
|
176
151
|
/**
|
|
177
|
-
*
|
|
152
|
+
* Create a field filter for equality
|
|
178
153
|
*/
|
|
179
154
|
export function createEqualFilter(
|
|
180
155
|
field: string,
|
|
181
156
|
value: string | number | boolean,
|
|
182
157
|
): FieldFilter {
|
|
183
|
-
return {
|
|
184
|
-
field,
|
|
185
|
-
operator: "==",
|
|
186
|
-
value,
|
|
187
|
-
};
|
|
158
|
+
return { field, operator: "==", value };
|
|
188
159
|
}
|