@rebasepro/client-firebase 0.0.1-canary.09e5ec5

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.
Files changed (63) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -0
  3. package/dist/components/FirebaseLoginView.d.ts +72 -0
  4. package/dist/components/RebaseFirebaseApp.d.ts +19 -0
  5. package/dist/components/RebaseFirebaseAppProps.d.ts +144 -0
  6. package/dist/components/index.d.ts +3 -0
  7. package/dist/components/social_icons.d.ts +6 -0
  8. package/dist/hooks/index.d.ts +8 -0
  9. package/dist/hooks/useAppCheck.d.ts +20 -0
  10. package/dist/hooks/useBuildUserManagement.d.ts +46 -0
  11. package/dist/hooks/useFirebaseAuthController.d.ts +15 -0
  12. package/dist/hooks/useFirebaseRealTimeDBDelegate.d.ts +5 -0
  13. package/dist/hooks/useFirebaseStorageSource.d.ts +14 -0
  14. package/dist/hooks/useFirestoreDriver.d.ts +56 -0
  15. package/dist/hooks/useInitialiseFirebase.d.ts +34 -0
  16. package/dist/hooks/useRecaptcha.d.ts +8 -0
  17. package/dist/index.d.ts +4 -0
  18. package/dist/index.es.js +3060 -0
  19. package/dist/index.es.js.map +1 -0
  20. package/dist/index.umd.js +3043 -0
  21. package/dist/index.umd.js.map +1 -0
  22. package/dist/social_icons.d.ts +6 -0
  23. package/dist/types/appcheck.d.ts +10 -0
  24. package/dist/types/auth.d.ts +41 -0
  25. package/dist/types/index.d.ts +3 -0
  26. package/dist/types/text_search.d.ts +39 -0
  27. package/dist/utils/algolia.d.ts +9 -0
  28. package/dist/utils/collections_firestore.d.ts +5 -0
  29. package/dist/utils/database.d.ts +2 -0
  30. package/dist/utils/index.d.ts +7 -0
  31. package/dist/utils/local_text_search_controller.d.ts +2 -0
  32. package/dist/utils/pinecone.d.ts +24 -0
  33. package/dist/utils/rebase_search_controller.d.ts +73 -0
  34. package/dist/utils/text_search_controller.d.ts +13 -0
  35. package/package.json +63 -0
  36. package/src/components/FirebaseLoginView.tsx +693 -0
  37. package/src/components/RebaseFirebaseApp.tsx +291 -0
  38. package/src/components/RebaseFirebaseAppProps.tsx +180 -0
  39. package/src/components/index.ts +3 -0
  40. package/src/components/social_icons.tsx +135 -0
  41. package/src/hooks/index.ts +8 -0
  42. package/src/hooks/useAppCheck.ts +101 -0
  43. package/src/hooks/useBuildUserManagement.tsx +374 -0
  44. package/src/hooks/useFirebaseAuthController.ts +334 -0
  45. package/src/hooks/useFirebaseRealTimeDBDelegate.ts +269 -0
  46. package/src/hooks/useFirebaseStorageSource.ts +207 -0
  47. package/src/hooks/useFirestoreDriver.ts +784 -0
  48. package/src/hooks/useInitialiseFirebase.ts +132 -0
  49. package/src/hooks/useRecaptcha.tsx +28 -0
  50. package/src/index.ts +4 -0
  51. package/src/social_icons.tsx +135 -0
  52. package/src/types/appcheck.ts +11 -0
  53. package/src/types/auth.tsx +74 -0
  54. package/src/types/index.ts +3 -0
  55. package/src/types/text_search.ts +42 -0
  56. package/src/utils/algolia.ts +27 -0
  57. package/src/utils/collections_firestore.ts +148 -0
  58. package/src/utils/database.ts +39 -0
  59. package/src/utils/index.ts +7 -0
  60. package/src/utils/local_text_search_controller.ts +143 -0
  61. package/src/utils/pinecone.ts +75 -0
  62. package/src/utils/rebase_search_controller.ts +357 -0
  63. package/src/utils/text_search_controller.ts +34 -0
