@rebasepro/client-firebase 0.6.1 → 0.8.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.
@@ -31,7 +31,7 @@ import {
31
31
  import type { FieldValue } from "@firebase/firestore";
32
32
  import { FirebaseApp } from "@firebase/app";
33
33
  import { FirestoreTextSearchController, FirestoreTextSearchControllerBuilder } from "../types/text_search";
34
- import { useCallback, useEffect, useRef } from "react";
34
+ import { useCallback, useEffect, useMemo, useRef } from "react";
35
35
  import { localSearchControllerBuilder } from "../utils";
36
36
  import { getAuth } from "@firebase/auth";
37
37
 
@@ -252,46 +252,86 @@ export function useFirestoreDriver({
252
252
 
253
253
  }, [firebaseApp, listenEntity]);
254
254
 
255
- return {
256
- key: "firestore",
255
+ const initTextSearch = useCallback(async (props: {
256
+ path: string,
257
+ databaseId?: string,
258
+ collection?: EntityCollection
259
+ }) => {
260
+ console.debug("Init text search controller", searchControllerRef.current, props.path);
261
+ if (!searchControllerRef.current) {
262
+ 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.");
263
+ return false;
264
+ }
265
+ try {
266
+ return searchControllerRef.current.init(props);
267
+ } catch (e) {
268
+ console.error("Error initializing text search controller", e);
269
+ return false;
270
+ }
271
+ }, []);
257
272
 
258
- currentTime,
273
+ /**
274
+ * Fetch entities in a Firestore path
275
+ * @param path
276
+ * @param collection
277
+ * @param filter
278
+ * @param limit
279
+ * @param startAfter
280
+ * @param searchString
281
+ * @param orderBy
282
+ * @param order
283
+ * @return Function to cancel subscription
284
+ * @see useCollectionFetch if you need this functionality implemented as a hook
285
+ * @group Firestore
286
+ */
287
+ const fetchCollection = useCallback(async <M extends Record<string, any>>({
288
+ path,
289
+ filter,
290
+ limit,
291
+ startAfter,
292
+ searchString,
293
+ orderBy,
294
+ order,
295
+ collection
296
+ }: FetchCollectionProps<M>
297
+ ): Promise<Entity<M>[]> => {
259
298
 
260
- initialised: Boolean(firebaseApp),
299
+ const databaseId = collection?.databaseId;
261
300
 
262
- initTextSearch: useCallback(async (props: {
263
- path: string,
264
- databaseId?: string,
265
- collection?: EntityCollection
266
- }) => {
267
- console.debug("Init text search controller", searchControllerRef.current, props.path);
268
- if (!searchControllerRef.current) {
269
- 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.");
270
- return false;
271
- }
272
- try {
273
- return searchControllerRef.current.init(props);
274
- } catch (e) {
275
- console.error("Error initializing text search controller", e);
276
- return false;
277
- }
278
- }, []),
279
-
280
- /**
281
- * Fetch entities in a Firestore path
282
- * @param path
283
- * @param collection
284
- * @param filter
285
- * @param limit
286
- * @param startAfter
287
- * @param searchString
288
- * @param orderBy
289
- * @param order
290
- * @return Function to cancel subscription
291
- * @see useCollectionFetch if you need this functionality implemented as a hook
292
- * @group Firestore
293
- */
294
- fetchCollection: useCallback(async <M extends Record<string, any>>({
301
+ const resolvedPath = path;
302
+
303
+ console.debug("Fetching collection", {
304
+ path,
305
+ limit,
306
+ filter,
307
+ startAfter,
308
+ orderBy,
309
+ order
310
+ });
311
+ const query = buildQuery(resolvedPath, filter, orderBy, order, startAfter as unknown[] | undefined, limit, databaseId);
312
+
313
+ const snapshot = await getDocs(query);
314
+ return snapshot.docs.map((doc) => createEntityFromDocument(doc, databaseId));
315
+ }, [buildQuery]);
316
+
317
+ /**
318
+ * Listen to a entities in a given path
319
+ * @param path
320
+ * @param collection
321
+ * @param onError
322
+ * @param filter
323
+ * @param limit
324
+ * @param startAfter
325
+ * @param searchString
326
+ * @param orderBy
327
+ * @param order
328
+ * @param onUpdate
329
+ * @return Function to cancel subscription
330
+ * @see useCollectionFetch if you need this functionality implemented as a hook
331
+ * @group Firestore
332
+ */
333
+ const listenCollection = useCallback(<M extends Record<string, any>>(
334
+ {
295
335
  path,
296
336
  filter,
297
337
  limit,
@@ -299,320 +339,287 @@ export function useFirestoreDriver({
299
339
  searchString,
300
340
  orderBy,
301
341
  order,
342
+ onUpdate,
343
+ onError,
344
+ collection
345
+ }: ListenCollectionProps<M>
346
+ ): () => void => {
347
+
348
+ console.debug("Listening collection", {
349
+ path,
350
+ searchString,
351
+ limit,
352
+ filter,
353
+ startAfter,
354
+ orderBy,
355
+ order,
302
356
  collection
303
- }: FetchCollectionProps<M>
304
- ): Promise<Entity<M>[]> => {
357
+ });
305
358
 
306
- const databaseId = collection?.databaseId;
359
+ if (!firebaseApp) {
360
+ throw Error("useFirestoreDriver Firebase not initialised");
361
+ }
307
362
 
308
- const resolvedPath = path;
363
+ const databaseId = collection?.databaseId;
309
364
 
310
- console.debug("Fetching collection", {
365
+ if (searchString) {
366
+ return performTextSearch<M>({
311
367
  path,
312
- limit,
313
- filter,
314
- startAfter,
315
- orderBy,
316
- order
317
- });
318
- const query = buildQuery(resolvedPath, filter, orderBy, order, startAfter as unknown[] | undefined, limit, databaseId);
319
-
320
- const snapshot = await getDocs(query);
321
- return snapshot.docs.map((doc) => createEntityFromDocument(doc, databaseId));
322
- }, [buildQuery]),
323
-
324
- /**
325
- * Listen to a entities in a given path
326
- * @param path
327
- * @param collection
328
- * @param onError
329
- * @param filter
330
- * @param limit
331
- * @param startAfter
332
- * @param searchString
333
- * @param orderBy
334
- * @param order
335
- * @param onUpdate
336
- * @return Function to cancel subscription
337
- * @see useCollectionFetch if you need this functionality implemented as a hook
338
- * @group Firestore
339
- */
340
- listenCollection: useCallback(<M extends Record<string, any>>(
341
- {
342
- path,
343
- filter,
344
- limit,
345
- startAfter,
346
368
  searchString,
347
- orderBy,
348
- order,
349
369
  onUpdate,
350
- onError,
351
- collection
352
- }: ListenCollectionProps<M>
353
- ): () => void => {
354
-
355
- console.debug("Listening collection", {
356
- path,
357
- searchString,
358
- limit,
359
- filter,
360
- startAfter,
361
- orderBy,
362
- order,
363
- collection
370
+ databaseId
364
371
  });
372
+ }
365
373
 
366
- if (!firebaseApp) {
367
- throw Error("useFirestoreDriver Firebase not initialised");
374
+ const resolvedPath = path;
375
+ console.debug("Resolved path for listening", {
376
+ path,
377
+ resolvedPath
378
+ });
379
+ const query = buildQuery(resolvedPath, filter, orderBy, order, startAfter as unknown[] | undefined, limit, databaseId);
380
+ return onSnapshot(query,
381
+ {
382
+ next: (snapshot) => {
383
+ if (!searchString)
384
+ onUpdate(snapshot.docs.map((doc) => createEntityFromDocument(doc, databaseId)));
385
+ },
386
+ error: onError
368
387
  }
388
+ );
369
389
 
370
- const databaseId = collection?.databaseId;
390
+ }, [buildQuery, firebaseApp, performTextSearch]);
371
391
 
372
- if (searchString) {
373
- return performTextSearch<M>({
374
- path,
375
- searchString,
376
- onUpdate,
377
- databaseId
378
- });
379
- }
392
+ /**
393
+ * Retrieve an entity given a path and a collection
394
+ * @param path
395
+ * @param entityId
396
+ * @param collection
397
+ * @group Firestore
398
+ */
399
+ const fetchEntity = useCallback(<M extends Record<string, any>>({
400
+ path,
401
+ entityId,
402
+ collection
403
+ }: FetchEntityProps<M>
404
+ ): Promise<Entity<M> | undefined> => {
405
+ const resolvedPath = path;
406
+ return getAndBuildEntity(resolvedPath, entityId, collection?.databaseId);
407
+ }, [getAndBuildEntity]);
380
408
 
381
- const resolvedPath = path;
382
- console.debug("Resolved path for listening", {
383
- path,
384
- resolvedPath
385
- });
386
- const query = buildQuery(resolvedPath, filter, orderBy, order, startAfter as unknown[] | undefined, limit, databaseId);
387
- return onSnapshot(query,
388
- {
389
- next: (snapshot) => {
390
- if (!searchString)
391
- onUpdate(snapshot.docs.map((doc) => createEntityFromDocument(doc, databaseId)));
392
- },
393
- error: onError
394
- }
395
- );
396
-
397
- }, [buildQuery, firebaseApp, performTextSearch]),
398
-
399
- /**
400
- * Retrieve an entity given a path and a collection
401
- * @param path
402
- * @param entityId
403
- * @param collection
404
- * @group Firestore
405
- */
406
- fetchEntity: useCallback(<M extends Record<string, any>>({
409
+ /**
410
+ * Save entity to the specified path. Note that Firestore does not allow
411
+ * undefined values.
412
+ * @param path
413
+ * @param entityId
414
+ * @param values
415
+ * @param schemaId
416
+ * @param collection
417
+ * @param status
418
+ * @group Firestore
419
+ */
420
+ const saveEntity = useCallback(<M extends Record<string, any>>(
421
+ {
407
422
  path,
408
423
  entityId,
409
- collection
410
- }: FetchEntityProps<M>
411
- ): Promise<Entity<M> | undefined> => {
412
- const resolvedPath = path;
413
- return getAndBuildEntity(resolvedPath, entityId, collection?.databaseId);
414
- }, [getAndBuildEntity]),
415
-
416
- /**
417
- *
418
- * @param path
419
- * @param entityId
420
- * @param collection
421
- * @param onUpdate
422
- * @param onError
423
- * @return Function to cancel subscription
424
- * @group Firestore
425
- */
426
- listenEntity,
424
+ values: valuesProp,
425
+ collection,
426
+ status
427
+ }: SaveEntityProps<M>): Promise<Entity<M>> => {
427
428
 
428
- /**
429
- * Save entity to the specified path. Note that Firestore does not allow
430
- * undefined values.
431
- * @param path
432
- * @param entityId
433
- * @param values
434
- * @param schemaId
435
- * @param collection
436
- * @param status
437
- * @group Firestore
438
- */
439
- saveEntity: useCallback(<M extends Record<string, any>>(
440
- {
441
- path,
442
- entityId,
443
- values: valuesProp,
444
- collection,
445
- status
446
- }: SaveEntityProps<M>): Promise<Entity<M>> => {
429
+ if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
447
430
 
448
- if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
431
+ console.debug("1", {
432
+ path,
433
+ entityId,
434
+ values: valuesProp,
435
+ collection
436
+ })
437
+ const values = cmsToFirestoreModel(valuesProp, getFirestore(firebaseApp));
449
438
 
450
- console.debug("1", {
451
- path,
452
- entityId,
453
- values: valuesProp,
454
- collection
455
- })
456
- const values = cmsToFirestoreModel(valuesProp, getFirestore(firebaseApp));
439
+ console.debug("2", {
440
+ path,
441
+ entityId,
442
+ values: valuesProp,
443
+ collection
444
+ })
445
+ const databaseId = collection?.databaseId;
446
+ const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
447
+ const resolvedPath = path;
457
448
 
458
- console.debug("2", {
459
- path,
460
- entityId,
461
- values: valuesProp,
462
- collection
449
+ const collectionReference: CollectionReference = collectionClause(firestore, path);
450
+ console.debug("Saving entity", {
451
+ path,
452
+ entityId,
453
+ values,
454
+ databaseId
455
+ });
456
+
457
+ let documentReference: DocumentReference;
458
+ if (entityId) {
459
+ documentReference = doc(collectionReference, String(entityId));
460
+ } else {
461
+ documentReference = doc(collectionReference);
462
+ }
463
+ return setDoc(documentReference, values as Record<string, unknown>, { merge: true })
464
+ .then(() => {
465
+ return {
466
+ id: documentReference.id,
467
+ path,
468
+ values: firestoreToCMSModel(values)
469
+ } as Entity<M>;
463
470
  })
464
- const databaseId = collection?.databaseId;
465
- const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
466
- const resolvedPath = path;
471
+ .catch((error) => {
472
+ console.error("Error saving entity", error);
473
+ throw error;
467
474
 
468
- const collectionReference: CollectionReference = collectionClause(firestore, path);
469
- console.debug("Saving entity", {
470
- path,
471
- entityId,
472
- values,
473
- databaseId
474
475
  });
476
+ }, [firebaseApp]);
475
477
 
476
- let documentReference: DocumentReference;
477
- if (entityId) {
478
- documentReference = doc(collectionReference, String(entityId));
479
- } else {
480
- documentReference = doc(collectionReference);
481
- }
482
- return setDoc(documentReference, values as Record<string, unknown>, { merge: true })
483
- .then(() => {
484
- return {
485
- id: documentReference.id,
486
- path,
487
- values: firestoreToCMSModel(values)
488
- } as Entity<M>;
489
- })
490
- .catch((error) => {
491
- console.error("Error saving entity", error);
492
- throw error;
478
+ /**
479
+ * Delete an entity
480
+ * @param entity
481
+ * @param collection
482
+ * @group Firestore
483
+ */
484
+ const deleteEntity = useCallback(<M extends Record<string, any>>(
485
+ {
486
+ entity
487
+ }: DeleteEntityProps<M>
488
+ ): Promise<void> => {
489
+ if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
493
490
 
494
- });
495
- }, [firebaseApp]),
496
-
497
- /**
498
- * Delete an entity
499
- * @param entity
500
- * @param collection
501
- * @group Firestore
502
- */
503
- deleteEntity: useCallback(<M extends Record<string, any>>(
504
- {
505
- entity
506
- }: DeleteEntityProps<M>
507
- ): Promise<void> => {
508
- if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
509
-
510
- const databaseId = entity.databaseId;
511
- const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
512
-
513
- return deleteDoc(doc(firestore, entity.path, String(entity.id)));
514
- }, [firebaseApp]),
515
-
516
- /**
517
- * Check if the given property is unique in the given collection
518
- * @param path Collection path
519
- * @param name of the property
520
- * @param value
521
- * @param property
522
- * @param entityId
523
- * @return `true` if there are no other fields besides the given entity
524
- * @group Firestore
525
- */
526
- checkUniqueField: useCallback(async (
527
- path: string,
528
- name: string,
529
- value: unknown,
530
- entityId?: string | number,
531
- collection?: EntityCollection<any>
532
- ): Promise<boolean> => {
491
+ const databaseId = entity.databaseId;
492
+ const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
533
493
 
534
- if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
494
+ return deleteDoc(doc(firestore, entity.path, String(entity.id)));
495
+ }, [firebaseApp]);
535
496
 
536
- const databaseId = collection?.databaseId;
537
- const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
497
+ /**
498
+ * Check if the given property is unique in the given collection
499
+ * @param path Collection path
500
+ * @param name of the property
501
+ * @param value
502
+ * @param property
503
+ * @param entityId
504
+ * @return `true` if there are no other fields besides the given entity
505
+ * @group Firestore
506
+ */
507
+ const checkUniqueField = useCallback(async (
508
+ path: string,
509
+ name: string,
510
+ value: unknown,
511
+ entityId?: string | number,
512
+ collection?: EntityCollection<any>
513
+ ): Promise<boolean> => {
538
514
 
539
- if (value === undefined || value === null) {
540
- return Promise.resolve(true);
541
- }
542
- const q = query(collectionClause(firestore, path), whereClause(name, "==", cmsToFirestoreModel(value, firestore)));
543
- const snapshot = await getDocs(q);
544
- return snapshot.docs.filter(doc => doc.id !== entityId).length === 0;
515
+ if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
545
516
 
546
- }, [firebaseApp]),
517
+ const databaseId = collection?.databaseId;
518
+ const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
547
519
 
548
- countEntities: useCallback(async ({
549
- path,
550
- filter,
551
- order,
552
- orderBy,
553
- collection
554
- }: FetchCollectionProps<any>): Promise<number> => {
555
- if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
556
- const databaseId = collection?.databaseId;
557
- const resolvedPath = path;
558
- const query = buildQuery(resolvedPath, filter, orderBy, order, undefined, undefined, databaseId);
559
- const snapshot = await getCountFromServer(query);
560
- return snapshot.data().count;
561
- }, [firebaseApp]),
562
-
563
- isFilterCombinationValid: useCallback(({
564
- path,
565
- collection,
566
- filterValues,
567
- sortBy
568
- }: {
569
- path: string,
570
- collection: EntityCollection<any>,
571
- filterValues: FilterValues<any>,
572
- sortBy?: [string, "asc" | "desc"],
573
- }): boolean => {
520
+ if (value === undefined || value === null) {
521
+ return Promise.resolve(true);
522
+ }
523
+ const q = query(collectionClause(firestore, path), whereClause(name, "==", cmsToFirestoreModel(value, firestore)));
524
+ const snapshot = await getDocs(q);
525
+ return snapshot.docs.filter(doc => doc.id !== entityId).length === 0;
526
+
527
+ }, [firebaseApp]);
574
528
 
575
- if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
529
+ const countEntities = useCallback(async ({
530
+ path,
531
+ filter,
532
+ order,
533
+ orderBy,
534
+ collection
535
+ }: FetchCollectionProps<any>): Promise<number> => {
536
+ if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
537
+ const databaseId = collection?.databaseId;
538
+ const resolvedPath = path;
539
+ const query = buildQuery(resolvedPath, filter, orderBy, order, undefined, undefined, databaseId);
540
+ const snapshot = await getCountFromServer(query);
541
+ return snapshot.data().count;
542
+ }, [firebaseApp]);
576
543
 
577
- // If no indexes are defined, we assume the query is valid.
578
- // If there is no index in Firestore, and error message will be shown
579
- if (firestoreIndexesBuilder === undefined) return true;
580
- const resolvedPath = path;
544
+ const isFilterCombinationValid = useCallback(({
545
+ path,
546
+ collection,
547
+ filterValues,
548
+ sortBy
549
+ }: {
550
+ path: string,
551
+ collection: EntityCollection<any>,
552
+ filterValues: FilterValues<any>,
553
+ sortBy?: [string, "asc" | "desc"],
554
+ }): boolean => {
581
555
 
582
- const indexes = firestoreIndexesBuilder?.({
583
- path: resolvedPath,
584
- collection
585
- });
556
+ if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
586
557
 
587
- const sortKey = sortBy ? sortBy[0] : undefined;
588
- const sortDirection = sortBy ? sortBy[1] : undefined;
558
+ // If no indexes are defined, we assume the query is valid.
559
+ // If there is no index in Firestore, and error message will be shown
560
+ if (firestoreIndexesBuilder === undefined) return true;
561
+ const resolvedPath = path;
589
562
 
590
- // Order by clause cannot contain a field with an equality filter
591
- const values: [WhereFilterOp, any][] = Object.values(filterValues) as [WhereFilterOp, any][];
563
+ const indexes = firestoreIndexesBuilder?.({
564
+ path: resolvedPath,
565
+ collection
566
+ });
592
567
 
593
- const filterKeys = Object.keys(filterValues);
594
- const filtersCount = filterKeys.length;
568
+ const sortKey = sortBy ? sortBy[0] : undefined;
569
+ const sortDirection = sortBy ? sortBy[1] : undefined;
595
570
 
596
- if (!sortKey && values.every((v) => v[0] === "==")) {
597
- return true;
598
- }
571
+ // Order by clause cannot contain a field with an equality filter
572
+ const values: [WhereFilterOp, any][] = Object.values(filterValues) as [WhereFilterOp, any][];
599
573
 
600
- if (filtersCount === 1 && (!sortKey || sortKey === filterKeys[0])) {
601
- return true;
602
- }
574
+ const filterKeys = Object.keys(filterValues);
575
+ const filtersCount = filterKeys.length;
603
576
 
604
- if (!indexes && filtersCount > 1) {
605
- return false;
606
- }
577
+ if (!sortKey && values.every((v) => v[0] === "==")) {
578
+ return true;
579
+ }
580
+
581
+ if (filtersCount === 1 && (!sortKey || sortKey === filterKeys[0])) {
582
+ return true;
583
+ }
607
584
 
608
- return !!indexes && indexes
609
- .filter((compositeIndex) => !sortKey || sortKey in compositeIndex)
610
- .find((compositeIndex) =>
611
- Object.entries(filterValues).every(([key, value]) => compositeIndex[key] !== undefined && (!sortDirection || compositeIndex[key] === sortDirection))
612
- ) !== undefined;
613
- }, [firebaseApp])
585
+ if (!indexes && filtersCount > 1) {
586
+ return false;
587
+ }
614
588
 
615
- };
589
+ return !!indexes && indexes
590
+ .filter((compositeIndex) => !sortKey || sortKey in compositeIndex)
591
+ .find((compositeIndex) =>
592
+ Object.entries(filterValues).every(([key, value]) => compositeIndex[key] !== undefined && (!sortDirection || compositeIndex[key] === sortDirection))
593
+ ) !== undefined;
594
+ }, [firebaseApp]);
595
+
596
+ return useMemo(() => ({
597
+ key: "firestore" as const,
598
+ currentTime,
599
+ initialised: Boolean(firebaseApp),
600
+ initTextSearch,
601
+ fetchCollection,
602
+ listenCollection,
603
+ fetchEntity,
604
+ listenEntity,
605
+ saveEntity,
606
+ deleteEntity,
607
+ checkUniqueField,
608
+ countEntities,
609
+ isFilterCombinationValid
610
+ }), [
611
+ firebaseApp,
612
+ initTextSearch,
613
+ fetchCollection,
614
+ listenCollection,
615
+ fetchEntity,
616
+ listenEntity,
617
+ saveEntity,
618
+ deleteEntity,
619
+ checkUniqueField,
620
+ countEntities,
621
+ isFilterCombinationValid
622
+ ]);
616
623
 
617
624
  }
618
625