@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.
- package/dist/components/RebaseFirebaseAppProps.d.ts +2 -2
- package/dist/hooks/useBuildUserManagement.d.ts +1 -0
- package/dist/hooks/useFirestoreDriver.d.ts +3 -3
- package/dist/index.es.js +328 -313
- package/dist/index.es.js.map +1 -1
- package/dist/index.umd.js +326 -311
- package/dist/index.umd.js.map +1 -1
- package/dist/types/text_search.d.ts +3 -3
- package/dist/utils/collections_firestore.d.ts +3 -3
- package/package.json +19 -19
- package/src/components/RebaseFirebaseApp.tsx +10 -5
- package/src/components/RebaseFirebaseAppProps.tsx +2 -2
- package/src/hooks/useBuildUserManagement.tsx +27 -28
- package/src/hooks/useFirebaseRealTimeDBDelegate.ts +47 -52
- package/src/hooks/useFirebaseStorageSource.ts +2 -2
- package/src/hooks/useFirestoreDriver.ts +380 -355
- package/src/types/text_search.ts +3 -3
- package/src/utils/collections_firestore.ts +5 -5
- package/src/utils/database.ts +2 -2
- package/src/utils/local_text_search_controller.ts +6 -6
- package/src/utils/pinecone.ts +2 -2
- package/src/utils/rebase_search_controller.ts +3 -3
- package/src/utils/text_search_controller.ts +2 -2
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { DataDriver,
|
|
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:
|
|
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?:
|
|
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(
|
|
143
|
-
|
|
162
|
+
const getAndBuildEntity = useCallback((path: string,
|
|
163
|
+
id: string | number,
|
|
144
164
|
databaseId?: string
|
|
145
|
-
): Promise<
|
|
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(
|
|
151
|
-
.then((
|
|
152
|
-
if (!
|
|
170
|
+
return getDoc(doc(firestore, path, String(id)))
|
|
171
|
+
.then((docEntity) => {
|
|
172
|
+
if (!docEntity.exists()) {
|
|
153
173
|
return undefined;
|
|
154
174
|
}
|
|
155
|
-
return
|
|
175
|
+
return createRowFromDocument(docEntity);
|
|
156
176
|
});
|
|
157
177
|
}, [firebaseApp]);
|
|
158
178
|
|
|
159
|
-
const
|
|
179
|
+
const listenOne = useCallback(<M extends Record<string, any>>(
|
|
160
180
|
{
|
|
161
181
|
path,
|
|
162
|
-
|
|
182
|
+
id,
|
|
163
183
|
collection,
|
|
164
184
|
onUpdate,
|
|
165
185
|
onError
|
|
166
|
-
}:
|
|
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(
|
|
194
|
+
doc(firestore, resolvedPath, String(id)),
|
|
175
195
|
{
|
|
176
|
-
next: (
|
|
177
|
-
onUpdate(
|
|
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: (
|
|
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
|
|
245
|
+
const rows: Record<string, unknown>[] = [];
|
|
226
246
|
const addedEntitiesSet = new Set<string | number>();
|
|
227
247
|
subscriptions = (ids ?? [])
|
|
228
|
-
.map((
|
|
229
|
-
return
|
|
248
|
+
.map((id) => {
|
|
249
|
+
return listenOne({
|
|
230
250
|
path,
|
|
231
|
-
|
|
232
|
-
onUpdate: (
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
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
|
|
240
|
-
addedEntitiesSet.delete(
|
|
241
|
-
onUpdate([...
|
|
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,
|
|
274
|
+
}, [firebaseApp, listenOne]);
|
|
254
275
|
|
|
255
|
-
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
|
|
320
|
+
const databaseId = collection?.databaseId;
|
|
261
321
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
}
|
|
304
|
-
): Promise<Entity<M>[]> => {
|
|
378
|
+
});
|
|
305
379
|
|
|
306
|
-
|
|
380
|
+
if (!firebaseApp) {
|
|
381
|
+
throw Error("useFirestoreDriver Firebase not initialised");
|
|
382
|
+
}
|
|
307
383
|
|
|
308
|
-
|
|
384
|
+
const databaseId = collection?.databaseId;
|
|
309
385
|
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
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
|
-
|
|
367
|
-
|
|
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
|
-
|
|
411
|
+
}, [buildQuery, firebaseApp, performTextSearch]);
|
|
371
412
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
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
|
-
|
|
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
|
-
}
|
|
411
|
-
|
|
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
|
-
|
|
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
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
470
|
+
const collectionReference: CollectionReference = collectionClause(firestore, path);
|
|
471
|
+
console.debug("Saving entity", {
|
|
472
|
+
path,
|
|
473
|
+
id,
|
|
474
|
+
values,
|
|
475
|
+
databaseId
|
|
476
|
+
});
|
|
457
477
|
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
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
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
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
|
-
|
|
515
|
+
return deleteDoc(doc(firestore, row.path, String(row.id)));
|
|
516
|
+
}, [firebaseApp]);
|
|
535
517
|
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
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
|
-
|
|
538
|
+
const databaseId = collection?.databaseId;
|
|
539
|
+
const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
|
|
547
540
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
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
|
-
|
|
548
|
+
}, [firebaseApp]);
|
|
576
549
|
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
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
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
588
|
-
|
|
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
|
-
|
|
591
|
-
|
|
584
|
+
const indexes = firestoreIndexesBuilder?.({
|
|
585
|
+
path: resolvedPath,
|
|
586
|
+
collection
|
|
587
|
+
});
|
|
592
588
|
|
|
593
|
-
|
|
594
|
-
|
|
589
|
+
const sortKey = sortBy ? sortBy[0] : undefined;
|
|
590
|
+
const sortDirection = sortBy ? sortBy[1] : undefined;
|
|
595
591
|
|
|
596
|
-
|
|
597
|
-
|
|
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
|
-
|
|
601
|
-
|
|
602
|
-
}
|
|
595
|
+
const filterKeys = Object.keys(filterValues);
|
|
596
|
+
const filtersCount = filterKeys.length;
|
|
603
597
|
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
598
|
+
if (!sortKey && values.every((v) => v[0] === "==")) {
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
607
601
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
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
|
|
620
|
-
docSnap: DocumentSnapshot
|
|
621
|
-
|
|
622
|
-
)
|
|
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
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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?:
|
|
791
|
+
collection?: CollectionConfig
|
|
767
792
|
}) => {
|
|
768
793
|
const b = await textSearchController.init(props);
|
|
769
794
|
if (b) {
|