@rebasepro/firebase 0.17.3 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
- }