@rebasepro/client-firebase 0.0.1-canary.4d4fb3e → 0.0.1-canary.ca2cb6e

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.
@@ -0,0 +1,374 @@
1
+ import React, { useCallback, useEffect } from "react";
2
+ import equal from "react-fast-compare"
3
+
4
+ import { removeUndefined } from "@rebasepro/utils";
5
+ import {
6
+ AuthController,
7
+ Authenticator,
8
+ DataDriver,
9
+ Entity,
10
+ Role,
11
+ User,
12
+ UserManagementDelegate
13
+ } from "@rebasepro/types";
14
+
15
+ type UserWithRoleIds<USER extends User = any> = Omit<USER, "roles"> & { roles: string[] };
16
+
17
+ export interface UserManagementDelegateParams<CONTROLLER extends AuthController<any> = AuthController<any>> {
18
+
19
+ authController: CONTROLLER;
20
+
21
+ /**
22
+ * The delegate in charge of persisting the data.
23
+ */
24
+ dataSourceDelegate?: DataDriver;
25
+
26
+ /**
27
+ * Path where the plugin users configuration is stored.
28
+ * Default: __FIRECMS/config/users
29
+ * You can specify a different path if you want to store the user management configuration in a different place.
30
+ * Please keep in mind that the FireCMS users are not necessarily the same as the Firebase users (but they can be).
31
+ * The path should be relative to the root of the database, and should always have an odd number of segments.
32
+ */
33
+ usersPath?: string;
34
+
35
+ /**
36
+ * Path where the plugin roles configuration is stored.
37
+ * Default: __FIRECMS/config/roles
38
+ */
39
+ rolesPath?: string;
40
+
41
+ /**
42
+ * The roles that are available in the user management system.
43
+ * If you provide this, the user management system will not fetch the roles from the database.
44
+ */
45
+ roles?: Role[];
46
+
47
+ /**
48
+ * If there are no roles in the database, provide a button to create the default roles.
49
+ */
50
+ allowDefaultRolesCreation?: boolean;
51
+
52
+ /**
53
+ * Include the collection config permissions in the user management system.
54
+ */
55
+ includeCollectionConfigPermissions?: boolean;
56
+
57
+ }
58
+
59
+ /**
60
+ * This hook is used to build a user management object that can be used to
61
+ * manage users and roles in a Firestore backend.
62
+ * @param authController
63
+ * @param dataSourceDelegate
64
+ * @param usersPath
65
+ * @param rolesPath
66
+ * @param roles
67
+ * @param allowDefaultRolesCreation
68
+ * @param includeCollectionConfigPermissions
69
+ */
70
+ export function useBuildUserManagement<CONTROLLER extends AuthController<any> = AuthController<any>,
71
+ USER extends User = CONTROLLER extends AuthController<infer U> ? U : any>
72
+ ({
73
+ authController,
74
+ dataSourceDelegate,
75
+ roles: rolesProp,
76
+ usersPath = "__FIRECMS/config/users",
77
+ rolesPath = "__FIRECMS/config/roles",
78
+ allowDefaultRolesCreation,
79
+ includeCollectionConfigPermissions
80
+ }: UserManagementDelegateParams<CONTROLLER>): UserManagementDelegate<USER> & CONTROLLER {
81
+
82
+ if (!authController) {
83
+ throw Error("useBuildUserManagement: You need to provide an authController since version 3.0.0-beta.11. Check https://firecms.co/docs/pro/migrating_from_v3_beta");
84
+ }
85
+
86
+ const rolesDefinedInCode = (rolesProp ?? [])?.length > 0;
87
+ const [rolesLoading, setRolesLoading] = React.useState<boolean>(!rolesDefinedInCode);
88
+ const [usersLoading, setUsersLoading] = React.useState<boolean>(true);
89
+ const [roles, setRoles] = React.useState<Role[]>(rolesProp ?? []);
90
+ const [usersWithRoleIds, setUsersWithRoleIds] = React.useState<UserWithRoleIds<USER>[]>([]);
91
+
92
+ const users = usersWithRoleIds.map(u => ({
93
+ ...u,
94
+ }) as unknown as USER);
95
+
96
+ const [rolesError, setRolesError] = React.useState<Error | undefined>();
97
+ const [usersError, setUsersError] = React.useState<Error | undefined>();
98
+
99
+ const _usersLoading = usersLoading;
100
+ const _rolesLoading = rolesLoading;
101
+
102
+ const loading = _rolesLoading || _usersLoading;
103
+
104
+ useEffect(() => {
105
+ if (rolesDefinedInCode) return;
106
+ if (!dataSourceDelegate || !rolesPath) return;
107
+ if (dataSourceDelegate.initialised !== undefined && !dataSourceDelegate.initialised) return;
108
+ if (authController?.initialLoading) return;
109
+
110
+ setRolesLoading(true);
111
+ return dataSourceDelegate.listenCollection?.({
112
+ path: rolesPath,
113
+ onUpdate(entities: Entity<any>[]): void {
114
+ setRolesError(undefined);
115
+ console.debug("Updating roles", entities);
116
+ try {
117
+ const newRoles = entityToRoles(entities);
118
+ if (!equal(newRoles, roles)) {
119
+ setRoles(newRoles);
120
+ }
121
+ } catch (e) {
122
+ setRoles([]);
123
+ console.error("Error loading roles", e);
124
+ setRolesError(e as Error);
125
+ }
126
+ setRolesLoading(false);
127
+ },
128
+ onError(e: any): void {
129
+ setRoles([]);
130
+ console.error("Error loading roles", e);
131
+ setRolesError(e);
132
+ setRolesLoading(false);
133
+ }
134
+ });
135
+
136
+ }, [rolesDefinedInCode, dataSourceDelegate?.initialised, authController?.initialLoading, authController?.user?.uid, rolesPath]);
137
+
138
+ useEffect(() => {
139
+ if (!dataSourceDelegate || !usersPath) return;
140
+ if (dataSourceDelegate.initialised !== undefined && !dataSourceDelegate.initialised) {
141
+ return;
142
+ }
143
+ if (authController?.initialLoading) {
144
+ return;
145
+ }
146
+
147
+ setUsersLoading(true);
148
+ return dataSourceDelegate.listenCollection?.({
149
+ path: usersPath,
150
+ onUpdate(entities: Entity<any>[]): void {
151
+ console.debug("Updating users", entities);
152
+ setUsersError(undefined);
153
+ try {
154
+ const newUsers = entitiesToUsers(entities);
155
+ // if (!equal(newUsers, usersWithRoleIds))
156
+ setUsersWithRoleIds(newUsers);
157
+ } catch (e) {
158
+ setUsersWithRoleIds([]);
159
+ console.error("Error loading users", e);
160
+ setUsersError(e as Error);
161
+ }
162
+ setUsersLoading(false);
163
+ },
164
+ onError(e: any): void {
165
+ console.error("Error loading users", e);
166
+ setUsersWithRoleIds([]);
167
+ setUsersError(e);
168
+ setUsersLoading(false);
169
+ }
170
+ });
171
+
172
+ }, [dataSourceDelegate?.initialised, authController?.initialLoading, authController?.user?.uid, usersPath]);
173
+
174
+ const saveUser = useCallback(async (user: USER): Promise<USER> => {
175
+ if (!dataSourceDelegate) throw Error("useBuildUserManagement Firebase not initialised");
176
+ if (!usersPath) throw Error("useBuildUserManagement Firestore not initialised");
177
+
178
+ console.debug("Persisting user", user);
179
+
180
+ const roleIds = user.roles;
181
+ const email = user.email?.toLowerCase().trim();
182
+ if (!email) throw Error("Email is required");
183
+
184
+ const userExists = users.find(u => u.email?.toLowerCase() === email);
185
+ const data = {
186
+ ...user,
187
+ roles: roleIds ?? []
188
+ };
189
+ if (!userExists) {
190
+ // @ts-ignore
191
+ data.created_on = new Date();
192
+ }
193
+ // delete the previous user entry if it exists and the uid has changed
194
+ if (userExists && userExists.uid !== user.uid) {
195
+ const entity: Entity<any> = {
196
+ values: {},
197
+ path: usersPath,
198
+ id: userExists.uid
199
+ }
200
+ await dataSourceDelegate.deleteEntity({ entity })
201
+ .then(() => {
202
+ console.debug("Deleted previous user", userExists);
203
+ })
204
+ .catch(e => {
205
+ console.error("Error deleting user", e);
206
+ });
207
+
208
+ }
209
+
210
+ return dataSourceDelegate.saveEntity({
211
+ status: "existing",
212
+ path: usersPath,
213
+ entityId: email,
214
+ values: removeUndefined(data) as Record<string, unknown>
215
+ }).then(() => user);
216
+ }, [usersPath, dataSourceDelegate?.initialised]);
217
+
218
+ const saveRole = useCallback((role: Role): Promise<void> => {
219
+ if (!dataSourceDelegate) throw Error("useBuildUserManagement Firebase not initialised");
220
+ if (!rolesPath) throw Error("useBuildUserManagement Firestore not initialised");
221
+ console.debug("Persisting role", role);
222
+ const {
223
+ id,
224
+ ...roleData
225
+ } = role;
226
+ return dataSourceDelegate.saveEntity({
227
+ status: "existing",
228
+ path: rolesPath,
229
+ entityId: id,
230
+ values: removeUndefined(roleData) as Record<string, unknown>
231
+ }).then(() => {
232
+ return;
233
+ });
234
+ }, [rolesPath, dataSourceDelegate?.initialised]);
235
+
236
+ const deleteUser = useCallback(async (user: User): Promise<void> => {
237
+ if (!dataSourceDelegate) throw Error("useBuildUserManagement Firebase not initialised");
238
+ if (!usersPath) throw Error("useBuildUserManagement Firestore not initialised");
239
+ console.debug("Deleting", user);
240
+ const { uid } = user;
241
+ const entity: Entity<any> = {
242
+ path: usersPath,
243
+ id: uid,
244
+ values: {}
245
+ };
246
+ await dataSourceDelegate.deleteEntity({ entity })
247
+ }, [usersPath, dataSourceDelegate?.initialised]);
248
+
249
+ const deleteRole = useCallback(async (role: Role): Promise<void> => {
250
+ if (!dataSourceDelegate) throw Error("useBuildUserManagement Firebase not initialised");
251
+ if (!rolesPath) throw Error("useBuildUserManagement Firestore not initialised");
252
+ console.debug("Deleting", role);
253
+ const { id } = role;
254
+ const entity: Entity<any> = {
255
+ path: rolesPath,
256
+ id: id,
257
+ values: {}
258
+ };
259
+ await dataSourceDelegate.deleteEntity({ entity })
260
+ }, [rolesPath, dataSourceDelegate?.initialised]);
261
+
262
+
263
+
264
+ const defineRolesFor: ((user: User) => Role[] | undefined) = useCallback((user) => {
265
+ if (!usersWithRoleIds) throw Error("Users not loaded");
266
+ const mgmtUser = usersWithRoleIds.find(u => u.email?.toLowerCase() === user?.email?.toLowerCase());
267
+ if (!mgmtUser || !mgmtUser.roles) return undefined;
268
+ return roles.filter(r => mgmtUser.roles.includes(r.id));
269
+ }, [roles, usersWithRoleIds]);
270
+
271
+ const authenticator: Authenticator<USER> = useCallback(({ user }) => {
272
+
273
+ if (loading) {
274
+ return false;
275
+ }
276
+ if (user === null) {
277
+ console.warn("User is null, returning");
278
+ return false;
279
+ }
280
+
281
+ if (users.length === 0) {
282
+ console.warn("No users created yet");
283
+ return true; // If there are no users created yet, we allow access to every user
284
+ }
285
+
286
+ const mgmtUser = users.find(u => u.email?.toLowerCase() === user?.email?.toLowerCase());
287
+ if (mgmtUser) {
288
+ // check if the uid or photoURL needs to be updated in the user management system
289
+ const needsUidUpdate = mgmtUser.uid !== user.uid;
290
+ const needsPhotoUpdate = user.photoURL && mgmtUser.photoURL !== user.photoURL;
291
+
292
+ if (needsUidUpdate || needsPhotoUpdate) {
293
+ const updateReason = needsUidUpdate ? "uid" : "photoURL";
294
+ console.debug(`User ${updateReason} has changed, updating user in user management system`);
295
+ saveUser({
296
+ ...mgmtUser,
297
+ uid: user.uid,
298
+ ...(needsPhotoUpdate ? { photoURL: user.photoURL } : {})
299
+ }).then(() => {
300
+ console.debug("User updated in user management system", mgmtUser);
301
+ }).catch(e => {
302
+ console.error("Error updating user in user management system", e);
303
+ });
304
+ }
305
+ console.debug("User found in user management system", mgmtUser);
306
+ return true;
307
+ }
308
+
309
+ throw Error("Could not find a user with the provided email in the user management system.");
310
+ }, [loading, users]);
311
+
312
+ const userRoles = authController.user ? defineRolesFor(authController.user) : undefined;
313
+ const isAdmin = (userRoles ?? []).some(r => r.id === "admin");
314
+
315
+ const userRoleIds = userRoles?.map(r => r.id);
316
+ useEffect(() => {
317
+ console.debug("Setting user roles", {
318
+ userRoles,
319
+ roles
320
+ });
321
+ authController.setUserRoles?.(userRoles ?? []);
322
+ }, [userRoleIds]);
323
+
324
+ const getUser = useCallback((uid: string): USER | null => {
325
+ if (!users) return null;
326
+ const user = users.find(u => u.uid === uid);
327
+ return user ?? null;
328
+ }, [users]);
329
+
330
+ return {
331
+ loading,
332
+ roles,
333
+ users,
334
+ saveUser,
335
+ saveRole,
336
+ rolesError,
337
+ deleteUser,
338
+ deleteRole,
339
+ usersError,
340
+ isAdmin,
341
+ allowDefaultRolesCreation: allowDefaultRolesCreation === undefined ? true : allowDefaultRolesCreation,
342
+ includeCollectionConfigPermissions: Boolean(includeCollectionConfigPermissions),
343
+ defineRolesFor,
344
+ authenticator,
345
+ ...authController,
346
+ initialLoading: authController.initialLoading || loading,
347
+ userRoles: userRoles,
348
+ getUser,
349
+ user: authController.user ? {
350
+ ...authController.user,
351
+ roles: userRoles
352
+ } : null
353
+ }
354
+ }
355
+
356
+ const entitiesToUsers = (docs: Entity<Omit<UserWithRoleIds, "id">>[]): (UserWithRoleIds)[] => {
357
+ return docs.map((doc) => {
358
+ const data = doc.values as any;
359
+ const newVar = {
360
+ uid: doc.id,
361
+ ...data,
362
+ created_on: data?.created_on,
363
+ updated_on: data?.updated_on
364
+ };
365
+ return newVar as (UserWithRoleIds);
366
+ });
367
+ }
368
+
369
+ const entityToRoles = (entities: Entity<Omit<Role, "id">>[]): Role[] => {
370
+ return entities.map((doc) => ({
371
+ id: doc.id,
372
+ ...doc.values
373
+ } as Role));
374
+ }
@@ -299,7 +299,7 @@ export const useFirebaseAuthController = <USER extends FirebaseUserWrapper = any
299
299
  const firebaseUserWrapper: FirebaseUserWrapper | null = loggedUser
