@umituz/react-native-firebase 2.4.4 → 2.4.6
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 +1 -1
- package/src/domains/account-deletion/infrastructure/services/account-deletion.service.ts +54 -12
- package/src/domains/account-deletion/infrastructure/services/reauthentication.service.ts +4 -8
- package/src/domains/auth/infrastructure/config/FirebaseAuthClient.ts +2 -1
- package/src/domains/auth/infrastructure/services/anonymous-auth.service.ts +8 -2
- package/src/domains/auth/infrastructure/services/auth-guard.service.ts +5 -5
- package/src/domains/auth/infrastructure/stores/auth.store.ts +5 -1
- package/src/domains/firestore/infrastructure/repositories/BasePaginatedRepository.ts +19 -0
- package/src/domains/firestore/infrastructure/repositories/BaseRepository.ts +3 -6
- package/src/shared/domain/utils/error-handlers/error-checkers.ts +6 -8
- package/src/shared/domain/utils/error-handlers/error-messages.ts +27 -12
- package/src/shared/domain/utils/executors/batch-executors.util.ts +16 -19
- package/src/shared/domain/utils/result/result-helpers.ts +1 -1
- package/src/shared/domain/utils/service-config.util.ts +2 -9
- package/src/shared/infrastructure/config/FirebaseConfigLoader.ts +1 -1
- package/src/shared/infrastructure/config/base/ServiceClientSingleton.ts +18 -0
- package/src/shared/infrastructure/config/validators/FirebaseConfigValidator.ts +2 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@umituz/react-native-firebase",
|
|
3
|
-
"version": "2.4.
|
|
3
|
+
"version": "2.4.6",
|
|
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",
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
reauthenticateWithPassword,
|
|
14
14
|
reauthenticateWithGoogle,
|
|
15
15
|
} from "./reauthentication.service";
|
|
16
|
-
import { successResult, type Result } from "../../../../shared/domain/utils";
|
|
16
|
+
import { successResult, type Result, toAuthErrorInfo } from "../../../../shared/domain/utils";
|
|
17
17
|
import type { AccountDeletionOptions } from "../../application/ports/reauthentication.types";
|
|
18
18
|
|
|
19
19
|
export interface AccountDeletionResult extends Result<void> {
|
|
@@ -67,16 +67,33 @@ export async function deleteCurrentUser(
|
|
|
67
67
|
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
68
68
|
console.log("[deleteCurrentUser] attemptReauth result:", reauth);
|
|
69
69
|
}
|
|
70
|
-
if (reauth)
|
|
70
|
+
if (reauth) {
|
|
71
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
72
|
+
console.log("[deleteCurrentUser] Reauth returned result, returning:", reauth);
|
|
73
|
+
}
|
|
74
|
+
return reauth;
|
|
75
|
+
}
|
|
76
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
77
|
+
console.log("[deleteCurrentUser] Reauth returned null, continuing to deleteUser");
|
|
78
|
+
}
|
|
71
79
|
}
|
|
72
80
|
|
|
73
81
|
try {
|
|
82
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
83
|
+
console.log("[deleteCurrentUser] Calling deleteUser");
|
|
84
|
+
}
|
|
74
85
|
await deleteUser(user);
|
|
86
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
87
|
+
console.log("[deleteCurrentUser] deleteUser successful");
|
|
88
|
+
}
|
|
75
89
|
return successResult();
|
|
76
90
|
} catch (error: unknown) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
91
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
92
|
+
console.error("[deleteCurrentUser] deleteUser failed:", error);
|
|
93
|
+
}
|
|
94
|
+
const errorInfo = toAuthErrorInfo(error);
|
|
95
|
+
const code = errorInfo.code;
|
|
96
|
+
const message = errorInfo.message;
|
|
80
97
|
|
|
81
98
|
const hasCredentials = !!(options.password || options.googleIdToken);
|
|
82
99
|
const shouldReauth = options.autoReauthenticate === true || hasCredentials;
|
|
@@ -149,43 +166,69 @@ async function attemptReauth(user: User, options: AccountDeletionOptions): Promi
|
|
|
149
166
|
console.log("[attemptReauth] onPasswordRequired returned:", pwd ? "password received" : "null/cancelled");
|
|
150
167
|
}
|
|
151
168
|
if (!pwd) {
|
|
169
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
170
|
+
console.log("[attemptReauth] Password was null/cancelled, returning error");
|
|
171
|
+
}
|
|
152
172
|
return {
|
|
153
173
|
success: false,
|
|
154
174
|
error: { code: "auth/password-reauth-cancelled", message: "Password reauth cancelled" },
|
|
155
175
|
requiresReauth: true
|
|
156
176
|
};
|
|
157
177
|
}
|
|
178
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
179
|
+
console.log("[attemptReauth] Password received, setting password variable");
|
|
180
|
+
}
|
|
158
181
|
password = pwd;
|
|
159
182
|
}
|
|
160
183
|
if (!password) {
|
|
184
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
185
|
+
console.log("[attemptReauth] No password available after callback, returning error");
|
|
186
|
+
}
|
|
161
187
|
return {
|
|
162
188
|
success: false,
|
|
163
189
|
error: { code: "auth/password-reauth", message: "Password required" },
|
|
164
190
|
requiresReauth: true
|
|
165
191
|
};
|
|
166
192
|
}
|
|
193
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
194
|
+
console.log("[attemptReauth] Calling reauthenticateWithPassword");
|
|
195
|
+
}
|
|
167
196
|
res = await reauthenticateWithPassword(user, password);
|
|
197
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
198
|
+
console.log("[attemptReauth] reauthenticateWithPassword result:", res);
|
|
199
|
+
}
|
|
168
200
|
} else {
|
|
169
201
|
return null;
|
|
170
202
|
}
|
|
171
203
|
|
|
172
204
|
if (res.success) {
|
|
205
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
206
|
+
console.log("[attemptReauth] Reauthentication successful, calling deleteUser");
|
|
207
|
+
}
|
|
173
208
|
try {
|
|
174
|
-
// After reauthentication, get fresh user reference from auth
|
|
175
209
|
const auth = getFirebaseAuth();
|
|
176
210
|
const currentUser = auth?.currentUser || user;
|
|
177
211
|
await deleteUser(currentUser);
|
|
212
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
213
|
+
console.log("[attemptReauth] deleteUser successful after reauth");
|
|
214
|
+
}
|
|
178
215
|
return successResult();
|
|
179
216
|
} catch (err: unknown) {
|
|
180
|
-
|
|
217
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
218
|
+
console.error("[attemptReauth] deleteUser failed after reauth:", err);
|
|
219
|
+
}
|
|
220
|
+
const errorInfo = toAuthErrorInfo(err);
|
|
181
221
|
return {
|
|
182
222
|
success: false,
|
|
183
|
-
error: { code:
|
|
223
|
+
error: { code: errorInfo.code, message: errorInfo.message },
|
|
184
224
|
requiresReauth: false
|
|
185
225
|
};
|
|
186
226
|
}
|
|
187
227
|
}
|
|
188
228
|
|
|
229
|
+
if (typeof __DEV__ !== "undefined" && __DEV__) {
|
|
230
|
+
console.log("[attemptReauth] Reauthentication failed, returning error");
|
|
231
|
+
}
|
|
189
232
|
return {
|
|
190
233
|
success: false,
|
|
191
234
|
error: {
|
|
@@ -209,12 +252,11 @@ export async function deleteUserAccount(user: User | null): Promise<AccountDelet
|
|
|
209
252
|
await deleteUser(user);
|
|
210
253
|
return successResult();
|
|
211
254
|
} catch (error: unknown) {
|
|
212
|
-
const
|
|
213
|
-
const code = authErr?.code ?? "auth/failed";
|
|
255
|
+
const errorInfo = toAuthErrorInfo(error);
|
|
214
256
|
return {
|
|
215
257
|
success: false,
|
|
216
|
-
error: { code, message:
|
|
217
|
-
requiresReauth: code === "auth/requires-recent-login"
|
|
258
|
+
error: { code: errorInfo.code, message: errorInfo.message },
|
|
259
|
+
requiresReauth: errorInfo.code === "auth/requires-recent-login"
|
|
218
260
|
};
|
|
219
261
|
}
|
|
220
262
|
}
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
import * as AppleAuthentication from "expo-apple-authentication";
|
|
14
14
|
import { Platform } from "react-native";
|
|
15
15
|
import { generateNonce, hashNonce } from "../../../auth/infrastructure/services/crypto.util";
|
|
16
|
-
import { executeOperation, failureResultFrom } from "../../../../shared/domain/utils";
|
|
16
|
+
import { executeOperation, failureResultFrom, toAuthErrorInfo } from "../../../../shared/domain/utils";
|
|
17
17
|
import { isCancelledError } from "../../../../shared/domain/utils/error-handler.util";
|
|
18
18
|
import type {
|
|
19
19
|
ReauthenticationResult,
|
|
@@ -98,15 +98,11 @@ export async function getAppleReauthCredential(): Promise<ReauthCredentialResult
|
|
|
98
98
|
credential
|
|
99
99
|
};
|
|
100
100
|
} catch (error: unknown) {
|
|
101
|
-
const
|
|
102
|
-
const
|
|
103
|
-
code: (err as { code?: string }).code ?? '',
|
|
104
|
-
message: err.message
|
|
105
|
-
};
|
|
106
|
-
const code = isCancelledError(errorInfo) ? "auth/cancelled" : "auth/failed";
|
|
101
|
+
const errorInfo = toAuthErrorInfo(error);
|
|
102
|
+
const code = isCancelledError(errorInfo) ? "auth/cancelled" : errorInfo.code;
|
|
107
103
|
return {
|
|
108
104
|
success: false,
|
|
109
|
-
error: { code, message:
|
|
105
|
+
error: { code, message: errorInfo.message }
|
|
110
106
|
};
|
|
111
107
|
}
|
|
112
108
|
}
|
|
@@ -64,7 +64,8 @@ class FirebaseAuthClientSingleton extends ServiceClientSingleton<Auth, FirebaseA
|
|
|
64
64
|
this.setError(errorMessage);
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
|
-
|
|
67
|
+
// Enable auto-initialization flag when getting instance
|
|
68
|
+
return this.getInstance(true);
|
|
68
69
|
}
|
|
69
70
|
}
|
|
70
71
|
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
import { signInAnonymously, type Auth, type User } from "firebase/auth";
|
|
7
7
|
import { toAnonymousUser, type AnonymousUser } from "../../domain/entities/AnonymousUser";
|
|
8
|
+
import { ERROR_MESSAGES } from "../../../../shared/domain/utils/error-handlers/error-messages";
|
|
8
9
|
|
|
9
10
|
export interface AnonymousAuthResult {
|
|
10
11
|
readonly user: User;
|
|
@@ -18,7 +19,7 @@ export interface AnonymousAuthServiceInterface {
|
|
|
18
19
|
|
|
19
20
|
export class AnonymousAuthService implements AnonymousAuthServiceInterface {
|
|
20
21
|
async signInAnonymously(auth: Auth): Promise<AnonymousAuthResult> {
|
|
21
|
-
if (!auth) throw new Error(
|
|
22
|
+
if (!auth) throw new Error(ERROR_MESSAGES.AUTH.NOT_INITIALIZED);
|
|
22
23
|
|
|
23
24
|
const currentUser = auth.currentUser;
|
|
24
25
|
|
|
@@ -31,10 +32,15 @@ export class AnonymousAuthService implements AnonymousAuthServiceInterface {
|
|
|
31
32
|
}
|
|
32
33
|
|
|
33
34
|
if (currentUser && !currentUser.isAnonymous) {
|
|
34
|
-
throw new Error(
|
|
35
|
+
throw new Error(ERROR_MESSAGES.AUTH.SIGN_OUT_REQUIRED);
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
const userCredential = await signInAnonymously(auth);
|
|
39
|
+
|
|
40
|
+
if (!userCredential.user.isAnonymous) {
|
|
41
|
+
throw new Error(ERROR_MESSAGES.AUTH.INVALID_USER);
|
|
42
|
+
}
|
|
43
|
+
|
|
38
44
|
const anonymousUser = toAnonymousUser(userCredential.user);
|
|
39
45
|
return {
|
|
40
46
|
user: userCredential.user,
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
getCurrentUserId,
|
|
12
12
|
getCurrentUser,
|
|
13
13
|
} from './auth-utils.service';
|
|
14
|
+
import { ERROR_MESSAGES } from '../../../../shared/domain/utils/error-handlers/error-messages';
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Auth Guard Service
|
|
@@ -24,22 +25,21 @@ export class AuthGuardService {
|
|
|
24
25
|
async requireAuthenticatedUser(): Promise<string> {
|
|
25
26
|
const auth = getFirebaseAuth();
|
|
26
27
|
if (!auth) {
|
|
27
|
-
throw new Error(
|
|
28
|
+
throw new Error(ERROR_MESSAGES.AUTH.NOT_INITIALIZED);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
31
|
const userId = getCurrentUserId(auth);
|
|
31
32
|
if (!userId) {
|
|
32
|
-
throw new Error(
|
|
33
|
+
throw new Error(ERROR_MESSAGES.AUTH.NOT_AUTHENTICATED);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
const currentUser = getCurrentUser(auth);
|
|
36
37
|
if (!currentUser) {
|
|
37
|
-
throw new Error(
|
|
38
|
+
throw new Error(ERROR_MESSAGES.AUTH.NOT_AUTHENTICATED);
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
// Check if user is anonymous (guest)
|
|
41
41
|
if (currentUser.isAnonymous) {
|
|
42
|
-
throw new Error(
|
|
42
|
+
throw new Error(ERROR_MESSAGES.AUTH.NON_ANONYMOUS_ONLY);
|
|
43
43
|
}
|
|
44
44
|
|
|
45
45
|
return userId;
|
|
@@ -62,7 +62,11 @@ export const useFirebaseAuthStore = createStore<AuthState, AuthActions>({
|
|
|
62
62
|
// Listener setup complete - keep mutex locked until cleanup
|
|
63
63
|
// (setupInProgress remains true to indicate active listener)
|
|
64
64
|
} catch (error) {
|
|
65
|
-
// On error,
|
|
65
|
+
// On error, clean up partially initialized listener and release the mutex
|
|
66
|
+
if (unsubscribe) {
|
|
67
|
+
unsubscribe();
|
|
68
|
+
unsubscribe = null;
|
|
69
|
+
}
|
|
66
70
|
setupInProgress = false;
|
|
67
71
|
set({ listenerSetup: false, loading: false });
|
|
68
72
|
throw error; // Re-throw to allow caller to handle
|
|
@@ -12,6 +12,19 @@ import type { PaginatedResult, PaginationParams } from "../../types/pagination.t
|
|
|
12
12
|
import { BaseQueryRepository } from "./BaseQueryRepository";
|
|
13
13
|
|
|
14
14
|
export abstract class BasePaginatedRepository extends BaseQueryRepository {
|
|
15
|
+
/**
|
|
16
|
+
* Validate cursor format
|
|
17
|
+
* Cursors must be non-empty strings without path separators
|
|
18
|
+
*/
|
|
19
|
+
private isValidCursor(cursor: string): boolean {
|
|
20
|
+
if (!cursor || typeof cursor !== 'string') return false;
|
|
21
|
+
// Check for invalid characters (path separators, null bytes)
|
|
22
|
+
if (cursor.includes('/') || cursor.includes('\\') || cursor.includes('\0')) return false;
|
|
23
|
+
// Check length (Firestore document IDs can't be longer than 1500 bytes)
|
|
24
|
+
if (cursor.length > 1500) return false;
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
|
|
15
28
|
/**
|
|
16
29
|
* Execute paginated query with cursor support
|
|
17
30
|
*
|
|
@@ -47,6 +60,12 @@ export abstract class BasePaginatedRepository extends BaseQueryRepository {
|
|
|
47
60
|
if (helper.hasCursor(params) && params?.cursor) {
|
|
48
61
|
cursorKey = params.cursor;
|
|
49
62
|
|
|
63
|
+
// Validate cursor format to prevent Firestore errors
|
|
64
|
+
if (!this.isValidCursor(params.cursor)) {
|
|
65
|
+
// Invalid cursor format - return empty result
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
|
|
50
69
|
// Fetch cursor document first
|
|
51
70
|
const cursorDocRef = doc(db, collectionName, params.cursor);
|
|
52
71
|
const cursorDoc = await getDoc(cursorDocRef);
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
import type { Firestore, CollectionReference, DocumentReference, DocumentData } from 'firebase/firestore';
|
|
12
12
|
import { getFirestore, collection, doc } from 'firebase/firestore';
|
|
13
13
|
import { isQuotaError as checkQuotaError } from '../../utils/quota-error-detector.util';
|
|
14
|
+
import { ERROR_MESSAGES } from '../../../../shared/domain/utils/error-handlers/error-messages';
|
|
14
15
|
|
|
15
16
|
export enum RepositoryState {
|
|
16
17
|
ACTIVE = 'active',
|
|
@@ -75,19 +76,15 @@ export abstract class BaseRepository implements IPathResolver {
|
|
|
75
76
|
operation: () => Promise<T>
|
|
76
77
|
): Promise<T> {
|
|
77
78
|
if (this.state === RepositoryState.DESTROYED) {
|
|
78
|
-
throw new Error(
|
|
79
|
+
throw new Error(ERROR_MESSAGES.REPOSITORY.DESTROYED);
|
|
79
80
|
}
|
|
80
81
|
|
|
81
82
|
try {
|
|
82
83
|
return await operation();
|
|
83
84
|
} catch (error) {
|
|
84
|
-
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
85
|
-
|
|
86
|
-
// Check if this is a quota error
|
|
87
85
|
if (checkQuotaError(error)) {
|
|
88
|
-
throw new Error(
|
|
86
|
+
throw new Error(ERROR_MESSAGES.FIRESTORE.QUOTA_EXCEEDED);
|
|
89
87
|
}
|
|
90
|
-
|
|
91
88
|
throw error;
|
|
92
89
|
}
|
|
93
90
|
}
|
|
@@ -84,8 +84,8 @@ export function isQuotaError(error: unknown): boolean {
|
|
|
84
84
|
if (!error || typeof error !== 'object') return false;
|
|
85
85
|
|
|
86
86
|
if (hasCodeProperty(error)) {
|
|
87
|
-
const code = error.code;
|
|
88
|
-
return QUOTA_ERROR_CODES.some(
|
|
87
|
+
const code = error.code.toLowerCase();
|
|
88
|
+
return QUOTA_ERROR_CODES.map(c => c.toLowerCase()).some(
|
|
89
89
|
(c) => code === c || code.endsWith(`/${c}`) || code.startsWith(`${c}/`)
|
|
90
90
|
);
|
|
91
91
|
}
|
|
@@ -94,12 +94,10 @@ export function isQuotaError(error: unknown): boolean {
|
|
|
94
94
|
const message = error.message.toLowerCase();
|
|
95
95
|
return QUOTA_ERROR_MESSAGES.some((m) => {
|
|
96
96
|
const pattern = m.toLowerCase();
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
message === pattern
|
|
102
|
-
);
|
|
97
|
+
// More flexible matching: handle hyphens, underscores, and no spaces
|
|
98
|
+
const normalizedMessage = message.replace(/[-_\s]+/g, ' ');
|
|
99
|
+
const normalizedPattern = pattern.replace(/[-_\s]+/g, ' ');
|
|
100
|
+
return normalizedMessage.includes(normalizedPattern);
|
|
103
101
|
});
|
|
104
102
|
}
|
|
105
103
|
|
|
@@ -1,18 +1,33 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
export const ERROR_MESSAGES = {
|
|
2
|
+
AUTH: {
|
|
3
|
+
NOT_INITIALIZED: 'Firebase Auth is not initialized',
|
|
4
|
+
NOT_AUTHENTICATED: 'User must be authenticated',
|
|
5
|
+
ANONYMOUS_ONLY: 'Anonymous users cannot perform this action',
|
|
6
|
+
NON_ANONYMOUS_ONLY: 'Guest users cannot perform this action',
|
|
7
|
+
SIGN_OUT_REQUIRED: 'Sign out first before performing this action',
|
|
8
|
+
NO_USER: 'No user is currently signed in',
|
|
9
|
+
INVALID_USER: 'Invalid user',
|
|
10
|
+
},
|
|
11
|
+
FIRESTORE: {
|
|
12
|
+
NOT_INITIALIZED: 'Firestore is not initialized',
|
|
13
|
+
QUOTA_EXCEEDED: 'Daily quota exceeded. Please try again tomorrow or upgrade your plan.',
|
|
14
|
+
},
|
|
15
|
+
REPOSITORY: {
|
|
16
|
+
DESTROYED: 'Repository has been destroyed',
|
|
17
|
+
},
|
|
18
|
+
SERVICE: {
|
|
19
|
+
NOT_CONFIGURED: 'Service is not configured',
|
|
20
|
+
},
|
|
21
|
+
GENERAL: {
|
|
22
|
+
RETRYABLE: 'Temporary error occurred. Please try again.',
|
|
23
|
+
UNKNOWN: 'Unknown error occurred',
|
|
24
|
+
},
|
|
25
|
+
} as const;
|
|
5
26
|
|
|
6
|
-
/**
|
|
7
|
-
* Get user-friendly quota error message
|
|
8
|
-
*/
|
|
9
27
|
export function getQuotaErrorMessage(): string {
|
|
10
|
-
return
|
|
28
|
+
return ERROR_MESSAGES.FIRESTORE.QUOTA_EXCEEDED;
|
|
11
29
|
}
|
|
12
30
|
|
|
13
|
-
/**
|
|
14
|
-
* Get user-friendly retryable error message
|
|
15
|
-
*/
|
|
16
31
|
export function getRetryableErrorMessage(): string {
|
|
17
|
-
return
|
|
32
|
+
return ERROR_MESSAGES.GENERAL.RETRYABLE;
|
|
18
33
|
}
|
|
@@ -1,41 +1,38 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Batch Async Executors
|
|
3
|
-
* Execute multiple operations in parallel or sequence
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
1
|
import type { Result, FailureResult } from '../result.util';
|
|
7
|
-
import { failureResultFromError, successResult } from '../result.util';
|
|
2
|
+
import { failureResultFromError, successResult, isSuccess } from '../result.util';
|
|
8
3
|
|
|
9
|
-
/**
|
|
10
|
-
* Execute multiple operations in parallel
|
|
11
|
-
* Returns success only if all operations succeed
|
|
12
|
-
*/
|
|
13
4
|
export async function executeAll<T>(
|
|
14
5
|
...operations: (() => Promise<Result<T>>)[]
|
|
15
6
|
): Promise<Result<T[]>> {
|
|
16
7
|
try {
|
|
17
8
|
const results = await Promise.all(operations.map((op) => op()));
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
9
|
+
|
|
10
|
+
for (const result of results) {
|
|
11
|
+
if (!result.success && result.error !== undefined) {
|
|
12
|
+
return result as FailureResult;
|
|
13
|
+
}
|
|
21
14
|
}
|
|
22
|
-
|
|
15
|
+
|
|
16
|
+
const data: T[] = [];
|
|
17
|
+
for (const result of results) {
|
|
18
|
+
if (isSuccess(result) && result.data !== undefined) {
|
|
19
|
+
data.push(result.data);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
23
|
return successResult(data);
|
|
24
24
|
} catch (error) {
|
|
25
25
|
return failureResultFromError(error);
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
/**
|
|
30
|
-
* Execute operations in sequence, stopping at first failure
|
|
31
|
-
*/
|
|
32
29
|
export async function executeSequence<T>(
|
|
33
30
|
...operations: (() => Promise<Result<T>>)[]
|
|
34
31
|
): Promise<Result<void>> {
|
|
35
32
|
for (const operation of operations) {
|
|
36
33
|
const result = await operation();
|
|
37
|
-
if (!result.success) {
|
|
38
|
-
return result
|
|
34
|
+
if (!result.success && result.error !== undefined) {
|
|
35
|
+
return { success: false, error: result.error };
|
|
39
36
|
}
|
|
40
37
|
}
|
|
41
38
|
return successResult();
|
|
@@ -17,7 +17,7 @@ export function isSuccess<T>(result: Result<T>): result is SuccessResult<T> {
|
|
|
17
17
|
* Check if result is a failure
|
|
18
18
|
*/
|
|
19
19
|
export function isFailure<T>(result: Result<T>): result is FailureResult {
|
|
20
|
-
return result.success === false;
|
|
20
|
+
return result.success === false && result.error !== undefined;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
/**
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Configurable Service Base Class
|
|
3
|
-
* Provides common service configuration pattern
|
|
4
|
-
* Eliminates code duplication across services
|
|
5
|
-
*/
|
|
1
|
+
import { ERROR_MESSAGES } from './error-handlers/error-messages';
|
|
6
2
|
|
|
7
3
|
/**
|
|
8
4
|
* Configuration state management
|
|
@@ -78,12 +74,9 @@ export class ConfigurableService<TConfig = unknown> implements IConfigurableServ
|
|
|
78
74
|
return true;
|
|
79
75
|
}
|
|
80
76
|
|
|
81
|
-
/**
|
|
82
|
-
* Get required configuration or throw error
|
|
83
|
-
*/
|
|
84
77
|
protected requireConfig(): TConfig {
|
|
85
78
|
if (!this.configState.config) {
|
|
86
|
-
throw new Error(
|
|
79
|
+
throw new Error(ERROR_MESSAGES.SERVICE.NOT_CONFIGURED);
|
|
87
80
|
}
|
|
88
81
|
return this.configState.config;
|
|
89
82
|
}
|
|
@@ -96,7 +96,7 @@ export function loadFirebaseConfig(): FirebaseConfig | null {
|
|
|
96
96
|
|
|
97
97
|
// Validate authDomain format (should be like "projectId.firebaseapp.com")
|
|
98
98
|
if (!isValidFirebaseAuthDomain(authDomain)) {
|
|
99
|
-
|
|
99
|
+
return null;
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
// Build type-safe FirebaseConfig object
|
|
@@ -33,6 +33,7 @@ export interface ServiceClientOptions<TInstance, TConfig = unknown> {
|
|
|
33
33
|
export class ServiceClientSingleton<TInstance, TConfig = unknown> {
|
|
34
34
|
protected state: ServiceClientState<TInstance>;
|
|
35
35
|
private readonly options: ServiceClientOptions<TInstance, TConfig>;
|
|
36
|
+
private initInProgress = false;
|
|
36
37
|
|
|
37
38
|
constructor(options: ServiceClientOptions<TInstance, TConfig>) {
|
|
38
39
|
this.options = options;
|
|
@@ -55,6 +56,12 @@ export class ServiceClientSingleton<TInstance, TConfig = unknown> {
|
|
|
55
56
|
return null;
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
// Prevent concurrent initialization attempts
|
|
60
|
+
if (this.initInProgress) {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
this.initInProgress = true;
|
|
58
65
|
try {
|
|
59
66
|
const instance = this.options.initializer ? this.options.initializer(config) : null;
|
|
60
67
|
if (instance) {
|
|
@@ -66,6 +73,8 @@ export class ServiceClientSingleton<TInstance, TConfig = unknown> {
|
|
|
66
73
|
const errorMessage = error instanceof Error ? error.message : `Failed to initialize ${this.options.serviceName}`;
|
|
67
74
|
this.state.initializationError = errorMessage;
|
|
68
75
|
return null;
|
|
76
|
+
} finally {
|
|
77
|
+
this.initInProgress = false;
|
|
69
78
|
}
|
|
70
79
|
}
|
|
71
80
|
|
|
@@ -81,7 +90,13 @@ export class ServiceClientSingleton<TInstance, TConfig = unknown> {
|
|
|
81
90
|
return null;
|
|
82
91
|
}
|
|
83
92
|
|
|
93
|
+
// Prevent concurrent auto-initialization attempts
|
|
94
|
+
if (this.initInProgress) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
84
98
|
if (autoInit && this.options.autoInitializer) {
|
|
99
|
+
this.initInProgress = true;
|
|
85
100
|
try {
|
|
86
101
|
const instance = this.options.autoInitializer();
|
|
87
102
|
if (instance) {
|
|
@@ -92,6 +107,8 @@ export class ServiceClientSingleton<TInstance, TConfig = unknown> {
|
|
|
92
107
|
} catch (error) {
|
|
93
108
|
const errorMessage = error instanceof Error ? error.message : `Failed to initialize ${this.options.serviceName}`;
|
|
94
109
|
this.state.initializationError = errorMessage;
|
|
110
|
+
} finally {
|
|
111
|
+
this.initInProgress = false;
|
|
95
112
|
}
|
|
96
113
|
}
|
|
97
114
|
|
|
@@ -119,6 +136,7 @@ export class ServiceClientSingleton<TInstance, TConfig = unknown> {
|
|
|
119
136
|
this.state.instance = null;
|
|
120
137
|
this.state.initializationError = null;
|
|
121
138
|
this.state.isInitialized = false;
|
|
139
|
+
this.initInProgress = false;
|
|
122
140
|
}
|
|
123
141
|
|
|
124
142
|
/**
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { FirebaseConfig } from '../../../domain/value-objects/FirebaseConfig';
|
|
9
9
|
import { FirebaseConfigurationError } from '../../../domain/errors/FirebaseError';
|
|
10
|
-
import { isValidString, isValidFirebaseApiKey, isValidFirebaseProjectId } from '../../../domain/utils/validation.util';
|
|
10
|
+
import { isValidString, isValidFirebaseApiKey, isValidFirebaseProjectId, isValidFirebaseAuthDomain } from '../../../domain/utils/validation.util';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Validation rule interface
|
|
@@ -70,7 +70,7 @@ class PlaceholderRule implements ValidationRule {
|
|
|
70
70
|
export class FirebaseConfigValidator {
|
|
71
71
|
private static rules: ValidationRule[] = [
|
|
72
72
|
new RequiredFieldRule('API Key', config => config.apiKey, isValidFirebaseApiKey),
|
|
73
|
-
new RequiredFieldRule('Auth Domain', config => config.authDomain),
|
|
73
|
+
new RequiredFieldRule('Auth Domain', config => config.authDomain, isValidFirebaseAuthDomain),
|
|
74
74
|
new RequiredFieldRule('Project ID', config => config.projectId, isValidFirebaseProjectId),
|
|
75
75
|
new PlaceholderRule('API Key', config => config.apiKey, 'your_firebase_api_key'),
|
|
76
76
|
new PlaceholderRule('Project ID', config => config.projectId, 'your-project-id'),
|