@umituz/react-native-firebase 1.13.36 → 1.13.38
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/dist/scripts/auth.d.ts +39 -0
- package/dist/scripts/auth.d.ts.map +1 -0
- package/dist/scripts/auth.js +114 -0
- package/dist/scripts/auth.js.map +1 -0
- package/dist/scripts/firestore.d.ts +53 -0
- package/dist/scripts/firestore.d.ts.map +1 -0
- package/dist/scripts/firestore.js +217 -0
- package/dist/scripts/firestore.js.map +1 -0
- package/dist/scripts/index.d.ts +25 -0
- package/dist/scripts/index.d.ts.map +1 -0
- package/dist/scripts/index.js +68 -0
- package/dist/scripts/index.js.map +1 -0
- package/dist/scripts/init.d.ts +29 -0
- package/dist/scripts/init.d.ts.map +1 -0
- package/dist/scripts/init.js +91 -0
- package/dist/scripts/init.js.map +1 -0
- package/dist/scripts/storage.d.ts +31 -0
- package/dist/scripts/storage.d.ts.map +1 -0
- package/dist/scripts/storage.js +93 -0
- package/dist/scripts/storage.js.map +1 -0
- package/dist/scripts/types.d.ts +49 -0
- package/dist/scripts/types.d.ts.map +1 -0
- package/dist/scripts/types.js +7 -0
- package/dist/scripts/types.js.map +1 -0
- package/dist/scripts/utils.d.ts +45 -0
- package/dist/scripts/utils.d.ts.map +1 -0
- package/dist/scripts/utils.js +93 -0
- package/dist/scripts/utils.js.map +1 -0
- package/package.json +18 -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
|
@@ -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
|
+
}
|