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