@umituz/react-native-firebase 1.13.20 → 1.13.23
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 +2 -2
- package/src/firestore/domain/entities/QuotaMetrics.ts +15 -17
- package/src/firestore/domain/entities/RequestLog.ts +18 -20
- package/src/firestore/index.ts +10 -26
- package/src/firestore/infrastructure/middleware/QuotaTrackingMiddleware.ts +81 -143
- package/src/firestore/utils/quota-error-detector.util.ts +33 -74
- package/src/storage/deleter.ts +80 -0
- package/src/storage/index.ts +7 -1
- package/src/storage/types.ts +19 -0
- package/src/storage/uploader.ts +19 -17
- package/src/firestore/infrastructure/services/QuotaMonitorService.ts +0 -108
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.23",
|
|
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",
|
|
@@ -52,4 +52,4 @@
|
|
|
52
52
|
"README.md",
|
|
53
53
|
"LICENSE"
|
|
54
54
|
]
|
|
55
|
-
}
|
|
55
|
+
}
|
|
@@ -1,28 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Quota Metrics
|
|
3
|
-
* Domain entity for tracking Firestore quota usage
|
|
2
|
+
* Quota Metrics Types
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
5
|
export interface QuotaMetrics {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
readCount: number;
|
|
7
|
+
writeCount: number;
|
|
8
|
+
deleteCount: number;
|
|
9
|
+
lastResetDate: string;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
export interface QuotaLimits {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
13
|
+
dailyReadLimit: number;
|
|
14
|
+
dailyWriteLimit: number;
|
|
15
|
+
dailyDeleteLimit: number;
|
|
17
16
|
}
|
|
18
17
|
|
|
19
18
|
export interface QuotaStatus {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
19
|
+
metrics: QuotaMetrics;
|
|
20
|
+
limits: QuotaLimits;
|
|
21
|
+
readPercentage: number;
|
|
22
|
+
writePercentage: number;
|
|
23
|
+
deletePercentage: number;
|
|
24
|
+
isNearLimit: boolean;
|
|
25
|
+
isOverLimit: boolean;
|
|
27
26
|
}
|
|
28
|
-
|
|
@@ -1,30 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Request Log
|
|
3
|
-
* Domain entity for tracking Firestore requests
|
|
2
|
+
* Request Log Types
|
|
4
3
|
*/
|
|
5
4
|
|
|
6
5
|
export type RequestType = 'read' | 'write' | 'delete' | 'listener';
|
|
7
6
|
|
|
8
7
|
export interface RequestLog {
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
8
|
+
id: string;
|
|
9
|
+
type: RequestType;
|
|
10
|
+
collection: string;
|
|
11
|
+
documentId?: string;
|
|
12
|
+
cached: boolean;
|
|
13
|
+
success: boolean;
|
|
14
|
+
error?: string;
|
|
15
|
+
duration?: number;
|
|
16
|
+
timestamp: number;
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
export interface RequestStats {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
20
|
+
totalRequests: number;
|
|
21
|
+
readRequests: number;
|
|
22
|
+
writeRequests: number;
|
|
23
|
+
deleteRequests: number;
|
|
24
|
+
listenerRequests: number;
|
|
25
|
+
cachedRequests: number;
|
|
26
|
+
failedRequests: number;
|
|
27
|
+
averageDuration: number;
|
|
29
28
|
}
|
|
30
|
-
|
package/src/firestore/index.ts
CHANGED
|
@@ -1,17 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* React Native Firestore Module
|
|
3
3
|
* Domain-Driven Design (DDD) Architecture
|
|
4
|
-
*
|
|
5
|
-
* This is the SINGLE SOURCE OF TRUTH for all Firestore operations.
|
|
6
|
-
* ALL imports from the Firestore module MUST go through this file.
|
|
7
|
-
*
|
|
8
|
-
* Architecture:
|
|
9
|
-
* - domain: Errors, Constants, Entities, Services
|
|
10
|
-
* - infrastructure: Firestore client, BaseRepository, utilities
|
|
11
|
-
* - utils: Date utilities, timestamp conversion, query builders
|
|
12
|
-
*
|
|
13
|
-
* This module is designed to be used across hundreds of apps.
|
|
14
|
-
* It provides a consistent interface for Firestore operations.
|
|
15
4
|
*/
|
|
16
5
|
|
|
17
6
|
// =============================================================================
|
|
@@ -98,16 +87,6 @@ export {
|
|
|
98
87
|
createDocumentMapper,
|
|
99
88
|
} from './utils/document-mapper.helper';
|
|
100
89
|
|
|
101
|
-
// =============================================================================
|
|
102
|
-
// UTILS - Quota Error Detection
|
|
103
|
-
// =============================================================================
|
|
104
|
-
|
|
105
|
-
export {
|
|
106
|
-
isQuotaError,
|
|
107
|
-
isRetryableError,
|
|
108
|
-
getQuotaErrorMessage,
|
|
109
|
-
} from './utils/quota-error-detector.util';
|
|
110
|
-
|
|
111
90
|
// =============================================================================
|
|
112
91
|
// UTILS - Path Resolver
|
|
113
92
|
// =============================================================================
|
|
@@ -148,6 +127,16 @@ export type {
|
|
|
148
127
|
|
|
149
128
|
export { QuotaCalculator } from './domain/services/QuotaCalculator';
|
|
150
129
|
|
|
130
|
+
// =============================================================================
|
|
131
|
+
// UTILS - Quota Error Detection
|
|
132
|
+
// =============================================================================
|
|
133
|
+
|
|
134
|
+
export {
|
|
135
|
+
isQuotaError,
|
|
136
|
+
isRetryableError,
|
|
137
|
+
getQuotaErrorMessage,
|
|
138
|
+
} from './utils/quota-error-detector.util';
|
|
139
|
+
|
|
151
140
|
// =============================================================================
|
|
152
141
|
// INFRASTRUCTURE LAYER - Middleware
|
|
153
142
|
// =============================================================================
|
|
@@ -166,11 +155,6 @@ export {
|
|
|
166
155
|
// INFRASTRUCTURE LAYER - Services
|
|
167
156
|
// =============================================================================
|
|
168
157
|
|
|
169
|
-
export {
|
|
170
|
-
QuotaMonitorService,
|
|
171
|
-
quotaMonitorService,
|
|
172
|
-
} from './infrastructure/services/QuotaMonitorService';
|
|
173
|
-
|
|
174
158
|
export {
|
|
175
159
|
RequestLoggerService,
|
|
176
160
|
requestLoggerService,
|
|
@@ -3,163 +3,101 @@
|
|
|
3
3
|
* Tracks Firestore operations for quota monitoring
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
7
|
-
import { requestLoggerService } from '../services/RequestLoggerService';
|
|
8
|
-
import type { RequestType } from '../../domain/entities/RequestLog';
|
|
6
|
+
declare const __DEV__: boolean;
|
|
9
7
|
|
|
10
|
-
interface
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
cached?: boolean;
|
|
8
|
+
interface OperationInfo {
|
|
9
|
+
type: 'read' | 'write' | 'delete';
|
|
10
|
+
collection: string;
|
|
11
|
+
count: number;
|
|
12
|
+
cached: boolean;
|
|
16
13
|
}
|
|
17
14
|
|
|
18
15
|
export class QuotaTrackingMiddleware {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
trackRead(collection: string, count: number = 1, cached: boolean = false): void {
|
|
23
|
-
quotaMonitorService.incrementRead(count);
|
|
24
|
-
requestLoggerService.logRequest({
|
|
25
|
-
type: 'read',
|
|
26
|
-
collection,
|
|
27
|
-
success: true,
|
|
28
|
-
cached,
|
|
29
|
-
});
|
|
30
|
-
}
|
|
16
|
+
private readCount = 0;
|
|
17
|
+
private writeCount = 0;
|
|
18
|
+
private deleteCount = 0;
|
|
31
19
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
quotaMonitorService.incrementWrite(count);
|
|
41
|
-
requestLoggerService.logRequest({
|
|
42
|
-
type: 'write',
|
|
43
|
-
collection,
|
|
44
|
-
documentId,
|
|
45
|
-
success: true,
|
|
46
|
-
cached: false,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
20
|
+
/**
|
|
21
|
+
* Track a Firestore operation
|
|
22
|
+
*/
|
|
23
|
+
async trackOperation<T>(
|
|
24
|
+
info: OperationInfo,
|
|
25
|
+
operation: () => Promise<T>
|
|
26
|
+
): Promise<T> {
|
|
27
|
+
const result = await operation();
|
|
49
28
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
success: true,
|
|
64
|
-
cached: false,
|
|
65
|
-
});
|
|
66
|
-
}
|
|
29
|
+
switch (info.type) {
|
|
30
|
+
case 'read':
|
|
31
|
+
if (!info.cached) {
|
|
32
|
+
this.readCount += info.count;
|
|
33
|
+
}
|
|
34
|
+
break;
|
|
35
|
+
case 'write':
|
|
36
|
+
this.writeCount += info.count;
|
|
37
|
+
break;
|
|
38
|
+
case 'delete':
|
|
39
|
+
this.deleteCount += info.count;
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
67
42
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
trackListener(collection: string, documentId?: string): void {
|
|
72
|
-
requestLoggerService.logRequest({
|
|
73
|
-
type: 'listener',
|
|
74
|
-
collection,
|
|
75
|
-
documentId,
|
|
76
|
-
success: true,
|
|
77
|
-
cached: false,
|
|
78
|
-
});
|
|
79
|
-
}
|
|
43
|
+
if (__DEV__) {
|
|
44
|
+
console.log(`[QuotaTracking] ${info.type}: ${info.collection} (${info.count})`);
|
|
45
|
+
}
|
|
80
46
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
*/
|
|
84
|
-
trackError(
|
|
85
|
-
type: RequestType,
|
|
86
|
-
collection: string,
|
|
87
|
-
error: string,
|
|
88
|
-
documentId?: string,
|
|
89
|
-
): void {
|
|
90
|
-
requestLoggerService.logRequest({
|
|
91
|
-
type,
|
|
92
|
-
collection,
|
|
93
|
-
documentId,
|
|
94
|
-
success: false,
|
|
95
|
-
error,
|
|
96
|
-
cached: false,
|
|
97
|
-
});
|
|
98
|
-
}
|
|
47
|
+
return result;
|
|
48
|
+
}
|
|
99
49
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
50
|
+
/**
|
|
51
|
+
* Track read operation
|
|
52
|
+
*/
|
|
53
|
+
trackRead(collection: string, count: number = 1, cached: boolean = false): void {
|
|
54
|
+
if (!cached) {
|
|
55
|
+
this.readCount += count;
|
|
56
|
+
}
|
|
57
|
+
if (__DEV__) {
|
|
58
|
+
console.log(`[QuotaTracking] read: ${collection} (${count})`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
108
61
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
62
|
+
/**
|
|
63
|
+
* Track write operation
|
|
64
|
+
*/
|
|
65
|
+
trackWrite(collection: string, _documentId?: string, count: number = 1): void {
|
|
66
|
+
this.writeCount += count;
|
|
67
|
+
if (__DEV__) {
|
|
68
|
+
console.log(`[QuotaTracking] write: ${collection} (${count})`);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
112
71
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
});
|
|
123
|
-
} else if (operation.type === 'write') {
|
|
124
|
-
quotaMonitorService.incrementWrite(operation.count);
|
|
125
|
-
requestLoggerService.logRequest({
|
|
126
|
-
type: 'write',
|
|
127
|
-
collection: operation.collection,
|
|
128
|
-
documentId: operation.documentId,
|
|
129
|
-
success: true,
|
|
130
|
-
cached: false,
|
|
131
|
-
duration,
|
|
132
|
-
});
|
|
133
|
-
} else if (operation.type === 'delete') {
|
|
134
|
-
quotaMonitorService.incrementDelete(operation.count);
|
|
135
|
-
requestLoggerService.logRequest({
|
|
136
|
-
type: 'delete',
|
|
137
|
-
collection: operation.collection,
|
|
138
|
-
documentId: operation.documentId,
|
|
139
|
-
success: true,
|
|
140
|
-
cached: false,
|
|
141
|
-
duration,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
72
|
+
/**
|
|
73
|
+
* Track delete operation
|
|
74
|
+
*/
|
|
75
|
+
trackDelete(collection: string, _documentId?: string, count: number = 1): void {
|
|
76
|
+
this.deleteCount += count;
|
|
77
|
+
if (__DEV__) {
|
|
78
|
+
console.log(`[QuotaTracking] delete: ${collection} (${count})`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
144
81
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
cached: false,
|
|
156
|
-
duration,
|
|
157
|
-
});
|
|
82
|
+
/**
|
|
83
|
+
* Get current counts
|
|
84
|
+
*/
|
|
85
|
+
getCounts(): { reads: number; writes: number; deletes: number } {
|
|
86
|
+
return {
|
|
87
|
+
reads: this.readCount,
|
|
88
|
+
writes: this.writeCount,
|
|
89
|
+
deletes: this.deleteCount,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
158
92
|
|
|
159
|
-
|
|
93
|
+
/**
|
|
94
|
+
* Reset counts
|
|
95
|
+
*/
|
|
96
|
+
reset(): void {
|
|
97
|
+
this.readCount = 0;
|
|
98
|
+
this.writeCount = 0;
|
|
99
|
+
this.deleteCount = 0;
|
|
160
100
|
}
|
|
161
|
-
}
|
|
162
101
|
}
|
|
163
102
|
|
|
164
103
|
export const quotaTrackingMiddleware = new QuotaTrackingMiddleware();
|
|
165
|
-
|
|
@@ -1,100 +1,59 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Quota Error
|
|
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
|
|
2
|
+
* Quota Error Detection Utilities
|
|
10
3
|
*/
|
|
11
4
|
|
|
5
|
+
const QUOTA_ERROR_CODES = [
|
|
6
|
+
'resource-exhausted',
|
|
7
|
+
'quota-exceeded',
|
|
8
|
+
'RESOURCE_EXHAUSTED',
|
|
9
|
+
];
|
|
10
|
+
|
|
11
|
+
const QUOTA_ERROR_MESSAGES = [
|
|
12
|
+
'quota',
|
|
13
|
+
'exceeded',
|
|
14
|
+
'limit',
|
|
15
|
+
'too many requests',
|
|
16
|
+
];
|
|
17
|
+
|
|
12
18
|
/**
|
|
13
|
-
* Check if error is a
|
|
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
|
|
19
|
+
* Check if error is a Firestore quota error
|
|
18
20
|
*/
|
|
19
21
|
export function isQuotaError(error: unknown): boolean {
|
|
20
|
-
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
22
|
+
if (!error) return false;
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
const errorObj = error as Record<string, unknown>;
|
|
25
|
+
const code = errorObj.code as string | undefined;
|
|
26
|
+
const message = errorObj.message as string | undefined;
|
|
25
27
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
}
|
|
28
|
+
if (code && QUOTA_ERROR_CODES.some((c) => code.includes(c))) {
|
|
29
|
+
return true;
|
|
30
|
+
}
|
|
30
31
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
errorMessage.includes("quota exceeded") ||
|
|
36
|
-
errorMessage.includes("resource-exhausted") ||
|
|
37
|
-
errorMessage.includes("daily limit")
|
|
38
|
-
) {
|
|
39
|
-
return true;
|
|
40
|
-
}
|
|
32
|
+
if (message) {
|
|
33
|
+
const lowerMessage = message.toLowerCase();
|
|
34
|
+
return QUOTA_ERROR_MESSAGES.some((m) => lowerMessage.includes(m));
|
|
35
|
+
}
|
|
41
36
|
|
|
42
|
-
|
|
43
|
-
const errorName = firebaseError.name?.toLowerCase() || "";
|
|
44
|
-
if (errorName.includes("quota") || errorName.includes("resource-exhausted")) {
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
return false;
|
|
37
|
+
return false;
|
|
49
38
|
}
|
|
50
39
|
|
|
51
40
|
/**
|
|
52
41
|
* 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
42
|
*/
|
|
58
43
|
export function isRetryableError(error: unknown): boolean {
|
|
59
|
-
|
|
60
|
-
if (isQuotaError(error)) {
|
|
61
|
-
return false;
|
|
62
|
-
}
|
|
44
|
+
if (!error) return false;
|
|
63
45
|
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
}
|
|
46
|
+
const errorObj = error as Record<string, unknown>;
|
|
47
|
+
const code = errorObj.code as string | undefined;
|
|
74
48
|
|
|
75
|
-
|
|
76
|
-
if (
|
|
77
|
-
firebaseError.code === "unavailable" ||
|
|
78
|
-
firebaseError.code === "deadline-exceeded"
|
|
79
|
-
) {
|
|
80
|
-
return true;
|
|
81
|
-
}
|
|
49
|
+
const retryableCodes = ['unavailable', 'deadline-exceeded', 'aborted'];
|
|
82
50
|
|
|
83
|
-
|
|
84
|
-
const errorMessage = firebaseError.message?.toLowerCase() || "";
|
|
85
|
-
if (errorMessage.includes("timeout")) {
|
|
86
|
-
return true;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
return false;
|
|
51
|
+
return code ? retryableCodes.some((c) => code.includes(c)) : false;
|
|
90
52
|
}
|
|
91
53
|
|
|
92
54
|
/**
|
|
93
55
|
* Get user-friendly quota error message
|
|
94
|
-
*
|
|
95
|
-
* @returns User-friendly error message
|
|
96
56
|
*/
|
|
97
57
|
export function getQuotaErrorMessage(): string {
|
|
98
|
-
|
|
58
|
+
return 'Daily quota exceeded. Please try again tomorrow or upgrade your plan.';
|
|
99
59
|
}
|
|
100
|
-
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Storage Deleter
|
|
3
|
+
* Handles image deletion from Firebase Storage
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { getStorage, ref, deleteObject } from "firebase/storage";
|
|
7
|
+
import { getFirebaseApp } from "../infrastructure/config/FirebaseClient";
|
|
8
|
+
import type { DeleteResult } from "./types";
|
|
9
|
+
|
|
10
|
+
declare const __DEV__: boolean;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract storage path from Firebase download URL
|
|
14
|
+
*/
|
|
15
|
+
function extractStoragePath(downloadUrl: string): string | null {
|
|
16
|
+
if (!downloadUrl.startsWith("https://")) {
|
|
17
|
+
return downloadUrl;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const urlObj = new URL(downloadUrl);
|
|
22
|
+
const pathMatch = urlObj.pathname.match(/\/o\/(.+)/);
|
|
23
|
+
|
|
24
|
+
if (!pathMatch) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return decodeURIComponent(pathMatch[1]);
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function getStorageInstance() {
|
|
35
|
+
const app = getFirebaseApp();
|
|
36
|
+
return app ? getStorage(app) : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Delete image from Firebase Storage
|
|
41
|
+
* Accepts either a download URL or storage path
|
|
42
|
+
*/
|
|
43
|
+
export async function deleteStorageFile(
|
|
44
|
+
downloadUrlOrPath: string
|
|
45
|
+
): Promise<DeleteResult> {
|
|
46
|
+
if (__DEV__) {
|
|
47
|
+
console.log("[StorageDeleter] Deleting", { url: downloadUrlOrPath });
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const storagePath = extractStoragePath(downloadUrlOrPath);
|
|
51
|
+
|
|
52
|
+
if (!storagePath) {
|
|
53
|
+
if (__DEV__) {
|
|
54
|
+
console.error("[StorageDeleter] Invalid URL", {
|
|
55
|
+
url: downloadUrlOrPath,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
return { success: false, storagePath: downloadUrlOrPath };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
try {
|
|
62
|
+
const storage = getStorageInstance();
|
|
63
|
+
if (!storage) {
|
|
64
|
+
throw new Error("Firebase Storage not initialized");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const storageRef = ref(storage, storagePath);
|
|
68
|
+
await deleteObject(storageRef);
|
|
69
|
+
|
|
70
|
+
if (__DEV__) {
|
|
71
|
+
console.log("[StorageDeleter] Deleted successfully", {
|
|
72
|
+
storagePath,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return { success: true, storagePath };
|
|
77
|
+
} catch {
|
|
78
|
+
return { success: false, storagePath };
|
|
79
|
+
}
|
|
80
|
+
}
|
package/src/storage/index.ts
CHANGED
|
@@ -1 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Storage Module
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export type { UploadResult, UploadOptions, DeleteResult } from "./types";
|
|
6
|
+
export { uploadBase64Image, uploadFile, getMimeType } from "./uploader";
|
|
7
|
+
export { deleteStorageFile } from "./deleter";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Storage Types
|
|
3
|
+
* Shared types for storage operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface UploadResult {
|
|
7
|
+
downloadUrl: string;
|
|
8
|
+
storagePath: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface UploadOptions {
|
|
12
|
+
mimeType?: string;
|
|
13
|
+
customMetadata?: Record<string, string>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface DeleteResult {
|
|
17
|
+
success: boolean;
|
|
18
|
+
storagePath: string;
|
|
19
|
+
}
|
package/src/storage/uploader.ts
CHANGED
|
@@ -11,20 +11,10 @@ import {
|
|
|
11
11
|
type UploadMetadata,
|
|
12
12
|
} from "firebase/storage";
|
|
13
13
|
import { getFirebaseApp } from "../infrastructure/config/FirebaseClient";
|
|
14
|
+
import type { UploadResult, UploadOptions } from "./types";
|
|
14
15
|
|
|
15
16
|
declare const __DEV__: boolean;
|
|
16
17
|
|
|
17
|
-
export interface UploadResult {
|
|
18
|
-
downloadUrl: string;
|
|
19
|
-
storagePath: string;
|
|
20
|
-
metadata?: any;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export interface UploadOptions {
|
|
24
|
-
mimeType?: string;
|
|
25
|
-
metadata?: any;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
18
|
/**
|
|
29
19
|
* Extract MIME type from base64 data URL or return default
|
|
30
20
|
*/
|
|
@@ -79,16 +69,22 @@ export async function uploadBase64Image(
|
|
|
79
69
|
|
|
80
70
|
const metadata: UploadMetadata = {
|
|
81
71
|
contentType: mimeType,
|
|
82
|
-
customMetadata: options?.
|
|
72
|
+
customMetadata: options?.customMetadata,
|
|
83
73
|
};
|
|
84
74
|
|
|
85
|
-
|
|
75
|
+
await uploadBytes(storageRef, blob, metadata);
|
|
86
76
|
const downloadUrl = await getDownloadURL(storageRef);
|
|
87
77
|
|
|
78
|
+
if (__DEV__) {
|
|
79
|
+
console.log("[StorageUploader] Upload complete", {
|
|
80
|
+
storagePath,
|
|
81
|
+
downloadUrl,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
88
85
|
return {
|
|
89
86
|
downloadUrl,
|
|
90
87
|
storagePath,
|
|
91
|
-
metadata: snapshot.metadata,
|
|
92
88
|
};
|
|
93
89
|
}
|
|
94
90
|
|
|
@@ -118,15 +114,21 @@ export async function uploadFile(
|
|
|
118
114
|
|
|
119
115
|
const metadata: UploadMetadata = {
|
|
120
116
|
contentType: options?.mimeType ?? "image/jpeg",
|
|
121
|
-
customMetadata: options?.
|
|
117
|
+
customMetadata: options?.customMetadata,
|
|
122
118
|
};
|
|
123
119
|
|
|
124
|
-
|
|
120
|
+
await uploadBytes(storageRef, blob, metadata);
|
|
125
121
|
const downloadUrl = await getDownloadURL(storageRef);
|
|
126
122
|
|
|
123
|
+
if (__DEV__) {
|
|
124
|
+
console.log("[StorageUploader] Upload complete", {
|
|
125
|
+
storagePath,
|
|
126
|
+
downloadUrl,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
127
130
|
return {
|
|
128
131
|
downloadUrl,
|
|
129
132
|
storagePath,
|
|
130
|
-
metadata: snapshot.metadata,
|
|
131
133
|
};
|
|
132
134
|
}
|
|
@@ -1,108 +0,0 @@
|
|
|
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
|
-
|