@rebasepro/client-firebase 0.0.1-canary.4d4fb3e
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 +21 -0
- package/README.md +4 -0
- package/dist/components/FirebaseLoginView.d.ts +72 -0
- package/dist/components/RebaseFirebaseApp.d.ts +19 -0
- package/dist/components/RebaseFirebaseAppProps.d.ts +144 -0
- package/dist/components/index.d.ts +3 -0
- package/dist/components/social_icons.d.ts +6 -0
- package/dist/hooks/index.d.ts +7 -0
- package/dist/hooks/useAppCheck.d.ts +20 -0
- package/dist/hooks/useFirebaseAuthController.d.ts +15 -0
- package/dist/hooks/useFirebaseRealTimeDBDelegate.d.ts +5 -0
- package/dist/hooks/useFirebaseStorageSource.d.ts +14 -0
- package/dist/hooks/useFirestoreDriver.d.ts +56 -0
- package/dist/hooks/useInitialiseFirebase.d.ts +34 -0
- package/dist/hooks/useRecaptcha.d.ts +8 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.es.js +2757 -0
- package/dist/index.es.js.map +1 -0
- package/dist/index.umd.js +2743 -0
- package/dist/index.umd.js.map +1 -0
- package/dist/social_icons.d.ts +6 -0
- package/dist/types/appcheck.d.ts +10 -0
- package/dist/types/auth.d.ts +41 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/text_search.d.ts +39 -0
- package/dist/utils/algolia.d.ts +9 -0
- package/dist/utils/collections_firestore.d.ts +5 -0
- package/dist/utils/database.d.ts +2 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/local_text_search_controller.d.ts +2 -0
- package/dist/utils/pinecone.d.ts +24 -0
- package/dist/utils/rebase_search_controller.d.ts +73 -0
- package/dist/utils/text_search_controller.d.ts +13 -0
- package/package.json +61 -0
- package/src/components/FirebaseLoginView.tsx +703 -0
- package/src/components/RebaseFirebaseApp.tsx +275 -0
- package/src/components/RebaseFirebaseAppProps.tsx +180 -0
- package/src/components/index.ts +3 -0
- package/src/components/social_icons.tsx +135 -0
- package/src/hooks/index.ts +7 -0
- package/src/hooks/useAppCheck.ts +101 -0
- package/src/hooks/useFirebaseAuthController.ts +334 -0
- package/src/hooks/useFirebaseRealTimeDBDelegate.ts +269 -0
- package/src/hooks/useFirebaseStorageSource.ts +208 -0
- package/src/hooks/useFirestoreDriver.ts +778 -0
- package/src/hooks/useInitialiseFirebase.ts +132 -0
- package/src/hooks/useRecaptcha.tsx +28 -0
- package/src/index.ts +4 -0
- package/src/social_icons.tsx +135 -0
- package/src/types/appcheck.ts +11 -0
- package/src/types/auth.tsx +74 -0
- package/src/types/index.ts +3 -0
- package/src/types/text_search.ts +42 -0
- package/src/utils/algolia.ts +27 -0
- package/src/utils/collections_firestore.ts +149 -0
- package/src/utils/database.ts +39 -0
- package/src/utils/index.ts +7 -0
- package/src/utils/local_text_search_controller.ts +143 -0
- package/src/utils/pinecone.ts +75 -0
- package/src/utils/rebase_search_controller.ts +356 -0
- package/src/utils/text_search_controller.ts +34 -0
|
@@ -0,0 +1,778 @@
|
|
|
1
|
+
import { DataDriver, DeleteEntityProps, Entity, EntityCollection, EntityReference, FetchCollectionProps, FetchEntityProps, FilterCombination, FilterValues, GeoPoint, ListenCollectionProps, ListenEntityProps, SaveEntityProps, WhereFilterOp } from "@rebasepro/types";
|
|
2
|
+
import {
|
|
3
|
+
collection as collectionClause,
|
|
4
|
+
CollectionReference,
|
|
5
|
+
deleteDoc,
|
|
6
|
+
deleteField,
|
|
7
|
+
doc,
|
|
8
|
+
DocumentReference,
|
|
9
|
+
DocumentSnapshot,
|
|
10
|
+
Firestore,
|
|
11
|
+
GeoPoint as FirestoreGeoPoint,
|
|
12
|
+
getCountFromServer,
|
|
13
|
+
getDoc,
|
|
14
|
+
getDocs,
|
|
15
|
+
getFirestore,
|
|
16
|
+
limit as limitClause,
|
|
17
|
+
onSnapshot,
|
|
18
|
+
orderBy as orderByClause,
|
|
19
|
+
Query,
|
|
20
|
+
query,
|
|
21
|
+
QueryConstraint,
|
|
22
|
+
serverTimestamp,
|
|
23
|
+
setDoc,
|
|
24
|
+
startAfter as startAfterClause,
|
|
25
|
+
Timestamp,
|
|
26
|
+
VectorValue,
|
|
27
|
+
vector,
|
|
28
|
+
where as whereClause
|
|
29
|
+
} from "@firebase/firestore";
|
|
30
|
+
import { FirebaseApp } from "@firebase/app";
|
|
31
|
+
import { FirestoreTextSearchController, FirestoreTextSearchControllerBuilder } from "../types/text_search";
|
|
32
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
33
|
+
import { localSearchControllerBuilder } from "../utils";
|
|
34
|
+
import { getAuth } from "@firebase/auth";
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* @group Firebase
|
|
38
|
+
*/
|
|
39
|
+
export interface FirestoreDataDriverProps {
|
|
40
|
+
firebaseApp?: FirebaseApp,
|
|
41
|
+
/**
|
|
42
|
+
* You can use this controller to return a list of ids from a search index, given a
|
|
43
|
+
* `path` and a `searchString`.
|
|
44
|
+
*/
|
|
45
|
+
textSearchControllerBuilder?: FirestoreTextSearchControllerBuilder,
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Fallback to local text search if no text search controller is specified,
|
|
49
|
+
* or if the controller does not support the given path.
|
|
50
|
+
*/
|
|
51
|
+
localTextSearchEnabled?: boolean,
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Use this builder to indicate which indexes are available in your
|
|
55
|
+
* Firestore database. This is used to allow filtering and sorting
|
|
56
|
+
* for multiple fields in the CMS.
|
|
57
|
+
*/
|
|
58
|
+
firestoreIndexesBuilder?: FirestoreIndexesBuilder;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type FirestoreIndexesBuilder = (params: {
|
|
62
|
+
path: string,
|
|
63
|
+
collection: EntityCollection<any>,
|
|
64
|
+
}) => FilterCombination<string>[] | undefined
|
|
65
|
+
|
|
66
|
+
export type FirestoreDataDriver = DataDriver & {
|
|
67
|
+
|
|
68
|
+
initTextSearch: (props: {
|
|
69
|
+
path: string,
|
|
70
|
+
databaseId?: string,
|
|
71
|
+
collection?: EntityCollection
|
|
72
|
+
}) => Promise<boolean>,
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Use this hook to build a {@link DataDriver} based on Firestore
|
|
77
|
+
* @param firebaseApp
|
|
78
|
+
* @param textSearchControllerBuilder
|
|
79
|
+
* @group Firebase
|
|
80
|
+
*/
|
|
81
|
+
export function useFirestoreDriver({
|
|
82
|
+
firebaseApp,
|
|
83
|
+
textSearchControllerBuilder,
|
|
84
|
+
firestoreIndexesBuilder,
|
|
85
|
+
localTextSearchEnabled
|
|
86
|
+
}: FirestoreDataDriverProps): FirestoreDataDriver {
|
|
87
|
+
|
|
88
|
+
const searchControllerRef = useRef<FirestoreTextSearchController>(undefined);
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (!searchControllerRef.current && firebaseApp) {
|
|
92
|
+
if ((textSearchControllerBuilder || localTextSearchEnabled) && !searchControllerRef.current) {
|
|
93
|
+
searchControllerRef.current = buildTextSearchControllerWithLocalSearch({
|
|
94
|
+
firebaseApp,
|
|
95
|
+
textSearchControllerBuilder,
|
|
96
|
+
localTextSearchEnabled: localTextSearchEnabled ?? false
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}, [firebaseApp, localTextSearchEnabled, textSearchControllerBuilder]);
|
|
101
|
+
|
|
102
|
+
const buildQuery = useCallback(<M>(path: string,
|
|
103
|
+
filter: FilterValues<Extract<keyof M, string>> | undefined,
|
|
104
|
+
orderBy: string | undefined,
|
|
105
|
+
order: "desc" | "asc" | undefined,
|
|
106
|
+
startAfter: unknown[] | undefined,
|
|
107
|
+
limit: number | undefined,
|
|
108
|
+
databaseId?: string) => {
|
|
109
|
+
|
|
110
|
+
if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
|
|
111
|
+
|
|
112
|
+
const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
|
|
113
|
+
const collectionReference: Query = collectionClause(firestore, path);
|
|
114
|
+
|
|
115
|
+
const queryParams: QueryConstraint[] = [];
|
|
116
|
+
if (filter) {
|
|
117
|
+
Object.entries(filter)
|
|
118
|
+
.filter(([_, entry]) => !!entry)
|
|
119
|
+
.forEach(([key, filterParameter]) => {
|
|
120
|
+
const [op, value] = filterParameter as [WhereFilterOp, any];
|
|
121
|
+
queryParams.push(whereClause(key, op, cmsToFirestoreModel(value, firestore)));
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (orderBy && order) {
|
|
126
|
+
queryParams.push(orderByClause(orderBy, order));
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (startAfter) {
|
|
130
|
+
queryParams.push(startAfterClause(startAfter));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (limit) {
|
|
134
|
+
queryParams.push(limitClause(limit));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return query(collectionReference, ...queryParams);
|
|
138
|
+
}, [firebaseApp]);
|
|
139
|
+
|
|
140
|
+
const getAndBuildEntity = useCallback(<M extends Record<string, any>>(path: string,
|
|
141
|
+
entityId: string | number,
|
|
142
|
+
databaseId?: string
|
|
143
|
+
): Promise<Entity<M> | undefined> => {
|
|
144
|
+
if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
|
|
145
|
+
|
|
146
|
+
const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
|
|
147
|
+
|
|
148
|
+
return getDoc(doc(firestore, path, String(entityId)))
|
|
149
|
+
.then((docSnapshot) => {
|
|
150
|
+
if (!docSnapshot.exists()) {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
return createEntityFromDocument(docSnapshot, databaseId);
|
|
154
|
+
});
|
|
155
|
+
}, [firebaseApp]);
|
|
156
|
+
|
|
157
|
+
const listenEntity = useCallback(<M extends Record<string, any>>(
|
|
158
|
+
{
|
|
159
|
+
path,
|
|
160
|
+
entityId,
|
|
161
|
+
collection,
|
|
162
|
+
onUpdate,
|
|
163
|
+
onError,
|
|
164
|
+
}: ListenEntityProps<M>): () => void => {
|
|
165
|
+
if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
|
|
166
|
+
|
|
167
|
+
const databaseId = collection?.databaseId;
|
|
168
|
+
const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
|
|
169
|
+
const resolvedPath = path;
|
|
170
|
+
|
|
171
|
+
return onSnapshot(
|
|
172
|
+
doc(firestore, resolvedPath, String(entityId)),
|
|
173
|
+
{
|
|
174
|
+
next: (docSnapshot) => {
|
|
175
|
+
onUpdate(createEntityFromDocument(docSnapshot, databaseId));
|
|
176
|
+
},
|
|
177
|
+
error: onError
|
|
178
|
+
}
|
|
179
|
+
);
|
|
180
|
+
}, [firebaseApp]);
|
|
181
|
+
|
|
182
|
+
const performTextSearch = useCallback(<M extends Record<string, any>>({
|
|
183
|
+
path,
|
|
184
|
+
databaseId,
|
|
185
|
+
searchString,
|
|
186
|
+
onUpdate,
|
|
187
|
+
}: {
|
|
188
|
+
path: string,
|
|
189
|
+
databaseId?: string,
|
|
190
|
+
searchString: string;
|
|
191
|
+
onUpdate: (entities: Entity<M>[]) => void,
|
|
192
|
+
}): () => void => {
|
|
193
|
+
|
|
194
|
+
if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
|
|
195
|
+
|
|
196
|
+
const textSearchController = searchControllerRef.current;
|
|
197
|
+
if (!textSearchController)
|
|
198
|
+
throw Error("Trying to make text search without specifying a FirestoreTextSearchController");
|
|
199
|
+
|
|
200
|
+
let subscriptions: (() => void)[] = [];
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
const auth = getAuth(firebaseApp);
|
|
204
|
+
const currentUser = auth?.currentUser;
|
|
205
|
+
|
|
206
|
+
const search = textSearchController.search({
|
|
207
|
+
path,
|
|
208
|
+
searchString,
|
|
209
|
+
currentUser: currentUser ?? undefined,
|
|
210
|
+
databaseId
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
if (!search) {
|
|
214
|
+
throw Error("The current path is not supported by the specified FirestoreTextSearchController");
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
search.then((ids) => {
|
|
218
|
+
if (!ids || ids.length === 0) {
|
|
219
|
+
subscriptions = [];
|
|
220
|
+
onUpdate([]);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const entities: Entity<M>[] = [];
|
|
224
|
+
const addedEntitiesSet = new Set<string | number>();
|
|
225
|
+
subscriptions = (ids ?? [])
|
|
226
|
+
.map((entityId) => {
|
|
227
|
+
return listenEntity({
|
|
228
|
+
path,
|
|
229
|
+
entityId,
|
|
230
|
+
onUpdate: (entity: Entity<any> | null) => {
|
|
231
|
+
if (entity?.values) {
|
|
232
|
+
if (entity.id && !addedEntitiesSet.has(entity.id)) {
|
|
233
|
+
addedEntitiesSet.add(entity.id);
|
|
234
|
+
entities.push(entity);
|
|
235
|
+
onUpdate(entities);
|
|
236
|
+
}
|
|
237
|
+
} else if (entity?.id) {
|
|
238
|
+
addedEntitiesSet.delete(entity.id);
|
|
239
|
+
onUpdate([...entities.filter(e => e.id !== entityId)])
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
})
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
return () => {
|
|
248
|
+
subscriptions.forEach((p) => p());
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
}, [firebaseApp, listenEntity]);
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
key: "firestore",
|
|
255
|
+
|
|
256
|
+
currentTime,
|
|
257
|
+
|
|
258
|
+
initialised: Boolean(firebaseApp),
|
|
259
|
+
|
|
260
|
+
initTextSearch: useCallback(async (props: {
|
|
261
|
+
path: string,
|
|
262
|
+
databaseId?: string,
|
|
263
|
+
collection?: EntityCollection
|
|
264
|
+
}) => {
|
|
265
|
+
console.debug("Init text search controller", searchControllerRef.current, props.path);
|
|
266
|
+
if (!searchControllerRef.current) {
|
|
267
|
+
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.");
|
|
268
|
+
return false;
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
return searchControllerRef.current.init(props);
|
|
272
|
+
} catch (e) {
|
|
273
|
+
console.error("Error initializing text search controller", e);
|
|
274
|
+
return false;
|
|
275
|
+
}
|
|
276
|
+
}, []),
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Fetch entities in a Firestore path
|
|
280
|
+
* @param path
|
|
281
|
+
* @param collection
|
|
282
|
+
* @param filter
|
|
283
|
+
* @param limit
|
|
284
|
+
* @param startAfter
|
|
285
|
+
* @param searchString
|
|
286
|
+
* @param orderBy
|
|
287
|
+
* @param order
|
|
288
|
+
* @return Function to cancel subscription
|
|
289
|
+
* @see useCollectionFetch if you need this functionality implemented as a hook
|
|
290
|
+
* @group Firestore
|
|
291
|
+
*/
|
|
292
|
+
fetchCollection: useCallback(async <M extends Record<string, any>>({
|
|
293
|
+
path,
|
|
294
|
+
filter,
|
|
295
|
+
limit,
|
|
296
|
+
startAfter,
|
|
297
|
+
searchString,
|
|
298
|
+
orderBy,
|
|
299
|
+
order,
|
|
300
|
+
collection,
|
|
301
|
+
}: FetchCollectionProps<M>
|
|
302
|
+
): Promise<Entity<M>[]> => {
|
|
303
|
+
|
|
304
|
+
const databaseId = collection?.databaseId;
|
|
305
|
+
|
|
306
|
+
const resolvedPath = path;
|
|
307
|
+
|
|
308
|
+
console.debug("Fetching collection", {
|
|
309
|
+
path,
|
|
310
|
+
limit,
|
|
311
|
+
filter,
|
|
312
|
+
startAfter,
|
|
313
|
+
orderBy,
|
|
314
|
+
order
|
|
315
|
+
});
|
|
316
|
+
const query = buildQuery(resolvedPath, filter, orderBy, order, startAfter as unknown[] | undefined, limit, databaseId);
|
|
317
|
+
|
|
318
|
+
const snapshot = await getDocs(query);
|
|
319
|
+
return snapshot.docs.map((doc) => createEntityFromDocument(doc, databaseId));
|
|
320
|
+
}, [buildQuery]),
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Listen to a entities in a given path
|
|
324
|
+
* @param path
|
|
325
|
+
* @param collection
|
|
326
|
+
* @param onError
|
|
327
|
+
* @param filter
|
|
328
|
+
* @param limit
|
|
329
|
+
* @param startAfter
|
|
330
|
+
* @param searchString
|
|
331
|
+
* @param orderBy
|
|
332
|
+
* @param order
|
|
333
|
+
* @param onUpdate
|
|
334
|
+
* @return Function to cancel subscription
|
|
335
|
+
* @see useCollectionFetch if you need this functionality implemented as a hook
|
|
336
|
+
* @group Firestore
|
|
337
|
+
*/
|
|
338
|
+
listenCollection: useCallback(<M extends Record<string, any>>(
|
|
339
|
+
{
|
|
340
|
+
path,
|
|
341
|
+
filter,
|
|
342
|
+
limit,
|
|
343
|
+
startAfter,
|
|
344
|
+
searchString,
|
|
345
|
+
orderBy,
|
|
346
|
+
order,
|
|
347
|
+
onUpdate,
|
|
348
|
+
onError,
|
|
349
|
+
collection,
|
|
350
|
+
}: ListenCollectionProps<M>
|
|
351
|
+
): () => void => {
|
|
352
|
+
|
|
353
|
+
console.debug("Listening collection", {
|
|
354
|
+
path,
|
|
355
|
+
searchString,
|
|
356
|
+
limit,
|
|
357
|
+
filter,
|
|
358
|
+
startAfter,
|
|
359
|
+
orderBy,
|
|
360
|
+
order,
|
|
361
|
+
collection
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
if (!firebaseApp) {
|
|
365
|
+
throw Error("useFirestoreDriver Firebase not initialised");
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const databaseId = collection?.databaseId;
|
|
369
|
+
|
|
370
|
+
if (searchString) {
|
|
371
|
+
return performTextSearch<M>({
|
|
372
|
+
path,
|
|
373
|
+
searchString,
|
|
374
|
+
onUpdate,
|
|
375
|
+
databaseId,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const resolvedPath = path;
|
|
380
|
+
console.debug("Resolved path for listening", {
|
|
381
|
+
path,
|
|
382
|
+
resolvedPath
|
|
383
|
+
});
|
|
384
|
+
const query = buildQuery(resolvedPath, filter, orderBy, order, startAfter as unknown[] | undefined, limit, databaseId);
|
|
385
|
+
return onSnapshot(query,
|
|
386
|
+
{
|
|
387
|
+
next: (snapshot) => {
|
|
388
|
+
if (!searchString)
|
|
389
|
+
onUpdate(snapshot.docs.map((doc) => createEntityFromDocument(doc, databaseId)));
|
|
390
|
+
},
|
|
391
|
+
error: onError
|
|
392
|
+
}
|
|
393
|
+
);
|
|
394
|
+
|
|
395
|
+
}, [buildQuery, firebaseApp, performTextSearch]),
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Retrieve an entity given a path and a collection
|
|
399
|
+
* @param path
|
|
400
|
+
* @param entityId
|
|
401
|
+
* @param collection
|
|
402
|
+
* @group Firestore
|
|
403
|
+
*/
|
|
404
|
+
fetchEntity: useCallback(<M extends Record<string, any>>({
|
|
405
|
+
path,
|
|
406
|
+
entityId,
|
|
407
|
+
collection,
|
|
408
|
+
}: FetchEntityProps<M>
|
|
409
|
+
): Promise<Entity<M> | undefined> => {
|
|
410
|
+
const resolvedPath = path;
|
|
411
|
+
return getAndBuildEntity(resolvedPath, entityId, collection?.databaseId);
|
|
412
|
+
}, [getAndBuildEntity]),
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
*
|
|
416
|
+
* @param path
|
|
417
|
+
* @param entityId
|
|
418
|
+
* @param collection
|
|
419
|
+
* @param onUpdate
|
|
420
|
+
* @param onError
|
|
421
|
+
* @return Function to cancel subscription
|
|
422
|
+
* @group Firestore
|
|
423
|
+
*/
|
|
424
|
+
listenEntity,
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Save entity to the specified path. Note that Firestore does not allow
|
|
428
|
+
* undefined values.
|
|
429
|
+
* @param path
|
|
430
|
+
* @param entityId
|
|
431
|
+
* @param values
|
|
432
|
+
* @param schemaId
|
|
433
|
+
* @param collection
|
|
434
|
+
* @param status
|
|
435
|
+
* @group Firestore
|
|
436
|
+
*/
|
|
437
|
+
saveEntity: useCallback(<M extends Record<string, any>>(
|
|
438
|
+
{
|
|
439
|
+
path,
|
|
440
|
+
entityId,
|
|
441
|
+
values: valuesProp,
|
|
442
|
+
collection,
|
|
443
|
+
status,
|
|
444
|
+
}: SaveEntityProps<M>): Promise<Entity<M>> => {
|
|
445
|
+
|
|
446
|
+
if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
|
|
447
|
+
|
|
448
|
+
console.debug("1", {
|
|
449
|
+
path,
|
|
450
|
+
entityId,
|
|
451
|
+
values: valuesProp,
|
|
452
|
+
collection
|
|
453
|
+
})
|
|
454
|
+
const values = cmsToFirestoreModel(valuesProp, getFirestore(firebaseApp));
|
|
455
|
+
|
|
456
|
+
console.debug("2", {
|
|
457
|
+
path,
|
|
458
|
+
entityId,
|
|
459
|
+
values: valuesProp,
|
|
460
|
+
collection
|
|
461
|
+
})
|
|
462
|
+
const databaseId = collection?.databaseId;
|
|
463
|
+
const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
|
|
464
|
+
const resolvedPath = path;
|
|
465
|
+
|
|
466
|
+
const collectionReference: CollectionReference = collectionClause(firestore, path);
|
|
467
|
+
console.debug("Saving entity", {
|
|
468
|
+
path,
|
|
469
|
+
entityId,
|
|
470
|
+
values,
|
|
471
|
+
databaseId
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
let documentReference: DocumentReference;
|
|
475
|
+
if (entityId) {
|
|
476
|
+
documentReference = doc(collectionReference, String(entityId));
|
|
477
|
+
} else {
|
|
478
|
+
documentReference = doc(collectionReference);
|
|
479
|
+
}
|
|
480
|
+
return setDoc(documentReference, values, { merge: true })
|
|
481
|
+
.then(() => {
|
|
482
|
+
return {
|
|
483
|
+
id: documentReference.id,
|
|
484
|
+
path,
|
|
485
|
+
values: firestoreToCMSModel(values),
|
|
486
|
+
} as Entity<M>;
|
|
487
|
+
})
|
|
488
|
+
.catch((error) => {
|
|
489
|
+
console.error("Error saving entity", error);
|
|
490
|
+
throw error;
|
|
491
|
+
|
|
492
|
+
});
|
|
493
|
+
}, [firebaseApp]),
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Delete an entity
|
|
497
|
+
* @param entity
|
|
498
|
+
* @param collection
|
|
499
|
+
* @group Firestore
|
|
500
|
+
*/
|
|
501
|
+
deleteEntity: useCallback(<M extends Record<string, any>>(
|
|
502
|
+
{
|
|
503
|
+
entity
|
|
504
|
+
}: DeleteEntityProps<M>
|
|
505
|
+
): Promise<void> => {
|
|
506
|
+
if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
|
|
507
|
+
|
|
508
|
+
const databaseId = entity.databaseId;
|
|
509
|
+
const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
|
|
510
|
+
|
|
511
|
+
return deleteDoc(doc(firestore, entity.path, String(entity.id)));
|
|
512
|
+
}, [firebaseApp]),
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* Check if the given property is unique in the given collection
|
|
516
|
+
* @param path Collection path
|
|
517
|
+
* @param name of the property
|
|
518
|
+
* @param value
|
|
519
|
+
* @param property
|
|
520
|
+
* @param entityId
|
|
521
|
+
* @return `true` if there are no other fields besides the given entity
|
|
522
|
+
* @group Firestore
|
|
523
|
+
*/
|
|
524
|
+
checkUniqueField: useCallback(async (
|
|
525
|
+
path: string,
|
|
526
|
+
name: string,
|
|
527
|
+
value: any,
|
|
528
|
+
entityId?: string | number,
|
|
529
|
+
collection?: EntityCollection<any>
|
|
530
|
+
): Promise<boolean> => {
|
|
531
|
+
|
|
532
|
+
if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
|
|
533
|
+
|
|
534
|
+
const databaseId = collection?.databaseId;
|
|
535
|
+
const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
|
|
536
|
+
|
|
537
|
+
if (value === undefined || value === null) {
|
|
538
|
+
return Promise.resolve(true);
|
|
539
|
+
}
|
|
540
|
+
const q = query(collectionClause(firestore, path), whereClause(name, "==", cmsToFirestoreModel(value, firestore)));
|
|
541
|
+
const snapshot = await getDocs(q);
|
|
542
|
+
return snapshot.docs.filter(doc => doc.id !== entityId).length === 0;
|
|
543
|
+
|
|
544
|
+
}, [firebaseApp]),
|
|
545
|
+
|
|
546
|
+
countEntities: useCallback(async ({
|
|
547
|
+
path,
|
|
548
|
+
filter,
|
|
549
|
+
order,
|
|
550
|
+
orderBy,
|
|
551
|
+
collection,
|
|
552
|
+
}: FetchCollectionProps<any>): Promise<number> => {
|
|
553
|
+
if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
|
|
554
|
+
const databaseId = collection?.databaseId;
|
|
555
|
+
const resolvedPath = path;
|
|
556
|
+
const query = buildQuery(resolvedPath, filter, orderBy, order, undefined, undefined, databaseId);
|
|
557
|
+
const snapshot = await getCountFromServer(query);
|
|
558
|
+
return snapshot.data().count;
|
|
559
|
+
}, [firebaseApp]),
|
|
560
|
+
|
|
561
|
+
isFilterCombinationValid: useCallback(({
|
|
562
|
+
path,
|
|
563
|
+
collection,
|
|
564
|
+
filterValues,
|
|
565
|
+
sortBy,
|
|
566
|
+
}: {
|
|
567
|
+
path: string,
|
|
568
|
+
collection: EntityCollection<any>,
|
|
569
|
+
filterValues: FilterValues<any>,
|
|
570
|
+
sortBy?: [string, "asc" | "desc"],
|
|
571
|
+
}): boolean => {
|
|
572
|
+
|
|
573
|
+
if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
|
|
574
|
+
|
|
575
|
+
// If no indexes are defined, we assume the query is valid.
|
|
576
|
+
// If there is no index in Firestore, and error message will be shown
|
|
577
|
+
if (firestoreIndexesBuilder === undefined) return true;
|
|
578
|
+
const resolvedPath = path;
|
|
579
|
+
|
|
580
|
+
const indexes = firestoreIndexesBuilder?.({
|
|
581
|
+
path: resolvedPath,
|
|
582
|
+
collection
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
const sortKey = sortBy ? sortBy[0] : undefined;
|
|
586
|
+
const sortDirection = sortBy ? sortBy[1] : undefined;
|
|
587
|
+
|
|
588
|
+
// Order by clause cannot contain a field with an equality filter
|
|
589
|
+
const values: [WhereFilterOp, any][] = Object.values(filterValues) as [WhereFilterOp, any][];
|
|
590
|
+
|
|
591
|
+
const filterKeys = Object.keys(filterValues);
|
|
592
|
+
const filtersCount = filterKeys.length;
|
|
593
|
+
|
|
594
|
+
if (!sortKey && values.every((v) => v[0] === "==")) {
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (filtersCount === 1 && (!sortKey || sortKey === filterKeys[0])) {
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
if (!indexes && filtersCount > 1) {
|
|
603
|
+
return false;
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return !!indexes && indexes
|
|
607
|
+
.filter((compositeIndex) => !sortKey || sortKey in compositeIndex)
|
|
608
|
+
.find((compositeIndex) =>
|
|
609
|
+
Object.entries(filterValues).every(([key, value]) => compositeIndex[key] !== undefined && (!sortDirection || compositeIndex[key] === sortDirection))
|
|
610
|
+
) !== undefined;
|
|
611
|
+
}, [firebaseApp])
|
|
612
|
+
|
|
613
|
+
};
|
|
614
|
+
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
const createEntityFromDocument = <M extends Record<string, any>>(
|
|
618
|
+
docSnap: DocumentSnapshot,
|
|
619
|
+
databaseId?: string
|
|
620
|
+
): Entity<M> => {
|
|
621
|
+
const values = firestoreToCMSModel(docSnap.data());
|
|
622
|
+
const path = getCMSPathFromFirestorePath(docSnap.ref.path);
|
|
623
|
+
return {
|
|
624
|
+
id: docSnap.id,
|
|
625
|
+
path: path,
|
|
626
|
+
values,
|
|
627
|
+
databaseId
|
|
628
|
+
};
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Recursive function that converts Firestore data types into CMS or plain
|
|
633
|
+
* JS types.
|
|
634
|
+
* Rebase uses Javascript dates internally instead of Firestore timestamps.
|
|
635
|
+
* This makes it easier to interact with the rest of the libraries and
|
|
636
|
+
* bindings.
|
|
637
|
+
* Also, Firestore references are replaced with {@link EntityReference}
|
|
638
|
+
* @param data
|
|
639
|
+
* @group Firestore
|
|
640
|
+
*/
|
|
641
|
+
export function firestoreToCMSModel(data: any): any {
|
|
642
|
+
if (data === null || data === undefined) return null;
|
|
643
|
+
if (deleteField().isEqual(data)) {
|
|
644
|
+
return undefined;
|
|
645
|
+
}
|
|
646
|
+
if (serverTimestamp().isEqual(data)) {
|
|
647
|
+
return null;
|
|
648
|
+
}
|
|
649
|
+
if (data instanceof Timestamp || (typeof data.toDate === "function" && data.toDate() instanceof Date)) {
|
|
650
|
+
return data.toDate();
|
|
651
|
+
}
|
|
652
|
+
if (data instanceof Date) {
|
|
653
|
+
return data;
|
|
654
|
+
}
|
|
655
|
+
if (typeof data === "object" && "__type__" in data && data.__type__ === "__vector__") {
|
|
656
|
+
return data; // already translated
|
|
657
|
+
}
|
|
658
|
+
if (data instanceof VectorValue || (typeof data === "object" && data !== null && typeof data.toArray === "function" && data.constructor?.name === "VectorValue")) {
|
|
659
|
+
return { __type__: "__vector__", value: data.toArray() };
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (data instanceof FirestoreGeoPoint) {
|
|
663
|
+
return new GeoPoint(data.latitude, data.longitude);
|
|
664
|
+
}
|
|
665
|
+
if (data instanceof DocumentReference) {
|
|
666
|
+
// @ts-ignore
|
|
667
|
+
const databaseId = data?.firestore?._databaseId?.database;
|
|
668
|
+
return new EntityReference({ id: data.id, path: getCMSPathFromFirestorePath(data.path), databaseId });
|
|
669
|
+
}
|
|
670
|
+
if (Array.isArray(data)) {
|
|
671
|
+
return data.map(firestoreToCMSModel).filter(v => v !== undefined);
|
|
672
|
+
}
|
|
673
|
+
if (typeof data === "object") {
|
|
674
|
+
const result: Record<string, any> = {};
|
|
675
|
+
for (const key of Object.keys(data)) {
|
|
676
|
+
const childValue = firestoreToCMSModel(data[key]);
|
|
677
|
+
if (childValue !== undefined)
|
|
678
|
+
result[key] = childValue;
|
|
679
|
+
}
|
|
680
|
+
return result;
|
|
681
|
+
}
|
|
682
|
+
return data;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Remove id from Firestore path
|
|
687
|
+
* @param fsPath
|
|
688
|
+
*/
|
|
689
|
+
function getCMSPathFromFirestorePath(fsPath: string): string {
|
|
690
|
+
let to = fsPath.lastIndexOf("/");
|
|
691
|
+
to = to === -1 ? fsPath.length : to;
|
|
692
|
+
return fsPath.substring(0, to);
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
|
|
696
|
+
export function cmsToFirestoreModel(data: any, firestore: Firestore, inArray = false): any {
|
|
697
|
+
if (data === undefined) {
|
|
698
|
+
return deleteField();
|
|
699
|
+
} else if (data === null) {
|
|
700
|
+
return null;
|
|
701
|
+
} else if (Array.isArray(data)) {
|
|
702
|
+
return data.filter(v => v !== undefined).map(v => cmsToFirestoreModel(v, firestore, true));
|
|
703
|
+
} else if (data.isEntityReference && data.isEntityReference()) {
|
|
704
|
+
const targetFirestore = data.databaseId ? getFirestore(firestore.app, data.databaseId) : firestore;
|
|
705
|
+
return doc(targetFirestore, data.path, data.id);
|
|
706
|
+
} else if (data instanceof GeoPoint) {
|
|
707
|
+
return new FirestoreGeoPoint(data.latitude, data.longitude);
|
|
708
|
+
} else if (data instanceof Date) {
|
|
709
|
+
return Timestamp.fromDate(data);
|
|
710
|
+
} else if (data && typeof data === "object" && "__type__" in data && data.__type__ === "__vector__") {
|
|
711
|
+
return vector(data.value || []);
|
|
712
|
+
} else if (data && typeof data === "object") {
|
|
713
|
+
return Object.entries(data)
|
|
714
|
+
.map(([key, v]) => {
|
|
715
|
+
const firestoreModel = cmsToFirestoreModel(v, firestore);
|
|
716
|
+
if (firestoreModel !== undefined)
|
|
717
|
+
return ({ [key]: firestoreModel });
|
|
718
|
+
else
|
|
719
|
+
return {};
|
|
720
|
+
})
|
|
721
|
+
.reduce((a, b) => ({ ...a, ...b }), {});
|
|
722
|
+
}
|
|
723
|
+
return data;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
function currentTime(): any {
|
|
727
|
+
return serverTimestamp();
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function buildTextSearchControllerWithLocalSearch({
|
|
731
|
+
textSearchControllerBuilder,
|
|
732
|
+
firebaseApp,
|
|
733
|
+
localTextSearchEnabled
|
|
734
|
+
}: {
|
|
735
|
+
textSearchControllerBuilder?: FirestoreTextSearchControllerBuilder,
|
|
736
|
+
firebaseApp: FirebaseApp,
|
|
737
|
+
localTextSearchEnabled: boolean
|
|
738
|
+
}): FirestoreTextSearchController | undefined {
|
|
739
|
+
if (!textSearchControllerBuilder && localTextSearchEnabled) {
|
|
740
|
+
console.debug("Using local search only");
|
|
741
|
+
return localSearchControllerBuilder({ firebaseApp });
|
|
742
|
+
}
|
|
743
|
+
if (!localTextSearchEnabled && textSearchControllerBuilder) {
|
|
744
|
+
console.debug("Using external text search only");
|
|
745
|
+
return textSearchControllerBuilder({ firebaseApp });
|
|
746
|
+
}
|
|
747
|
+
if (!textSearchControllerBuilder && !localTextSearchEnabled) {
|
|
748
|
+
return undefined;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
const localSearchController = localSearchControllerBuilder({ firebaseApp })
|
|
752
|
+
const textSearchController = textSearchControllerBuilder!({ firebaseApp });
|
|
753
|
+
return {
|
|
754
|
+
init: async (props: {
|
|
755
|
+
path: string,
|
|
756
|
+
databaseId?: string,
|
|
757
|
+
collection?: EntityCollection
|
|
758
|
+
}) => {
|
|
759
|
+
const b = await textSearchController.init(props);
|
|
760
|
+
if (b) {
|
|
761
|
+
console.debug("External Text search controller supports path", props.path);
|
|
762
|
+
return true;
|
|
763
|
+
}
|
|
764
|
+
if (localTextSearchEnabled)
|
|
765
|
+
return localSearchController.init(props);
|
|
766
|
+
return false;
|
|
767
|
+
},
|
|
768
|
+
search: async (props: {
|
|
769
|
+
searchString: string,
|
|
770
|
+
path: string,
|
|
771
|
+
currentUser?: any,
|
|
772
|
+
databaseId?: string
|
|
773
|
+
}) => {
|
|
774
|
+
const search = await textSearchController.search(props);
|
|
775
|
+
return search ?? await localSearchController.search(props);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
}
|