300
300
  ? {
301
301
  ...loggedUser,
302
- roles: userRoles?.map(r => r.id), // User.roles is string[], keep Role[] internally only
302
+ roles: userRoles?.map(r => r.id), // User.roles is string[], keep Role[] internally only
303
303
  firebaseUser: loggedUser
304
304
  }
305
305
  : null;
@@ -48,7 +48,7 @@ export function useFirebaseRTDBDelegate({ firebaseApp }: { firebaseApp?: Firebas
48
48
  return Object.entries(snapshot.val()).map(([id, values]) => ({
49
49
  id,
50
50
  path: path,
51
- values: delegateToCMSModel(values) as M,
51
+ values: delegateToCMSModel(values) as M
52
52
  }));
53
53
  }
54
54
  return [];
@@ -56,7 +56,7 @@ export function useFirebaseRTDBDelegate({ firebaseApp }: { firebaseApp?: Firebas
56
56
 
57
57
  const listenCollection = useCallback(<M extends Record<string, any>>({
58
58
  path,
59
- onUpdate,
59
+ onUpdate
60
60
  // Realtime Database does not directly support onError in onValue
61
61
  }: ListenCollectionProps<M>): () => void => {
62
62
  if (!firebaseApp) {
@@ -70,7 +70,7 @@ export function useFirebaseRTDBDelegate({ firebaseApp }: { firebaseApp?: Firebas
70
70
  const result: Entity<M>[] = Object.entries(snapshot.val()).map(([id, values]) => ({
71
71
  id,
72
72
  path: path,
73
- values: delegateToCMSModel(values) as M,
73
+ values: delegateToCMSModel(values) as M
74
74
  }));
75
75
  onUpdate(result);
76
76
  } else {
@@ -83,7 +83,7 @@ export function useFirebaseRTDBDelegate({ firebaseApp }: { firebaseApp?: Firebas
83
83
 
84
84
  const fetchEntity = useCallback(async <M extends Record<string, any>>({
85
85
  path,
86
- entityId,
86
+ entityId
87
87
  }: FetchEntityProps<M>): Promise<Entity<M> | undefined> => {
88
88
  if (!firebaseApp) {
89
89
  throw new Error("Firebase app not provided");
@@ -131,7 +131,7 @@ export function useFirebaseRTDBDelegate({ firebaseApp }: { firebaseApp?: Firebas
131
131
  const saveEntity = useCallback(async <M extends Record<string, any>>({
132
132
  path,
133
133
  entityId,
134
- values,
134
+ values
135
135
  }: SaveEntityProps<M>): Promise<Entity<M>> => {
136
136
  if (!firebaseApp) {
137
137
  throw new Error("Firebase app not provided");
@@ -156,7 +156,7 @@ export function useFirebaseRTDBDelegate({ firebaseApp }: { firebaseApp?: Firebas
156
156
  }, [firebaseApp]);
157
157
 
158
158
  const deleteEntity = useCallback(async <M extends Record<string, any>>({
159
- entity,
159
+ entity
160
160
  }: DeleteEntityProps<M>): Promise<void> => {
161
161
  if (!firebaseApp) {
162
162
  throw new Error("Firebase app not provided");
@@ -213,7 +213,6 @@ export function useFirebaseRTDBDelegate({ firebaseApp }: { firebaseApp?: Firebas
213
213
  }
214
214
 
215
215
 
216
-
217
216
  /**
218
217
  * Transform data from RTDB format back to CMS format
219
218
  * This is used internally when fetching/listening to data
@@ -263,7 +262,8 @@ function cmsToRTDBModel(data: any, database: Database): any {
263
262
  else
264
263
  return {};
265
264
  })
266
- .reduce((a, b) => ({ ...a, ...b }), {});
265
+ .reduce((a, b) => ({ ...a,
266
+ ...b }), {});
267
267
  }
268
268
  return data;
269
269
  }
@@ -29,10 +29,9 @@ export function useFirebaseStorageSource({
29
29
  const projectId = firebaseApp?.options?.projectId;
30
30
  const urlsCache: Record<string, DownloadConfig> = {};
31
31
  return {
32
- uploadFile({
32
+ putObject({
33
33
  file,
34
- fileName,
35
- path,
34
+ key,
36
35
  metadata,
37
36
  bucket
38
37
  }: UploadFileProps)
@@ -42,9 +41,8 @@ export function useFirebaseStorageSource({
42
41
  const storageBucketUrl = bucket ?? bucketUrl;
43
42
  const storage = getStorage(firebaseApp, storageBucketUrl);
44
43
  if (!storage) throw Error("useFirebaseStorageSource Firebase not initialised");
45
- const usedFilename = fileName ?? file.name;
46
44
 
47
- const storageRef = ref(storage, `${path}/${usedFilename}`);
45
+ const storageRef = ref(storage, key);
48
46
  const uploadTask = uploadBytesResumable(storageRef, file, metadata);
49
47
 
50
48
  return new Promise((resolve, reject) => {
@@ -111,9 +109,9 @@ export function useFirebaseStorageSource({
111
109
  const fullPath = uploadTask.snapshot.ref.fullPath;
112
110
  const bucketName = uploadTask.snapshot.ref.bucket;
113
111
  resolve({
114
- path: fullPath,
112
+ key: fullPath,
115
113
  bucket: bucketName,
116
- storageUrl: `gs://${bucketName}/${fullPath}`
114
+ storageUrl: `s3://${bucketName}/${fullPath}`
117
115
  });
118
116
  }
119
117
  );
@@ -123,7 +121,7 @@ export function useFirebaseStorageSource({
123
121
  }
124
122
  },
125
123
 
126
- async getFile(path: string, bucket?: string): Promise<File | null> {
124
+ async getObject(path: string, bucket?: string): Promise<File | null> {
127
125
  try {
128
126
  if (!firebaseApp) throw Error("useFirebaseStorageSource Firebase not initialised");
129
127
  const storageBucketUrl = bucket ?? bucketUrl;
@@ -140,15 +138,16 @@ export function useFirebaseStorageSource({
140
138
  }
141
139
  },
142
140
 
143
- async getDownloadURL(storagePathOrUrl: string, bucket?: string): Promise<DownloadConfig> {
141
+ async getSignedUrl(storagePathOrUrl: string, bucket?: string): Promise<DownloadConfig> {
144
142
  if (!firebaseApp) throw Error("useFirebaseStorageSource Firebase not initialised");
145
143
 
146
- // Support fully-qualified gs:// URLs
144
+ // Support fully-qualified s3:// and gs:// URLs for backward compatibility
147
145
  let resolvedPathOrUrl = storagePathOrUrl;
148
146
  let resolvedBucket = bucket;
149
- if (storagePathOrUrl.startsWith("gs://")) {
150
- // Format: gs://<bucket>/<path>
151
- const withoutProtocol = storagePathOrUrl.substring("gs://".length);
147
+ const match = storagePathOrUrl.match(/^(s3|gs):\/\//);
148
+ if (match) {
149
+ const protocolLength = match[0].length;
150
+ const withoutProtocol = storagePathOrUrl.substring(protocolLength);
152
151
  const firstSlash = withoutProtocol.indexOf("/");
153
152
  if (firstSlash > 0) {
154
153
  resolvedBucket = withoutProtocol.substring(0, firstSlash);
@@ -180,7 +179,7 @@ export function useFirebaseStorageSource({
180
179
  }
181
180
  },
182
181
 
183
- async list(path: string, options?: {
182
+ async listObjects(prefix: string, options?: {
184
183
  bucket?: string,
185
184
  maxResults?: number,
186
185
  pageToken?: string
@@ -189,14 +188,14 @@ export function useFirebaseStorageSource({
189
188
  const storageBucketUrl = options?.bucket ?? bucketUrl;
190
189
  const storage = getStorage(firebaseApp, storageBucketUrl);
191
190
  if (!storage) throw Error("useFirebaseStorageSource Firebase not initialised");
192
- const folderRef = ref(storage, path);
191
+ const folderRef = ref(storage, prefix);
193
192
  return await list(folderRef, {
194
193
  maxResults: options?.maxResults,
195
194
  pageToken: options?.pageToken
196
195
  });
197
196
  },
198
197
 
199
- async deleteFile(path: string, bucket?: string): Promise<void> {
198
+ async deleteObject(path: string, bucket?: string): Promise<void> {
200
199
  if (!firebaseApp) throw Error("useFirebaseStorageSource Firebase not initialised");
201
200
  const storageBucketUrl = bucket ?? bucketUrl;
202
201
  const storage = getStorage(firebaseApp, storageBucketUrl);
@@ -160,7 +160,7 @@ export function useFirestoreDriver({
160
160
  entityId,
161
161
  collection,
162
162
  onUpdate,
163
- onError,
163
+ onError
164
164
  }: ListenEntityProps<M>): () => void => {
165
165
  if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
166
166
 
@@ -183,7 +183,7 @@ export function useFirestoreDriver({
183
183
  path,
184
184
  databaseId,
185
185
  searchString,
186
- onUpdate,
186
+ onUpdate
187
187
  }: {
188
188
  path: string,
189
189
  databaseId?: string,
@@ -297,7 +297,7 @@ export function useFirestoreDriver({
297
297
  searchString,
298
298
  orderBy,
299
299
  order,
300
- collection,
300
+ collection
301
301
  }: FetchCollectionProps<M>
302
302
  ): Promise<Entity<M>[]> => {
303
303
 
@@ -346,7 +346,7 @@ export function useFirestoreDriver({
346
346
  order,
347
347
  onUpdate,
348
348
  onError,
349
- collection,
349
+ collection
350
350
  }: ListenCollectionProps<M>
351
351
  ): () => void => {
352
352
 
@@ -372,7 +372,7 @@ export function useFirestoreDriver({
372
372
  path,
373
373
  searchString,
374
374
  onUpdate,
375
- databaseId,
375
+ databaseId
376
376
  });
377
377
  }
378
378
 
@@ -404,7 +404,7 @@ export function useFirestoreDriver({
404
404
  fetchEntity: useCallback(<M extends Record<string, any>>({
405
405
  path,
406
406
  entityId,
407
- collection,
407
+ collection
408
408
  }: FetchEntityProps<M>
409
409
  ): Promise<Entity<M> | undefined> => {
410
410
  const resolvedPath = path;
@@ -440,7 +440,7 @@ export function useFirestoreDriver({
440
440
  entityId,
441
441
  values: valuesProp,
442
442
  collection,
443
- status,
443
+ status
444
444
  }: SaveEntityProps<M>): Promise<Entity<M>> => {
445
445
 
446
446
  if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
@@ -482,7 +482,7 @@ export function useFirestoreDriver({
482
482
  return {
483
483
  id: documentReference.id,
484
484
  path,
485
- values: firestoreToCMSModel(values),
485
+ values: firestoreToCMSModel(values)
486
486
  } as Entity<M>;
487
487
  })
488
488
  .catch((error) => {
@@ -548,7 +548,7 @@ export function useFirestoreDriver({
548
548
  filter,
549
549
  order,
550
550
  orderBy,
551
- collection,
551
+ collection
552
552
  }: FetchCollectionProps<any>): Promise<number> => {
553
553
  if (!firebaseApp) throw Error("useFirestoreDriver Firebase not initialised");
554
554
  const databaseId = collection?.databaseId;
@@ -562,7 +562,7 @@ export function useFirestoreDriver({
562
562
  path,
563
563
  collection,
564
564
  filterValues,
565
- sortBy,
565
+ sortBy
566
566
  }: {
567
567
  path: string,
568
568
  collection: EntityCollection<any>,
@@ -656,7 +656,8 @@ export function firestoreToCMSModel(data: any): any {
656
656
  return data; // already translated
657
657
  }
658
658
  if (data instanceof VectorValue || (typeof data === "object" && data !== null && typeof data.toArray === "function" && data.constructor?.name === "VectorValue")) {
659
- return { __type__: "__vector__", value: data.toArray() };
659
+ return { __type__: "__vector__",
660
+ value: data.toArray() };
660
661
  }
661
662
 
662
663
  if (data instanceof FirestoreGeoPoint) {
@@ -665,7 +666,9 @@ export function firestoreToCMSModel(data: any): any {
665
666
  if (data instanceof DocumentReference) {
666
667
  // @ts-ignore
667
668
  const databaseId = data?.firestore?._databaseId?.database;
668
- return new EntityReference({ id: data.id, path: getCMSPathFromFirestorePath(data.path), databaseId });
669
+ return new EntityReference({ id: data.id,
670
+ path: getCMSPathFromFirestorePath(data.path),
671
+ databaseId });
669
672
  }
670
673
  if (Array.isArray(data)) {
671
674
  return data.map(firestoreToCMSModel).filter(v => v !== undefined);
@@ -703,6 +706,8 @@ export function cmsToFirestoreModel(data: any, firestore: Firestore, inArray = f
703
706
  } else if (data.isEntityReference && data.isEntityReference()) {
704
707
  const targetFirestore = data.databaseId ? getFirestore(firestore.app, data.databaseId) : firestore;
705
708
  return doc(targetFirestore, data.path, data.id);
709
+ } else if (data && typeof data === "object" && data.__type === "relation" && data.path && data.id) {
710
+ return doc(firestore, data.path, String(data.id));
706
711
  } else if (data instanceof GeoPoint) {
707
712
  return new FirestoreGeoPoint(data.latitude, data.longitude);
708
713
  } else if (data instanceof Date) {
@@ -718,7 +723,8 @@ export function cmsToFirestoreModel(data: any, firestore: Firestore, inArray = f
718
723
  else
719
724
  return {};
720
725
  })
721
- .reduce((a, b) => ({ ...a, ...b }), {});
726
+ .reduce((a, b) => ({ ...a,
727
+ ...b }), {});
722
728
  }
723
729
  return data;
724
730
  }
@@ -14,7 +14,7 @@ export function performAlgoliaTextSearch(client: any, indexName: string, query:
14
14
 
15
15
  return client.searchSingleIndex({
16
16
  indexName,
17
- searchParams: { query },
17
+ searchParams: { query }
18
18
  }).then(({ hits }: any) => {
19
19
  return hits.map((hit: any) => hit.objectID as string);
20
20
  })