@rebasepro/firebase 0.17.3 → 0.18.1

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,132 +0,0 @@
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: unknown) {
75
- console.error("Error initialising Firebase", e);
76
- setConfigError(hostingError + "\n" + (e instanceof Error ? 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
- }
@@ -1,28 +0,0 @@
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 DELETED
@@ -1,4 +0,0 @@
1
- export * from "./hooks";
2
- export * from "./types";
3
- export * from "./utils";
4
- export * from "./components";
@@ -1,11 +0,0 @@
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
- }
@@ -1,75 +0,0 @@
1
- import { ApplicationVerifier, ConfirmationResult, User as FirebaseUser } from "firebase/auth";
2
-
3
- import type { User } from "@rebasepro/types";
4
- import type { AuthController } from "@rebasepro/cms-types";
5
-
6
- /**
7
- * @group Firebase
8
- */
9
- export type FirebaseSignInProvider =
10
- | "password"
11
- | "phone"
12
- | "anonymous"
13
- | "google.com"
14
- | "facebook.com"
15
- | "github.com"
16
- | "twitter.com"
17
- | "microsoft.com"
18
- | "apple.com";
19
-
20
- /**
21
- * @group Firebase
22
- */
23
- export type FirebaseSignInOption = {
24
- provider: FirebaseSignInProvider;
25
- scopes?: string[];
26
- customParameters?: Record<string, string>;
27
- }
28
-
29
- export type FirebaseUserWrapper = User & FirebaseUser & {
30
- firebaseUser: FirebaseUser | null;
31
- }
32
-
33
- /**
34
- * @group Firebase
35
- */
36
- export type FirebaseAuthController<USER extends User = FirebaseUserWrapper, ExtraData = any> =
37
- AuthController<USER, ExtraData>
38
- & {
39
-
40
- confirmationResult?: ConfirmationResult;
41
-
42
- googleLogin: () => void;
43
-
44
- anonymousLogin: () => void;
45
-
46
- appleLogin: () => void;
47
-
48
- facebookLogin: () => void;
49
-
50
- githubLogin: () => void;
51
-
52
- microsoftLogin: () => void;
53
-
54
- twitterLogin: () => void;
55
-
56
- emailPasswordLogin: (email: string, password: string) => void;
57
-
58
- fetchSignInMethodsForEmail: (email: string) => Promise<string[]>;
59
-
60
- createUserWithEmailAndPassword: (email: string, password: string) => void;
61
-
62
- sendPasswordResetEmail: (email: string) => Promise<void>;
63
-
64
- phoneLogin: (phone: string, applicationVerifier: ApplicationVerifier) => void;
65
-
66
- /**
67
- * Skip login
68
- */
69
- skipLogin: () => void;
70
-
71
- setUser: (user: USER | null) => void;
72
-
73
- setUserRoles: (roles: string[]) => void;
74
-
75
- };
@@ -1,3 +0,0 @@
1
- export * from "./auth";
2
- export * from "./text_search";
3
- export * from "./appcheck";
@@ -1,42 +0,0 @@
1
- import { User as FirebaseUser } from "firebase/auth";
2
- import { FirebaseApp } from "firebase/app";
3
- import { CollectionConfig } 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?: CollectionConfig
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?: CollectionConfig
40
- }) => (Promise<readonly string[] | undefined>),
41
-
42
- };
@@ -1,27 +0,0 @@
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: { searchSingleIndex: (params: Record<string, unknown>) => Promise<{ hits: Array<{ objectID: string }> }> }, 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 }) => {
19
- return hits.map((hit) => hit.objectID);
20
- })
21
- .catch((err: unknown) => {
22
- console.error(err);
23
- return [];
24
- });
25
- }
26
-
27
-
@@ -1,150 +0,0 @@
1
- import { deleteField, DocumentSnapshot } from "firebase/firestore";
2
- import { CollectionConfig, FirebaseCollectionConfig, Properties, Property } from "@rebasepro/types";
3
- import { COLLECTION_PATH_SEPARATOR, sortProperties, stripCollectionPath } from "@rebasepro/common";
4
-
5
- export function buildCollectionId(idOrPath: string, parentCollectionSlugs?: string[], parentEntityIds?: string[]): string {
6
- if (!parentCollectionSlugs)
7
- return stripCollectionPath(idOrPath);
8
- return [...parentCollectionSlugs.map(stripCollectionPath), stripCollectionPath(idOrPath)].join(COLLECTION_PATH_SEPARATOR);
9
- }
10
-
11
-
12
- export const docsToCollectionTree = (docs: DocumentSnapshot[]): CollectionConfig[] => {
13
-
14
- const collectionsMap = docs.map((doc) => {
15
- const id: string = doc.id;
16
- const collection = docToCollection(doc);
17
- return { [id]: collection };
18
- }).reduce((a, b) => ({ ...a,
19
- ...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 FirebaseCollectionConfig).subcollections = () => [...((parentCollection as FirebaseCollectionConfig).subcollections?.() ?? []), collection];
30
- delete collectionsMap[id];
31
- }
32
- });
33
-
34
- return Object.values(collectionsMap);
35
- }
36
-
37
- export const docToCollection = (doc: DocumentSnapshot): CollectionConfig => {
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 CollectionConfig;
52
- }
53
-
54
-
55
- /**
56
- * Converts enum values from object format to array format.
57
- * Firestore doesn't preserve object key order, so we must use arrays.
58
- * When enum values are already stored as an array, their order is preserved
59
- * (this is intentional - users can reorder columns in Kanban view).
60
- * Only sort alphabetically when converting from legacy object format.
61
- * @param enumValues - The enum values (object or array format)
62
- * @param sortObjectFormat - If true, sort by id alphabetically when converting from object format
63
- * @returns Array of EnumValueConfig objects
64
- */
65
- function normalizeEnumValuesToArray(
66
- enumValues: unknown,
67
- sortObjectFormat = false
68
- ): unknown[] {
69
- if (Array.isArray(enumValues)) {
70
- // Already an array - preserve order! This order is intentional
71
- // (e.g., user reordered Kanban columns)
72
- return enumValues;
73
- } else if (typeof enumValues === "object" && enumValues !== null) {
74
- // Convert object to array format
75
- // Object keys don't have guaranteed order in Firestore, so we sort alphabetically
76
- const entries = Object.entries(enumValues).map(([id, value]) =>
77
- typeof value === "string"
78
- ? {
79
- id,
80
- label: value
81
- }
82
- : {
83
- ...(value as object),
84
- id
85
- }
86
- );
87
- // Sort alphabetically by id when loading from Firestore object format
88
- // This is the only case where sorting makes sense, since object key order is not preserved
89
- if (sortObjectFormat) {
90
- entries.sort((a, b) => String(a.id).localeCompare(String(b.id)));
91
- }
92
- return entries;
93
- }
94
- return [];
95
- }
96
-
97
- /**
98
- * Normalizes all enum values in a properties object.
99
- * @param properties - The properties object to normalize
100
- * @param sortObjectFormat - If true, sort enum values alphabetically when converting from object format
101
- * @returns Properties with normalized enum values
102
- */
103
- function normalizePropertiesEnumValues(
104
- properties: Properties,
105
- sortObjectFormat = false
106
- ): Properties {
107
- const result: Properties = {};
108
- Object.entries(properties).forEach(([key, property]) => {
109
- if (typeof property === "object" && property !== null) {
110
- const normalizedProperty = { ...property } as Record<string, unknown>;
111
-
112
- // Handle direct enum values
113
- if (normalizedProperty.enum) {
114
- normalizedProperty.enum = normalizeEnumValuesToArray(
115
- normalizedProperty.enum,
116
- sortObjectFormat
117
- );
118
- }
119
-
120
- const propType = normalizedProperty.type ?? normalizedProperty.dataType;
121
-
122
- // Handle array properties with enum values in "of"
123
- if (propType === "array" && typeof normalizedProperty.of === "object" && normalizedProperty.of !== null) {
124
- const ofProp = normalizedProperty.of as Record<string, unknown>;
125
- if (ofProp.enum) {
126
- normalizedProperty.of = {
127
- ...ofProp,
128
- enum: normalizeEnumValuesToArray(
129
- ofProp.enum,
130
- sortObjectFormat
131
- )
132
- };
133
- }
134
- }
135
-
136
- // Handle map properties recursively
137
- if (propType === "map" && normalizedProperty.properties) {
138
- normalizedProperty.properties = normalizePropertiesEnumValues(
139
- normalizedProperty.properties as Properties,
140
- sortObjectFormat
141
- );
142
- }
143
-
144
- result[key] = normalizedProperty as unknown as Property;
145
- } else {
146
- result[key] = property;
147
- }
148
- });
149
- return result;
150
- }
@@ -1,39 +0,0 @@
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((queryEntity) => {
16
- return queryEntity.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
- }
@@ -1,7 +0,0 @@
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";
@@ -1,143 +0,0 @@
1
- import { collection, getFirestore, onSnapshot, query } from "firebase/firestore";
2
- import Fuse from "fuse.js"
3
-
4
- import { FirebaseApp } from "firebase/app";
5
- import { CollectionConfig } from "@rebasepro/types";
6
- import { FirestoreTextSearchController, FirestoreTextSearchControllerBuilder } from "../types";
7
-
8
- const MAX_SEARCH_RESULTS = 80;
9
-
10
- export const localSearchControllerBuilder: FirestoreTextSearchControllerBuilder = ({
11
- firebaseApp
12
- }: {
13
-
14
- firebaseApp: FirebaseApp,
15
- }): FirestoreTextSearchController => {
16
-
17
- let currentPath: string | undefined;
18
- const indexes: Record<string, Fuse<object & { id: string }>> = {};
19
- const listeners: Record<string, () => void> = {};
20
-
21
- const destroyListener = (path: string) => {
22
- if (listeners[path]) {
23
- listeners[path]();
24
- delete listeners[path];
25
- delete indexes[path];
26
- }
27
- }
28
-
29
- const init = ({
30
- path,
31
- collection: collectionProp,
32
- databaseId
33
- }: {
34
- path: string,
35
- collection?: CollectionConfig,
36
- databaseId?: string
37
- }): Promise<boolean> => {
38
-
39
- if (currentPath && path !== currentPath) {
40
- destroyListener(currentPath)
41
- }
42
-
43
- currentPath = path;
44
-
45
- return new Promise((resolve, reject) => {
46
- if (collectionProp) {
47
- console.debug("Init local search controller", path);
48
- const firestore = databaseId ? getFirestore(firebaseApp, databaseId) : getFirestore(firebaseApp);
49
- const col = collection(firestore, path);
50
- listeners[path] = onSnapshot(query(col),
51
- {
52
- next: (entity) => {
53
- if (entity.metadata.fromCache && entity.metadata.hasPendingWrites) {
54
- return;
55
- }
56
- const docs = entity.docs.map(doc => ({
57
- id: doc.id,
58
- ...doc.data()
59
- }));
60
-
61
- indexes[path] = buildIndex(docs, collectionProp);
62
- console.debug("Added docs to index", path, docs.length);
63
- resolve(true);
64
- },
65
- error: (e) => {
66
- console.error("Error initializing local search controller", path);
67
- console.error(e);
68
- reject(e);
69
- }
70
- }
71
- );
72
- }
73
- });
74
- }
75
-
76
- const search = async ({
77
- searchString,
78
- path
79
- }: {
80
- searchString: string,
81
- path: string,
82
- databaseId?: string
83
- }) => {
84
- console.debug("Searching local index", path, searchString);
85
- const index = indexes[path];
86
- if (!index) {
87
- throw new Error(`Index not found for path ${path}`);
88
- }
89
- let searchResult = index.search(searchString);
90
- searchResult = searchResult.splice(0, MAX_SEARCH_RESULTS);
91
- searchResult = searchResult.sort((a, b) => {
92
- // Check if item A is an exact match
93
- const aExactMatch = a.item.id === searchString;
94
- // Check if item B is an exact match
95
- const bExactMatch = b.item.id === searchString;
96
-
97
- if (aExactMatch && !bExactMatch) {
98
- return -1; // Prioritize item A
99
- } else if (!aExactMatch && bExactMatch) {
100
- return 1; // Prioritize item B
101
- } else {
102
- // If both are exact matches or both are not, sort by Fuse's original score
103
- return (a.score ?? 0) - (b.score ?? 0);
104
- }
105
- });
106
- return searchResult.map((e) => e.item.id);
107
- };
108
-
109
- return {
110
- init,
111
- search
112
- }
113
- }
114
-
115
- function buildIndex(list: (object & { id: string })[], collection: CollectionConfig) {
116
-
117
- const keys = ["id", ...Object.keys(collection.properties)];
118
-
119
- const fuseOptions = {
120
- // isCaseSensitive: false,
121
- // includeScore: false,
122
- // shouldSort: true,
123
- // includeMatches: false,
124
- // findAllMatches: false,
125
- // minMatchCharLength: 1,
126
- // location: 0,
127
- threshold: 0.6,
128
- // distance: 100,
129
- // useExtendedSearch: false,
130
- // ignoreLocation: false,
131
- // ignoreFieldNorm: false,
132
- // fieldNormWeight: 1,
133
- includeScore: true,
134
- keys: [{
135
- name: "title",
136
- weight: 1.0
137
- }, ...keys.map(key => ({
138
- name: key,
139
- weight: 0.5
140
- }))]
141
- };
142
- return new Fuse<object & { id: string }>(list, fuseOptions);
143
- }