@rebasepro/client-firebase 0.7.0 → 0.9.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.
@@ -1,4 +1,4 @@
1
- import { DataDriver, DeleteEntityProps, Entity, EntityCollection, EntityReference, FetchCollectionProps, FetchEntityProps, FilterCombination, FilterValues, GeoPoint, ListenCollectionProps, ListenEntityProps, SaveEntityProps, WhereFilterOp } from "@rebasepro/types";
1
+ import { DataDriver, DeleteProps, CollectionConfig, EntityReference, FetchCollectionProps, FetchOneProps, FilterCombination, FilterValues, GeoPoint, ListenCollectionProps, ListenOneProps, SaveProps, WhereFilterOp } from "@rebasepro/types";
2
2
  import { User } from "@firebase/auth";
3
3
  import {
4
4
  collection as collectionClause,
@@ -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
 
@@ -62,7 +62,7 @@ export interface FirestoreDataDriverProps {
62
62
 
63
63
  export type FirestoreIndexesBuilder = (params: {
64
64
  path: string,
65
- collection: EntityCollection<any>,
65
+ collection: CollectionConfig<any>,
66
66
  }) => FilterCombination<string>[] | undefined
67
67
 
68
68
  export type FirestoreDataDriver = DataDriver & {
@@ -70,7 +70,7 @@ export type FirestoreDataDriver = DataDriver & {
70
70
  initTextSearch: (props: {
71
71
  path: string,
72
72
  databaseId?: string,
73
- collection?: EntityCollection
73
+ collection?: CollectionConfig
74
74
  }) => Promise<boolean>,
75
75
  }
76
76
 
@@ -120,6 +120,26 @@ export function useFirestoreDriver({
120
120
  .filter(([_, entry]) => !!entry)
121
121
  .forEach(([key, filterParameter]) => {
122
122
  const [op, value] = filterParameter as [WhereFilterOp, any];
123
+
124
+ // Null-testing operators map to Firestore's == / != against null.
125
+ if (op === "is-null") {
126
+ queryParams.push(whereClause(key, "==", null));
127
+ return;
128
+ }
129
+ if (op === "is-not-null") {
130
+ queryParams.push(whereClause(key, "!=", null));
131
+ return;
132
+ }
133
+
134
+ // Firestore has no LIKE/ILIKE — fail loudly rather than silently
135
+ // returning wrong results. Use `searchString` / a search index instead.
136
+ if (op === "like" || op === "ilike" || op === "not-like" || op === "not-ilike") {
137
+ throw new Error(
138
+ `Firestore does not support the "${op}" operator (SQL pattern matching). ` +
139
+ "Use a full-text search index or the collection's searchString instead."
140
+ );
141
+ }
142
+
123
143
  queryParams.push(whereClause(key, op, cmsToFirestoreModel(value, firestore)));
124
144
  });
125
145
  }
@@ -139,31 +159,31 @@ export function useFirestoreDriver({
139
159
  return query(collectionReference, ...queryParams);
140
160
  }, [firebaseApp]);
141
161
 
142
- const getAndBuildEntity = useCallback(<M extends Record<string, any>>(path: string,
143
- entityId: string | number,
162
+ const getAndBuildEntity = useCallback((path: string,
163
+ id: string | number,
144
164
  databaseId?: string
145
- ): Promise<Entity<M> | undefined> => {
165
+ ): Promise<Record<string, unknown> | undefined> => {
146
166
  if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
147
167
 
148
168
  const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
149
169
 
150
- return getDoc(doc(firestore, path, String(entityId)))
151
- .then((docSnapshot) => {
152
- if (!docSnapshot.exists()) {
170
+ return getDoc(doc(firestore, path, String(id)))
171
+ .then((docEntity) => {
172
+ if (!docEntity.exists()) {
153
173
  return undefined;
154
174
  }
155
- return createEntityFromDocument(docSnapshot, databaseId);
175
+ return createRowFromDocument(docEntity);
156
176
  });
157
177
  }, [firebaseApp]);
158
178
 
159
- const listenEntity = useCallback(<M extends Record<string, any>>(
179
+ const listenOne = useCallback(<M extends Record<string, any>>(
160
180
  {
161
181
  path,
162
- entityId,
182
+ id,
163
183
  collection,
164
184
  onUpdate,
165
185
  onError
166
- }: ListenEntityProps<M>): () => void => {
186
+ }: ListenOneProps<M>): () => void => {
167
187
  if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
168
188
 
169
189
  const databaseId = collection?.databaseId;
@@ -171,10 +191,10 @@ export function useFirestoreDriver({
171
191
  const resolvedPath = path;
172
192
 
173
193
  return onSnapshot(
174
- doc(firestore, resolvedPath, String(entityId)),
194
+ doc(firestore, resolvedPath, String(id)),
175
195
  {
176
- next: (docSnapshot) => {
177
- onUpdate(createEntityFromDocument(docSnapshot, databaseId));
196
+ next: (docEntity) => {
197
+ onUpdate(docEntity.exists() ? createRowFromDocument(docEntity) : null);
178
198
  },
179
199
  error: onError
180
200
  }
@@ -190,7 +210,7 @@ export function useFirestoreDriver({
190
210
  path: string,
191
211
  databaseId?: string,
192
212
  searchString: string;
193
- onUpdate: (entities: Entity<M>[]) => void,
213
+ onUpdate: (rows: Record<string, unknown>[]) => void,
194
214
  }): () => void => {
195
215
 
196
216
  if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
@@ -222,23 +242,24 @@ export function useFirestoreDriver({
222
242
  onUpdate([]);
223
243
  }
224
244
 
225
- const entities: Entity<M>[] = [];
245
+ const rows: Record<string, unknown>[] = [];
226
246
  const addedEntitiesSet = new Set<string | number>();
227
247
  subscriptions = (ids ?? [])
228
- .map((entityId) => {
229
- return listenEntity({
248
+ .map((id) => {
249
+ return listenOne({
230
250
  path,
231
- entityId,
232
- onUpdate: (entity: Entity<any> | null) => {
233
- if (entity?.values) {
234
- if (entity.id && !addedEntitiesSet.has(entity.id)) {
235
- addedEntitiesSet.add(entity.id);
236
- entities.push(entity);
237
- onUpdate(entities);
251
+ id,
252
+ onUpdate: (row: Record<string, unknown> | null) => {
253
+ const incomingId = row?.id as string | number | undefined;
254
+ if (row && incomingId !== undefined) {
255
+ if (!addedEntitiesSet.has(incomingId)) {
256
+ addedEntitiesSet.add(incomingId);
257
+ rows.push(row);
258
+ onUpdate(rows);
238
259
  }
239
- } else if (entity?.id) {
240
- addedEntitiesSet.delete(entity.id);
241
- onUpdate([...entities.filter(e => e.id !== entityId)])
260
+ } else {
261
+ addedEntitiesSet.delete(id);
262
+ onUpdate([...rows.filter(r => r.id !== id)])
242
263
  }
243
264
  }
244
265
  })
@@ -250,48 +271,88 @@ export function useFirestoreDriver({
250
271
  subscriptions.forEach((p) => p());
251
272
  }
252
273
 
253
- }, [firebaseApp, listenEntity]);
274
+ }, [firebaseApp, listenOne]);
254
275
 
255
- return {
256
- key: "firestore",
276
+ const initTextSearch = useCallback(async (props: {
277
+ path: string,
278
+ databaseId?: string,
279
+ collection?: CollectionConfig
280
+ }) => {
281
+ console.debug("Init text search controller", searchControllerRef.current, props.path);
282
+ if (!searchControllerRef.current) {
283
+ 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.");
284
+ return false;
285
+ }
286
+ try {
287
+ return searchControllerRef.current.init(props);
288
+ } catch (e) {
289
+ console.error("Error initializing text search controller", e);
290
+ return false;
291
+ }
292
+ }, []);
257
293
 
258
- currentTime,
294
+ /**
295
+ * Fetch entities in a Firestore path
296
+ * @param path
297
+ * @param collection
298
+ * @param filter
299
+ * @param limit
300
+ * @param startAfter
301
+ * @param searchString
302
+ * @param orderBy
303
+ * @param order
304
+ * @return Function to cancel subscription
305
+ * @see useCollection if you need this functionality implemented as a hook
306
+ * @group Firestore
307
+ */
308
+ const fetchCollection = useCallback(async <M extends Record<string, any>>({
309
+ path,
310
+ filter,
311
+ limit,
312
+ startAfter,
313
+ searchString,
314
+ orderBy,
315
+ order,
316
+ collection
317
+ }: FetchCollectionProps<M>
318
+ ): Promise<Record<string, unknown>[]> => {
259
319
 
260
- initialised: Boolean(firebaseApp),
320
+ const databaseId = collection?.databaseId;
261
321
 
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>>({
322
+ const resolvedPath = path;
323
+
324
+ console.debug("Fetching collection", {
325
+ path,
326
+ limit,
327
+ filter,
328
+ startAfter,
329
+ orderBy,
330
+ order
331
+ });
332
+ const query = buildQuery(resolvedPath, filter, orderBy, order, startAfter as unknown[] | undefined, limit, databaseId);
333
+
334
+ const entity = await getDocs(query);
335
+ return entity.docs.map((doc) => createRowFromDocument(doc));
336
+ }, [buildQuery]);
337
+
338
+ /**
339
+ * Listen to a entities in a given path
340
+ * @param path
341
+ * @param collection
342
+ * @param onError
343
+ * @param filter
344
+ * @param limit
345
+ * @param startAfter
346
+ * @param searchString
347
+ * @param orderBy
348
+ * @param order
349
+ * @param onUpdate
350
+ * @return Function to cancel subscription
351
+ * @see useCollection if you need this functionality implemented as a hook
352
+ * @group Firestore
353
+ */
354
+ const listenCollection = useCallback(<M extends Record<string, any>>(
355
+ {
295
356
  path,
296
357
  filter,
297
358
  limit,
@@ -299,334 +360,298 @@ export function useFirestoreDriver({
299
360
  searchString,
300
361
  orderBy,
301
362
  order,
363
+ onUpdate,
364
+ onError,
365
+ collection
366
+ }: ListenCollectionProps<M>
367
+ ): () => void => {
368
+
369
+ console.debug("Listening collection", {
370
+ path,
371
+ searchString,
372
+ limit,
373
+ filter,
374
+ startAfter,
375
+ orderBy,
376
+ order,
302
377
  collection
303
- }: FetchCollectionProps<M>
304
- ): Promise<Entity<M>[]> => {
378
+ });
305
379
 
306
- const databaseId = collection?.databaseId;
380
+ if (!firebaseApp) {
381
+ throw Error("useFirestoreDriver Firebase not initialised");
382
+ }
307
383
 
308
- const resolvedPath = path;
384
+ const databaseId = collection?.databaseId;
309
385
 
310
- console.debug("Fetching collection", {
311
- 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
- {
386
+ if (searchString) {
387
+ return performTextSearch<M>({
342
388
  path,
343
- filter,
344
- limit,
345
- startAfter,
346
389
  searchString,
347
- orderBy,
348
- order,
349
390
  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
391
+ databaseId
364
392
  });
393
+ }
365
394
 
366
- if (!firebaseApp) {
367
- throw Error("useFirestoreDriver Firebase not initialised");
395
+ const resolvedPath = path;
396
+ console.debug("Resolved path for listening", {
397
+ path,
398
+ resolvedPath
399
+ });
400
+ const query = buildQuery(resolvedPath, filter, orderBy, order, startAfter as unknown[] | undefined, limit, databaseId);
401
+ return onSnapshot(query,
402
+ {
403
+ next: (entity) => {
404
+ if (!searchString)
405
+ onUpdate(entity.docs.map((doc) => createRowFromDocument(doc)));
406
+ },
407
+ error: onError
368
408
  }
409
+ );
369
410
 
370
- const databaseId = collection?.databaseId;
411
+ }, [buildQuery, firebaseApp, performTextSearch]);
371
412
 
372
- if (searchString) {
373
- return performTextSearch<M>({
374
- path,
375
- searchString,
376
- onUpdate,
377
- databaseId
378
- });
379
- }
413
+ /**
414
+ * Retrieve a entity given a path and a collection
415
+ * @param path
416
+ * @param id
417
+ * @param collection
418
+ * @group Firestore
419
+ */
420
+ const fetchOne = useCallback(<M extends Record<string, any>>({
421
+ path,
422
+ id,
423
+ collection
424
+ }: FetchOneProps<M>
425
+ ): Promise<Record<string, unknown> | undefined> => {
426
+ const resolvedPath = path;
427
+ return getAndBuildEntity(resolvedPath, id, collection?.databaseId);
428
+ }, [getAndBuildEntity]);
380
429
 
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>>({
430
+ /**
431
+ * Save entity to the specified path. Note that Firestore does not allow
432
+ * undefined values.
433
+ * @param path
434
+ * @param id
435
+ * @param values
436
+ * @param schemaId
437
+ * @param collection
438
+ * @param status
439
+ * @group Firestore
440
+ */
441
+ const save = useCallback(<M extends Record<string, any>>(
442
+ {
407
443
  path,
408
- entityId,
444
+ id,
445
+ values: valuesProp,
446
+ collection,
447
+ status
448
+ }: SaveProps<M>): Promise<Record<string, unknown>> => {
449
+
450
+ if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
451
+
452
+ console.debug("1", {
453
+ path,
454
+ id,
455
+ values: valuesProp,
409
456
  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,
427
-
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>> => {
457
+ })
458
+ const values = cmsToFirestoreModel(valuesProp, getFirestore(firebaseApp));
447
459
 
448
- if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
460
+ console.debug("2", {
461
+ path,
462
+ id,
463
+ values: valuesProp,
464
+ collection
465
+ })
466
+ const databaseId = collection?.databaseId;
467
+ const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
468
+ const resolvedPath = path;
449
469
 
450
- console.debug("1", {
451
- path,
452
- entityId,
453
- values: valuesProp,
454
- collection
455
- })
456
- const values = cmsToFirestoreModel(valuesProp, getFirestore(firebaseApp));
470
+ const collectionReference: CollectionReference = collectionClause(firestore, path);
471
+ console.debug("Saving entity", {
472
+ path,
473
+ id,
474
+ values,
475
+ databaseId
476
+ });
457
477
 
458
- console.debug("2", {
459
- path,
460
- entityId,
461
- values: valuesProp,
462
- collection
478
+ let documentReference: DocumentReference;
479
+ if (id) {
480
+ documentReference = doc(collectionReference, String(id));
481
+ } else {
482
+ documentReference = doc(collectionReference);
483
+ }
484
+ return setDoc(documentReference, values as Record<string, unknown>, { merge: true })
485
+ .then(() => {
486
+ return {
487
+ ...(firestoreToCMSModel(values) as Record<string, unknown>),
488
+ id: documentReference.id
489
+ };
463
490
  })
464
- const databaseId = collection?.databaseId;
465
- const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
466
- const resolvedPath = path;
491
+ .catch((error) => {
492
+ console.error("Error saving entity", error);
493
+ throw error;
467
494
 
468
- const collectionReference: CollectionReference = collectionClause(firestore, path);
469
- console.debug("Saving entity", {
470
- path,
471
- entityId,
472
- values,
473
- databaseId
474
495
  });
496
+ }, [firebaseApp]);
475
497
 
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;
498
+ /**
499
+ * Delete a entity
500
+ * @param entity
501
+ * @param collection
502
+ * @group Firestore
503
+ */
504
+ const deleteOne = useCallback(<M extends Record<string, any>>(
505
+ {
506
+ row,
507
+ collection
508
+ }: DeleteProps<M>
509
+ ): Promise<void> => {
510
+ if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
493
511
 
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> => {
512
+ const databaseId = collection?.databaseId;
513
+ const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
533
514
 
534
- if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
515
+ return deleteDoc(doc(firestore, row.path, String(row.id)));
516
+ }, [firebaseApp]);
535
517
 
536
- const databaseId = collection?.databaseId;
537
- const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
518
+ /**
519
+ * Check if the given property is unique in the given collection
520
+ * @param path Collection path
521
+ * @param name of the property
522
+ * @param value
523
+ * @param property
524
+ * @param id
525
+ * @return `true` if there are no other fields besides the given entity
526
+ * @group Firestore
527
+ */
528
+ const checkUniqueField = useCallback(async (
529
+ path: string,
530
+ name: string,
531
+ value: unknown,
532
+ id?: string | number,
533
+ collection?: CollectionConfig<any>
534
+ ): Promise<boolean> => {
538
535
 
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;
536
+ if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
545
537
 
546
- }, [firebaseApp]),
538
+ const databaseId = collection?.databaseId;
539
+ const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
547
540
 
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 => {
541
+ if (value === undefined || value === null) {
542
+ return Promise.resolve(true);
543
+ }
544
+ const q = query(collectionClause(firestore, path), whereClause(name, "==", cmsToFirestoreModel(value, firestore)));
545
+ const entity = await getDocs(q);
546
+ return entity.docs.filter(doc => doc.id !== id).length === 0;
574
547
 
575
- if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
548
+ }, [firebaseApp]);
576
549
 
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;
550
+ const count = useCallback(async ({
551
+ path,
552
+ filter,
553
+ order,
554
+ orderBy,
555
+ collection
556
+ }: FetchCollectionProps<any>): Promise<number> => {
557
+ if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
558
+ const databaseId = collection?.databaseId;
559
+ const resolvedPath = path;
560
+ const query = buildQuery(resolvedPath, filter, orderBy, order, undefined, undefined, databaseId);
561
+ const entity = await getCountFromServer(query);
562
+ return entity.data().count;
563
+ }, [firebaseApp]);
581
564
 
582
- const indexes = firestoreIndexesBuilder?.({
583
- path: resolvedPath,
584
- collection
585
- });
565
+ const isFilterCombinationValid = useCallback(({
566
+ path,
567
+ collection,
568
+ filterValues,
569
+ sortBy
570
+ }: {
571
+ path: string,
572
+ collection: CollectionConfig<any>,
573
+ filterValues: FilterValues<any>,
574
+ sortBy?: [string, "asc" | "desc"],
575
+ }): boolean => {
576
+
577
+ if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
586
578
 
587
- const sortKey = sortBy ? sortBy[0] : undefined;
588
- const sortDirection = sortBy ? sortBy[1] : undefined;
579
+ // If no indexes are defined, we assume the query is valid.
580
+ // If there is no index in Firestore, and error message will be shown
581
+ if (firestoreIndexesBuilder === undefined) return true;
582
+ const resolvedPath = path;
589
583
 
590
- // Order by clause cannot contain a field with an equality filter
591
- const values: [WhereFilterOp, any][] = Object.values(filterValues) as [WhereFilterOp, any][];
584
+ const indexes = firestoreIndexesBuilder?.({
585
+ path: resolvedPath,
586
+ collection
587
+ });
592
588
 
593
- const filterKeys = Object.keys(filterValues);
594
- const filtersCount = filterKeys.length;
589
+ const sortKey = sortBy ? sortBy[0] : undefined;
590
+ const sortDirection = sortBy ? sortBy[1] : undefined;
595
591
 
596
- if (!sortKey && values.every((v) => v[0] === "==")) {
597
- return true;
598
- }
592
+ // Order by clause cannot contain a field with an equality filter
593
+ const values: [WhereFilterOp, any][] = Object.values(filterValues) as [WhereFilterOp, any][];
599
594
 
600
- if (filtersCount === 1 && (!sortKey || sortKey === filterKeys[0])) {
601
- return true;
602
- }
595
+ const filterKeys = Object.keys(filterValues);
596
+ const filtersCount = filterKeys.length;
603
597
 
604
- if (!indexes && filtersCount > 1) {
605
- return false;
606
- }
598
+ if (!sortKey && values.every((v) => v[0] === "==")) {
599
+ return true;
600
+ }
607
601
 
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])
602
+ if (filtersCount === 1 && (!sortKey || sortKey === filterKeys[0])) {
603
+ return true;
604
+ }
614
605
 
615
- };
606
+ if (!indexes && filtersCount > 1) {
607
+ return false;
608
+ }
609
+
610
+ return !!indexes && indexes
611
+ .filter((compositeIndex) => !sortKey || sortKey in compositeIndex)
612
+ .find((compositeIndex) =>
613
+ Object.entries(filterValues).every(([key, value]) => compositeIndex[key] !== undefined && (!sortDirection || compositeIndex[key] === sortDirection))
614
+ ) !== undefined;
615
+ }, [firebaseApp]);
616
+
617
+ return useMemo(() => ({
618
+ key: "firestore" as const,
619
+ currentTime,
620
+ initialised: Boolean(firebaseApp),
621
+ initTextSearch,
622
+ fetchCollection,
623
+ listenCollection,
624
+ fetchOne,
625
+ listenOne,
626
+ save,
627
+ delete: deleteOne,
628
+ checkUniqueField,
629
+ count,
630
+ isFilterCombinationValid
631
+ }), [
632
+ firebaseApp,
633
+ initTextSearch,
634
+ fetchCollection,
635
+ listenCollection,
636
+ fetchOne,
637
+ listenOne,
638
+ save,
639
+ deleteOne,
640
+ checkUniqueField,
641
+ count,
642
+ isFilterCombinationValid
643
+ ]);
616
644
 
617
645
  }
618
646
 
619
- const createEntityFromDocument = <M extends Record<string, any>>(
620
- docSnap: DocumentSnapshot,
621
- databaseId?: string
622
- ): Entity<M> => {
623
- const values = firestoreToCMSModel(docSnap.data()) as M;
624
- const path = getCMSPathFromFirestorePath(docSnap.ref.path);
647
+ const createRowFromDocument = (
648
+ docSnap: DocumentSnapshot
649
+ ): Record<string, unknown> => {
650
+ const values = firestoreToCMSModel(docSnap.data()) as Record<string, unknown>;
625
651
  return {
626
- id: docSnap.id,
627
- path: path,
628
- values,
629
- databaseId
652
+ ...values,
653
+ // Spread the canonical document id last so it wins over a literal `id` field
654
+ id: docSnap.id
630
655
  };
631
656
  };
632
657
 
@@ -763,7 +788,7 @@ function buildTextSearchControllerWithLocalSearch({
763
788
  init: async (props: {
764
789
  path: string,
765
790
  databaseId?: string,
766
- collection?: EntityCollection
791
+ collection?: CollectionConfig
767
792
  }) => {
768
793
  const b = await textSearchController.init(props);
769
794
  if (b) {