@rebasepro/firebase 0.17.3 → 0.18.0
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,855 +0,0 @@
|
|
|
1
|
-
import { DataDriver, DeleteProps, CollectionConfig, EntityReference, FetchCollectionProps, FetchOneProps, FilterValues, GeoPoint, ListenCollectionProps, ListenOneProps, OrderByTuple, SaveProps, WhereFilterOp } from "@rebasepro/types";
|
|
2
|
-
import { normalizeDriverOrderBy } from "@rebasepro/common";
|
|
3
|
-
import { FilterCombination } from "@rebasepro/cms-types";
|
|
4
|
-
import { User } from "firebase/auth";
|
|
5
|
-
import {
|
|
6
|
-
collection as collectionClause,
|
|
7
|
-
CollectionReference,
|
|
8
|
-
deleteDoc,
|
|
9
|
-
deleteField,
|
|
10
|
-
doc,
|
|
11
|
-
DocumentReference,
|
|
12
|
-
DocumentSnapshot,
|
|
13
|
-
Firestore,
|
|
14
|
-
GeoPoint as FirestoreGeoPoint,
|
|
15
|
-
getCountFromServer,
|
|
16
|
-
getDoc,
|
|
17
|
-
getDocs,
|
|
18
|
-
getFirestore,
|
|
19
|
-
limit as limitClause,
|
|
20
|
-
onSnapshot,
|
|
21
|
-
orderBy as orderByClause,
|
|
22
|
-
Query,
|
|
23
|
-
query,
|
|
24
|
-
QueryConstraint,
|
|
25
|
-
serverTimestamp,
|
|
26
|
-
setDoc,
|
|
27
|
-
startAfter as startAfterClause,
|
|
28
|
-
Timestamp,
|
|
29
|
-
VectorValue,
|
|
30
|
-
vector,
|
|
31
|
-
where as whereClause
|
|
32
|
-
} from "firebase/firestore";
|
|
33
|
-
import type { FieldValue } from "firebase/firestore";
|
|
34
|
-
import { FirebaseApp } from "firebase/app";
|
|
35
|
-
import { FirestoreTextSearchController, FirestoreTextSearchControllerBuilder } from "../types/text_search";
|
|
36
|
-
import { useCallback, useEffect, useMemo, useRef } from "react";
|
|
37
|
-
import { localSearchControllerBuilder } from "../utils";
|
|
38
|
-
import { getAuth } from "firebase/auth";
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* @group Firebase
|
|
42
|
-
*/
|
|
43
|
-
export interface FirestoreDataDriverProps {
|
|
44
|
-
firebaseApp?: FirebaseApp,
|
|
45
|
-
/**
|
|
46
|
-
* You can use this controller to return a list of ids from a search index, given a
|
|
47
|
-
* `path` and a `searchString`.
|
|
48
|
-
*/
|
|
49
|
-
textSearchControllerBuilder?: FirestoreTextSearchControllerBuilder,
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Fallback to local text search if no text search controller is specified,
|
|
53
|
-
* or if the controller does not support the given path.
|
|
54
|
-
*/
|
|
55
|
-
localTextSearchEnabled?: boolean,
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Use this builder to indicate which indexes are available in your
|
|
59
|
-
* Firestore database. This is used to allow filtering and sorting
|
|
60
|
-
* for multiple fields in the CMS.
|
|
61
|
-
*/
|
|
62
|
-
firestoreIndexesBuilder?: FirestoreIndexesBuilder;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export type FirestoreIndexesBuilder = (params: {
|
|
66
|
-
path: string,
|
|
67
|
-
collection: CollectionConfig<any>,
|
|
68
|
-
}) => FilterCombination<string>[] | undefined
|
|
69
|
-
|
|
70
|
-
export type FirestoreDataDriver = DataDriver & {
|
|
71
|
-
|
|
72
|
-
initTextSearch: (props: {
|
|
73
|
-
path: string,
|
|
74
|
-
databaseId?: string,
|
|
75
|
-
collection?: CollectionConfig
|
|
76
|
-
}) => Promise<boolean>,
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* The window a read has to ask Firestore for in order to honour `offset`.
|
|
81
|
-
*
|
|
82
|
-
* Firestore's web SDK has no `offset()` — it pages by cursor (`startAfter`)
|
|
83
|
-
* only. Callers that page by offset (`buildRebaseData`, and through it every
|
|
84
|
-
* `findAll()` and `iterate()`) were therefore served page one every time:
|
|
85
|
-
* `count` is a real server count, so `hasMore` never went false, the walk
|
|
86
|
-
* accumulated the same rows over and over, and it ended by tripping its row
|
|
87
|
-
* cap and reporting "matched more than N rows" — a condition that had not
|
|
88
|
-
* occurred.
|
|
89
|
-
*
|
|
90
|
-
* So the read asks for `offset + limit` documents and drops the first
|
|
91
|
-
* `offset`. Those documents are billed either way: Firestore charges for every
|
|
92
|
-
* document a cursor walks past, which is why `startAfter` is the cheap way to
|
|
93
|
-
* page and offset paging over a large collection is not.
|
|
94
|
-
*/
|
|
95
|
-
export function resolveOffsetWindow(
|
|
96
|
-
limit: number | undefined,
|
|
97
|
-
offset: number | undefined
|
|
98
|
-
): { fetchLimit: number | undefined, skip: number } {
|
|
99
|
-
const skip = offset !== undefined && Number.isFinite(offset) && offset > 0
|
|
100
|
-
? Math.floor(offset)
|
|
101
|
-
: 0;
|
|
102
|
-
return {
|
|
103
|
-
fetchLimit: limit === undefined ? undefined : limit + skip,
|
|
104
|
-
skip
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Use this hook to build a {@link DataDriver} based on Firestore
|
|
110
|
-
* @param firebaseApp
|
|
111
|
-
* @param textSearchControllerBuilder
|
|
112
|
-
* @group Firebase
|
|
113
|
-
*/
|
|
114
|
-
export function useFirestoreDriver({
|
|
115
|
-
firebaseApp,
|
|
116
|
-
textSearchControllerBuilder,
|
|
117
|
-
firestoreIndexesBuilder,
|
|
118
|
-
localTextSearchEnabled
|
|
119
|
-
}: FirestoreDataDriverProps): FirestoreDataDriver {
|
|
120
|
-
|
|
121
|
-
const searchControllerRef = useRef<FirestoreTextSearchController>(undefined);
|
|
122
|
-
|
|
123
|
-
useEffect(() => {
|
|
124
|
-
if (!searchControllerRef.current && firebaseApp) {
|
|
125
|
-
if ((textSearchControllerBuilder || localTextSearchEnabled) && !searchControllerRef.current) {
|
|
126
|
-
searchControllerRef.current = buildTextSearchControllerWithLocalSearch({
|
|
127
|
-
firebaseApp,
|
|
128
|
-
textSearchControllerBuilder,
|
|
129
|
-
localTextSearchEnabled: localTextSearchEnabled ?? false
|
|
130
|
-
});
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
}, [firebaseApp, localTextSearchEnabled, textSearchControllerBuilder]);
|
|
134
|
-
|
|
135
|
-
const buildQuery = useCallback(<M>(path: string,
|
|
136
|
-
filter: FilterValues<Extract<keyof M, string>> | undefined,
|
|
137
|
-
orderBy: string | OrderByTuple[] | undefined,
|
|
138
|
-
order: "desc" | "asc" | undefined,
|
|
139
|
-
startAfter: unknown[] | undefined,
|
|
140
|
-
limit: number | undefined,
|
|
141
|
-
databaseId?: string) => {
|
|
142
|
-
|
|
143
|
-
if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
|
|
144
|
-
|
|
145
|
-
const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
|
|
146
|
-
const collectionReference: Query = collectionClause(firestore, path);
|
|
147
|
-
|
|
148
|
-
const queryParams: QueryConstraint[] = [];
|
|
149
|
-
if (filter) {
|
|
150
|
-
Object.entries(filter)
|
|
151
|
-
.filter(([_, entry]) => !!entry)
|
|
152
|
-
.forEach(([key, filterParameter]) => {
|
|
153
|
-
const [op, value] = filterParameter as [WhereFilterOp, any];
|
|
154
|
-
|
|
155
|
-
// Null-testing operators map to Firestore's == / != against null.
|
|
156
|
-
if (op === "is-null") {
|
|
157
|
-
queryParams.push(whereClause(key, "==", null));
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
if (op === "is-not-null") {
|
|
161
|
-
queryParams.push(whereClause(key, "!=", null));
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
// Firestore has no LIKE/ILIKE — fail loudly rather than silently
|
|
166
|
-
// returning wrong results. Use `searchString` / a search index instead.
|
|
167
|
-
if (op === "like" || op === "ilike" || op === "not-like" || op === "not-ilike") {
|
|
168
|
-
throw new Error(
|
|
169
|
-
`Firestore does not support the "${op}" operator (SQL pattern matching). ` +
|
|
170
|
-
"Use a full-text search index or the collection's searchString instead."
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
queryParams.push(whereClause(key, op, rebaseToFirestoreModel(value, firestore)));
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Firestore composes several `orderBy` constraints into a compound sort,
|
|
179
|
-
// applied in the order they are added — the same meaning the tuple list
|
|
180
|
-
// carries. It needs a matching composite index; without one Firestore
|
|
181
|
-
// refuses the query outright rather than answering it half-sorted.
|
|
182
|
-
for (const [field, direction] of normalizeDriverOrderBy(orderBy, order) ?? []) {
|
|
183
|
-
queryParams.push(orderByClause(field, direction));
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (startAfter) {
|
|
187
|
-
queryParams.push(startAfterClause(startAfter));
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
if (limit) {
|
|
191
|
-
queryParams.push(limitClause(limit));
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
return query(collectionReference, ...queryParams);
|
|
195
|
-
}, [firebaseApp]);
|
|
196
|
-
|
|
197
|
-
const getAndBuildEntity = useCallback((path: string,
|
|
198
|
-
id: string | number,
|
|
199
|
-
databaseId?: string
|
|
200
|
-
): Promise<Record<string, unknown> | undefined> => {
|
|
201
|
-
if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
|
|
202
|
-
|
|
203
|
-
const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
|
|
204
|
-
|
|
205
|
-
return getDoc(doc(firestore, path, String(id)))
|
|
206
|
-
.then((docEntity) => {
|
|
207
|
-
if (!docEntity.exists()) {
|
|
208
|
-
return undefined;
|
|
209
|
-
}
|
|
210
|
-
return createRowFromDocument(docEntity);
|
|
211
|
-
});
|
|
212
|
-
}, [firebaseApp]);
|
|
213
|
-
|
|
214
|
-
const listenOne = useCallback(<M extends Record<string, any>>(
|
|
215
|
-
{
|
|
216
|
-
path,
|
|
217
|
-
id,
|
|
218
|
-
collection,
|
|
219
|
-
onUpdate,
|
|
220
|
-
onError
|
|
221
|
-
}: ListenOneProps<M>): () => void => {
|
|
222
|
-
if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
|
|
223
|
-
|
|
224
|
-
const databaseId = collection?.databaseId;
|
|
225
|
-
const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
|
|
226
|
-
const resolvedPath = path;
|
|
227
|
-
|
|
228
|
-
return onSnapshot(
|
|
229
|
-
doc(firestore, resolvedPath, String(id)),
|
|
230
|
-
{
|
|
231
|
-
next: (docEntity) => {
|
|
232
|
-
onUpdate(docEntity.exists() ? createRowFromDocument(docEntity) : null);
|
|
233
|
-
},
|
|
234
|
-
error: onError
|
|
235
|
-
}
|
|
236
|
-
);
|
|
237
|
-
}, [firebaseApp]);
|
|
238
|
-
|
|
239
|
-
const performTextSearch = useCallback(<M extends Record<string, any>>({
|
|
240
|
-
path,
|
|
241
|
-
databaseId,
|
|
242
|
-
searchString,
|
|
243
|
-
onUpdate
|
|
244
|
-
}: {
|
|
245
|
-
path: string,
|
|
246
|
-
databaseId?: string,
|
|
247
|
-
searchString: string;
|
|
248
|
-
onUpdate: (rows: Record<string, unknown>[]) => void,
|
|
249
|
-
}): () => void => {
|
|
250
|
-
|
|
251
|
-
if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
|
|
252
|
-
|
|
253
|
-
const textSearchController = searchControllerRef.current;
|
|
254
|
-
if (!textSearchController)
|
|
255
|
-
throw Error("Trying to make text search without specifying a FirestoreTextSearchController");
|
|
256
|
-
|
|
257
|
-
let subscriptions: (() => void)[] = [];
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
const auth = getAuth(firebaseApp);
|
|
261
|
-
const currentUser = auth?.currentUser;
|
|
262
|
-
|
|
263
|
-
const search = textSearchController.search({
|
|
264
|
-
path,
|
|
265
|
-
searchString,
|
|
266
|
-
currentUser: currentUser ?? undefined,
|
|
267
|
-
databaseId
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
if (!search) {
|
|
271
|
-
throw Error("The current path is not supported by the specified FirestoreTextSearchController");
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
search.then((ids) => {
|
|
275
|
-
if (!ids || ids.length === 0) {
|
|
276
|
-
subscriptions = [];
|
|
277
|
-
onUpdate([]);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const rows: Record<string, unknown>[] = [];
|
|
281
|
-
const addedEntitiesSet = new Set<string | number>();
|
|
282
|
-
subscriptions = (ids ?? [])
|
|
283
|
-
.map((id) => {
|
|
284
|
-
return listenOne({
|
|
285
|
-
path,
|
|
286
|
-
id,
|
|
287
|
-
onUpdate: (row: Record<string, unknown> | null) => {
|
|
288
|
-
const incomingId = row?.id as string | number | undefined;
|
|
289
|
-
if (row && incomingId !== undefined) {
|
|
290
|
-
if (!addedEntitiesSet.has(incomingId)) {
|
|
291
|
-
addedEntitiesSet.add(incomingId);
|
|
292
|
-
rows.push(row);
|
|
293
|
-
onUpdate(rows);
|
|
294
|
-
}
|
|
295
|
-
} else {
|
|
296
|
-
addedEntitiesSet.delete(id);
|
|
297
|
-
onUpdate([...rows.filter(r => r.id !== id)])
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
})
|
|
301
|
-
}
|
|
302
|
-
);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
return () => {
|
|
306
|
-
subscriptions.forEach((p) => p());
|
|
307
|
-
}
|
|
308
|
-
|
|
309
|
-
}, [firebaseApp, listenOne]);
|
|
310
|
-
|
|
311
|
-
const initTextSearch = useCallback(async (props: {
|
|
312
|
-
path: string,
|
|
313
|
-
databaseId?: string,
|
|
314
|
-
collection?: CollectionConfig
|
|
315
|
-
}) => {
|
|
316
|
-
console.debug("Init text search controller", searchControllerRef.current, props.path);
|
|
317
|
-
if (!searchControllerRef.current) {
|
|
318
|
-
console.warn("You are trying to use text search, but have not provided a text search controller in `useFirestoreDriver`. You can also set the flag `localTextSearchEnabled` to use local search in `useFirestoreDriver`. Local text search can incur in performance issues and higher costs for large datasets.");
|
|
319
|
-
return false;
|
|
320
|
-
}
|
|
321
|
-
try {
|
|
322
|
-
return searchControllerRef.current.init(props);
|
|
323
|
-
} catch (e) {
|
|
324
|
-
console.error("Error initializing text search controller", e);
|
|
325
|
-
return false;
|
|
326
|
-
}
|
|
327
|
-
}, []);
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Fetch entities in a Firestore path
|
|
331
|
-
* @param path
|
|
332
|
-
* @param collection
|
|
333
|
-
* @param filter
|
|
334
|
-
* @param limit
|
|
335
|
-
* @param offset
|
|
336
|
-
* @param startAfter
|
|
337
|
-
* @param searchString
|
|
338
|
-
* @param orderBy
|
|
339
|
-
* @param order
|
|
340
|
-
* @return The rows in the requested window
|
|
341
|
-
* @see useCollection if you need this functionality implemented as a hook
|
|
342
|
-
* @group Firestore
|
|
343
|
-
*/
|
|
344
|
-
const fetchCollection = useCallback(async <M extends Record<string, any>>({
|
|
345
|
-
path,
|
|
346
|
-
filter,
|
|
347
|
-
limit,
|
|
348
|
-
offset,
|
|
349
|
-
startAfter,
|
|
350
|
-
searchString,
|
|
351
|
-
orderBy,
|
|
352
|
-
order,
|
|
353
|
-
collection
|
|
354
|
-
}: FetchCollectionProps<M>
|
|
355
|
-
): Promise<Record<string, unknown>[]> => {
|
|
356
|
-
|
|
357
|
-
const databaseId = collection?.databaseId;
|
|
358
|
-
|
|
359
|
-
const resolvedPath = path;
|
|
360
|
-
|
|
361
|
-
console.debug("Fetching collection", {
|
|
362
|
-
path,
|
|
363
|
-
limit,
|
|
364
|
-
offset,
|
|
365
|
-
filter,
|
|
366
|
-
startAfter,
|
|
367
|
-
orderBy,
|
|
368
|
-
order
|
|
369
|
-
});
|
|
370
|
-
// Firestore has no `offset()`; see resolveOffsetWindow.
|
|
371
|
-
const {
|
|
372
|
-
fetchLimit,
|
|
373
|
-
skip
|
|
374
|
-
} = resolveOffsetWindow(limit, offset);
|
|
375
|
-
const query = buildQuery(resolvedPath, filter, orderBy, order, startAfter as unknown[] | undefined, fetchLimit, databaseId);
|
|
376
|
-
|
|
377
|
-
const entity = await getDocs(query);
|
|
378
|
-
return entity.docs.slice(skip).map((doc) => createRowFromDocument(doc));
|
|
379
|
-
}, [buildQuery]);
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* Listen to a entities in a given path
|
|
383
|
-
* @param path
|
|
384
|
-
* @param collection
|
|
385
|
-
* @param onError
|
|
386
|
-
* @param filter
|
|
387
|
-
* @param limit
|
|
388
|
-
* @param startAfter
|
|
389
|
-
* @param searchString
|
|
390
|
-
* @param orderBy
|
|
391
|
-
* @param order
|
|
392
|
-
* @param onUpdate
|
|
393
|
-
* @return Function to cancel subscription
|
|
394
|
-
* @see useCollection if you need this functionality implemented as a hook
|
|
395
|
-
* @group Firestore
|
|
396
|
-
*/
|
|
397
|
-
const listenCollection = useCallback(<M extends Record<string, any>>(
|
|
398
|
-
{
|
|
399
|
-
path,
|
|
400
|
-
filter,
|
|
401
|
-
limit,
|
|
402
|
-
startAfter,
|
|
403
|
-
searchString,
|
|
404
|
-
orderBy,
|
|
405
|
-
order,
|
|
406
|
-
onUpdate,
|
|
407
|
-
onError,
|
|
408
|
-
collection
|
|
409
|
-
}: ListenCollectionProps<M>
|
|
410
|
-
): () => void => {
|
|
411
|
-
|
|
412
|
-
console.debug("Listening collection", {
|
|
413
|
-
path,
|
|
414
|
-
searchString,
|
|
415
|
-
limit,
|
|
416
|
-
filter,
|
|
417
|
-
startAfter,
|
|
418
|
-
orderBy,
|
|
419
|
-
order,
|
|
420
|
-
collection
|
|
421
|
-
});
|
|
422
|
-
|
|
423
|
-
if (!firebaseApp) {
|
|
424
|
-
throw Error("useFirestoreDriver Firebase not initialised");
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
const databaseId = collection?.databaseId;
|
|
428
|
-
|
|
429
|
-
if (searchString) {
|
|
430
|
-
return performTextSearch<M>({
|
|
431
|
-
path,
|
|
432
|
-
searchString,
|
|
433
|
-
onUpdate,
|
|
434
|
-
databaseId
|
|
435
|
-
});
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
const resolvedPath = path;
|
|
439
|
-
console.debug("Resolved path for listening", {
|
|
440
|
-
path,
|
|
441
|
-
resolvedPath
|
|
442
|
-
});
|
|
443
|
-
const query = buildQuery(resolvedPath, filter, orderBy, order, startAfter as unknown[] | undefined, limit, databaseId);
|
|
444
|
-
return onSnapshot(query,
|
|
445
|
-
{
|
|
446
|
-
next: (entity) => {
|
|
447
|
-
if (!searchString)
|
|
448
|
-
onUpdate(entity.docs.map((doc) => createRowFromDocument(doc)));
|
|
449
|
-
},
|
|
450
|
-
error: onError
|
|
451
|
-
}
|
|
452
|
-
);
|
|
453
|
-
|
|
454
|
-
}, [buildQuery, firebaseApp, performTextSearch]);
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Retrieve a entity given a path and a collection
|
|
458
|
-
* @param path
|
|
459
|
-
* @param id
|
|
460
|
-
* @param collection
|
|
461
|
-
* @group Firestore
|
|
462
|
-
*/
|
|
463
|
-
const fetchOne = useCallback(<M extends Record<string, any>>({
|
|
464
|
-
path,
|
|
465
|
-
id,
|
|
466
|
-
collection
|
|
467
|
-
}: FetchOneProps<M>
|
|
468
|
-
): Promise<Record<string, unknown> | undefined> => {
|
|
469
|
-
const resolvedPath = path;
|
|
470
|
-
return getAndBuildEntity(resolvedPath, id, collection?.databaseId);
|
|
471
|
-
}, [getAndBuildEntity]);
|
|
472
|
-
|
|
473
|
-
/**
|
|
474
|
-
* Save entity to the specified path. Note that Firestore does not allow
|
|
475
|
-
* undefined values.
|
|
476
|
-
* @param path
|
|
477
|
-
* @param id
|
|
478
|
-
* @param values
|
|
479
|
-
* @param schemaId
|
|
480
|
-
* @param collection
|
|
481
|
-
* @param status
|
|
482
|
-
* @group Firestore
|
|
483
|
-
*/
|
|
484
|
-
const save = useCallback(<M extends Record<string, any>>(
|
|
485
|
-
{
|
|
486
|
-
path,
|
|
487
|
-
id,
|
|
488
|
-
values: valuesProp,
|
|
489
|
-
collection,
|
|
490
|
-
status
|
|
491
|
-
}: SaveProps<M>): Promise<Record<string, unknown>> => {
|
|
492
|
-
|
|
493
|
-
if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
|
|
494
|
-
|
|
495
|
-
console.debug("1", {
|
|
496
|
-
path,
|
|
497
|
-
id,
|
|
498
|
-
values: valuesProp,
|
|
499
|
-
collection
|
|
500
|
-
})
|
|
501
|
-
const values = rebaseToFirestoreModel(valuesProp, getFirestore(firebaseApp));
|
|
502
|
-
|
|
503
|
-
console.debug("2", {
|
|
504
|
-
path,
|
|
505
|
-
id,
|
|
506
|
-
values: valuesProp,
|
|
507
|
-
collection
|
|
508
|
-
})
|
|
509
|
-
const databaseId = collection?.databaseId;
|
|
510
|
-
const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
|
|
511
|
-
const resolvedPath = path;
|
|
512
|
-
|
|
513
|
-
const collectionReference: CollectionReference = collectionClause(firestore, path);
|
|
514
|
-
console.debug("Saving entity", {
|
|
515
|
-
path,
|
|
516
|
-
id,
|
|
517
|
-
values,
|
|
518
|
-
databaseId
|
|
519
|
-
});
|
|
520
|
-
|
|
521
|
-
let documentReference: DocumentReference;
|
|
522
|
-
if (id) {
|
|
523
|
-
documentReference = doc(collectionReference, String(id));
|
|
524
|
-
} else {
|
|
525
|
-
documentReference = doc(collectionReference);
|
|
526
|
-
}
|
|
527
|
-
return setDoc(documentReference, values as Record<string, unknown>, { merge: true })
|
|
528
|
-
.then(() => {
|
|
529
|
-
return {
|
|
530
|
-
...(firestoreToRebaseModel(values) as Record<string, unknown>),
|
|
531
|
-
id: documentReference.id
|
|
532
|
-
};
|
|
533
|
-
})
|
|
534
|
-
.catch((error) => {
|
|
535
|
-
console.error("Error saving entity", error);
|
|
536
|
-
throw error;
|
|
537
|
-
|
|
538
|
-
});
|
|
539
|
-
}, [firebaseApp]);
|
|
540
|
-
|
|
541
|
-
/**
|
|
542
|
-
* Delete a entity
|
|
543
|
-
* @param entity
|
|
544
|
-
* @param collection
|
|
545
|
-
* @group Firestore
|
|
546
|
-
*/
|
|
547
|
-
const deleteOne = useCallback(<M extends Record<string, any>>(
|
|
548
|
-
{
|
|
549
|
-
row,
|
|
550
|
-
collection
|
|
551
|
-
}: DeleteProps<M>
|
|
552
|
-
): Promise<void> => {
|
|
553
|
-
if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
|
|
554
|
-
|
|
555
|
-
const databaseId = collection?.databaseId;
|
|
556
|
-
const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
|
|
557
|
-
|
|
558
|
-
return deleteDoc(doc(firestore, row.path, String(row.id)));
|
|
559
|
-
}, [firebaseApp]);
|
|
560
|
-
|
|
561
|
-
/**
|
|
562
|
-
* Check if the given property is unique in the given collection
|
|
563
|
-
* @param path Collection path
|
|
564
|
-
* @param name of the property
|
|
565
|
-
* @param value
|
|
566
|
-
* @param property
|
|
567
|
-
* @param id
|
|
568
|
-
* @return `true` if there are no other fields besides the given entity
|
|
569
|
-
* @group Firestore
|
|
570
|
-
*/
|
|
571
|
-
const checkUniqueField = useCallback(async (
|
|
572
|
-
path: string,
|
|
573
|
-
name: string,
|
|
574
|
-
value: unknown,
|
|
575
|
-
id?: string | number,
|
|
576
|
-
collection?: CollectionConfig<any>
|
|
577
|
-
): Promise<boolean> => {
|
|
578
|
-
|
|
579
|
-
if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
|
|
580
|
-
|
|
581
|
-
const databaseId = collection?.databaseId;
|
|
582
|
-
const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
|
|
583
|
-
|
|
584
|
-
if (value === undefined || value === null) {
|
|
585
|
-
return Promise.resolve(true);
|
|
586
|
-
}
|
|
587
|
-
const q = query(collectionClause(firestore, path), whereClause(name, "==", rebaseToFirestoreModel(value, firestore)));
|
|
588
|
-
const entity = await getDocs(q);
|
|
589
|
-
return entity.docs.filter(doc => doc.id !== id).length === 0;
|
|
590
|
-
|
|
591
|
-
}, [firebaseApp]);
|
|
592
|
-
|
|
593
|
-
const count = useCallback(async ({
|
|
594
|
-
path,
|
|
595
|
-
filter,
|
|
596
|
-
order,
|
|
597
|
-
orderBy,
|
|
598
|
-
collection
|
|
599
|
-
}: FetchCollectionProps<any>): Promise<number> => {
|
|
600
|
-
if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
|
|
601
|
-
const databaseId = collection?.databaseId;
|
|
602
|
-
const resolvedPath = path;
|
|
603
|
-
const query = buildQuery(resolvedPath, filter, orderBy, order, undefined, undefined, databaseId);
|
|
604
|
-
const entity = await getCountFromServer(query);
|
|
605
|
-
return entity.data().count;
|
|
606
|
-
}, [firebaseApp]);
|
|
607
|
-
|
|
608
|
-
const isFilterCombinationValid = useCallback(({
|
|
609
|
-
path,
|
|
610
|
-
collection,
|
|
611
|
-
filterValues,
|
|
612
|
-
sortBy
|
|
613
|
-
}: {
|
|
614
|
-
path: string,
|
|
615
|
-
collection: CollectionConfig<any>,
|
|
616
|
-
filterValues: FilterValues<any>,
|
|
617
|
-
sortBy?: [string, "asc" | "desc"],
|
|
618
|
-
}): boolean => {
|
|
619
|
-
|
|
620
|
-
if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
|
|
621
|
-
|
|
622
|
-
// If no indexes are defined, we assume the query is valid.
|
|
623
|
-
// If there is no index in Firestore, and error message will be shown
|
|
624
|
-
if (firestoreIndexesBuilder === undefined) return true;
|
|
625
|
-
const resolvedPath = path;
|
|
626
|
-
|
|
627
|
-
const indexes = firestoreIndexesBuilder?.({
|
|
628
|
-
path: resolvedPath,
|
|
629
|
-
collection
|
|
630
|
-
});
|
|
631
|
-
|
|
632
|
-
const sortKey = sortBy ? sortBy[0] : undefined;
|
|
633
|
-
const sortDirection = sortBy ? sortBy[1] : undefined;
|
|
634
|
-
|
|
635
|
-
// Order by clause cannot contain a field with an equality filter
|
|
636
|
-
const values: [WhereFilterOp, any][] = Object.values(filterValues) as [WhereFilterOp, any][];
|
|
637
|
-
|
|
638
|
-
const filterKeys = Object.keys(filterValues);
|
|
639
|
-
const filtersCount = filterKeys.length;
|
|
640
|
-
|
|
641
|
-
if (!sortKey && values.every((v) => v[0] === "==")) {
|
|
642
|
-
return true;
|
|
643
|
-
}
|
|
644
|
-
|
|
645
|
-
if (filtersCount === 1 && (!sortKey || sortKey === filterKeys[0])) {
|
|
646
|
-
return true;
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
if (!indexes && filtersCount > 1) {
|
|
650
|
-
return false;
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
return !!indexes && indexes
|
|
654
|
-
.filter((compositeIndex) => !sortKey || sortKey in compositeIndex)
|
|
655
|
-
.find((compositeIndex) =>
|
|
656
|
-
Object.entries(filterValues).every(([key, value]) => compositeIndex[key] !== undefined && (!sortDirection || compositeIndex[key] === sortDirection))
|
|
657
|
-
) !== undefined;
|
|
658
|
-
}, [firebaseApp]);
|
|
659
|
-
|
|
660
|
-
return useMemo(() => ({
|
|
661
|
-
key: "firestore" as const,
|
|
662
|
-
currentTime,
|
|
663
|
-
initialised: Boolean(firebaseApp),
|
|
664
|
-
initTextSearch,
|
|
665
|
-
fetchCollection,
|
|
666
|
-
listenCollection,
|
|
667
|
-
fetchOne,
|
|
668
|
-
listenOne,
|
|
669
|
-
save,
|
|
670
|
-
delete: deleteOne,
|
|
671
|
-
checkUniqueField,
|
|
672
|
-
count,
|
|
673
|
-
isFilterCombinationValid
|
|
674
|
-
}), [
|
|
675
|
-
firebaseApp,
|
|
676
|
-
initTextSearch,
|
|
677
|
-
fetchCollection,
|
|
678
|
-
listenCollection,
|
|
679
|
-
fetchOne,
|
|
680
|
-
listenOne,
|
|
681
|
-
save,
|
|
682
|
-
deleteOne,
|
|
683
|
-
checkUniqueField,
|
|
684
|
-
count,
|
|
685
|
-
isFilterCombinationValid
|
|
686
|
-
]);
|
|
687
|
-
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
const createRowFromDocument = (
|
|
691
|
-
docSnap: DocumentSnapshot
|
|
692
|
-
): Record<string, unknown> => {
|
|
693
|
-
const values = firestoreToRebaseModel(docSnap.data()) as Record<string, unknown>;
|
|
694
|
-
return {
|
|
695
|
-
...values,
|
|
696
|
-
// Spread the canonical document id last so it wins over a literal `id` field
|
|
697
|
-
id: docSnap.id
|
|
698
|
-
};
|
|
699
|
-
};
|
|
700
|
-
|
|
701
|
-
/**
|
|
702
|
-
* Recursive function that converts Firestore data types into CMS or plain
|
|
703
|
-
* JS types.
|
|
704
|
-
* Rebase uses Javascript dates internally instead of Firestore timestamps.
|
|
705
|
-
* This makes it easier to interact with the rest of the libraries and
|
|
706
|
-
* bindings.
|
|
707
|
-
* Also, Firestore references are replaced with {@link EntityReference}
|
|
708
|
-
* @param data
|
|
709
|
-
* @group Firestore
|
|
710
|
-
*/
|
|
711
|
-
export function firestoreToRebaseModel(data: unknown): unknown {
|
|
712
|
-
if (data === null || data === undefined) return null;
|
|
713
|
-
if (typeof data === "object" && data !== null && "isEqual" in data && typeof (data as FieldValue).isEqual === "function" && deleteField().isEqual(data as FieldValue)) {
|
|
714
|
-
return undefined;
|
|
715
|
-
}
|
|
716
|
-
if (typeof data === "object" && data !== null && "isEqual" in data && typeof (data as FieldValue).isEqual === "function" && serverTimestamp().isEqual(data as FieldValue)) {
|
|
717
|
-
return null;
|
|
718
|
-
}
|
|
719
|
-
if (data instanceof Timestamp || (typeof data === "object" && data !== null && "toDate" in data && typeof (data as Record<string, unknown>).toDate === "function" && ((data as Record<string, unknown>).toDate as () => unknown)() instanceof Date)) {
|
|
720
|
-
return (data as { toDate: () => Date }).toDate();
|
|
721
|
-
}
|
|
722
|
-
if (data instanceof Date) {
|
|
723
|
-
return data;
|
|
724
|
-
}
|
|
725
|
-
if (typeof data === "object" && "__type__" in data && data.__type__ === "__vector__") {
|
|
726
|
-
return data; // already translated
|
|
727
|
-
}
|
|
728
|
-
if (data instanceof VectorValue || (typeof data === "object" && data !== null && "toArray" in data && typeof (data as Record<string, unknown>).toArray === "function" && (data as { constructor?: { name?: string } }).constructor?.name === "VectorValue")) {
|
|
729
|
-
return { __type__: "__vector__",
|
|
730
|
-
value: (data as { toArray: () => number[] }).toArray() };
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
if (data instanceof FirestoreGeoPoint) {
|
|
734
|
-
return new GeoPoint(data.latitude, data.longitude);
|
|
735
|
-
}
|
|
736
|
-
if (data instanceof DocumentReference) {
|
|
737
|
-
const databaseId = (data?.firestore as unknown as { _databaseId?: { database?: string } })?._databaseId?.database;
|
|
738
|
-
return new EntityReference({ id: data.id,
|
|
739
|
-
path: getCMSPathFromFirestorePath(data.path),
|
|
740
|
-
databaseId });
|
|
741
|
-
}
|
|
742
|
-
if (Array.isArray(data)) {
|
|
743
|
-
return data.map(firestoreToRebaseModel).filter(v => v !== undefined);
|
|
744
|
-
}
|
|
745
|
-
if (typeof data === "object") {
|
|
746
|
-
const result: Record<string, unknown> = {};
|
|
747
|
-
for (const key of Object.keys(data)) {
|
|
748
|
-
const childValue = firestoreToRebaseModel((data as Record<string, unknown>)[key]);
|
|
749
|
-
if (childValue !== undefined)
|
|
750
|
-
result[key] = childValue;
|
|
751
|
-
}
|
|
752
|
-
return result;
|
|
753
|
-
}
|
|
754
|
-
return data;
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
/**
|
|
758
|
-
* Remove id from Firestore path
|
|
759
|
-
* @param fsPath
|
|
760
|
-
*/
|
|
761
|
-
function getCMSPathFromFirestorePath(fsPath: string): string {
|
|
762
|
-
let to = fsPath.lastIndexOf("/");
|
|
763
|
-
to = to === -1 ? fsPath.length : to;
|
|
764
|
-
return fsPath.substring(0, to);
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
export function rebaseToFirestoreModel(data: unknown, firestore: Firestore, inArray = false): unknown {
|
|
769
|
-
if (data === undefined) {
|
|
770
|
-
return deleteField();
|
|
771
|
-
} else if (data === null) {
|
|
772
|
-
return null;
|
|
773
|
-
} else if (Array.isArray(data)) {
|
|
774
|
-
return (data as unknown[]).filter(v => v !== undefined).map(v => rebaseToFirestoreModel(v, firestore, true));
|
|
775
|
-
} else if (typeof data === "object" && data !== null && "isEntityReference" in data && typeof (data as Record<string, unknown>).isEntityReference === "function" && (data as { isEntityReference: () => boolean }).isEntityReference()) {
|
|
776
|
-
const entityRef = data as EntityReference;
|
|
777
|
-
const targetFirestore = entityRef.databaseId ? getFirestore(firestore.app, entityRef.databaseId) : firestore;
|
|
778
|
-
return doc(targetFirestore, entityRef.path, entityRef.id);
|
|
779
|
-
} else if (data && typeof data === "object" && "__type" in data && (data as Record<string, unknown>).__type === "relation" && "path" in data && "id" in data) {
|
|
780
|
-
const rel = data as { path: string; id: string | number };
|
|
781
|
-
return doc(firestore, rel.path, String(rel.id));
|
|
782
|
-
} else if (data instanceof GeoPoint) {
|
|
783
|
-
return new FirestoreGeoPoint(data.latitude, data.longitude);
|
|
784
|
-
} else if (data instanceof Date) {
|
|
785
|
-
return Timestamp.fromDate(data);
|
|
786
|
-
} else if (data && typeof data === "object" && "__type__" in data && (data as Record<string, unknown>).__type__ === "__vector__") {
|
|
787
|
-
return vector((data as { value?: number[] }).value || []);
|
|
788
|
-
} else if (data && typeof data === "object") {
|
|
789
|
-
return Object.entries(data)
|
|
790
|
-
.map(([key, v]) => {
|
|
791
|
-
const firestoreModel = rebaseToFirestoreModel(v, firestore);
|
|
792
|
-
if (firestoreModel !== undefined)
|
|
793
|
-
return ({ [key]: firestoreModel });
|
|
794
|
-
else
|
|
795
|
-
return {};
|
|
796
|
-
})
|
|
797
|
-
.reduce((a, b) => ({ ...a,
|
|
798
|
-
...b }), {});
|
|
799
|
-
}
|
|
800
|
-
return data;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
function currentTime(): unknown {
|
|
804
|
-
return serverTimestamp();
|
|
805
|
-
}
|
|
806
|
-
|
|
807
|
-
function buildTextSearchControllerWithLocalSearch({
|
|
808
|
-
textSearchControllerBuilder,
|
|
809
|
-
firebaseApp,
|
|
810
|
-
localTextSearchEnabled
|
|
811
|
-
}: {
|
|
812
|
-
textSearchControllerBuilder?: FirestoreTextSearchControllerBuilder,
|
|
813
|
-
firebaseApp: FirebaseApp,
|
|
814
|
-
localTextSearchEnabled: boolean
|
|
815
|
-
}): FirestoreTextSearchController | undefined {
|
|
816
|
-
if (!textSearchControllerBuilder && localTextSearchEnabled) {
|
|
817
|
-
console.debug("Using local search only");
|
|
818
|
-
return localSearchControllerBuilder({ firebaseApp });
|
|
819
|
-
}
|
|
820
|
-
if (!localTextSearchEnabled && textSearchControllerBuilder) {
|
|
821
|
-
console.debug("Using external text search only");
|
|
822
|
-
return textSearchControllerBuilder({ firebaseApp });
|
|
823
|
-
}
|
|
824
|
-
if (!textSearchControllerBuilder && !localTextSearchEnabled) {
|
|
825
|
-
return undefined;
|
|
826
|
-
}
|
|
827
|
-
|
|
828
|
-
const localSearchController = localSearchControllerBuilder({ firebaseApp })
|
|
829
|
-
const textSearchController = textSearchControllerBuilder!({ firebaseApp });
|
|
830
|
-
return {
|
|
831
|
-
init: async (props: {
|
|
832
|
-
path: string,
|
|
833
|
-
databaseId?: string,
|
|
834
|
-
collection?: CollectionConfig
|
|
835
|
-
}) => {
|
|
836
|
-
const b = await textSearchController.init(props);
|
|
837
|
-
if (b) {
|
|
838
|
-
console.debug("External Text search controller supports path", props.path);
|
|
839
|
-
return true;
|
|
840
|
-
}
|
|
841
|
-
if (localTextSearchEnabled)
|
|
842
|
-
return localSearchController.init(props);
|
|
843
|
-
return false;
|
|
844
|
-
},
|
|
845
|
-
search: async (props: {
|
|
846
|
-
searchString: string,
|
|
847
|
-
path: string,
|
|
848
|
-
currentUser?: User,
|
|
849
|
-
databaseId?: string
|
|
850
|
-
}) => {
|
|
851
|
-
const search = await textSearchController.search(props);
|
|
852
|
-
return search ?? await localSearchController.search(props);
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
}
|