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

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.
Files changed (61) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +4 -0
  3. package/dist/components/FirebaseLoginView.d.ts +72 -0
  4. package/dist/components/RebaseFirebaseApp.d.ts +19 -0
  5. package/dist/components/RebaseFirebaseAppProps.d.ts +144 -0
  6. package/dist/components/index.d.ts +3 -0
  7. package/dist/components/social_icons.d.ts +6 -0
  8. package/dist/hooks/index.d.ts +7 -0
  9. package/dist/hooks/useAppCheck.d.ts +20 -0
  10. package/dist/hooks/useFirebaseAuthController.d.ts +15 -0
  11. package/dist/hooks/useFirebaseRealTimeDBDelegate.d.ts +5 -0
  12. package/dist/hooks/useFirebaseStorageSource.d.ts +14 -0
  13. package/dist/hooks/useFirestoreDriver.d.ts +56 -0
  14. package/dist/hooks/useInitialiseFirebase.d.ts +34 -0
  15. package/dist/hooks/useRecaptcha.d.ts +8 -0
  16. package/dist/index.d.ts +4 -0
  17. package/dist/index.es.js +2757 -0
  18. package/dist/index.es.js.map +1 -0
  19. package/dist/index.umd.js +2743 -0
  20. package/dist/index.umd.js.map +1 -0
  21. package/dist/social_icons.d.ts +6 -0
  22. package/dist/types/appcheck.d.ts +10 -0
  23. package/dist/types/auth.d.ts +41 -0
  24. package/dist/types/index.d.ts +3 -0
  25. package/dist/types/text_search.d.ts +39 -0
  26. package/dist/utils/algolia.d.ts +9 -0
  27. package/dist/utils/collections_firestore.d.ts +5 -0
  28. package/dist/utils/database.d.ts +2 -0
  29. package/dist/utils/index.d.ts +7 -0
  30. package/dist/utils/local_text_search_controller.d.ts +2 -0
  31. package/dist/utils/pinecone.d.ts +24 -0
  32. package/dist/utils/rebase_search_controller.d.ts +73 -0
  33. package/dist/utils/text_search_controller.d.ts +13 -0
  34. package/package.json +61 -0
  35. package/src/components/FirebaseLoginView.tsx +703 -0
  36. package/src/components/RebaseFirebaseApp.tsx +275 -0
  37. package/src/components/RebaseFirebaseAppProps.tsx +180 -0
  38. package/src/components/index.ts +3 -0
  39. package/src/components/social_icons.tsx +135 -0
  40. package/src/hooks/index.ts +7 -0
  41. package/src/hooks/useAppCheck.ts +101 -0
  42. package/src/hooks/useFirebaseAuthController.ts +334 -0
  43. package/src/hooks/useFirebaseRealTimeDBDelegate.ts +269 -0
  44. package/src/hooks/useFirebaseStorageSource.ts +208 -0
  45. package/src/hooks/useFirestoreDriver.ts +778 -0
  46. package/src/hooks/useInitialiseFirebase.ts +132 -0
  47. package/src/hooks/useRecaptcha.tsx +28 -0
  48. package/src/index.ts +4 -0
  49. package/src/social_icons.tsx +135 -0
  50. package/src/types/appcheck.ts +11 -0
  51. package/src/types/auth.tsx +74 -0
  52. package/src/types/index.ts +3 -0
  53. package/src/types/text_search.ts +42 -0
  54. package/src/utils/algolia.ts +27 -0
  55. package/src/utils/collections_firestore.ts +149 -0
  56. package/src/utils/database.ts +39 -0
  57. package/src/utils/index.ts +7 -0
  58. package/src/utils/local_text_search_controller.ts +143 -0
  59. package/src/utils/pinecone.ts +75 -0
  60. package/src/utils/rebase_search_controller.ts +356 -0
  61. package/src/utils/text_search_controller.ts +34 -0
