@umituz/react-native-firebase 1.13.35 → 1.13.37
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 +11 -2
- package/scripts/auth.ts +142 -0
- package/scripts/firestore.ts +280 -0
- package/scripts/index.ts +85 -0
- package/scripts/init.ts +61 -0
- package/scripts/storage.ts +119 -0
- package/scripts/types.ts +55 -0
- package/scripts/utils.ts +95 -0
- package/src/auth/infrastructure/services/firestore-utils.service.ts +9 -9
- package/src/auth/presentation/hooks/useAnonymousAuth.ts +0 -3
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.37",
|
|
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",
|
|
@@ -49,16 +49,25 @@
|
|
|
49
49
|
"expo-apple-authentication": "^8.0.8",
|
|
50
50
|
"expo-crypto": "^15.0.8",
|
|
51
51
|
"firebase": "^12.6.0",
|
|
52
|
+
"firebase-admin": "^13.0.2",
|
|
52
53
|
"react": "19.1.0",
|
|
53
54
|
"react-native": "0.81.5",
|
|
54
55
|
"typescript": "~5.9.2"
|
|
55
56
|
},
|
|
57
|
+
"optionalDependencies": {
|
|
58
|
+
"firebase-admin": "^13.0.2"
|
|
59
|
+
},
|
|
56
60
|
"publishConfig": {
|
|
57
61
|
"access": "public"
|
|
58
62
|
},
|
|
59
63
|
"files": [
|
|
60
64
|
"src",
|
|
65
|
+
"scripts",
|
|
61
66
|
"README.md",
|
|
62
67
|
"LICENSE"
|
|
63
|
-
]
|
|
68
|
+
],
|
|
69
|
+
"exports": {
|
|
70
|
+
".": "./src/index.ts",
|
|
71
|
+
"./scripts": "./scripts/index.ts"
|
|
72
|
+
}
|
|
64
73
|
}
|
package/scripts/auth.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Admin Auth Utilities
|
|
3
|
+
* Generic auth operations for admin scripts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as admin from "firebase-admin";
|
|
7
|
+
import type { UserInfo, CleanupResult } from "./types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* List all users from Firebase Auth
|
|
11
|
+
*/
|
|
12
|
+
export async function listAllUsers(auth: admin.auth.Auth): Promise<UserInfo[]> {
|
|
13
|
+
const users: UserInfo[] = [];
|
|
14
|
+
let nextPageToken: string | undefined;
|
|
15
|
+
|
|
16
|
+
do {
|
|
17
|
+
const result = await auth.listUsers(1000, nextPageToken);
|
|
18
|
+
|
|
19
|
+
result.users.forEach((user) => {
|
|
20
|
+
users.push({
|
|
21
|
+
uid: user.uid,
|
|
22
|
+
email: user.email,
|
|
23
|
+
displayName: user.displayName,
|
|
24
|
+
isAnonymous: !user.providerData || user.providerData.length === 0,
|
|
25
|
+
createdAt: user.metadata.creationTime
|
|
26
|
+
? new Date(user.metadata.creationTime)
|
|
27
|
+
: undefined,
|
|
28
|
+
providerCount: user.providerData?.length ?? 0,
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
nextPageToken = result.pageToken;
|
|
33
|
+
} while (nextPageToken);
|
|
34
|
+
|
|
35
|
+
return users;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* List only authenticated users (with email/providers)
|
|
40
|
+
*/
|
|
41
|
+
export async function listAuthenticatedUsers(
|
|
42
|
+
auth: admin.auth.Auth
|
|
43
|
+
): Promise<UserInfo[]> {
|
|
44
|
+
const allUsers = await listAllUsers(auth);
|
|
45
|
+
return allUsers.filter((user) => !user.isAnonymous && user.email);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* List only anonymous users
|
|
50
|
+
*/
|
|
51
|
+
export async function listAnonymousUsers(
|
|
52
|
+
auth: admin.auth.Auth
|
|
53
|
+
): Promise<UserInfo[]> {
|
|
54
|
+
const allUsers = await listAllUsers(auth);
|
|
55
|
+
return allUsers.filter((user) => user.isAnonymous);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Delete users by UIDs
|
|
60
|
+
*/
|
|
61
|
+
export async function deleteUsers(
|
|
62
|
+
auth: admin.auth.Auth,
|
|
63
|
+
uids: string[],
|
|
64
|
+
onProgress?: (deleted: number, total: number) => void
|
|
65
|
+
): Promise<CleanupResult> {
|
|
66
|
+
const result: CleanupResult = {
|
|
67
|
+
totalProcessed: uids.length,
|
|
68
|
+
deleted: 0,
|
|
69
|
+
preserved: 0,
|
|
70
|
+
errors: [],
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
for (const uid of uids) {
|
|
74
|
+
try {
|
|
75
|
+
await auth.deleteUser(uid);
|
|
76
|
+
result.deleted++;
|
|
77
|
+
onProgress?.(result.deleted, uids.length);
|
|
78
|
+
} catch (error) {
|
|
79
|
+
result.errors.push(`Failed to delete ${uid}: ${error}`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Cleanup anonymous users - delete all users without providers
|
|
88
|
+
*/
|
|
89
|
+
export async function cleanupAnonymousUsers(
|
|
90
|
+
auth: admin.auth.Auth,
|
|
91
|
+
onProgress?: (deleted: number, total: number) => void
|
|
92
|
+
): Promise<CleanupResult> {
|
|
93
|
+
const anonymousUsers = await listAnonymousUsers(auth);
|
|
94
|
+
const uids = anonymousUsers.map((u) => u.uid);
|
|
95
|
+
|
|
96
|
+
if (uids.length === 0) {
|
|
97
|
+
return {
|
|
98
|
+
totalProcessed: 0,
|
|
99
|
+
deleted: 0,
|
|
100
|
+
preserved: 0,
|
|
101
|
+
errors: [],
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const deleteResult = await deleteUsers(auth, uids, onProgress);
|
|
106
|
+
|
|
107
|
+
// Count preserved (authenticated) users
|
|
108
|
+
const authenticatedUsers = await listAuthenticatedUsers(auth);
|
|
109
|
+
deleteResult.preserved = authenticatedUsers.length;
|
|
110
|
+
|
|
111
|
+
return deleteResult;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Delete all users from Firebase Auth
|
|
116
|
+
*/
|
|
117
|
+
export async function deleteAllUsers(
|
|
118
|
+
auth: admin.auth.Auth,
|
|
119
|
+
onProgress?: (deleted: number, total: number) => void
|
|
120
|
+
): Promise<CleanupResult> {
|
|
121
|
+
const allUsers = await listAllUsers(auth);
|
|
122
|
+
const uids = allUsers.map((u) => u.uid);
|
|
123
|
+
return deleteUsers(auth, uids, onProgress);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Get user statistics
|
|
128
|
+
*/
|
|
129
|
+
export async function getUserStats(auth: admin.auth.Auth): Promise<{
|
|
130
|
+
total: number;
|
|
131
|
+
anonymous: number;
|
|
132
|
+
authenticated: number;
|
|
133
|
+
}> {
|
|
134
|
+
const allUsers = await listAllUsers(auth);
|
|
135
|
+
const anonymous = allUsers.filter((u) => u.isAnonymous).length;
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
total: allUsers.length,
|
|
139
|
+
anonymous,
|
|
140
|
+
authenticated: allUsers.length - anonymous,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Admin Firestore Utilities
|
|
3
|
+
* Generic Firestore operations for admin scripts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as admin from "firebase-admin";
|
|
7
|
+
import type { CollectionInfo, BatchResult } from "./types";
|
|
8
|
+
|
|
9
|
+
const BATCH_SIZE = 500;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* List all root-level collections
|
|
13
|
+
*/
|
|
14
|
+
export async function listCollections(
|
|
15
|
+
db: admin.firestore.Firestore
|
|
16
|
+
): Promise<CollectionInfo[]> {
|
|
17
|
+
const collections = await db.listCollections();
|
|
18
|
+
const result: CollectionInfo[] = [];
|
|
19
|
+
|
|
20
|
+
for (const collection of collections) {
|
|
21
|
+
const snapshot = await collection.limit(1000).get();
|
|
22
|
+
const info: CollectionInfo = {
|
|
23
|
+
name: collection.id,
|
|
24
|
+
documentCount: snapshot.docs.length,
|
|
25
|
+
sampleDocumentId: snapshot.docs[0]?.id,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
if (!snapshot.empty) {
|
|
29
|
+
const subcollections = await snapshot.docs[0].ref.listCollections();
|
|
30
|
+
info.hasSubcollections = subcollections.length > 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
result.push(info);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return result.sort((a, b) => b.documentCount - a.documentCount);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* List subcollections for a user document
|
|
41
|
+
*/
|
|
42
|
+
export async function listUserSubcollections(
|
|
43
|
+
db: admin.firestore.Firestore,
|
|
44
|
+
userId: string
|
|
45
|
+
): Promise<CollectionInfo[]> {
|
|
46
|
+
const userRef = db.collection("users").doc(userId);
|
|
47
|
+
const subcollections = await userRef.listCollections();
|
|
48
|
+
const result: CollectionInfo[] = [];
|
|
49
|
+
|
|
50
|
+
for (const subcollection of subcollections) {
|
|
51
|
+
const count = await subcollection.count().get();
|
|
52
|
+
result.push({
|
|
53
|
+
name: subcollection.id,
|
|
54
|
+
documentCount: count.data().count,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Delete collection in batches
|
|
63
|
+
*/
|
|
64
|
+
export async function deleteCollection(
|
|
65
|
+
db: admin.firestore.Firestore,
|
|
66
|
+
collectionPath: string,
|
|
67
|
+
onProgress?: (deleted: number) => void
|
|
68
|
+
): Promise<number> {
|
|
69
|
+
let totalDeleted = 0;
|
|
70
|
+
let hasMore = true;
|
|
71
|
+
|
|
72
|
+
while (hasMore) {
|
|
73
|
+
const snapshot = await db
|
|
74
|
+
.collection(collectionPath)
|
|
75
|
+
.orderBy("__name__")
|
|
76
|
+
.limit(BATCH_SIZE)
|
|
77
|
+
.get();
|
|
78
|
+
|
|
79
|
+
if (snapshot.empty) {
|
|
80
|
+
hasMore = false;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const batch = db.batch();
|
|
85
|
+
snapshot.docs.forEach((doc) => batch.delete(doc.ref));
|
|
86
|
+
await batch.commit();
|
|
87
|
+
|
|
88
|
+
totalDeleted += snapshot.docs.length;
|
|
89
|
+
onProgress?.(totalDeleted);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return totalDeleted;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Delete user subcollection for all users
|
|
97
|
+
*/
|
|
98
|
+
export async function deleteUserSubcollection(
|
|
99
|
+
db: admin.firestore.Firestore,
|
|
100
|
+
subcollectionName: string,
|
|
101
|
+
onProgress?: (deleted: number) => void
|
|
102
|
+
): Promise<number> {
|
|
103
|
+
let totalDeleted = 0;
|
|
104
|
+
const usersSnapshot = await db.collection("users").get();
|
|
105
|
+
|
|
106
|
+
for (const userDoc of usersSnapshot.docs) {
|
|
107
|
+
const subcollectionRef = userDoc.ref.collection(subcollectionName);
|
|
108
|
+
const subcollectionSnapshot = await subcollectionRef.get();
|
|
109
|
+
|
|
110
|
+
if (!subcollectionSnapshot.empty) {
|
|
111
|
+
const batch = db.batch();
|
|
112
|
+
subcollectionSnapshot.docs.forEach((doc) => batch.delete(doc.ref));
|
|
113
|
+
await batch.commit();
|
|
114
|
+
totalDeleted += subcollectionSnapshot.docs.length;
|
|
115
|
+
onProgress?.(totalDeleted);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return totalDeleted;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Delete all Firestore data
|
|
124
|
+
*/
|
|
125
|
+
export async function deleteAllData(
|
|
126
|
+
db: admin.firestore.Firestore,
|
|
127
|
+
onProgress?: (collection: string, deleted: number) => void
|
|
128
|
+
): Promise<number> {
|
|
129
|
+
let totalDeleted = 0;
|
|
130
|
+
const collections = await db.listCollections();
|
|
131
|
+
|
|
132
|
+
for (const collection of collections) {
|
|
133
|
+
const snapshot = await collection.get();
|
|
134
|
+
|
|
135
|
+
// Delete subcollections first for users collection
|
|
136
|
+
if (collection.id === "users") {
|
|
137
|
+
for (const doc of snapshot.docs) {
|
|
138
|
+
const subcollections = await doc.ref.listCollections();
|
|
139
|
+
for (const subcollection of subcollections) {
|
|
140
|
+
const subSnapshot = await subcollection.get();
|
|
141
|
+
if (!subSnapshot.empty) {
|
|
142
|
+
const batch = db.batch();
|
|
143
|
+
subSnapshot.docs.forEach((subDoc) => batch.delete(subDoc.ref));
|
|
144
|
+
await batch.commit();
|
|
145
|
+
totalDeleted += subSnapshot.docs.length;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Delete main collection documents
|
|
152
|
+
if (!snapshot.empty) {
|
|
153
|
+
const batch = db.batch();
|
|
154
|
+
snapshot.docs.forEach((doc) => batch.delete(doc.ref));
|
|
155
|
+
await batch.commit();
|
|
156
|
+
totalDeleted += snapshot.docs.length;
|
|
157
|
+
onProgress?.(collection.id, totalDeleted);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return totalDeleted;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Seed documents in batches
|
|
166
|
+
*/
|
|
167
|
+
export async function seedBatch(
|
|
168
|
+
db: admin.firestore.Firestore,
|
|
169
|
+
collectionPath: string,
|
|
170
|
+
docs: Array<{ id: string; data: Record<string, unknown> }>
|
|
171
|
+
): Promise<BatchResult> {
|
|
172
|
+
const result: BatchResult = {
|
|
173
|
+
success: true,
|
|
174
|
+
processed: 0,
|
|
175
|
+
errors: [],
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
for (let i = 0; i < docs.length; i += BATCH_SIZE) {
|
|
179
|
+
const batch = db.batch();
|
|
180
|
+
const slice = docs.slice(i, i + BATCH_SIZE);
|
|
181
|
+
|
|
182
|
+
for (const { id, data } of slice) {
|
|
183
|
+
const ref = db.collection(collectionPath).doc(id);
|
|
184
|
+
const clean = Object.fromEntries(
|
|
185
|
+
Object.entries(data).filter(([, v]) => v !== undefined)
|
|
186
|
+
);
|
|
187
|
+
batch.set(ref, clean);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
await batch.commit();
|
|
192
|
+
result.processed += slice.length;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
result.success = false;
|
|
195
|
+
result.errors.push(`Batch failed at index ${i}: ${error}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return result;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Seed user subcollection
|
|
204
|
+
*/
|
|
205
|
+
export async function seedUserSubcollection(
|
|
206
|
+
db: admin.firestore.Firestore,
|
|
207
|
+
userId: string,
|
|
208
|
+
subcollectionName: string,
|
|
209
|
+
docs: Array<{ id: string; data: Record<string, unknown> }>
|
|
210
|
+
): Promise<BatchResult> {
|
|
211
|
+
const result: BatchResult = {
|
|
212
|
+
success: true,
|
|
213
|
+
processed: 0,
|
|
214
|
+
errors: [],
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const batch = db.batch();
|
|
218
|
+
|
|
219
|
+
for (const { id, data } of docs) {
|
|
220
|
+
const ref = db
|
|
221
|
+
.collection("users")
|
|
222
|
+
.doc(userId)
|
|
223
|
+
.collection(subcollectionName)
|
|
224
|
+
.doc(id);
|
|
225
|
+
const clean = Object.fromEntries(
|
|
226
|
+
Object.entries(data).filter(([, v]) => v !== undefined)
|
|
227
|
+
);
|
|
228
|
+
batch.set(ref, clean);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
try {
|
|
232
|
+
await batch.commit();
|
|
233
|
+
result.processed = docs.length;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
result.success = false;
|
|
236
|
+
result.errors.push(`Failed to seed subcollection: ${error}`);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return result;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Count documents in collection
|
|
244
|
+
*/
|
|
245
|
+
export async function countDocuments(
|
|
246
|
+
db: admin.firestore.Firestore,
|
|
247
|
+
collectionPath: string
|
|
248
|
+
): Promise<number> {
|
|
249
|
+
const count = await db.collection(collectionPath).count().get();
|
|
250
|
+
return count.data().count;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Get user document count statistics
|
|
255
|
+
*/
|
|
256
|
+
export async function getUserStats(db: admin.firestore.Firestore): Promise<{
|
|
257
|
+
total: number;
|
|
258
|
+
anonymous: number;
|
|
259
|
+
authenticated: number;
|
|
260
|
+
}> {
|
|
261
|
+
const usersSnapshot = await db.collection("users").get();
|
|
262
|
+
|
|
263
|
+
let anonymous = 0;
|
|
264
|
+
let authenticated = 0;
|
|
265
|
+
|
|
266
|
+
usersSnapshot.docs.forEach((doc) => {
|
|
267
|
+
const data = doc.data();
|
|
268
|
+
if (data.isAnonymous) {
|
|
269
|
+
anonymous++;
|
|
270
|
+
} else {
|
|
271
|
+
authenticated++;
|
|
272
|
+
}
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
total: usersSnapshot.docs.length,
|
|
277
|
+
anonymous,
|
|
278
|
+
authenticated,
|
|
279
|
+
};
|
|
280
|
+
}
|
package/scripts/index.ts
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Admin Scripts
|
|
3
|
+
*
|
|
4
|
+
* Generic utilities for Firebase Admin operations.
|
|
5
|
+
* Use these for CLI scripts, seeding, cleanup, and testing.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* import { initFirebaseAdmin, cleanupAnonymousUsers } from "@umituz/react-native-firebase/scripts";
|
|
9
|
+
*
|
|
10
|
+
* const app = initFirebaseAdmin({
|
|
11
|
+
* serviceAccountPath: "./service-account.json",
|
|
12
|
+
* projectId: "my-project",
|
|
13
|
+
* storageBucket: "my-project.appspot.com",
|
|
14
|
+
* });
|
|
15
|
+
*
|
|
16
|
+
* const auth = getAuthAdmin(app);
|
|
17
|
+
* await cleanupAnonymousUsers(auth);
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
// Types
|
|
21
|
+
export type {
|
|
22
|
+
FirebaseAdminConfig,
|
|
23
|
+
CollectionInfo,
|
|
24
|
+
UserInfo,
|
|
25
|
+
CleanupResult,
|
|
26
|
+
BatchResult,
|
|
27
|
+
StorageFileInfo,
|
|
28
|
+
ResetSummary,
|
|
29
|
+
} from "./types";
|
|
30
|
+
|
|
31
|
+
// Initialization
|
|
32
|
+
export {
|
|
33
|
+
initFirebaseAdmin,
|
|
34
|
+
getFirestoreAdmin,
|
|
35
|
+
getAuthAdmin,
|
|
36
|
+
getStorageAdmin,
|
|
37
|
+
resetFirebaseAdmin,
|
|
38
|
+
} from "./init";
|
|
39
|
+
|
|
40
|
+
// Auth utilities
|
|
41
|
+
export {
|
|
42
|
+
listAllUsers,
|
|
43
|
+
listAuthenticatedUsers,
|
|
44
|
+
listAnonymousUsers,
|
|
45
|
+
deleteUsers,
|
|
46
|
+
cleanupAnonymousUsers,
|
|
47
|
+
deleteAllUsers,
|
|
48
|
+
getUserStats as getAuthUserStats,
|
|
49
|
+
} from "./auth";
|
|
50
|
+
|
|
51
|
+
// Firestore utilities
|
|
52
|
+
export {
|
|
53
|
+
listCollections,
|
|
54
|
+
listUserSubcollections,
|
|
55
|
+
deleteCollection,
|
|
56
|
+
deleteUserSubcollection,
|
|
57
|
+
deleteAllData,
|
|
58
|
+
seedBatch,
|
|
59
|
+
seedUserSubcollection,
|
|
60
|
+
countDocuments,
|
|
61
|
+
getUserStats as getFirestoreUserStats,
|
|
62
|
+
} from "./firestore";
|
|
63
|
+
|
|
64
|
+
// Storage utilities
|
|
65
|
+
export {
|
|
66
|
+
listFiles,
|
|
67
|
+
deleteAllFiles,
|
|
68
|
+
deleteFilesByPrefix,
|
|
69
|
+
getStorageStats,
|
|
70
|
+
deleteUserFiles,
|
|
71
|
+
} from "./storage";
|
|
72
|
+
|
|
73
|
+
// Utility functions
|
|
74
|
+
export {
|
|
75
|
+
randomId,
|
|
76
|
+
randomDate,
|
|
77
|
+
randomItem,
|
|
78
|
+
randomNumber,
|
|
79
|
+
randomBoolean,
|
|
80
|
+
sleep,
|
|
81
|
+
formatBytes,
|
|
82
|
+
createConfirmationTimer,
|
|
83
|
+
printSeparator,
|
|
84
|
+
printHeader,
|
|
85
|
+
} from "./utils";
|
package/scripts/init.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Admin Initialization
|
|
3
|
+
* Dynamic configuration - no hardcoded values
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as admin from "firebase-admin";
|
|
7
|
+
import type { FirebaseAdminConfig } from "./types";
|
|
8
|
+
|
|
9
|
+
let initializedApp: admin.app.App | null = null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Initialize Firebase Admin SDK with config
|
|
13
|
+
* @param config - Firebase Admin configuration
|
|
14
|
+
* @returns Initialized Firebase Admin app
|
|
15
|
+
*/
|
|
16
|
+
export function initFirebaseAdmin(config: FirebaseAdminConfig): admin.app.App {
|
|
17
|
+
if (initializedApp) {
|
|
18
|
+
return initializedApp;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (admin.apps.length > 0) {
|
|
22
|
+
initializedApp = admin.apps[0]!;
|
|
23
|
+
return initializedApp;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
initializedApp = admin.initializeApp({
|
|
27
|
+
credential: admin.credential.cert(config.serviceAccountPath),
|
|
28
|
+
projectId: config.projectId,
|
|
29
|
+
storageBucket: config.storageBucket,
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return initializedApp;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get Firestore instance
|
|
37
|
+
*/
|
|
38
|
+
export function getFirestoreAdmin(app: admin.app.App): admin.firestore.Firestore {
|
|
39
|
+
return admin.firestore(app);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Get Auth instance
|
|
44
|
+
*/
|
|
45
|
+
export function getAuthAdmin(app: admin.app.App): admin.auth.Auth {
|
|
46
|
+
return admin.auth(app);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get Storage bucket
|
|
51
|
+
*/
|
|
52
|
+
export function getStorageAdmin(app: admin.app.App): admin.storage.Storage {
|
|
53
|
+
return admin.storage(app);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Reset initialized app (for testing)
|
|
58
|
+
*/
|
|
59
|
+
export function resetFirebaseAdmin(): void {
|
|
60
|
+
initializedApp = null;
|
|
61
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Admin Storage Utilities
|
|
3
|
+
* Generic Storage operations for admin scripts
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as admin from "firebase-admin";
|
|
7
|
+
import type { StorageFileInfo, CleanupResult } from "./types";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* List all files in storage bucket
|
|
11
|
+
*/
|
|
12
|
+
export async function listFiles(
|
|
13
|
+
storage: admin.storage.Storage,
|
|
14
|
+
prefix?: string
|
|
15
|
+
): Promise<StorageFileInfo[]> {
|
|
16
|
+
const bucket = storage.bucket();
|
|
17
|
+
const [files] = await bucket.getFiles({ prefix });
|
|
18
|
+
|
|
19
|
+
return files.map((file) => ({
|
|
20
|
+
name: file.name,
|
|
21
|
+
size: parseInt(file.metadata.size as string, 10) || 0,
|
|
22
|
+
contentType: file.metadata.contentType,
|
|
23
|
+
createdAt: file.metadata.timeCreated
|
|
24
|
+
? new Date(file.metadata.timeCreated)
|
|
25
|
+
: undefined,
|
|
26
|
+
}));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Delete all files in storage
|
|
31
|
+
*/
|
|
32
|
+
export async function deleteAllFiles(
|
|
33
|
+
storage: admin.storage.Storage,
|
|
34
|
+
onProgress?: (deleted: number, total: number) => void
|
|
35
|
+
): Promise<CleanupResult> {
|
|
36
|
+
const bucket = storage.bucket();
|
|
37
|
+
const [files] = await bucket.getFiles();
|
|
38
|
+
|
|
39
|
+
const result: CleanupResult = {
|
|
40
|
+
totalProcessed: files.length,
|
|
41
|
+
deleted: 0,
|
|
42
|
+
preserved: 0,
|
|
43
|
+
errors: [],
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
for (const file of files) {
|
|
47
|
+
try {
|
|
48
|
+
await file.delete();
|
|
49
|
+
result.deleted++;
|
|
50
|
+
onProgress?.(result.deleted, files.length);
|
|
51
|
+
} catch (error) {
|
|
52
|
+
result.errors.push(`Failed to delete ${file.name}: ${error}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Delete files by prefix (folder)
|
|
61
|
+
*/
|
|
62
|
+
export async function deleteFilesByPrefix(
|
|
63
|
+
storage: admin.storage.Storage,
|
|
64
|
+
prefix: string,
|
|
65
|
+
onProgress?: (deleted: number, total: number) => void
|
|
66
|
+
): Promise<CleanupResult> {
|
|
67
|
+
const bucket = storage.bucket();
|
|
68
|
+
const [files] = await bucket.getFiles({ prefix });
|
|
69
|
+
|
|
70
|
+
const result: CleanupResult = {
|
|
71
|
+
totalProcessed: files.length,
|
|
72
|
+
deleted: 0,
|
|
73
|
+
preserved: 0,
|
|
74
|
+
errors: [],
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
for (const file of files) {
|
|
78
|
+
try {
|
|
79
|
+
await file.delete();
|
|
80
|
+
result.deleted++;
|
|
81
|
+
onProgress?.(result.deleted, files.length);
|
|
82
|
+
} catch (error) {
|
|
83
|
+
result.errors.push(`Failed to delete ${file.name}: ${error}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return result;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Get storage statistics
|
|
92
|
+
*/
|
|
93
|
+
export async function getStorageStats(
|
|
94
|
+
storage: admin.storage.Storage
|
|
95
|
+
): Promise<{
|
|
96
|
+
totalFiles: number;
|
|
97
|
+
totalSizeBytes: number;
|
|
98
|
+
totalSizeMB: number;
|
|
99
|
+
}> {
|
|
100
|
+
const files = await listFiles(storage);
|
|
101
|
+
const totalSizeBytes = files.reduce((sum, file) => sum + file.size, 0);
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
totalFiles: files.length,
|
|
105
|
+
totalSizeBytes,
|
|
106
|
+
totalSizeMB: Math.round((totalSizeBytes / (1024 * 1024)) * 100) / 100,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Delete user files (files in users/{userId}/ folder)
|
|
112
|
+
*/
|
|
113
|
+
export async function deleteUserFiles(
|
|
114
|
+
storage: admin.storage.Storage,
|
|
115
|
+
userId: string,
|
|
116
|
+
onProgress?: (deleted: number, total: number) => void
|
|
117
|
+
): Promise<CleanupResult> {
|
|
118
|
+
return deleteFilesByPrefix(storage, `users/${userId}/`, onProgress);
|
|
119
|
+
}
|
package/scripts/types.ts
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Admin Scripts - Shared Types
|
|
3
|
+
* Generic types for Firebase Admin operations
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface FirebaseAdminConfig {
|
|
7
|
+
/** Path to service account JSON file */
|
|
8
|
+
serviceAccountPath: string;
|
|
9
|
+
/** Firebase project ID */
|
|
10
|
+
projectId: string;
|
|
11
|
+
/** Storage bucket name (optional) */
|
|
12
|
+
storageBucket?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CollectionInfo {
|
|
16
|
+
name: string;
|
|
17
|
+
documentCount: number;
|
|
18
|
+
sampleDocumentId?: string;
|
|
19
|
+
hasSubcollections?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface UserInfo {
|
|
23
|
+
uid: string;
|
|
24
|
+
email?: string;
|
|
25
|
+
displayName?: string;
|
|
26
|
+
isAnonymous: boolean;
|
|
27
|
+
createdAt?: Date;
|
|
28
|
+
providerCount: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CleanupResult {
|
|
32
|
+
totalProcessed: number;
|
|
33
|
+
deleted: number;
|
|
34
|
+
preserved: number;
|
|
35
|
+
errors: string[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface BatchResult {
|
|
39
|
+
success: boolean;
|
|
40
|
+
processed: number;
|
|
41
|
+
errors: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface StorageFileInfo {
|
|
45
|
+
name: string;
|
|
46
|
+
size: number;
|
|
47
|
+
contentType?: string;
|
|
48
|
+
createdAt?: Date;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface ResetSummary {
|
|
52
|
+
authUsersDeleted: number;
|
|
53
|
+
firestoreDocsDeleted: number;
|
|
54
|
+
storageFilesDeleted: number;
|
|
55
|
+
}
|
package/scripts/utils.ts
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Firebase Admin Scripts - Utility Functions
|
|
3
|
+
* Generic helpers for seeding and testing
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Generate random ID
|
|
8
|
+
*/
|
|
9
|
+
export function randomId(): string {
|
|
10
|
+
return (
|
|
11
|
+
Math.random().toString(36).substring(2, 15) +
|
|
12
|
+
Math.random().toString(36).substring(2, 15)
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Generate random date within the past N days
|
|
18
|
+
*/
|
|
19
|
+
export function randomDate(daysAgo: number): Date {
|
|
20
|
+
const now = new Date();
|
|
21
|
+
const pastDate = new Date(now.getTime() - daysAgo * 24 * 60 * 60 * 1000);
|
|
22
|
+
const randomTime =
|
|
23
|
+
pastDate.getTime() + Math.random() * (now.getTime() - pastDate.getTime());
|
|
24
|
+
return new Date(randomTime);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get random item from array
|
|
29
|
+
*/
|
|
30
|
+
export function randomItem<T>(arr: T[]): T {
|
|
31
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Generate random number in range
|
|
36
|
+
*/
|
|
37
|
+
export function randomNumber(min: number, max: number): number {
|
|
38
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Generate random boolean
|
|
43
|
+
*/
|
|
44
|
+
export function randomBoolean(): boolean {
|
|
45
|
+
return Math.random() > 0.5;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Sleep for specified milliseconds
|
|
50
|
+
*/
|
|
51
|
+
export function sleep(ms: number): Promise<void> {
|
|
52
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Format bytes to human readable string
|
|
57
|
+
*/
|
|
58
|
+
export function formatBytes(bytes: number): string {
|
|
59
|
+
if (bytes === 0) return "0 Bytes";
|
|
60
|
+
const k = 1024;
|
|
61
|
+
const sizes = ["Bytes", "KB", "MB", "GB"];
|
|
62
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
63
|
+
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Create a confirmation prompt (for CLI scripts)
|
|
68
|
+
*/
|
|
69
|
+
export function createConfirmationTimer(
|
|
70
|
+
seconds: number,
|
|
71
|
+
warningMessage: string
|
|
72
|
+
): Promise<void> {
|
|
73
|
+
return new Promise((resolve) => {
|
|
74
|
+
console.log(warningMessage);
|
|
75
|
+
console.log(`\nPress Ctrl+C to cancel, or wait ${seconds} seconds to continue...\n`);
|
|
76
|
+
setTimeout(resolve, seconds * 1000);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Print separator line
|
|
82
|
+
*/
|
|
83
|
+
export function printSeparator(char = "=", length = 70): void {
|
|
84
|
+
console.log(char.repeat(length));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Print header with separators
|
|
89
|
+
*/
|
|
90
|
+
export function printHeader(title: string): void {
|
|
91
|
+
printSeparator();
|
|
92
|
+
console.log(title);
|
|
93
|
+
printSeparator();
|
|
94
|
+
console.log();
|
|
95
|
+
}
|
|
@@ -44,7 +44,7 @@ export interface FirestoreQueryResult {
|
|
|
44
44
|
readonly shouldSkip: boolean;
|
|
45
45
|
readonly reason?: FirestoreQuerySkipReason;
|
|
46
46
|
readonly userId: string | null;
|
|
47
|
-
readonly
|
|
47
|
+
readonly isAnonymous: boolean;
|
|
48
48
|
readonly isAuthenticated: boolean;
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -70,7 +70,7 @@ export function shouldSkipFirestoreQuery(
|
|
|
70
70
|
shouldSkip: true,
|
|
71
71
|
reason: "no_auth",
|
|
72
72
|
userId: null,
|
|
73
|
-
|
|
73
|
+
isAnonymous: false,
|
|
74
74
|
isAuthenticated: false,
|
|
75
75
|
};
|
|
76
76
|
}
|
|
@@ -85,20 +85,20 @@ export function shouldSkipFirestoreQuery(
|
|
|
85
85
|
shouldSkip: true,
|
|
86
86
|
reason: "not_authenticated",
|
|
87
87
|
userId: null,
|
|
88
|
-
|
|
88
|
+
isAnonymous: false,
|
|
89
89
|
isAuthenticated: false,
|
|
90
90
|
};
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
93
|
|
|
94
|
-
//
|
|
95
|
-
if (authState.
|
|
94
|
+
// Anonymous user
|
|
95
|
+
if (authState.isAnonymous) {
|
|
96
96
|
if (skipForGuest) {
|
|
97
97
|
return {
|
|
98
98
|
shouldSkip: true,
|
|
99
99
|
reason: "is_guest",
|
|
100
100
|
userId: authState.userId,
|
|
101
|
-
|
|
101
|
+
isAnonymous: true,
|
|
102
102
|
isAuthenticated: false,
|
|
103
103
|
};
|
|
104
104
|
}
|
|
@@ -111,7 +111,7 @@ export function shouldSkipFirestoreQuery(
|
|
|
111
111
|
shouldSkip: true,
|
|
112
112
|
reason: "user_id_mismatch",
|
|
113
113
|
userId: authState.userId,
|
|
114
|
-
|
|
114
|
+
isAnonymous: authState.isAnonymous,
|
|
115
115
|
isAuthenticated: authState.isAuthenticated,
|
|
116
116
|
};
|
|
117
117
|
}
|
|
@@ -121,7 +121,7 @@ export function shouldSkipFirestoreQuery(
|
|
|
121
121
|
return {
|
|
122
122
|
shouldSkip: false,
|
|
123
123
|
userId: authState.userId,
|
|
124
|
-
|
|
124
|
+
isAnonymous: authState.isAnonymous,
|
|
125
125
|
isAuthenticated: authState.isAuthenticated,
|
|
126
126
|
};
|
|
127
127
|
} catch (error) {
|
|
@@ -134,7 +134,7 @@ export function shouldSkipFirestoreQuery(
|
|
|
134
134
|
shouldSkip: true,
|
|
135
135
|
reason: "invalid_options",
|
|
136
136
|
userId: null,
|
|
137
|
-
|
|
137
|
+
isAnonymous: false,
|
|
138
138
|
isAuthenticated: false,
|
|
139
139
|
};
|
|
140
140
|
}
|
|
@@ -63,7 +63,6 @@ export function useAnonymousAuth(auth: Auth | null): UseAnonymousAuthResult {
|
|
|
63
63
|
setAuthState({
|
|
64
64
|
isAuthenticated: false,
|
|
65
65
|
isAnonymous: false,
|
|
66
|
-
isGuest: false,
|
|
67
66
|
currentUser: null,
|
|
68
67
|
userId: null,
|
|
69
68
|
});
|
|
@@ -72,7 +71,6 @@ export function useAnonymousAuth(auth: Auth | null): UseAnonymousAuthResult {
|
|
|
72
71
|
setAuthState({
|
|
73
72
|
isAuthenticated: true,
|
|
74
73
|
isAnonymous: anonymous,
|
|
75
|
-
isGuest: anonymous,
|
|
76
74
|
currentUser: user,
|
|
77
75
|
userId: user.uid,
|
|
78
76
|
});
|
|
@@ -104,7 +102,6 @@ export function useAnonymousAuth(auth: Auth | null): UseAnonymousAuthResult {
|
|
|
104
102
|
setAuthState({
|
|
105
103
|
isAuthenticated: false,
|
|
106
104
|
isAnonymous: false,
|
|
107
|
-
isGuest: false,
|
|
108
105
|
currentUser: null,
|
|
109
106
|
userId: null,
|
|
110
107
|
});
|