@rebasepro/firebase 0.17.3 → 0.18.1
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/LICENSE +1 -1
- package/README.md +4 -0
- package/package.json +34 -20
- package/src/components/FirebaseLoginView.tsx +0 -704
- package/src/components/RebaseFirebaseApp.tsx +0 -293
- package/src/components/RebaseFirebaseAppProps.tsx +0 -177
- package/src/components/index.ts +0 -3
- package/src/components/social_icons.tsx +0 -135
- package/src/hooks/index.ts +0 -9
- package/src/hooks/useAppCheck.ts +0 -101
- package/src/hooks/useBuildUserManagement.tsx +0 -429
- package/src/hooks/useFirebaseAccessGate.tsx +0 -146
- package/src/hooks/useFirebaseAuthController.ts +0 -357
- package/src/hooks/useFirebaseRealTimeDBDelegate.ts +0 -423
- package/src/hooks/useFirebaseStorageSource.ts +0 -207
- package/src/hooks/useFirestoreDriver.ts +0 -855
- package/src/hooks/useInitialiseFirebase.ts +0 -132
- package/src/hooks/useRecaptcha.tsx +0 -28
- package/src/index.ts +0 -4
- package/src/types/appcheck.ts +0 -11
- package/src/types/auth.tsx +0 -75
- package/src/types/index.ts +0 -3
- package/src/types/text_search.ts +0 -42
- package/src/utils/algolia.ts +0 -27
- package/src/utils/collections_firestore.ts +0 -150
- package/src/utils/database.ts +0 -39
- package/src/utils/index.ts +0 -7
- package/src/utils/local_text_search_controller.ts +0 -143
- package/src/utils/pinecone.ts +0 -75
- package/src/utils/rebase_search_controller.ts +0 -357
- package/src/utils/text_search_controller.ts +0 -34
|
@@ -1,423 +0,0 @@
|
|
|
1
|
-
import { FirebaseApp } from "firebase/app";
|
|
2
|
-
import {
|
|
3
|
-
Database,
|
|
4
|
-
endAt,
|
|
5
|
-
equalTo,
|
|
6
|
-
get,
|
|
7
|
-
getDatabase,
|
|
8
|
-
limitToFirst,
|
|
9
|
-
onValue,
|
|
10
|
-
orderByChild,
|
|
11
|
-
orderByKey,
|
|
12
|
-
push,
|
|
13
|
-
query,
|
|
14
|
-
QueryConstraint,
|
|
15
|
-
ref,
|
|
16
|
-
remove,
|
|
17
|
-
set,
|
|
18
|
-
startAfter,
|
|
19
|
-
startAt
|
|
20
|
-
} from "firebase/database";
|
|
21
|
-
import { useCallback } from "react";
|
|
22
|
-
import { DataDriver, DeleteProps, FetchCollectionProps, FetchOneProps, FilterValues, ListenCollectionProps, ListenOneProps, SaveProps, WhereFilterOp } from "@rebasepro/types";
|
|
23
|
-
|
|
24
|
-
/** The values the Realtime Database can order or bound a query by. */
|
|
25
|
-
type RTDBFilterValue = string | number | boolean | null;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* A read expressed in the Realtime Database's own query model.
|
|
29
|
-
*
|
|
30
|
-
* @see planRTDBQuery
|
|
31
|
-
*/
|
|
32
|
-
export type RTDBQueryPlan = {
|
|
33
|
-
/** Child key to order — and therefore to bound — by. Absent means order by key. */
|
|
34
|
-
orderByChild?: string;
|
|
35
|
-
equalTo?: RTDBFilterValue;
|
|
36
|
-
startAt?: RTDBFilterValue;
|
|
37
|
-
/** Key the window starts after, exclusive. Only valid in key order. */
|
|
38
|
-
startAfter?: RTDBFilterValue;
|
|
39
|
-
endAt?: RTDBFilterValue;
|
|
40
|
-
limitToFirst?: number;
|
|
41
|
-
/**
|
|
42
|
-
* Rows to drop from the front of the result — the caller's `offset`, which
|
|
43
|
-
* the database has no constraint for. Applied by the caller, not by
|
|
44
|
-
* {@link rtdbConstraints}.
|
|
45
|
-
*/
|
|
46
|
-
skip?: number;
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
const RTDB = "useFirebaseRTDBDelegate";
|
|
50
|
-
|
|
51
|
-
const isRTDBValue = (value: unknown): value is RTDBFilterValue =>
|
|
52
|
-
value === null ||
|
|
53
|
-
typeof value === "string" ||
|
|
54
|
-
typeof value === "number" ||
|
|
55
|
-
typeof value === "boolean";
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Translate a driver read into the Realtime Database's query model, or refuse it.
|
|
59
|
-
*
|
|
60
|
-
* The Realtime Database orders by a single child key per query and bounds that
|
|
61
|
-
* one key with `equalTo`/`startAt`/`endAt`. Nothing else is expressible: no
|
|
62
|
-
* second field, no descending order, no text search, no `or(...)` group.
|
|
63
|
-
*
|
|
64
|
-
* Everything beyond `limit` and `startAfter` used to be destructured out of the
|
|
65
|
-
* read and then never referenced, so a caller asking for `status == "draft"`
|
|
66
|
-
* was handed the entire collection, presented as the answer. A query this
|
|
67
|
-
* database cannot express is refused here instead — a caller that sees an error
|
|
68
|
-
* can fall back, a caller that sees the wrong rows cannot.
|
|
69
|
-
*/
|
|
70
|
-
export function planRTDBQuery<M extends Record<string, any>>({
|
|
71
|
-
filter,
|
|
72
|
-
orderBy,
|
|
73
|
-
order,
|
|
74
|
-
searchString,
|
|
75
|
-
logical,
|
|
76
|
-
limit,
|
|
77
|
-
offset,
|
|
78
|
-
startAfter: startAfterKey
|
|
79
|
-
}: Pick<FetchCollectionProps<M>, "filter" | "orderBy" | "order" | "searchString" | "logical" | "limit" | "offset" | "startAfter">): RTDBQueryPlan {
|
|
80
|
-
|
|
81
|
-
if (searchString) {
|
|
82
|
-
throw new Error(`${RTDB}: the Realtime Database has no text search, so \`searchString\` cannot be applied. Index the data in a search service instead.`);
|
|
83
|
-
}
|
|
84
|
-
if (logical) {
|
|
85
|
-
throw new Error(`${RTDB}: the Realtime Database cannot evaluate \`or(...)\`/\`and(...)\` groups.`);
|
|
86
|
-
}
|
|
87
|
-
if (order === "desc") {
|
|
88
|
-
throw new Error(`${RTDB}: the Realtime Database only orders ascending, so \`order: "desc"\` cannot be applied.`);
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const conditions: [string, WhereFilterOp, unknown][] = [];
|
|
92
|
-
Object.entries((filter ?? {}) as FilterValues<string>).forEach(([key, entry]) => {
|
|
93
|
-
if (!entry) return;
|
|
94
|
-
const tuples = Array.isArray(entry[0])
|
|
95
|
-
? entry as [WhereFilterOp, unknown][]
|
|
96
|
-
: [entry as [WhereFilterOp, unknown]];
|
|
97
|
-
tuples.forEach(([op, value]) => conditions.push([key, op, value]));
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
const fields = Array.from(new Set(conditions.map(([key]) => key)));
|
|
101
|
-
if (fields.length > 1) {
|
|
102
|
-
throw new Error(`${RTDB}: the Realtime Database filters on one child key per query; this read asked for ${fields.join(", ")}.`);
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
const [field] = fields;
|
|
106
|
-
if (field && orderBy && orderBy !== field) {
|
|
107
|
-
throw new Error(`${RTDB}: a query is ordered by the key it filters on; cannot filter \`${field}\` while ordering by \`${orderBy}\`.`);
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const orderChild = field ?? orderBy;
|
|
111
|
-
const plan: RTDBQueryPlan = orderChild ? { orderByChild: orderChild } : {};
|
|
112
|
-
|
|
113
|
-
for (const [key, op, value] of conditions) {
|
|
114
|
-
if (!isRTDBValue(value)) {
|
|
115
|
-
throw new Error(`${RTDB}: cannot bound \`${key}\` by a ${Array.isArray(value) ? "array" : typeof value} value; the Realtime Database compares strings, numbers, booleans and null.`);
|
|
116
|
-
}
|
|
117
|
-
if (op === "==") {
|
|
118
|
-
if (conditions.length > 1) {
|
|
119
|
-
throw new Error(`${RTDB}: \`==\` bounds a query on its own; it cannot be combined with another condition on \`${key}\`.`);
|
|
120
|
-
}
|
|
121
|
-
plan.equalTo = value;
|
|
122
|
-
} else if (op === ">=") {
|
|
123
|
-
plan.startAt = value;
|
|
124
|
-
} else if (op === "<=") {
|
|
125
|
-
plan.endAt = value;
|
|
126
|
-
} else {
|
|
127
|
-
throw new Error(`${RTDB}: the Realtime Database does not support the "${op}" operator (on \`${key}\`). It bounds a single child key with ==, >= and <=.`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (startAfterKey !== undefined) {
|
|
132
|
-
if (orderChild) {
|
|
133
|
-
throw new Error(`${RTDB}: \`startAfter\` pages in key order and cannot be combined with a filter or \`orderBy\`.`);
|
|
134
|
-
}
|
|
135
|
-
plan.startAfter = String(startAfterKey);
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
// No constraint expresses `offset`, so the window is read `offset` rows
|
|
139
|
-
// wider and the front is dropped. Dropping it instead — which is what this
|
|
140
|
-
// driver did — serves page one to every page, and a paginated walk that
|
|
141
|
-
// takes the driver at its word never terminates.
|
|
142
|
-
const skip = offset !== undefined && Number.isFinite(offset) && offset > 0
|
|
143
|
-
? Math.floor(offset)
|
|
144
|
-
: 0;
|
|
145
|
-
if (skip > 0) {
|
|
146
|
-
plan.skip = skip;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
if (limit !== undefined) {
|
|
150
|
-
plan.limitToFirst = limit + skip;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
return plan;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
/** The plan as Realtime Database query constraints. */
|
|
157
|
-
function rtdbConstraints(plan: RTDBQueryPlan): QueryConstraint[] {
|
|
158
|
-
const constraints: QueryConstraint[] = [];
|
|
159
|
-
const bounded = plan.equalTo !== undefined ||
|
|
160
|
-
plan.startAt !== undefined ||
|
|
161
|
-
plan.startAfter !== undefined ||
|
|
162
|
-
plan.endAt !== undefined;
|
|
163
|
-
|
|
164
|
-
if (plan.orderByChild !== undefined) {
|
|
165
|
-
constraints.push(orderByChild(plan.orderByChild));
|
|
166
|
-
} else if (bounded) {
|
|
167
|
-
constraints.push(orderByKey());
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
if (plan.equalTo !== undefined) constraints.push(equalTo(plan.equalTo));
|
|
171
|
-
if (plan.startAt !== undefined) constraints.push(startAt(plan.startAt));
|
|
172
|
-
if (plan.startAfter !== undefined) constraints.push(startAfter(plan.startAfter));
|
|
173
|
-
if (plan.endAt !== undefined) constraints.push(endAt(plan.endAt));
|
|
174
|
-
if (plan.limitToFirst !== undefined) constraints.push(limitToFirst(plan.limitToFirst));
|
|
175
|
-
|
|
176
|
-
return constraints;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
export function useFirebaseRTDBDelegate({ firebaseApp }: { firebaseApp?: FirebaseApp }): DataDriver {
|
|
180
|
-
|
|
181
|
-
const fetchCollection = useCallback(async <M extends Record<string, any>>(
|
|
182
|
-
props: FetchCollectionProps<M>
|
|
183
|
-
): Promise<Record<string, unknown>[]> => {
|
|
184
|
-
if (!firebaseApp) {
|
|
185
|
-
throw new Error("Firebase app not provided");
|
|
186
|
-
}
|
|
187
|
-
const database = getDatabase(firebaseApp);
|
|
188
|
-
|
|
189
|
-
// Throws on any narrowing this database cannot express, rather than
|
|
190
|
-
// answering a filtered read with the whole collection.
|
|
191
|
-
const plan = planRTDBQuery(props);
|
|
192
|
-
const dbQuery = query(ref(database, props.path), ...rtdbConstraints(plan));
|
|
193
|
-
|
|
194
|
-
const entity = await get(dbQuery);
|
|
195
|
-
if (entity.exists()) {
|
|
196
|
-
return Object.entries(entity.val()).slice(plan.skip ?? 0).map(([id, values]) => ({
|
|
197
|
-
...(delegateToCMSModel(values) as Record<string, unknown>),
|
|
198
|
-
id
|
|
199
|
-
}));
|
|
200
|
-
}
|
|
201
|
-
return [];
|
|
202
|
-
}, [firebaseApp]);
|
|
203
|
-
|
|
204
|
-
const listenCollection = useCallback(<M extends Record<string, any>>(
|
|
205
|
-
props: ListenCollectionProps<M>
|
|
206
|
-
): () => void => {
|
|
207
|
-
if (!firebaseApp) {
|
|
208
|
-
throw new Error("Firebase app not provided");
|
|
209
|
-
}
|
|
210
|
-
const database = getDatabase(firebaseApp);
|
|
211
|
-
|
|
212
|
-
const {
|
|
213
|
-
onUpdate,
|
|
214
|
-
onError
|
|
215
|
-
} = props;
|
|
216
|
-
|
|
217
|
-
// Same refusal as `fetchCollection`: this used to read the whole node
|
|
218
|
-
// regardless of what the subscription asked for.
|
|
219
|
-
const plan = planRTDBQuery(props);
|
|
220
|
-
const dbQuery = query(ref(database, props.path), ...rtdbConstraints(plan));
|
|
221
|
-
const unsubscribe = onValue(dbQuery, (entity) => {
|
|
222
|
-
if (entity.exists()) {
|
|
223
|
-
const result: Record<string, unknown>[] = Object.entries(entity.val()).slice(plan.skip ?? 0).map(([id, values]) => ({
|
|
224
|
-
...(delegateToCMSModel(values) as Record<string, unknown>),
|
|
225
|
-
id
|
|
226
|
-
}));
|
|
227
|
-
onUpdate(result);
|
|
228
|
-
} else {
|
|
229
|
-
onUpdate([]);
|
|
230
|
-
}
|
|
231
|
-
}, (error) => onError?.(error));
|
|
232
|
-
|
|
233
|
-
return () => unsubscribe();
|
|
234
|
-
}, [firebaseApp]);
|
|
235
|
-
|
|
236
|
-
const fetchOne = useCallback(async <M extends Record<string, any>>({
|
|
237
|
-
path,
|
|
238
|
-
id
|
|
239
|
-
}: FetchOneProps<M>): Promise<Record<string, unknown> | undefined> => {
|
|
240
|
-
if (!firebaseApp) {
|
|
241
|
-
throw new Error("Firebase app not provided");
|
|
242
|
-
}
|
|
243
|
-
const database = getDatabase(firebaseApp);
|
|
244
|
-
|
|
245
|
-
const entity = await get(ref(database, `${path}/${id}`));
|
|
246
|
-
if (entity.exists()) {
|
|
247
|
-
return {
|
|
248
|
-
...(delegateToCMSModel(entity.val()) as Record<string, unknown>),
|
|
249
|
-
id: id
|
|
250
|
-
};
|
|
251
|
-
}
|
|
252
|
-
return undefined;
|
|
253
|
-
}, [firebaseApp]);
|
|
254
|
-
|
|
255
|
-
const listenOne = useCallback(<M extends Record<string, any>>({
|
|
256
|
-
path,
|
|
257
|
-
id,
|
|
258
|
-
onUpdate,
|
|
259
|
-
onError
|
|
260
|
-
}: ListenOneProps<M>): () => void => {
|
|
261
|
-
if (!firebaseApp) {
|
|
262
|
-
throw new Error("Firebase app not provided");
|
|
263
|
-
}
|
|
264
|
-
const database = getDatabase(firebaseApp);
|
|
265
|
-
|
|
266
|
-
const dbRef = ref(database, `${path}/${id}`);
|
|
267
|
-
const unsubscribe = onValue(dbRef, (entity) => {
|
|
268
|
-
if (entity.exists()) {
|
|
269
|
-
onUpdate({
|
|
270
|
-
...(delegateToCMSModel(entity.val()) as Record<string, unknown>),
|
|
271
|
-
id: id
|
|
272
|
-
});
|
|
273
|
-
} else {
|
|
274
|
-
onError?.(new Error("Entity does not exist"));
|
|
275
|
-
}
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
return () => unsubscribe();
|
|
279
|
-
}, [firebaseApp]);
|
|
280
|
-
|
|
281
|
-
const save = useCallback(async <M extends Record<string, any>>({
|
|
282
|
-
path,
|
|
283
|
-
id,
|
|
284
|
-
values
|
|
285
|
-
}: SaveProps<M>): Promise<Record<string, unknown>> => {
|
|
286
|
-
if (!firebaseApp) {
|
|
287
|
-
throw new Error("Firebase app not provided");
|
|
288
|
-
}
|
|
289
|
-
const database = getDatabase(firebaseApp);
|
|
290
|
-
|
|
291
|
-
// If id is not provided, a new entity will be created
|
|
292
|
-
const finalId = id ?? push(ref(database, path)).key;
|
|
293
|
-
if (!finalId) {
|
|
294
|
-
throw new Error("Could not generate a new id");
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
// Transform the data to RTDB format before saving
|
|
298
|
-
const transformedValues = cmsToRTDBModel(values, database);
|
|
299
|
-
await set(ref(database, `${path}/${finalId}`), transformedValues);
|
|
300
|
-
|
|
301
|
-
return {
|
|
302
|
-
...values,
|
|
303
|
-
id: finalId
|
|
304
|
-
};
|
|
305
|
-
}, [firebaseApp]);
|
|
306
|
-
|
|
307
|
-
const deleteOne = useCallback(async <M extends Record<string, any>>({
|
|
308
|
-
row
|
|
309
|
-
}: DeleteProps<M>): Promise<void> => {
|
|
310
|
-
if (!firebaseApp) {
|
|
311
|
-
throw new Error("Firebase app not provided");
|
|
312
|
-
}
|
|
313
|
-
const database = getDatabase(firebaseApp);
|
|
314
|
-
|
|
315
|
-
await remove(ref(database, `${row.path}/${row.id}`));
|
|
316
|
-
}, [firebaseApp]);
|
|
317
|
-
|
|
318
|
-
// Implementing additional methods required by DataDriver
|
|
319
|
-
const checkUniqueField = useCallback(async (slug: string, name: string, value: unknown, id?: string | number): Promise<boolean> => {
|
|
320
|
-
if (!firebaseApp) {
|
|
321
|
-
throw new Error("Firebase app not provided");
|
|
322
|
-
}
|
|
323
|
-
const database = getDatabase(firebaseApp);
|
|
324
|
-
|
|
325
|
-
// Simplified example; the Realtime Database does not support querying with "not equal" conditions
|
|
326
|
-
const dbRef = query(ref(database, slug), orderByChild(name), startAt(value as string | number | boolean | null), limitToFirst(1));
|
|
327
|
-
const entity = await get(dbRef);
|
|
328
|
-
|
|
329
|
-
if (!entity.exists()) {
|
|
330
|
-
return true;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Check if the found entity is the same as the one being checked
|
|
334
|
-
const [key, entityValue] = Object.entries(entity.val())[0];
|
|
335
|
-
if (entityValue && typeof entityValue === "object" && (entityValue as Record<string, unknown>)[name] === value && key === id) {
|
|
336
|
-
return true;
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
return false;
|
|
340
|
-
}, [firebaseApp]);
|
|
341
|
-
|
|
342
|
-
const isFilterCombinationValid = useCallback(({
|
|
343
|
-
path,
|
|
344
|
-
filter,
|
|
345
|
-
sortBy
|
|
346
|
-
}: {
|
|
347
|
-
path: string;
|
|
348
|
-
filter?: FilterValues<string>;
|
|
349
|
-
sortBy?: [string, "asc" | "desc"];
|
|
350
|
-
}): boolean => {
|
|
351
|
-
return false;
|
|
352
|
-
}, []);
|
|
353
|
-
|
|
354
|
-
return {
|
|
355
|
-
key: "firebase_rtdb",
|
|
356
|
-
fetchCollection,
|
|
357
|
-
listenCollection,
|
|
358
|
-
fetchOne,
|
|
359
|
-
listenOne,
|
|
360
|
-
save,
|
|
361
|
-
delete: deleteOne,
|
|
362
|
-
checkUniqueField,
|
|
363
|
-
isFilterCombinationValid,
|
|
364
|
-
currentTime: () => new Date()
|
|
365
|
-
};
|
|
366
|
-
}
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
/**
|
|
370
|
-
* Transform data from RTDB format back to CMS format
|
|
371
|
-
* This is used internally when fetching/listening to data
|
|
372
|
-
*/
|
|
373
|
-
function delegateToCMSModel(data: unknown): unknown {
|
|
374
|
-
if (data === null || data === undefined) return null;
|
|
375
|
-
|
|
376
|
-
if (Array.isArray(data)) {
|
|
377
|
-
return data.map(delegateToCMSModel).filter(v => v !== undefined);
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
if (typeof data === "object") {
|
|
381
|
-
const result: Record<string, unknown> = {};
|
|
382
|
-
for (const key of Object.keys(data as Record<string, unknown>)) {
|
|
383
|
-
const childValue = delegateToCMSModel((data as Record<string, unknown>)[key]);
|
|
384
|
-
if (childValue !== undefined)
|
|
385
|
-
result[key] = childValue;
|
|
386
|
-
}
|
|
387
|
-
return result;
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
return data;
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Transform data from CMS format to RTDB format
|
|
395
|
-
* This is used internally when saving data
|
|
396
|
-
*/
|
|
397
|
-
function cmsToRTDBModel(data: unknown, database: Database): unknown {
|
|
398
|
-
if (data === undefined) {
|
|
399
|
-
return null;
|
|
400
|
-
} else if (data === null) {
|
|
401
|
-
return null;
|
|
402
|
-
} else if (Array.isArray(data)) {
|
|
403
|
-
return data.filter(v => v !== undefined).map(v => cmsToRTDBModel(v, database));
|
|
404
|
-
} else if (typeof data === "object" && data !== null && "isEntityReference" in data && typeof (data as Record<string, unknown>).isEntityReference === "function" && (data as { isEntityReference: () => boolean }).isEntityReference()) {
|
|
405
|
-
const entityRef = data as unknown as { slug: string; id: string };
|
|
406
|
-
return ref(database, `${entityRef.slug}/${entityRef.id}`);
|
|
407
|
-
} else if (data instanceof Date) {
|
|
408
|
-
// For dates, convert to ISO string or timestamp.
|
|
409
|
-
return data.toISOString();
|
|
410
|
-
} else if (data && typeof data === "object") {
|
|
411
|
-
return Object.entries(data as Record<string, unknown>)
|
|
412
|
-
.map(([key, v]) => {
|
|
413
|
-
const rtdbModel = cmsToRTDBModel(v, database);
|
|
414
|
-
if (rtdbModel !== undefined)
|
|
415
|
-
return { [key]: rtdbModel };
|
|
416
|
-
else
|
|
417
|
-
return {};
|
|
418
|
-
})
|
|
419
|
-
.reduce((a, b) => ({ ...a,
|
|
420
|
-
...b }), {});
|
|
421
|
-
}
|
|
422
|
-
return data;
|
|
423
|
-
}
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
import { FirebaseApp } from "firebase/app";
|
|
2
|
-
import {
|
|
3
|
-
deleteObject,
|
|
4
|
-
getDownloadURL,
|
|
5
|
-
getMetadata,
|
|
6
|
-
getStorage,
|
|
7
|
-
list,
|
|
8
|
-
ref,
|
|
9
|
-
uploadBytesResumable
|
|
10
|
-
} from "firebase/storage";
|
|
11
|
-
import { DownloadConfig, DownloadMetadata, StorageListResult, StorageSource, UploadFileProps } from "@rebasepro/types";
|
|
12
|
-
|
|
13
|
-
/**
|
|
14
|
-
* @group Firebase
|
|
15
|
-
*/
|
|
16
|
-
export interface FirebaseStorageSourceProps {
|
|
17
|
-
firebaseApp?: FirebaseApp
|
|
18
|
-
bucketUrl?: string
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Use this hook to build an {@link StorageSource} based on Firebase storage
|
|
23
|
-
* @group Firebase
|
|
24
|
-
*/
|
|
25
|
-
export function useFirebaseStorageSource({
|
|
26
|
-
firebaseApp,
|
|
27
|
-
bucketUrl
|
|
28
|
-
}: FirebaseStorageSourceProps): StorageSource {
|
|
29
|
-
const projectId = firebaseApp?.options?.projectId;
|
|
30
|
-
const urlsCache: Record<string, DownloadConfig> = {};
|
|
31
|
-
return {
|
|
32
|
-
putObject({
|
|
33
|
-
file,
|
|
34
|
-
key,
|
|
35
|
-
metadata,
|
|
36
|
-
bucket
|
|
37
|
-
}: UploadFileProps)
|
|
38
|
-
: Promise<any> {
|
|
39
|
-
try {
|
|
40
|
-
if (!firebaseApp) throw Error("useFirebaseStorageSource Firebase not initialised");
|
|
41
|
-
const storageBucketUrl = bucket ?? bucketUrl;
|
|
42
|
-
const storage = getStorage(firebaseApp, storageBucketUrl);
|
|
43
|
-
if (!storage) throw Error("useFirebaseStorageSource Firebase not initialised");
|
|
44
|
-
|
|
45
|
-
const storageRef = ref(storage, key);
|
|
46
|
-
const uploadTask = uploadBytesResumable(storageRef, file, metadata);
|
|
47
|
-
|
|
48
|
-
return new Promise((resolve, reject) => {
|
|
49
|
-
let lastProgress = 0;
|
|
50
|
-
let timeoutId: NodeJS.Timeout | null = null;
|
|
51
|
-
|
|
52
|
-
const clearTimeoutIfExists = () => {
|
|
53
|
-
if (timeoutId) {
|
|
54
|
-
clearTimeout(timeoutId);
|
|
55
|
-
timeoutId = null;
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const setProgressTimeout = () => {
|
|
60
|
-
clearTimeoutIfExists();
|
|
61
|
-
timeoutId = setTimeout(() => {
|
|
62
|
-
uploadTask.cancel();
|
|
63
|
-
reject(new Error("Upload failed - This is likely a CORS configuration issue. " +
|
|
64
|
-
"Make sure Firebase Storage is enabled in your project: " + `https://console.firebase.google.com/u/0/project/${projectId}/storage` + ". " +
|
|
65
|
-
"If it is, check Firebase Storage CORS settings."));
|
|
66
|
-
}, 5000);
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
setProgressTimeout();
|
|
70
|
-
|
|
71
|
-
uploadTask.on("state_changed",
|
|
72
|
-
(entity) => {
|
|
73
|
-
const progress = (entity.bytesTransferred / entity.totalBytes) * 100;
|
|
74
|
-
|
|
75
|
-
if (progress > lastProgress) {
|
|
76
|
-
lastProgress = progress;
|
|
77
|
-
setProgressTimeout();
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
(error) => {
|
|
81
|
-
clearTimeoutIfExists();
|
|
82
|
-
console.error("Firebase Storage upload error:", error);
|
|
83
|
-
|
|
84
|
-
let errorMessage = "Unknown upload error";
|
|
85
|
-
|
|
86
|
-
if (error?.message) {
|
|
87
|
-
errorMessage = error.message;
|
|
88
|
-
} else if (typeof error === "string") {
|
|
89
|
-
errorMessage = error;
|
|
90
|
-
} else if (error?.code) {
|
|
91
|
-
errorMessage = error.code;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (error?.code === "storage/unauthorized") {
|
|
95
|
-
reject(new Error("Unauthorized: Check Firebase Storage security rules"));
|
|
96
|
-
} else if (error?.code === "storage/canceled") {
|
|
97
|
-
reject(new Error("Upload canceled"));
|
|
98
|
-
} else if (error?.code === "storage/unknown" || !error?.code) {
|
|
99
|
-
reject(new Error("Upload failed - Check Firebase Storage CORS configuration or network connection"));
|
|
100
|
-
} else if (errorMessage.toLowerCase().includes("network")) {
|
|
101
|
-
reject(new Error("Network error: Check your internet connection"));
|
|
102
|
-
} else {
|
|
103
|
-
const newError = Object.assign(new Error(errorMessage), { code: error?.code });
|
|
104
|
-
reject(newError);
|
|
105
|
-
}
|
|
106
|
-
},
|
|
107
|
-
() => {
|
|
108
|
-
clearTimeoutIfExists();
|
|
109
|
-
const fullPath = uploadTask.snapshot.ref.fullPath;
|
|
110
|
-
const bucketName = uploadTask.snapshot.ref.bucket;
|
|
111
|
-
resolve({
|
|
112
|
-
key: fullPath,
|
|
113
|
-
bucket: bucketName,
|
|
114
|
-
storageUrl: `s3://${bucketName}/${fullPath}`
|
|
115
|
-
});
|
|
116
|
-
}
|
|
117
|
-
);
|
|
118
|
-
});
|
|
119
|
-
} catch (error) {
|
|
120
|
-
return Promise.reject(error);
|
|
121
|
-
}
|
|
122
|
-
},
|
|
123
|
-
|
|
124
|
-
async getObject(path: string, bucket?: string): Promise<File | null> {
|
|
125
|
-
try {
|
|
126
|
-
if (!firebaseApp) throw Error("useFirebaseStorageSource Firebase not initialised");
|
|
127
|
-
const storageBucketUrl = bucket ?? bucketUrl;
|
|
128
|
-
const storage = getStorage(firebaseApp, storageBucketUrl);
|
|
129
|
-
if (!storage) throw Error("useFirebaseStorageSource Firebase not initialised");
|
|
130
|
-
const fileRef = ref(storage, path);
|
|
131
|
-
const url = await getDownloadURL(fileRef);
|
|
132
|
-
const response = await fetch(url);
|
|
133
|
-
const blob = await response.blob();
|
|
134
|
-
return new File([blob], path);
|
|
135
|
-
} catch (e: unknown) {
|
|
136
|
-
if (typeof e === "object" && e !== null && "code" in e && (e as { code: string }).code === "storage/object-not-found") return null;
|
|
137
|
-
throw e;
|
|
138
|
-
}
|
|
139
|
-
},
|
|
140
|
-
|
|
141
|
-
async getSignedUrl(storagePathOrUrl: string, bucket?: string): Promise<DownloadConfig> {
|
|
142
|
-
if (!firebaseApp) throw Error("useFirebaseStorageSource Firebase not initialised");
|
|
143
|
-
|
|
144
|
-
// Support fully-qualified s3:// and gs:// URLs for backward compatibility
|
|
145
|
-
let resolvedPathOrUrl = storagePathOrUrl;
|
|
146
|
-
let resolvedBucket = bucket;
|
|
147
|
-
const match = storagePathOrUrl.match(/^(s3|gs):\/\//);
|
|
148
|
-
if (match) {
|
|
149
|
-
const protocolLength = match[0].length;
|
|
150
|
-
const withoutProtocol = storagePathOrUrl.substring(protocolLength);
|
|
151
|
-
const firstSlash = withoutProtocol.indexOf("/");
|
|
152
|
-
if (firstSlash > 0) {
|
|
153
|
-
resolvedBucket = withoutProtocol.substring(0, firstSlash);
|
|
154
|
-
resolvedPathOrUrl = withoutProtocol.substring(firstSlash + 1);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
const storageBucketUrl = resolvedBucket ?? bucketUrl;
|
|
159
|
-
const storage = getStorage(firebaseApp, storageBucketUrl);
|
|
160
|
-
if (!storage) throw Error("useFirebaseStorageSource Firebase not initialised");
|
|
161
|
-
|
|
162
|
-
if (urlsCache[storagePathOrUrl])
|
|
163
|
-
return urlsCache[storagePathOrUrl];
|
|
164
|
-
try {
|
|
165
|
-
const fileRef = ref(storage, resolvedPathOrUrl);
|
|
166
|
-
const [url, metadata] = await Promise.all([getDownloadURL(fileRef), getMetadata(fileRef)]);
|
|
167
|
-
const result: DownloadConfig = {
|
|
168
|
-
url,
|
|
169
|
-
metadata: metadata as DownloadMetadata
|
|
170
|
-
}
|
|
171
|
-
urlsCache[storagePathOrUrl] = result;
|
|
172
|
-
return result;
|
|
173
|
-
} catch (e: unknown) {
|
|
174
|
-
if (typeof e === "object" && e !== null && "code" in e && (e as { code: string }).code === "storage/object-not-found") return {
|
|
175
|
-
url: null,
|
|
176
|
-
fileNotFound: true
|
|
177
|
-
};
|
|
178
|
-
throw e;
|
|
179
|
-
}
|
|
180
|
-
},
|
|
181
|
-
|
|
182
|
-
async listObjects(prefix: string, options?: {
|
|
183
|
-
bucket?: string,
|
|
184
|
-
maxResults?: number,
|
|
185
|
-
pageToken?: string
|
|
186
|
-
}): Promise<StorageListResult> {
|
|
187
|
-
if (!firebaseApp) throw Error("useFirebaseStorageSource Firebase not initialised");
|
|
188
|
-
const storageBucketUrl = options?.bucket ?? bucketUrl;
|
|
189
|
-
const storage = getStorage(firebaseApp, storageBucketUrl);
|
|
190
|
-
if (!storage) throw Error("useFirebaseStorageSource Firebase not initialised");
|
|
191
|
-
const folderRef = ref(storage, prefix);
|
|
192
|
-
return await list(folderRef, {
|
|
193
|
-
maxResults: options?.maxResults,
|
|
194
|
-
pageToken: options?.pageToken
|
|
195
|
-
});
|
|
196
|
-
},
|
|
197
|
-
|
|
198
|
-
async deleteObject(path: string, bucket?: string): Promise<void> {
|
|
199
|
-
if (!firebaseApp) throw Error("useFirebaseStorageSource Firebase not initialised");
|
|
200
|
-
const storageBucketUrl = bucket ?? bucketUrl;
|
|
201
|
-
const storage = getStorage(firebaseApp, storageBucketUrl);
|
|
202
|
-
if (!storage) throw Error("useFirebaseStorageSource Firebase not initialised");
|
|
203
|
-
const fileRef = ref(storage, path);
|
|
204
|
-
return deleteObject(fileRef);
|
|
205
|
-
}
|
|
206
|
-
};
|
|
207
|
-
}
|