@@ -0,0 +1,132 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+
3
+ import { deleteApp, FirebaseApp, getApps, initializeApp } from "@firebase/app";
4
+
5
+ /**
6
+ * @group Firebase
7
+ */
8
+ export interface InitialiseFirebaseResult {
9
+ firebaseConfigLoading: boolean,
10
+ firebaseApp?: FirebaseApp;
11
+ configError?: string,
12
+ firebaseConfigError?: Error
13
+ }
14
+
15
+ const hostingError = "It seems like the provided Firebase config is not correct. If you \n" +
16
+ "are using the credentials provided automatically by Firebase \n" +
17
+ "Hosting, make sure you link your Firebase app to Firebase Hosting. \n";
18
+
19
+ /**
20
+ * Function used to initialise Firebase, either by using the provided config,
21
+ * or by fetching it by Firebase Hosting, if not specified.
22
+ *
23
+ * It works as a hook that gives you the loading state and the used
24
+ * configuration.
25
+ *
26
+ * You most likely only need to use this if you are developing a custom app. You can also not use this component
27
+ * and initialise Firebase yourself.
28
+ *
29
+ * @param onFirebaseInit
30
+ * @param firebaseConfig
31
+ * @param fromUrl
32
+ * @param name
33
+ * @param authDomain
34
+ * @group Firebase
35
+ */
36
+ export function useInitialiseFirebase({
37
+ firebaseConfig,
38
+ fromUrl,
39
+ onFirebaseInit,
40
+ name,
41
+ authDomain
42
+ }: {
43
+ firebaseConfig?: Record<string, unknown>,
44
+ fromUrl?: string | undefined,
45
+ onFirebaseInit?: ((config: object, firebaseApp: FirebaseApp) => void) | undefined,
46
+ name?: string;
47
+ authDomain?: string;
48
+ }): InitialiseFirebaseResult {
49
+
50
+ const [firebaseApp, setFirebaseApp] = useState<FirebaseApp | undefined>();
51
+ const [firebaseConfigLoading, setFirebaseConfigLoading] = useState<boolean>(false);
52
+ const [configError, setConfigError] = useState<string>();
53
+
54
+ const initFirebase = useCallback((config: Record<string, unknown>) => {
55
+
56
+ if (config.projectId === firebaseApp?.options.projectId) {
57
+ console.debug("Firebase app already initialised with the same project ID. This should happen only in development mode.");
58
+ setConfigError(undefined);
59
+ setFirebaseConfigLoading(false);
60
+ return;
61
+ }
62
+
63
+ try {
64
+ const targetName = name ?? "[DEFAULT]";
65
+ const currentApps = getApps();
66
+ const existingApp = currentApps.find(app => app.name === targetName);
67
+ if (existingApp) {
68
+ deleteApp(existingApp);
69
+ }
70
+ const initialisedFirebaseApp = initializeApp(config, targetName);
71
+ setConfigError(undefined);
72
+ setFirebaseConfigLoading(false);
73
+ setFirebaseApp(initialisedFirebaseApp);
74
+ } catch (e: any) {
75
+ console.error("Error initialising Firebase", e);
76
+ setConfigError(hostingError + "\n" + (e.message ?? JSON.stringify(e)));
77
+ }
78
+ }, [name]);
79
+
80
+ useEffect(() => {
81
+ if (onFirebaseInit && firebaseConfig && firebaseApp) {
82
+ onFirebaseInit(firebaseConfig, firebaseApp);
83
+ }
84
+ }, [firebaseApp]);
85
+
86
+ useEffect(() => {
87
+
88
+ setFirebaseConfigLoading(true);
89
+
90
+ function fetchFromUrl(url: string) {
91
+ fetch(url)
92
+ .then(async response => {
93
+ console.debug("Firebase init response", response.status);
94
+ if (response && response.status < 300) {
95
+ const config = await response.json();
96
+ if (authDomain) config.authDomain = authDomain;
97
+ initFirebase(config);
98
+ }
99
+ })
100
+ .catch(e => {
101
+ setFirebaseConfigLoading(false);
102
+ setConfigError(
103
+ "Could not load Firebase configuration from Firebase hosting. " +
104
+ "If the app is not deployed in Firebase hosting, you need to specify the configuration manually" +
105
+ e.toString()
106
+ );
107
+ }
108
+ );
109
+ }
110
+
111
+ if (firebaseConfig) {
112
+ initFirebase(firebaseConfig);
113
+ } else {
114
+ if (fromUrl) {
115
+ fetchFromUrl(fromUrl);
116
+ } else if (process.env.NODE_ENV === "production") {
117
+ fetchFromUrl("/__/firebase/init.json");
118
+ } else {
119
+ setFirebaseConfigLoading(false);
120
+ setConfigError(
121
+ "You need to deploy the app to Firebase hosting or specify a Firebase configuration object"
122
+ );
123
+ }
124
+ }
125
+ }, []);
126
+
127
+ return {
128
+ firebaseApp,
129
+ firebaseConfigLoading,
130
+ configError
131
+ };
132
+ }
@@ -0,0 +1,28 @@
1
+ import { useEffect } from "react";
2
+ import { getAuth, RecaptchaVerifier } from "@firebase/auth";
3
+
4
+ declare global {
5
+ interface Window {
6
+ recaptchaVerifier: RecaptchaVerifier;
7
+ }
8
+ }
9
+
10
+ export const RECAPTCHA_CONTAINER_ID = "recaptcha-container" as const;
11
+
12
+ export function useRecaptcha() {
13
+ useEffect(() => {
14
+ if (!window || window?.recaptchaVerifier) return;
15
+
16
+ const auth = getAuth();
17
+
18
+ window.recaptchaVerifier = new RecaptchaVerifier(
19
+ auth,
20
+ RECAPTCHA_CONTAINER_ID,
21
+ {
22
+ size: "invisible"
23
+ }
24
+ );
25
+ }, []);
26
+
27
+ return null;
28
+ }
package/src/index.ts ADDED
@@ -0,0 +1,4 @@
1
+ export * from "./hooks";
2
+ export * from "./types";
3
+ export * from "./utils";
4
+ export * from "./components";
@@ -0,0 +1,135 @@
1
+ export const googleIcon = (mode: "light" | "dark") => <>
2
+ <svg
3
+ xmlns="http://www.w3.org/2000/svg"
4
+ viewBox="0 0 64 64"
5
+ width={32}
6
+ height={32}
7
+ >
8
+ <linearGradient
9
+ id="95yY7w43Oj6n2vH63j6HJb"
10
+ x1="29.401"
11
+ x2="29.401"
12
+ y1="4.064"
13
+ y2="106.734"
14
+ gradientTransform="matrix(1 0 0 -1 0 66)"
15
+ gradientUnits="userSpaceOnUse"
16
+ >
17
+ <stop offset="0" stopColor="#ff5840"/>
18
+ <stop offset=".007" stopColor="#ff5840"/>
19
+ <stop offset=".989" stopColor="#fa528c"/>
20
+ <stop offset="1" stopColor="#fa528c"/>
21
+ </linearGradient>
22
+ <path
23
+ fill="url(#95yY7w43Oj6n2vH63j6HJb)"
24
+ d="M47.46,15.5l-1.37,1.48c-1.34,1.44-3.5,1.67-5.15,0.6c-2.71-1.75-6.43-3.13-11-2.37 c-4.94,0.83-9.17,3.85-11.64, 7.97l-8.03-6.08C14.99,9.82,23.2,5,32.5,5c5,0,9.94,1.56,14.27,4.46 C48.81,10.83,49.13,13.71,47.46,15.5z"
25
+ />
26
+ <linearGradient
27
+ id="95yY7w43Oj6n2vH63j6HJc"
28
+ x1="12.148"
29
+ x2="12.148"
30
+ y1=".872"
31
+ y2="47.812"
32
+ gradientTransform="matrix(1 0 0 -1 0 66)"
33
+ gradientUnits="userSpaceOnUse"
34
+ >
35
+ <stop offset="0" stopColor="#feaa53"/>
36
+ <stop offset=".612" stopColor="#ffcd49"/>
37
+ <stop offset="1" stopColor="#ffde44"/>
38
+ </linearGradient>
39
+ <path
40
+ fill="url(#95yY7w43Oj6n2vH63j6HJc)"
41
+ d="M16.01,30.91c-0.09,2.47,0.37,4.83,1.27,6.96l-8.21,6.05c-1.35-2.51-2.3-5.28-2.75-8.22 c-1.06-6.88,0.54-13.38, 3.95-18.6l8.03,6.08C16.93,25.47,16.1,28.11,16.01,30.91z"
42
+ />
43
+ <linearGradient
44
+ id="95yY7w43Oj6n2vH63j6HJd"
45
+ x1="29.76"
46
+ x2="29.76"
47
+ y1="32.149"
48
+ y2="-6.939"
49
+ gradientTransform="matrix(1 0 0 -1 0 66)"
50
+ gradientUnits="userSpaceOnUse"
51
+ >
52
+ <stop offset="0" stopColor="#42d778"/>
53
+ <stop offset=".428" stopColor="#3dca76"/>
54
+ <stop offset="1" stopColor="#34b171"/>
55
+ </linearGradient>
56
+ <path
57
+ fill="url(#95yY7w43Oj6n2vH63j6HJd)"
58
+ d="M50.45,51.28c-4.55,4.07-10.61,6.57-17.36,6.71C22.91,58.2,13.66,52.53,9.07,43.92l8.21-6.05 C19.78,43.81, 25.67,48,32.5,48c3.94,0,7.52-1.28,10.33-3.44L50.45,51.28z"
59
+ />
60
+ <linearGradient
61
+ id="95yY7w43Oj6n2vH63j6HJe"
62
+ x1="46"
63
+ x2="46"
64
+ y1="3.638"
65
+ y2="35.593"
66
+ gradientTransform="matrix(1 0 0 -1 0 66)"
67
+ gradientUnits="userSpaceOnUse"
68
+ >
69
+ <stop offset="0" stopColor="#155cde"/>
70
+ <stop offset=".278" stopColor="#1f7fe5"/>
71
+ <stop offset=".569" stopColor="#279ceb"/>
72
+ <stop offset=".82" stopColor="#2cafef"/>
73
+ <stop offset="1" stopColor="#2eb5f0"/>
74
+ </linearGradient>
75
+ <path
76
+ fill="url(#95yY7w43Oj6n2vH63j6HJe)"
77
+ d="M59,31.97c0.01,7.73-3.26,14.58-8.55,19.31l-7.62-6.72c2.1-1.61,3.77-3.71,4.84-6.15
78
+ c0.29-0.66-0.2-1.41-0.92-1.41H37c-2.21,0-4-1.79-4-4v-2c0-2.21,1.79-4,4-4h17C56.75,27,59,29.22,59,31.97z"
79
+ />
80
+ </svg>
81
+ </>;
82
+
83
+ export const appleIcon = (mode: "light" | "dark") => <svg width={32} height={32}
84
+ viewBox="0 0 56 56"
85
+ style={{ transform: "scale(2.8)" }}
86
+ version="1.1"
87
+ xmlns="http://www.w3.org/2000/svg">
88
+ <g stroke={mode === "light" ? "#424245" : "white"} strokeWidth="0.5"
89
+ fillRule="evenodd">
90
+ <path
91
+ d="M28.2226562,20.3846154 C29.0546875,20.3846154 30.0976562,19.8048315 30.71875,19.0317864 C31.28125,18.3312142 31.6914062,17.352829 31.6914062,16.3744437 C31.6914062,16.2415766 31.6796875,16.1087095 31.65625,16 C30.7304687,16.0362365 29.6171875,16.640178 28.9492187,17.4494596 C28.421875,18.06548 27.9414062,19.0317864 27.9414062,20.0222505 C27.9414062,20.1671964 27.9648438,20.3121424 27.9765625,20.3604577 C28.0351562,20.3725366 28.1289062,20.3846154 28.2226562,20.3846154 Z M25.2929688,35 C26.4296875,35 26.9335938,34.214876 28.3515625,34.214876 C29.7929688,34.214876 30.109375,34.9758423 31.375,34.9758423 C32.6171875,34.9758423 33.4492188,33.792117 34.234375,32.6325493 C35.1132812,31.3038779 35.4765625,29.9993643 35.5,29.9389701 C35.4179688,29.9148125 33.0390625,28.9122695 33.0390625,26.0979021 C33.0390625,23.6579784 34.9140625,22.5588048 35.0195312,22.474253 C33.7773438,20.6382708 31.890625,20.5899555 31.375,20.5899555 C29.9804688,20.5899555 28.84375,21.4596313 28.1289062,21.4596313 C27.3554688,21.4596313 26.3359375,20.6382708 25.1289062,20.6382708 C22.8320312,20.6382708 20.5,22.5950413 20.5,26.2911634 C20.5,28.5861411 21.3671875,31.013986 22.4335938,32.5842339 C23.3476562,33.9129053 24.1445312,35 25.2929688,35 Z"
92
+ fill={mode === "light" ? "#424245" : "white"} fillRule="nonzero"/>
93
+ </g>
94
+ </svg>;
95
+
96
+ export const githubIcon = (mode: "light" | "dark") => <svg
97
+ fill={mode === "light" ? "#1c1e21" : "white"}
98
+ role="img"
99
+ viewBox="0 0 24 24"
100
+ width={28}
101
+ height={28}
102
+ xmlns="http://www.w3.org/2000/svg">
103
+ <path
104
+ d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/>
105
+ </svg>;
106
+
107
+
108
+ export const facebookIcon = (mode: "light" | "dark") => <svg
109
+ xmlns="http://www.w3.org/2000/svg"
110
+ width={28} height={28}
111
+ viewBox="0 0 90 90">
112
+ <g>
113
+ <path
114
+ d="M90,15.001C90,7.119,82.884,0,75,0H15C7.116,0,0,7.119,0,15.001v59.998 C0,82.881,7.116,90,15.001,90H45V56H34V41h11v-5.844C45,25.077,52.568,16,61.875,16H74v15H61.875C60.548,31,59,32.611,59,35.024V41 h15v15H59v34h16c7.884,0,15-7.119,15-15.001V15.001z"
115
+ fill={mode === "light" ? "#39569c" : "white"}/>
116
+ </g>
117
+ </svg>;
118
+
119
+ export const microsoftIcon = (mode: "light" | "dark") => <svg
120
+ xmlns="http://www.w3.org/2000/svg" width={28} height={28}
121
+ viewBox="0 0 480 480">
122
+ <g>
123
+ <path
124
+ d="M0.176,224L0.001,67.963l192-26.072V224H0.176z M224.001,37.241L479.937,0v224H224.001V37.241z M479.999,256l-0.062,224 l-255.936-36.008V256H479.999z M192.001,439.918L0.157,413.621L0.147,256h191.854V439.918z"
125
+ fill={mode === "light" ? "#00a2ed" : "white"}/>
126
+ </g>
127
+ </svg>;
128
+
129
+ export const twitterIcon = (mode: "light" | "dark") => <svg
130
+ xmlns="http://www.w3.org/2000/svg" width={28} height={28}
131
+ viewBox="0 0 24 24">
132
+ <path fill={mode === "light" ? "#00acee" : "white"}
133
+ d="M24 4.557c-.883.392-1.832.656-2.828.775 1.017-.609 1.798-1.574 2.165-2.724-.951.564-2.005.974-3.127 1.195-.897-.957-2.178-1.555-3.594-1.555-3.179 0-5.515 2.966-4.797 6.045-4.091-.205-7.719-2.165-10.148-5.144-1.29 2.213-.669 5.108 1.523 6.574-.806-.026-1.566-.247-2.229-.616-.054 2.281 1.581 4.415 3.949 4.89-.693.188-1.452.232-2.224.084.626 1.956 2.444 3.379 4.6 3.419-2.07 1.623-4.678 2.348-7.29 2.04 2.179 1.397 4.768 2.212 7.548 2.212 9.142 0 14.307-7.721 13.995-14.646.962-.695 1.797-1.562 2.457-2.549z"/>
134
+ </svg>;
135
+
@@ -0,0 +1,11 @@
1
+ import { CustomProvider, ReCaptchaEnterpriseProvider, ReCaptchaV3Provider } from "@firebase/app-check";
2
+
3
+ /**
4
+ * @group Firebase
5
+ */
6
+ export interface AppCheckOptions {
7
+ provider: CustomProvider | ReCaptchaV3Provider | ReCaptchaEnterpriseProvider;
8
+ isTokenAutoRefreshEnabled?: boolean;
9
+ debugToken?: string;
10
+ forceRefresh?: boolean;
11
+ }
@@ -0,0 +1,74 @@
1
+ import { ApplicationVerifier, ConfirmationResult, User as FirebaseUser } from "@firebase/auth";
2
+
3
+ import { AuthController, Role, User } from "@rebasepro/types";
4
+
5
+ /**
6
+ * @group Firebase
7
+ */
8
+ export type FirebaseSignInProvider =
9
+ | "password"
10
+ | "phone"
11
+ | "anonymous"
12
+ | "google.com"
13
+ | "facebook.com"
14
+ | "github.com"
15
+ | "twitter.com"
16
+ | "microsoft.com"
17
+ | "apple.com";
18
+
19
+ /**
20
+ * @group Firebase
21
+ */
22
+ export type FirebaseSignInOption = {
23
+ provider: FirebaseSignInProvider;
24
+ scopes?: string[];
25
+ customParameters?: Record<string, string>;
26
+ }
27
+
28
+ export type FirebaseUserWrapper = User & FirebaseUser & {
29
+ firebaseUser: FirebaseUser | null;
30
+ }
31
+
32
+ /**
33
+ * @group Firebase
34
+ */
35
+ export type FirebaseAuthController<USER extends User = FirebaseUserWrapper, ExtraData = any> =
36
+ AuthController<USER, ExtraData>
37
+ & {
38
+
39
+ confirmationResult?: ConfirmationResult;
40
+
41
+ googleLogin: () => void;
42
+
43
+ anonymousLogin: () => void;
44
+
45
+ appleLogin: () => void;
46
+
47
+ facebookLogin: () => void;
48
+
49
+ githubLogin: () => void;
50
+
51
+ microsoftLogin: () => void;
52
+
53
+ twitterLogin: () => void;
54
+
55
+ emailPasswordLogin: (email: string, password: string) => void;
56
+
57
+ fetchSignInMethodsForEmail: (email: string) => Promise<string[]>;
58
+
59
+ createUserWithEmailAndPassword: (email: string, password: string) => void;
60
+
61
+ sendPasswordResetEmail: (email: string) => Promise<void>;
62
+
63
+ phoneLogin: (phone: string, applicationVerifier: ApplicationVerifier) => void;
64
+
65
+ /**
66
+ * Skip login
67
+ */
68
+ skipLogin: () => void;
69
+
70
+ setUser: (user: USER | null) => void;
71
+
72
+ setUserRoles: (roles: Role[]) => void;
73
+
74
+ };
@@ -0,0 +1,3 @@
1
+ export * from "./auth";
2
+ export * from "./text_search";
3
+ export * from "./appcheck";
@@ -0,0 +1,42 @@
1
+ import { User as FirebaseUser } from "@firebase/auth";
2
+ import { FirebaseApp } from "@firebase/app";
3
+ import { EntityCollection } from "@rebasepro/types";
4
+
5
+ export type FirestoreTextSearchControllerBuilder = (props: {
6
+ firebaseApp: FirebaseApp;
7
+ }) => FirestoreTextSearchController;
8
+
9
+ /**
10
+ * Use this controller to return a list of ids from a search index, given a
11
+ * `path` and a `searchString`.
12
+ * Firestore does not support text search directly, so we need to rely on an external
13
+ * index, such as Algolia.
14
+ * Note that you will get text search requests for collections that have the
15
+ * `textSearchEnabled` flag set to `true`.
16
+ * @see performAlgoliaTextSearch
17
+ * @group Firebase
18
+ */
19
+ export type FirestoreTextSearchController = {
20
+ /**
21
+ * This method is called when a search delegate is ready to be used.
22
+ * Return true if this path can be handled by this controller.
23
+ * @param props
24
+ */
25
+ init: (props: {
26
+ path: string,
27
+ databaseId?: string,
28
+ collection?: EntityCollection
29
+ }) => Promise<boolean>,
30
+ /**
31
+ * Do the search and return a list of ids.
32
+ * @param props
33
+ */
34
+ search: (props: {
35
+ searchString: string,
36
+ path: string,
37
+ currentUser?: FirebaseUser,
38
+ databaseId?: string,
39
+ collection?: EntityCollection
40
+ }) => (Promise<readonly string[] | undefined>),
41
+
42
+ };
@@ -0,0 +1,27 @@
1
+ import { buildExternalSearchController } from "./text_search_controller";
2
+
3
+ /**
4
+ * Utility function to perform a text search in an algolia index,
5
+ * returning the ids of the entities.
6
+ * @param client The algolia client
7
+ * @param indexName
8
+ * @param query
9
+ * @group Firebase
10
+ */
11
+ export function performAlgoliaTextSearch(client: any, indexName: string, query: string): Promise<readonly string[]> {
12
+
13
+ console.debug("Performing Algolia query", client, query);
14
+
15
+ return client.searchSingleIndex({
16
+ indexName,
17
+ searchParams: { query },
18
+ }).then(({ hits }: any) => {
19
+ return hits.map((hit: any) => hit.objectID as string);
20
+ })
21
+ .catch((err: any) => {
22
+ console.error(err);
23
+ return [];
24
+ });
25
+ }
26
+
27
+
@@ -0,0 +1,149 @@
1
+ import { deleteField, DocumentSnapshot } from "@firebase/firestore";
2
+ import { EntityCollection, FirebaseCollection, Properties, Property } from "@rebasepro/types";
3
+ import { COLLECTION_PATH_SEPARATOR, sortProperties, stripCollectionPath } from "@rebasepro/common";
4
+
5
+ export function buildCollectionId(idOrPath: string, parentCollectionIds?: string[]): string {
6
+ if (!parentCollectionIds)
7
+ return stripCollectionPath(idOrPath);
8
+ return [...parentCollectionIds.map(stripCollectionPath), stripCollectionPath(idOrPath)].join(COLLECTION_PATH_SEPARATOR);
9
+ }
10
+
11
+
12
+
13
+ export const docsToCollectionTree = (docs: DocumentSnapshot[]): EntityCollection[] => {
14
+
15
+ const collectionsMap = docs.map((doc) => {
16
+ const id: string = doc.id;
17
+ const collection = docToCollection(doc);
18
+ return { [id]: collection };
19
+ }).reduce((a, b) => ({ ...a, ...b }), {});
20
+
21
+ const orderedKeys = Object.keys(collectionsMap).sort((a, b) => b.split(COLLECTION_PATH_SEPARATOR).length - a.split(COLLECTION_PATH_SEPARATOR).length);
22
+
23
+ orderedKeys.forEach((id) => {
24
+ const collection = collectionsMap[id];
25
+ if (id.includes(COLLECTION_PATH_SEPARATOR)) {
26
+ const parentId = id.split(COLLECTION_PATH_SEPARATOR).slice(0, -1).join(COLLECTION_PATH_SEPARATOR);
27
+ const parentCollection = collectionsMap[parentId];
28
+ if (parentCollection)
29
+ (parentCollection as FirebaseCollection).subcollections = () => [...((parentCollection as FirebaseCollection).subcollections?.() ?? []), collection];
30
+ delete collectionsMap[id];
31
+ }
32
+ });
33
+
34
+ return Object.values(collectionsMap);
35
+ }
36
+
37
+ export const docToCollection = (doc: DocumentSnapshot): EntityCollection => {
38
+ const data = doc.data();
39
+ if (!data)
40
+ throw Error("Entity collection has not been persisted correctly");
41
+ const propertiesOrder = data.propertiesOrder;
42
+ const properties = data.properties as Properties ?? {};
43
+
44
+ // Normalize enum values from object format to array format (sorted alphabetically)
45
+ const normalizedProperties = normalizePropertiesEnumValues(properties, true);
46
+ const sortedProperties = sortProperties(normalizedProperties, propertiesOrder);
47
+ return {
48
+ ...data,
49
+ properties: sortedProperties,
50
+ slug: data.id ?? data.alias ?? data.slug
51
+ } as EntityCollection;
52
+ }
53
+
54
+
55
+
56
+ /**
57
+ * Converts enum values from object format to array format.
58
+ * Firestore doesn't preserve object key order, so we must use arrays.
59
+ * When enum values are already stored as an array, their order is preserved
60
+ * (this is intentional - users can reorder columns in Kanban view).
61
+ * Only sort alphabetically when converting from legacy object format.
62
+ * @param enumValues - The enum values (object or array format)
63
+ * @param sortObjectFormat - If true, sort by id alphabetically when converting from object format
64
+ * @returns Array of EnumValueConfig objects
65
+ */
66
+ function normalizeEnumValuesToArray(
67
+ enumValues: unknown,
68
+ sortObjectFormat: boolean = false
69
+ ): unknown[] {
70
+ if (Array.isArray(enumValues)) {
71
+ // Already an array - preserve order! This order is intentional
72
+ // (e.g., user reordered Kanban columns)
73
+ return enumValues;
74
+ } else if (typeof enumValues === "object" && enumValues !== null) {
75
+ // Convert object to array format
76
+ // Object keys don't have guaranteed order in Firestore, so we sort alphabetically
77
+ const entries = Object.entries(enumValues).map(([id, value]) =>
78
+ typeof value === "string"
79
+ ? {
80
+ id,
81
+ label: value
82
+ }
83
+ : {
84
+ ...(value as object),
85
+ id
86
+ }
87
+ );
88
+ // Sort alphabetically by id when loading from Firestore object format
89
+ // This is the only case where sorting makes sense, since object key order is not preserved
90
+ if (sortObjectFormat) {
91
+ entries.sort((a, b) => String(a.id).localeCompare(String(b.id)));
92
+ }
93
+ return entries;
94
+ }
95
+ return [];
96
+ }
97
+
98
+ /**
99
+ * Normalizes all enum values in a properties object.
100
+ * @param properties - The properties object to normalize
101
+ * @param sortObjectFormat - If true, sort enum values alphabetically when converting from object format
102
+ * @returns Properties with normalized enum values
103
+ */
104
+ function normalizePropertiesEnumValues(
105
+ properties: Properties,
106
+ sortObjectFormat: boolean = false
107
+ ): Properties {
108
+ const result: Properties = {};
109
+ Object.entries(properties).forEach(([key, property]) => {
110
+ if (typeof property === "object" && property !== null) {
111
+ const normalizedProperty = { ...property } as Record<string, unknown>;
112
+
113
+ // Handle direct enum values
114
+ if (normalizedProperty.enum) {
115
+ normalizedProperty.enum = normalizeEnumValuesToArray(
116
+ normalizedProperty.enum,
117
+ sortObjectFormat
118
+ );
119
+ }
120
+
121
+ // Handle array properties with enum values in "of"
122
+ if (normalizedProperty.dataType === "array" && typeof normalizedProperty.of === "object" && normalizedProperty.of !== null) {
123
+ const ofProp = normalizedProperty.of as Record<string, unknown>;
124
+ if (ofProp.enum) {
125
+ normalizedProperty.of = {
126
+ ...ofProp,
127
+ enum: normalizeEnumValuesToArray(
128
+ ofProp.enum,
129
+ sortObjectFormat
130
+ )
131
+ };
132
+ }
133
+ }
134
+
135
+ // Handle map properties recursively
136
+ if (normalizedProperty.dataType === "map" && normalizedProperty.properties) {
137
+ normalizedProperty.properties = normalizePropertiesEnumValues(
138
+ normalizedProperty.properties as Properties,
139
+ sortObjectFormat
140
+ );
141
+ }
142
+
143
+ result[key] = normalizedProperty as unknown as Property;
144
+ } else {
145
+ result[key] = property;
146
+ }
147
+ });
148
+ return result;
149
+ }
@@ -0,0 +1,39 @@
1
+ import { FirebaseApp } from "@firebase/app";
2
+ import {
3
+ collection,
4
+ getDocs,
5
+ getFirestore,
6
+ limit as limitClause,
7
+ query,
8
+ QueryDocumentSnapshot
9
+ } from "@firebase/firestore";
10
+
11
+ export async function getFirestoreDataInPath(firebaseApp: FirebaseApp, path: string, parentPaths: string[], limit: number): Promise<object[]> {
12
+ const firestore = getFirestore(firebaseApp);
13
+ if (!parentPaths || parentPaths.length === 0) {
14
+ const q = query(collection(firestore, path), limitClause(limit));
15
+ return getDocs(q).then((querySnapshot) => {
16
+ return querySnapshot.docs.map(doc => doc.data());
17
+ });
18
+ } else {
19
+ let currentDocs: QueryDocumentSnapshot[] | undefined = undefined;
20
+ let index = 0;
21
+ const allPaths = parentPaths;
22
+ allPaths.push(path);
23
+ let parentPath: string | undefined = allPaths[0];
24
+ while (parentPath) {
25
+ if (currentDocs) {
26
+ currentDocs = (await Promise.all(currentDocs.map(async (doc) => {
27
+ const q = query(collection(firestore, doc.ref.path, parentPath as string), limitClause(5));
28
+ return (await getDocs(q)).docs;
29
+ }))).flat();
30
+ } else {
31
+ const q = query(collection(firestore, parentPath), limitClause(5));
32
+ currentDocs = (await getDocs(q)).docs;
33
+ }
34
+ index++;
35
+ parentPath = index < allPaths.length ? allPaths[index] : undefined;
36
+ }
37
+ return currentDocs ? currentDocs.map(doc => doc.data()) : [];
38
+ }
39
+ }
@@ -0,0 +1,7 @@
1
+ export * from "./collections_firestore";
2
+ export * from "./database";
3
+ export * from "./algolia";
4
+ export * from "./pinecone";
5
+ export * from "./text_search_controller";
6
+ export * from "./local_text_search_controller";
7
+ export * from "./rebase_search_controller";