@@ -0,0 +1,784 @@
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__",
660
+ value: data.toArray() };
661
+ }
662
+
663
+ if (data instanceof FirestoreGeoPoint) {
664
+ return new GeoPoint(data.latitude, data.longitude);
665
+ }
666
+ if (data instanceof DocumentReference) {
667
+ // @ts-ignore
668
+ const databaseId = data?.firestore?._databaseId?.database;
669
+ return new EntityReference({ id: data.id,
670
+ path: getCMSPathFromFirestorePath(data.path),
671
+ databaseId });
672
+ }
673
+ if (Array.isArray(data)) {
674
+ return data.map(firestoreToCMSModel).filter(v => v !== undefined);
675
+ }
676
+ if (typeof data === "object") {
677
+ const result: Record<string, any> = {};
678
+ for (const key of Object.keys(data)) {
679
+ const childValue = firestoreToCMSModel(data[key]);
680
+ if (childValue !== undefined)
681
+ result[key] = childValue;
682
+ }
683
+ return result;
684
+ }
685
+ return data;
686
+ }
687
+
688
+ /**
689
+ * Remove id from Firestore path
690
+ * @param fsPath
691
+ */
692
+ function getCMSPathFromFirestorePath(fsPath: string): string {
693
+ let to = fsPath.lastIndexOf("/");
694
+ to = to === -1 ? fsPath.length : to;
695
+ return fsPath.substring(0, to);
696
+ }
697
+
698
+
699
+ export function cmsToFirestoreModel(data: any, firestore: Firestore, inArray = false): any {
700
+ if (data === undefined) {
701
+ return deleteField();
702
+ } else if (data === null) {
703
+ return null;
704
+ } else if (Array.isArray(data)) {
705
+ return data.filter(v => v !== undefined).map(v => cmsToFirestoreModel(v, firestore, true));
706
+ } else if (data.isEntityReference && data.isEntityReference()) {
707
+ const targetFirestore = data.databaseId ? getFirestore(firestore.app, data.databaseId) : firestore;
708
+ return doc(targetFirestore, data.path, data.id);
709
+ } else if (data && typeof data === "object" && data.__type === "relation" && data.path && data.id) {
710
+ return doc(firestore, data.path, String(data.id));
711
+ } else if (data instanceof GeoPoint) {
712
+ return new FirestoreGeoPoint(data.latitude, data.longitude);
713
+ } else if (data instanceof Date) {
714
+ return Timestamp.fromDate(data);
715
+ } else if (data && typeof data === "object" && "__type__" in data && data.__type__ === "__vector__") {
716
+ return vector(data.value || []);
717
+ } else if (data && typeof data === "object") {
718
+ return Object.entries(data)
719
+ .map(([key, v]) => {
720
+ const firestoreModel = cmsToFirestoreModel(v, firestore);
721
+ if (firestoreModel !== undefined)
722
+ return ({ [key]: firestoreModel });
723
+ else
724
+ return {};
725
+ })
726
+ .reduce((a, b) => ({ ...a,
727
+ ...b }), {});
728
+ }
729
+ return data;
730
+ }
731
+
732
+ function currentTime(): any {
733
+ return serverTimestamp();
734
+ }
735
+
736
+ function buildTextSearchControllerWithLocalSearch({
737
+ textSearchControllerBuilder,
738
+ firebaseApp,
739
+ localTextSearchEnabled
740
+ }: {
741
+ textSearchControllerBuilder?: FirestoreTextSearchControllerBuilder,
742
+ firebaseApp: FirebaseApp,
743
+ localTextSearchEnabled: boolean
744
+ }): FirestoreTextSearchController | undefined {
745
+ if (!textSearchControllerBuilder && localTextSearchEnabled) {
746
+ console.debug("Using local search only");
747
+ return localSearchControllerBuilder({ firebaseApp });
748
+ }
749
+ if (!localTextSearchEnabled && textSearchControllerBuilder) {
750
+ console.debug("Using external text search only");
751
+ return textSearchControllerBuilder({ firebaseApp });
752
+ }
753
+ if (!textSearchControllerBuilder && !localTextSearchEnabled) {
754
+ return undefined;
755
+ }
756
+
757
+ const localSearchController = localSearchControllerBuilder({ firebaseApp })
758
+ const textSearchController = textSearchControllerBuilder!({ firebaseApp });
759
+ return {
760
+ init: async (props: {
761
+ path: string,
762
+ databaseId?: string,
763
+ collection?: EntityCollection
764
+ }) => {
765
+ const b = await textSearchController.init(props);
766
+ if (b) {
767
+ console.debug("External Text search controller supports path", props.path);
768
+ return true;
769
+ }
770
+ if (localTextSearchEnabled)
771
+ return localSearchController.init(props);
772
+ return false;
773
+ },
774
+ search: async (props: {
775
+ searchString: string,
776
+ path: string,
777
+ currentUser?: any,
778
+ databaseId?: string
779
+ }) => {
780
+ const search = await textSearchController.search(props);
781
+ return search ?? await localSearchController.search(props);
782
+ }
783
+ }
784
+ }