<?xml version="1.0" encoding="utf-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Code.Build 💻</title><description>Reimagining Code. - A blog about Databases, Searching, Indexing, Programming, Security, Hosting, and Other Website Technologies!</description><link>https://code.build</link><atom:link rel="self" type="application/rss+xml" href="https://code.build/feed"/><language>en</language><item><title>Firestore Tree Data Model</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Sun, 22 Sep 2024 13:02:05 GMT</pubDate><link>https://code.build/p/firestore-tree-data-model-iL6jjs</link><guid>https://code.build/p/firestore-tree-data-model-iL6jjs</guid><description><![CDATA[<p>We often need to model tree data in Firestore. Nested comments, categories, graphs, and groups are just some examples. Modeling is difficult, but it can be done with a few hacks.</p>
<p><img src="https://code.build/images/path/posts/25756056-906c-4111-a6ca-141a3da9e188/eadpcphkwnaugemvfg95c7al8.png" title="null" alt="Firestore Tree" /></p>
<h2>TL;DR</h2>
<p>When you want to query nested data in Firestore, you must model the data so that one query can get all documents. This can be possible by using a long string for the path. </p>
<h2>We Need One Query</h2>
<p>You can easily model the data for multiple queries. The original thought was to query all posts where the parent equals some ID. If there were children, query them separately. The problem with this method is that you will have multiple calls to Firestore. While you would be reading the same documents, you don’t want to have several fetches to Firestore on one page, especially when it can be avoided.</p>
<h2>Tree Demo in SvelteKit</h2>
<p>This demo uses SvelteKit, so I recommend you read the <a href="https://code.build/p/sveltekit-todo-app-with-firebase-C73xaa">Firebase SvelteKit</a> post first. There are many advanced techniques, but I am only concentrating on the data model for this post. I use Firebase Client, but I could have equally used Firebase Admin with SvelteKit Form Actions.</p>
<h2>Hierarchy</h2>
<p>A tree is data that can belong to other data in a hierarchy. To nest data, we must use <code>startsWith</code> character hack mentioned in the <a href="https://code.build/p/firestore-fuzzy-full-text-search-Ut2Smh#idea-2---character-hack">Fuzzy Full-Text Search</a> post. The algorithm will be slightly different, but we will order the <code>path</code> and <code>parent</code> variables to get the desired results.</p>
<h2>Tree Data Model</h2>
<p>We are building a comment system similar to a Reddit Clone, where people can comment on one post in a tree fashion.</p>
<h3>Short Document ID</h3>
<p>We will create a short ID for faster searching and longer string storage. Theoretically, a document can be as big as 1MB, so one field could be up to that large.</p>
<pre><code class="language-tsx">const id = doc(
    collection(db, &#39;comments&#39;)
).id.substring(0, 5);
</code></pre>
<p>This limits the document ID to 5 characters, although you can use whatever length you like.</p>
<h3>Creating a Comment Document</h3>
<p>We will create the document with a few fields.</p>
<pre><code class="language-tsx">export const addComment = async (event: SubmitEvent) =&gt; {

    const { text, parent: _parent, formElement } = getComment(event);

    const level = _parent.split(&#39;/&#39;).length + 1 || 1;
    const parent = _parent.split(&#39;/&#39;).join(&#39;_&#39;);

    const currentUser = auth.currentUser;

    if (!currentUser) {
        throw &#39;No user!&#39;;
    }

    // create short ID
    const id = doc(
        collection(db, &#39;comments&#39;)
    ).id.substring(0, 5);

    const path = parent ? `${parent}_${id}` : id;

    try {
        await setDoc(
            doc(db, `comments/${id}`),
            {
                createdBy: currentUser.uid,
                createdAt: serverTimestamp(),
                text,
                level,
                path,
                parent,
                votes: 0
            }
        );
        formElement.reset();

    } catch (e) {
        if (e instanceof FirebaseError) {
            console.error(e);
        }
    }
};
</code></pre>
<ul>
<li><code>text</code> field stores the comment</li>
<li><code>level</code> stores the level number from the root</li>
<li><code>path</code> stores the full path to the document. <code>/</code> will be replaced with <code>_</code> for searching.</li>
<li><code>parent</code> stores the full path to the parent document, or null. <code>/</code> will be replaced with <code>_</code> for searching.</li>
<li><code>votes</code> is not used in this demo, but could be used to create a voting mechanism similar to Reddit.</li>
</ul>
<h3>Deleting a Comment</h3>
<p>When we delete a comment, we must also delete all of its children. We search for them, then use <a href="https://code.build/p/firestore-secure-batch-increment-IKO13Z">batch</a> to delete them in a transaction.</p>
<pre><code class="language-tsx">export const deleteComment = async (path: string) =&gt; {

    const _path = path.split(&#39;/&#39;).join(&#39;_&#39;);

    const childrenSnap = await getDocs(
        query(
            collection(db, &#39;comments&#39;),
            ...startsWith(&#39;path&#39;, _path)
        )
    );

    if (childrenSnap.empty) {
        throw &#39;No ids!&#39;;
    }

    const batch = writeBatch(db);

    try {
        childrenSnap.docs.map(doc =&gt; {
            batch.delete(doc.ref);
        });
        await batch.commit();
    } catch (e) {
        if (e instanceof FirebaseError) {
            console.error(e);
        }
    }
};
</code></pre>
<h3>Starts With</h3>
<p>We have a shorter version of the <a href="https://code.build/p/firestore-fuzzy-full-text-search-Ut2Smh#idea-2---character-hack">startsWith in Fuzzy Text Search</a>. This allows you to search for something which starts with a character. We need to use it with the spread operator, as we cannot return more than one item, only an array of items.</p>
<pre><code class="language-tsx">export const startsWith = (
    fieldName: string,
    term: string
) =&gt; {
    return [
        orderBy(fieldName),
        startAt(term),
        endAt(term + &#39;~&#39;)
    ];
};
</code></pre>
<h3>Querying the Tree</h3>
<p>We query the tree by searching for <code>comments</code> ordered by the <code>parent</code> document. If we want the votes feature, we order by the <code>votes</code> secondly. This demo does not use votes, but the feature could easily be added.</p>
<pre><code class="language-tsx">export const useComments = (
    term: string | null = null,
    levels: number[] = []
) =&gt; {

    const user = useUser();

    // filtering comments depend on user
    return derived&lt;
        Readable&lt;UserType | null&gt;,
        CommentType[]
    &gt;(
        user, ($user, set) =&gt; {
            set([]);
            if (!$user) {
                return;
            }
            const queryConstraints = [];
            if (term) {
                const _term = term.split(&#39;/&#39;).join(&#39;_&#39;);
                queryConstraints.push(
                    where(&#39;path&#39;, &#39;&gt;=&#39;, _term),
                    where(&#39;path&#39;, &#39;&lt;=&#39;, _term + &#39;~&#39;)
                );
            }
            if (levels?.length) {
                queryConstraints.push(
                    where(&#39;level&#39;, &#39;in&#39;, levels)
                );
            }
            return onSnapshot(
                query(
                    collection(db, &#39;comments&#39;),
                    where(&#39;createdBy&#39;, &#39;==&#39;, $user.uid),
                    orderBy(&#39;path&#39;),
                    orderBy(&#39;votes&#39;, &#39;desc&#39;),
                    ...queryConstraints
                ), (q) =&gt; set(snapToData(q))
            );
        });
};
</code></pre>
<p>If we enter a <code>term</code> when creating this query, it will only display items under that parent. This is where we need the <a href="https://code.build/p/firestore-fuzzy-full-text-search-Ut2Smh#idea-2---character-hack">startsWith</a> hack again.</p>
<h3>Snapshot Data</h3>
<p>We must take our data and modify it in a tree format. Remember our data is sorted correctly, but we need to display it in a tree fashion.</p>
<pre><code class="language-tsx">export const snapToData = (
    q: QuerySnapshot&lt;DocumentData, DocumentData&gt;
) =&gt; {

    // creates comment data from snapshot
    if (q.empty) {
        return [];
    }
    const comments = q.docs.map((doc) =&gt; {
        const data = doc.data({
            serverTimestamps: &#39;estimate&#39;
        });
        const createdAt = data[&#39;createdAt&#39;] as Timestamp;
        const comment = {
            ...data,
            path: data[&#39;path&#39;]?.split(&#39;_&#39;).join(&#39;/&#39;),
            parent: data[&#39;parent&#39;]?.split(&#39;_&#39;).join(&#39;/&#39;),
            createdAt: createdAt.toDate(),
            id: doc.id
        };
        return comment;
    }) as CommentType[];

    // create children from IDs
    const _comments = nestedComments(comments);
    if (dev) {
        console.log(_comments);
    }
    return _comments;
}
</code></pre>
<p>Our <code>parent</code> and <code>path</code> are reformed to a path format.</p>
<h3>Threaded Comments</h3>
<p>To get our data in the right place, we need to put all children in a <code>children</code> array, and all their children in a <code>children</code> array etc. This will allow us to display the data correctly in our component template.</p>
<pre><code class="language-tsx">export const nestedComments = (comments: CommentType[]) =&gt; {
    const commentMap: Record&lt;string, CommentType&gt; = {};
    const result: CommentType[] = [];

    // Initialize the commentMap and set up children arrays
    for (let i = 0; i &lt; comments.length; i++) {
        const comment = comments[i];
        comment.children = [];
        commentMap[comment.path] = comment;
    }

    // Nest comments under their parents, or handle cases where parent is missing
    for (const comment of comments) {
        // If the comment doesn&#39;t have a parent, add it to the top-level result immediately
        if (!comment.parent) {
            result.push(comment);
            continue;
        }

        // If the parent is not in the dataset, handle the child as a top-level comment (or change as needed)
        const parent = commentMap[comment.parent];
        if (!parent) {
            result.push(comment);
            continue;
        }

        // Add the comment to the parent&#39;s children array if the parent exists
        parent.children!.push(comment);
    }

    return result;
};
</code></pre>
<h2>Recursive Component</h2>
<p>Now our data is ready to be displayed recursively in our component.</p>
<pre><code class="language-tsx">&lt;script lang=&quot;ts&quot;&gt;
	import Comment from &#39;$lib/components/comment.svelte&#39;;
	import Comments from &#39;$lib/components/comments.svelte&#39;;

	export let comments: CommentType[];
&lt;/script&gt;

{#each comments as comment}
	&lt;Comment text={comment.text} path={comment.path}&gt;
		{#if comment.children?.length}
			&lt;Comments comments={comment.children} /&gt;
		{/if}
	&lt;/Comment&gt;
{/each}
</code></pre>
<h2>Depth</h2>
<p>Finally, we may want to display only a certain depth in our tree. Remember, a depth is calculated from the current post in whatever position it may be in. For example, a comment in the 5th position but the 3rd position on that page will have a depth of 5 on the main page and 3 on that comment page.</p>
<pre><code class="language-tsx">where(&#39;level&#39;, &#39;in&#39;, levels)
</code></pre>
<p>We calculate the levels on that page and add them to the query in an array (or multiple where clauses). Three positions starting at level 3, will have a value of <code>[3, 4, 5]</code> in the levels array. This works as a filter, so we don’t over-fetch unneeded data.</p>
<h2>About the Demo Tree</h2>
<p>This demo creates each comment tree using the user’s ID. A real comment tree will probably use a post ID. This is only for demo purposes, so users can have their own tree; we don’t want spam.</p>
<h2>Bonus: Votes</h2>
<p>With <code>votes</code> being the second <code>orderBy</code> clause, you could easily modify this demo to be a Reddit Clone. Up and Down clicks on the votes would increment the votes counter on the post. I didn’t do that for simplification, but it would be an easy addition.</p>
<p>⚠️ Don’t forget to add the necessary indexes from the <code>console.log</code> when you run the demo locally using your database.</p>
<p><strong>Demo:</strong> <a href="https://firebase-nested-data.vercel.app/">Vercel Serverless</a></p>
<p><strong>Repo:</strong> <a href="https://github.com/jdgamble555/firebase-nested-data">GitHub</a></p>
]]></description><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category></item><item><title>Passwordless Login in Firebase</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Sun, 15 Sep 2024 13:00:00 GMT</pubDate><link>https://code.build/p/passwordless-login-in-firebase-GekbXt</link><guid>https://code.build/p/passwordless-login-in-firebase-GekbXt</guid><description><![CDATA[<p>I’m a huge fan of passwordless login. You will notice I do not have any articles on creating, resetting, or changing a password. I hate passwords. We do not need them in 2024, and we should not design an application that still uses them. Not only are they insecure, they require more work to implement. Let’s simplify, shall we?</p>
<p><img src="https://code.build/images/path/posts/2d547c4e-5cc1-4223-840d-0e6df43c2386/gapjizkbcqylkryufg6jmjige.png" title="null" alt="Login with Magic Link" /></p>
<h2>TL;DR</h2>
<p>Passwordless login is the future. You can log in with only an email using a Magic Link. While you can attach the email address to the link, Firebase recommends saving the email address in the local storage for retrieval. Passwords are insecure.</p>
<h2>Magic Link Login</h2>
<p>Our application will request an email from the user. The email is saved to the local storage. The user will receive an email confirming their email address. When the user clicks on the link, the login page obtains the email address from the local storage and logs in. They must confirm their email address if they click the link from a different device.</p>
<h2>Note on SvelteKit</h2>
<p>This demo uses SvelteKit, but all premises are the same for any framework. See the <a href="https://code.build/p/sveltekit-todo-app-with-firebase-C73xaa">SvelteKit Setup</a> for Firebase configuration.</p>
<h2>Login Functions</h2>
<p>First, we need to send the magic link.</p>
<pre><code class="language-jsx">export const sendMagicLink = async (
    email: string,
    originURL: string
) =&gt; {
    try {
        await sendSignInLinkToEmail(auth, email, {
            handleCodeInApp: true,
            url: originURL + &#39;/auth/callback&#39;
        });
    } catch (e) {
        if (e instanceof FirebaseError) {
            console.error(e);
            return e;
        }
    }
};
</code></pre>
<p>We also need to sign in using the magic link.</p>
<pre><code class="language-jsx">export const signInWithMagic = async (
    email: string,
    url: string
) =&gt; {
    try {
        await signInWithEmailLink(auth, email, url);
    } catch (e) {
        if (e instanceof FirebaseError) {
            console.error(e);
            return e;
        }
    }
};
</code></pre>
<p>Finally, we should be able to test if the sign in URL is valid.</p>
<pre><code class="language-jsx">export const isMagicLinkURL = (url: string) =&gt; {
    return isSignInWithEmailLink(auth, url);
};
</code></pre>
<h3>Firebase Helper</h3>
<p>This is optional, but it allows you to check for a user immediately in the loader function. This can only run on the browser. If you need server authentication, see <a href="https://code.build/p/firebase-now-works-on-the-server-eFtYMt">Server App</a> or <a href="https://code.build/p/sveltekit-todo-app-with-firebase-admin-tqdc5j">Firebase Admin</a>.</p>
<pre><code class="language-jsx">export const getUser = async () =&gt;
    new Promise&lt;User | null&gt;((resolve, reject) =&gt; {
        const unsubscribe = onIdTokenChanged(auth, (user) =&gt; {
            unsubscribe();
            resolve(user);
        }, (error) =&gt; {
            unsubscribe();
            reject(error);
        });
    });
</code></pre>
<p>📝 You should not use Svelte Stores in a load function, but you can use promises. </p>
<p>📝 You must wait for <code>onIdTokenChanged</code> or <code>onAuthStateChanged</code> as <code>auth.currentUser</code> does not WAIT for the latest value.</p>
<h2>Login Page</h2>
<p>The login page at <code>/login</code> works as expected. </p>
<h3>Route Guard</h3>
<p>We create a route guard in the loader function at <code>page.ts</code> using the <code>getUser()</code> method.</p>
<pre><code class="language-jsx">import { getUser } from &#39;$lib/use-user&#39;;
import { redirect } from &#39;@sveltejs/kit&#39;;
import type { PageLoad } from &#39;./$types&#39;;
import { browser } from &#39;$app/environment&#39;;

export const load: PageLoad = async () =&gt; {

    if (!browser) {
        return {
            loading: true
        };
    }

    const loggedIn = !!(await getUser());

    if (loggedIn) {
        return redirect(302, &#39;/&#39;);
    }
};
</code></pre>
<p>You should not see a login page if a user is already logged in.</p>
<h3>Login</h3>
<p>When not loading, the login page sends the user an email with a valid login link.</p>
<pre><code class="language-tsx">&lt;script lang=&quot;ts&quot;&gt;
	import { page } from &#39;$app/stores&#39;;
	import LoginForm from &#39;$lib/login-form.svelte&#39;;
	import { getEmail } from &#39;$lib/tools&#39;;
	import { sendMagicLink } from &#39;$lib/use-user&#39;;
	import { error } from &#39;@sveltejs/kit&#39;;
	import type { PageData } from &#39;./$types&#39;;
	import Loading from &#39;$lib/loading.svelte&#39;;

	export let data: PageData;

	let emailSent = false;

	const sendLink = async (event: SubmitEvent) =&gt; {
		const email = getEmail(event);
		const originURL = $page.url.origin;
		const sendError = await sendMagicLink(email, originURL);
		if (sendError) {
			error(500, sendError.message);
		}
		localStorage.setItem(&#39;emailForSignIn&#39;, email);
		emailSent = true;
		setTimeout(() =&gt; (emailSent = false), 5000);
	};
&lt;/script&gt;

{#if data.loading}
	&lt;Loading /&gt;
	&lt;p class=&quot;my-5 text-center text-xl font-bold&quot;&gt;Loading...&lt;/p&gt;
{:else}
	&lt;main class=&quot;mt-10 flex flex-col items-center justify-center gap-5&quot;&gt;
		&lt;LoginForm isConfirmPage={false} on:submit={sendLink} /&gt;
		{#if emailSent}
			&lt;p class=&quot;text-blue-500&quot;&gt;
				Email Sent! Check your mailbox. If you don&#39;t see it look under junk or spam!
			&lt;/p&gt;
		{/if}
	&lt;/main&gt;
{/if}
</code></pre>
<p>It then saves the email as <code>emailForSignIn</code> in local storage.</p>
<h3>Login Form</h3>
<p>I share the login form with the confirm email form since they share almost identical characteristics.</p>
<pre><code class="language-tsx">&lt;script lang=&quot;ts&quot;&gt;
	export let isConfirmPage: boolean;
&lt;/script&gt;

&lt;form on:submit&gt;
	&lt;div class=&quot;flex min-w-72 flex-col gap-5 rounded border p-5&quot;&gt;
		&lt;h1 class=&quot;text-3xl&quot;&gt;Login with Magic Link&lt;/h1&gt;
		&lt;p class=&quot;my-5&quot;&gt;
			{#if isConfirmPage}
				Please confirm your email address to login!
			{:else}
				Enter your email address to receive a magic link to login!
			{/if}
		&lt;/p&gt;
		&lt;div&gt;
			&lt;label for=&quot;email&quot; class=&quot;mb-2 block text-sm font-medium text-gray-900 dark:text-white&quot;&gt;
				Email Address
			&lt;/label&gt;
			&lt;input
				type=&quot;email&quot;
				id=&quot;email&quot;
				name=&quot;email&quot;
				class=&quot;block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500 dark:border-gray-600 dark:bg-gray-700 dark:text-white dark:placeholder-gray-400 dark:focus:border-blue-500 dark:focus:ring-blue-500&quot;
			/&gt;
		&lt;/div&gt;
		&lt;button
			type=&quot;submit&quot;
			class=&quot;rounded-lg bg-stone-600 p-5 font-semibold text-white hover:opacity-75&quot;
		&gt;
			{isConfirmPage ? &#39;Login&#39; : &#39;Send Link&#39;}
		&lt;/button&gt;
	&lt;/div&gt;
&lt;/form&gt;
</code></pre>
<p>The form inputs an email and does something.</p>
<h3>Form Helper</h3>
<p>I also have a form helper that gets the email from a form event.</p>
<pre><code class="language-tsx">export const getEmail = (event: SubmitEvent) =&gt; {

    event.preventDefault();

    const { email } = Object.fromEntries(
        new FormData(event.target as HTMLFormElement)
    );

    if (!email || typeof email !== &#39;string&#39;) {
        throw &quot;No email!&quot;;
    }
    return email;
};
</code></pre>
<h2>Callback</h2>
<p>I created a callback at the url <code>auth/callback</code>, but this could be anywhere or even on the same login page.</p>
<h3>Loader</h3>
<p>The callback load function at <code>+page.ts</code> does a few things.</p>
<pre><code class="language-tsx">import { error, redirect } from &#39;@sveltejs/kit&#39;;
import type { PageLoad } from &#39;./$types&#39;;
import { getUser, isMagicLinkURL, signInWithMagic } from &#39;$lib/use-user&#39;;
import { browser } from &#39;$app/environment&#39;;

export const load: PageLoad = async ({ url }) =&gt; {

    const urlString = url.toString();

    // you can get the email this way as well
    // const _email = url.searchParams.get(&#39;email&#39;);
    // console.log(_email);

    if (!isMagicLinkURL(urlString)) {
        error(400, &#39;Bad request&#39;);
    }

    // just load on server
    if (!browser) {
        return {
            loading: true
        };
    }

    const loggedIn = !!(await getUser());

    // redirect if already logged in
    if (loggedIn) {
        return redirect(302, &#39;/&#39;);
    }

    // ask for email if no email
    const email = localStorage.getItem(&#39;emailForSignIn&#39;);
    if (!email) {
        return {
            loading: false
        };
    }

    // otherwise login
    const signInError = await signInWithMagic(email, urlString);
    if (signInError) {
        error(500, signInError.message);
    }
    localStorage.removeItem(&#39;emailForSignIn&#39;);
    return redirect(302, &#39;/&#39;);
};
</code></pre>
<ol>
<li>It displays an error if it is not a valid callback URL.</li>
<li>It will redirect you home if you have already logged in.</li>
<li>It checks for an email.<ol>
<li>Logs in if one exists</li>
<li>Displays the confirm email page otherwise</li>
</ol>
</li>
</ol>
<h2>Confirm Email Page</h2>
<p>You should only need to confirm an email address if the user is logging in from a different device or if local storage is disabled.</p>
<pre><code class="language-tsx">&lt;script lang=&quot;ts&quot;&gt;
	import { error } from &#39;@sveltejs/kit&#39;;
	import { goto } from &#39;$app/navigation&#39;;
	import { page } from &#39;$app/stores&#39;;
	import LoginForm from &#39;$lib/login-form.svelte&#39;;
	import { getEmail } from &#39;$lib/tools&#39;;
	import { signInWithMagic } from &#39;$lib/use-user&#39;;
	import Loading from &#39;$lib/loading.svelte&#39;;
	import type { PageData } from &#39;./$types&#39;;

	export let data: PageData;

	const confirmMagicLink = async (event: SubmitEvent) =&gt; {
		const email = getEmail(event);
		const urlString = $page.url.toString();
		const signInError = await signInWithMagic(email, urlString);
		if (signInError) {
			error(500, signInError.message);
		}
		localStorage.removeItem(&#39;emailForSignIn&#39;);
		goto(&#39;/&#39;);
	};
&lt;/script&gt;

{#if data.loading}
	&lt;Loading /&gt;
	&lt;p class=&quot;my-5 text-center text-xl font-bold&quot;&gt;Logging you in...&lt;/p&gt;
{:else}
	&lt;main class=&quot;mt-10 flex flex-col items-center justify-center gap-5&quot;&gt;
		&lt;LoginForm isConfirmPage={true} on:submit={confirmMagicLink} /&gt;
	&lt;/main&gt;
{/if}
</code></pre>
<h2>Add email to callback URL?</h2>
<p>Yes, you can.</p>
<pre><code class="language-tsx">// sendMagicLink()
...
await sendSignInLinkToEmail(auth, email, {
    handleCodeInApp: true,
    url: originURL + &#39;/auth/callback&#39; + `?email=${email}`
});
...
</code></pre>
<p>And you can receive it from the URL.</p>
<pre><code class="language-tsx">// auth/callback/+page.ts

export const load: PageLoad = async ({ url }) =&gt; {

    const _email = url.searchParams.get(&#39;email&#39;);    
    
    ...
</code></pre>
<p>☢️ The problem is that this is theoretically not secure. A user could hack your email and easily log in to a website with a script. A user could technically do this anyway, as they already have your email address from the email itself, but Firebase recommends against this. However, other login systems see no problem with this method. It is up to you.</p>
<p><strong>Repo:</strong> <a href="https://github.com/jdgamble555/svelte-firebase-magic-link">GitHub</a></p>
<p><strong>Demo:</strong> <a href="https://svelte-firebase-magic-link.vercel.app/">Vercel Serverless</a></p>
<p>J</p>
]]></description><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category></item><item><title>Handling Usernames in Firestore</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Sun, 08 Sep 2024 13:00:00 GMT</pubDate><link>https://code.build/p/handling-usernames-in-firestore-ilpEaH</link><guid>https://code.build/p/handling-usernames-in-firestore-ilpEaH</guid><description><![CDATA[<p>Most modern apps need usernames. We don’t want to share our email addresses publicly, using a long user ID would look odd. Firebase doesn’t handle usernames out of the box, but you can use them with a few minor modifications.</p>
<p><img src="https://code.build/images/path/posts/c0883b53-132e-4e64-b0bb-f81f6e29021f/yozpowfymunxcfwofg372cimk.png" title="null" alt="Firestore Username" /></p>
<h2>TL;DR</h2>
<p>When adding a new username, a separate <code>usernames</code> collection will complement the <code>users</code> collection. This will enforce uniqueness and allow for easy searchability. To save speed and reads, you should also add the <em>username</em> to a custom claim that can be accessible directly in the ID Token.</p>
<h2>Collections</h2>
<p>Depending on our app, our username could have three main points of reference:</p>
<ol>
<li><code>usernames/{username}</code> document</li>
<li>The field <code>username</code> in the <code>users/{user_id}</code> document</li>
<li>In the custom claim of the <code>currentUser</code> object</li>
</ol>
<p>📝 We need the <code>usernames</code> collection to maintain distinct usernames. Firestore does not handle unique values, so we must use a document ID to emulate this behavior.</p>
<h2>Adding a Username</h2>
<p>When we add a username, we should add it to both collections simultaneously. Here, I use <code>firebase-admin</code>, but you could also do this on the client.</p>
<pre><code class="language-jsx">const batch = adminDB.batch();

batch.set(
    adminDB.doc(`usernames/${username}`),
    {
        uid,
        username
    }
);

batch.set(
    adminDB.doc(`users/${uid}`),
    {
        username
    },
    { merge: true }
);

await batch.commit();
</code></pre>
<p>However, we should also delete the old username to keep our records clean. This allows you to use the same method for <em>updating a username</em> as for <em>adding a new one</em>.</p>
<pre><code class="language-jsx">const uid = token.uid;

// get current username if exists
const querySnap = await adminDB
    .collection(&#39;usernames&#39;)
    .where(&#39;uid&#39;, &#39;==&#39;, uid)
    .get();

const batch = adminDB.batch();

// delete current username if exists
if (!querySnap.empty) {

    querySnap.docs.forEach((snap) =&gt; {
        batch.delete(
            adminDB.doc(`usernames/${snap.id}`)
        )
    });
}

// add new username
try {
    batch.set(
        adminDB.doc(`usernames/${username}`),
        {
            uid,
            username
        }
    );

    batch.set(
        adminDB.doc(`users/${uid}`),
        {
            username
        },
        { merge: true }
    );

    await batch.commit();

} catch (e) {
    if (e instanceof FirebaseError) {
        error(400, e.message);
    }
}
</code></pre>
<p>📝 The <code>error()</code> method is in SvelteKit, but you could use whatever method your SSR framework uses to handle errors. You could also use a <a href="https://code.build/p/firestore-server-side-counter-Kj5PZ5#callable-function">callable function</a> if your app is only client-side.</p>
<h2>Custom Claims</h2>
<p>It is extremely easy to add a <code>username</code> custom claim on the backend.</p>
<pre><code class="language-jsx">try {
    await adminAuth.setCustomUserClaims(uid, {
        username
    });
} catch (e) {
    if (e instanceof FirebaseError) {
        error(400, e.message);
    }
}
</code></pre>
<p>⚠️ You cannot create custom claims on the client side; hence, it is recommended that you use the backend for the entire function.</p>
<p>📝 Keeping everything on the client side prevents you from having complex Firestore Rules when adding a username. If you keep these functions on the server, make the <code>usernames</code> collection <code>read-only</code>. See <a href="https://code.build/p/firestore-security-rules-example-guide-eyfhvI">Firestore Rules</a>.</p>
<h3>Refreshing Token on the Client</h3>
<p>Anytime you update a custom claim, you should update the client token.</p>
<pre><code class="language-jsx">await currentUser.getIdToken(true);
</code></pre>
<p>This should be done after your backend function is safely finished running. </p>
<h2>Verify the User</h2>
<p>Before adding any custom claims, you should always verify the user. You must pass the <code>idToken</code> to the server or Firebase function to do this.</p>
<pre><code class="language-jsx">export const verifyIdToken = async (idToken: string) =&gt; {
    try {
        const token = await adminAuth.verifyIdToken(idToken);
        return {
            token,
            error: null
        };
    } catch (e) {
        if (e instanceof FirebaseError) {
            return {
                error: e.message,
                token: null
            };
        }
    }
    return {
        error: null,
        token: null
    };
};
</code></pre>
<h3>Usage</h3>
<pre><code class="language-jsx">const { token, error: tokenError } = await verifyIdToken(idToken);

if (!token || tokenError) {
    error(401, &#39;Unauthorized!&#39;);
}
</code></pre>
<h2>Checking for Valid Username</h2>
<p>If you want to check for a valid username, you check for a username in the <code>usernames</code> collection.</p>
<pre><code class="language-jsx">export const usernameAvailable = async (
    username: string
) =&gt; {
    try {
        const snap = await getDoc(
            doc(db, &#39;usernames&#39;, username)
        );
        return {
            available: !snap.exists(),
            error: null
        };
    } catch (e) {
        if (e instanceof FirebaseError) {
            return {
                error: e.message,
                available: null
            };
        }
    }
    return {
        error: null,
        available: null
    };
};
</code></pre>
<h3>Debounce</h3>
<p>You can check while typing in an <em>input</em> field, but you should wait a few seconds before fetching Firebase to prevent unnecessary reads.</p>
<pre><code class="language-jsx">// reusable debounce function
export function useDebounce&lt;F extends (
    ...args: Parameters&lt;F&gt;
) =&gt; ReturnType&lt;F&gt;&gt;(
    func: F,
    waitFor: number,
): (...args: Parameters&lt;F&gt;) =&gt; void {
    let timeout: NodeJS.Timeout;
    return (...args: Parameters&lt;F&gt;): void =&gt; {
        clearTimeout(timeout);
        timeout = setTimeout(() =&gt; func(...args), waitFor);
    };
}
</code></pre>
<p>Then you call it with <code>oninput</code>. This varies depending on your framework.</p>
<pre><code class="language-jsx">const debouceUsername = useDebounce(() =&gt; { // do something here });

&lt;input oninput={debounceUsername} ... /&gt;
</code></pre>
<p>This reusable hook should work anywhere.</p>
<pre><code class="language-jsx">const { available, error } = await usernameAvailable(username);
</code></pre>
<h2>Login with Username Feature</h2>
<p>It is worth noting that you could also simulate a login with username feature, even though this is <em>NOT</em> supported out-of-the-box, by simply using the username to fetch the user&#39;s UID. This would have to be done on the backend, and would use <code>firebase-admin</code> as well.</p>
<h2>Example</h2>
<p><strong>Demo:</strong> <a href="https://svelte-firebase-username.vercel.app/">Vercel Serverless</a></p>
<p><strong>Repo:</strong> <a href="https://github.com/jdgamble555/svelte-firebase-username">GitHub</a></p>
<p>📝 Remember <code>firebase-admin</code> will ONLY work in NodeJS Environments. Use a <a href="https://code.build/p/firestore-server-side-counter-Kj5PZ5#callable-function">callable Firebase Function</a> to deploy to Bun, Vercel Edge, Cloudflare, or Deno environments.</p>
]]></description><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category></item><item><title>Firestore Fuzzy Full-Text Search</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Sun, 01 Sep 2024 13:06:05 GMT</pubDate><link>https://code.build/p/firestore-fuzzy-full-text-search-Ut2Smh</link><guid>https://code.build/p/firestore-fuzzy-full-text-search-Ut2Smh</guid><description><![CDATA[<p>After years of getting different ideas for self-indexing search using Firestore, I have finally reached a usable conclusion that doesn’t use too much space, allows typos, sorts by relevance, and works without a separate database.</p>
<p><img src="https://code.build/images/path/posts/632df51d-34e7-47f1-aec9-2963b7addf24/cpruypvnhxbrkudgfg2ai9e9c.png" title="null" alt="Firestore Fuzzy Search" /></p>
<h2>TL;DR</h2>
<p>Adding full-text search capabilities to Firestore can be easy; it just requires your own index. The fuzzy index with soundex makes full-text searching possible with little headache.</p>
<h2>Other Databases</h2>
<p>If you need advanced features, it is recommended to use an external database built for searching. However, I believe this is overkill for most apps, even large ones.</p>
<ul>
<li><a href="https://www.algolia.com/">Algolia</a></li>
<li><a href="https://www.elastic.co/">Elastic Search</a></li>
<li><a href="https://www.meilisearch.com/">MeiliSearch</a></li>
<li><a href="https://typesense.org/">Typesense</a></li>
<li><a href="https://github.com/prabhatsharma/zinc">Zinc Search</a></li>
</ul>
<p>🤦Yes, Google, the biggest search engine in the world, recommends a non-google product for Full-Text searching.</p>
<h2>Idea 1 - Arrays and Maps</h2>
<p>If all else fails, you can store keywords inside arrays and search for them with where clauses.</p>
<pre><code class="language-jsx">const db = getFirestore();

const q = query(
  collection(db, &#39;posts&#39;), 
  where(&#39;terms&#39;, &#39;array-contains&#39;, &#39;query&#39;)
);
</code></pre>
<p>For more on this:</p>
<ul>
<li><a href="https://code.build/p/firestore-many-to-many-array-contains-all-dyFZgf">array-contains-all</a></li>
<li><a href="https://code.build/p/firestore-many-to-many-maps-gm3B5X">maps</a></li>
</ul>
<h2>Idea 2 - Character Hack</h2>
<p>Firestore has a hack that allows you to search for the first character of a value.  You search for the word up until the end of the character set. I don’t understand why his works, but it does.</p>
<pre><code class="language-jsx">function startsWith(
    collectionRef: CollectionReference,
    fieldName: string,
    term: string
) {
    return query(
        collectionRef,
        orderBy(fieldName),
        startAt(term),
        endAt(term + &#39;~&#39;)
    );
}
</code></pre>
<p>📝 You can use <code>\uf8ff</code> or the character <code>~</code>. Both of these are known for the last character in the utf8 dataset.</p>
<pre><code class="language-jsx">// THIS IS THE SAME THING BTW
query(
  collectionRef,
  where(fieldName, &#39;&gt;=&#39;, term),
  where(fieldName, &#39;&lt;=&#39;, term + &#39;\uf8ff&#39;)
);
</code></pre>
<h3>Usage</h3>
<pre><code class="language-jsx">const d = await getDocs(
    startsWith(
        collection(db, &#39;posts&#39;),
        &#39;searchField&#39;,
        &#39;my-term&#39;
    )
);
</code></pre>
<h3>☢️ Problems</h3>
<ul>
<li>You can’t search for the start of a word, only the start of a string.</li>
<li>This will not work on maps or arrays.</li>
</ul>
<p>However, this can be useful in certain situations.</p>
<h2>Idea 3 - Trigram Fuzzy Search</h2>
<p>I built a <a href="https://github.com/jdgamble555/adv-firestore-functions/blob/master/src/shared/search.ts#L352">trigram search</a> once upon a time ago. I archived the package. It saves trigrams of a word in an array for searching. Imagine <code>elon musk baseball</code>:</p>
<pre><code class="language-jsx">// could be stored as array or map
[elo, lon, mus, usk, bas, ase, bal, all]
</code></pre>
<p>Then, you would search by using where clauses for each trigram or <a href="https://code.build/p/firestore-many-to-many-array-contains-all-dyFZgf">array-contains-all</a>.</p>
<pre><code class="language-jsx">where(&#39;elo&#39;, &#39;==&#39;, true),
where(&#39;lon&#39;, &#39;==&#39;, true)
</code></pre>
<h3>☢️ Problems</h3>
<p>Just like in all databases, trigram indexing is slow and painful. You also must convert each search phrase to a trigram for searching, as your query gets complex.</p>
<h2>Idea 4 - Every Character Index</h2>
<p>You could index your array or map to contain any iteration of the word for searching. Let’s say you want to store <code>elon musk</code> in a title.</p>
<pre><code class="language-jsx">[&#39;e&#39;]
[&#39;el&#39;]
[&#39;elo&#39;]
[&#39;elon&#39;]
[&#39;elon &#39;]
[&#39;elon m&#39;]
...
</code></pre>
<h3>☢️ Problems</h3>
<ul>
<li>You need to index every letter manually with repeated information. Imagine if you had a paragraph of text!</li>
</ul>
<h2>Solution</h2>
<p>We can build on these ideas and group the text in clusters. No one will search for a whole paragraph; they will search for a few words at a time.</p>
<pre><code class="language-jsx">const NUMBER_OF_WORDS = 4;

const createIndex = (text: string) =&gt; {

    // regex matches any alphanumeric from any language and strips spaces
    const finalArray: string[] = [];
    
    const wordArray = text
        .toLowerCase()
        .replace(/[^\p{L}\p{N}]+/gu, &#39; &#39;)
        .replace(/ +/g, &#39; &#39;)
        .trim()
        .split(&#39; &#39;);

    do {
        finalArray.push(
            wordArray.slice(0, NUMBER_OF_WORDS).join(&#39; &#39;)
        );
        wordArray.shift();

    } while (wordArray.length !== 0);

    return finalArray;
};
</code></pre>
<p>This will strip out all non-alphanumeric characters and make an array based on the words in groups of 4. For example, this text: </p>
<ul>
<li>Baseball is a game of strategy and skill, where every pitch, swing, and catch can change the course of the game in an instant.</li>
</ul>
<p>We would have something like this:</p>
<pre><code class="language-jsx">[&quot;baseball is a game&quot;],
[&quot;is a game of&quot;],
[&quot;a game of strategy&quot;],
[&quot;game of strategy and&quot;],
[&quot;of strategy and skill&quot;],
...
</code></pre>
<p>This does not help us, as we need to be able to search as we type the word. So, again, we must go through every iteration of that iteration. </p>
<pre><code class="language-jsx">for (const phrase of index) {
    if (phrase) {
        let v = &#39;&#39;;
        for (let i = 0; i &lt; phrase.length; i++) {
            v = phrase.slice(0, i + 1).trim();
            // increment for relevance
            m[v] = m[v] ? m[v] + 1 : 1;
        }
    }
}
</code></pre>
<p>And we end up with something like this:</p>
<p><img src="https://code.build/images/path/posts/632df51d-34e7-47f1-aec9-2963b7addf24/ftyhqlktzqywoltpfg2ajl0je.png" title="null" alt="Word Index Firestore" /></p>
<h3>Relevance</h3>
<p>Instead of an array, we store it as a map. Notice the number by the word. Luckily, we don’t have to store repeated phrases like “and.” We can increment the value for each repetition of the words. This gives us relevance. </p>
<h3>Searching</h3>
<p>To search, we find where the search field is equal to <code>searchField.term</code>. Since we are ordering by this also, we don’t need a <code>where</code> clause.</p>
<pre><code class="language-jsx">const data = await getDocs(
  query(
    collection(db, &#39;posts&#39;),
    orderBy(`searchField.${term}`),
    limit(5)
  )
);
</code></pre>
<h2>Fuzzy Search</h2>
<p>What we really want is not an exact search but a fuzzy search. We need some kind of typo tolerance.</p>
<h3>Typo Tolerance with Soundex</h3>
<p>The soundex algorithm has been used for years to simulate typo tolerance. It allows you to store text as sound patterns, not just as text.</p>
<pre><code class="language-jsx">// Take any string, and return the soundex
export function soundex(s: string): string {
    const a = s.toLowerCase().split(&quot;&quot;);
    const f = a.shift() as string;
    let r = &quot;&quot;;
    const codes: Record&lt;string, number | string&gt; = {
        a: &quot;&quot;,
        e: &quot;&quot;,
        i: &quot;&quot;,
        o: &quot;&quot;,
        u: &quot;&quot;,
        b: 1,
        f: 1,
        p: 1,
        v: 1,
        c: 2,
        g: 2,
        j: 2,
        k: 2,
        q: 2,
        s: 2,
        x: 2,
        z: 2,
        d: 3,
        t: 3,
        l: 4,
        m: 5,
        n: 5,
        r: 6,
    };
    r = f + a
        .map((v: string) =&gt; codes[v])
        .filter((v, i: number, b) =&gt;
            i === 0 ? v !== codes[f] : v !== b[i - 1])
        .join(&quot;&quot;);
    return (r + &quot;000&quot;).slice(0, 4).toUpperCase();
}
</code></pre>
<p>📝 There are different versions for different languages. Here is one for <a href="https://github.com/thejellyfish/soundex-fr/blob/master/index.js">French</a>, for example.</p>
<p>Using this pattern, we can slightly modify our storage mechanism to translate for soundex first.</p>
<pre><code class="language-jsx">const temp = [];
// translate to soundex
for (const i of index) {
  temp.push(i.split(&#39; &#39;).map(
	  (v: string) =&gt; soundex(v)
  ).join(&#39; &#39;));
}
index = temp;
// add each iteration from the createIndex
for (const phrase of index) {
  if (phrase) {
	  let v = &#39;&#39;;
	  const t = phrase.split(&#39; &#39;);
	  while (t.length &gt; 0) {
		  const r = t.shift();
		  v += v ? &#39; &#39; + r : r;
		  // increment for relevance
		  m[v] = m[v] ? m[v] + 1 : 1;
	  }
  }
}
</code></pre>
<p>The beauty of this is it greatly simplifies your storage. </p>
<p><img src="https://code.build/images/path/posts/632df51d-34e7-47f1-aec9-2963b7addf24/zbmzjdfjvbluolrhfg2ajmg2h.png" title="null" alt="Fuzzy Index Firestore" /></p>
<p>You’re only storing the sound itself, not the letters, and there are fewer iterations of sounds than words. </p>
<p>Remember to translate the text to soundex before you search as well!</p>
<pre><code class="language-jsx">
// get the soundex terms
const searchText = text
  .trim()
  .split(&#39; &#39;)
  .map(v =&gt; soundex(v))
  .join(&#39; &#39;);

// search
const data = getDocs(
  query(
    collection(db, &#39;posts&#39;),
    orderBy(`search.${searchText}`),
    limit(50)
  )
);
</code></pre>
<p>💵 This is simpler and faster than <a href="https://code.build/p/firestore-vector-full-text-search-Gli7vK">Vector Search</a>!</p>
<p>🤷 I have archived my <a href="https://github.com/jdgamble555/j-firebase/tree/master">j-firebase</a> package, as I lack time to keep up with a repo. </p>
<p>📝 For an extra idea, you could also theoretically add <a href="https://gist.github.com/sebleier/554280">stop-words</a>, and ignore indexing them.</p>
<h2>Backend or Frontend</h2>
<p>In practice, you should build a trigger function on each document creation for your collection to handle this indexing. The second best option is to use a backend function to add the data simultaneously. Technically, you can do this on the frontend with no problem, but offloading the generation to the backend makes more sense.</p>
<p><strong>Demo:</strong> <a href="https://firebase-fuzzy-search.vercel.app/">Vercel Serverless</a></p>
<p><strong>Repo:</strong> <a href="https://github.com/jdgamble555/firebase-fuzzy-search">GitHub</a></p>
<p>J</p>
]]></description><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category></item><item><title>Firestore Vector Full-Text Search</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Sun, 25 Aug 2024 13:00:00 GMT</pubDate><link>https://code.build/p/firestore-vector-full-text-search-Gli7vK</link><guid>https://code.build/p/firestore-vector-full-text-search-Gli7vK</guid><description><![CDATA[<p>With the AI revolution, Vector Databases have become popular. Almost every big database that didn’t already have the option has added Vector support, and Firestore is no different. This demo was incredibly tedious and difficult to build; I will try to simplify it and show you what I learned.</p>
<p><img src="https://code.build/images/path/posts/25b23b5f-d9f2-4dde-baf3-1d37adc417ea/vqzjhbqyfhgathdrfg0b25mn5.png" title="null" alt="Firebase Vector Search Example" /></p>
<h2>TL;DR</h2>
<p>While Vector Search adds a type of full-text search support to your app, you must create an embedding for every search term before searching. This can be slow and cumbersome. The Vector Search is really just a document sort, and not a filter. However, it is very powerful and pretty cool in my book. </p>
<h2>Vector Setup</h2>
<ol>
<li>Enable the <a href="https://cloud.google.com/vertex-ai/generative-ai/docs/embeddings/get-text-embeddings">Vector AI API</a> in Google Console.</li>
<li>Get a <a href="https://code.build/p/sveltekit-todo-app-with-firebase-admin-tqdc5j#firebase-admin">Firebase Admin Private Key</a> and add it to your <code>.env</code> file.</li>
<li>Add <a href="https://console.cloud.google.com/iam-admin/iam">Vertex AI Administrator</a> to the Firebase IAM account you just generated.</li>
<li>Install <em>Firebase Admin for NodeJS</em></li>
</ol>
<pre><code class="language-jsx">npm i -D firebase-admin
</code></pre>
<p><em>For non-Node environments, you must use the Firebase REST API instead.</em></p>
<ol>
<li>(Optional) Install Vertex AI for NodeJS</li>
</ol>
<pre><code class="language-jsx">npm i -D @google-cloud/aiplatform
</code></pre>
<p>I have also included the REST API version, which I find more intuitive in this case.</p>
<h2>Firebase Admin Setup</h2>
<p>Depending on your Framework, this could vary a bit, but the premise is the same. </p>
<pre><code class="language-jsx">import { PRIVATE_FIREBASE_ADMIN_CONFIG } from &#39;$env/static/private&#39;;
import { getApps, initializeApp, cert, getApp } from &#39;firebase-admin/app&#39;;
import { getAuth } from &#39;firebase-admin/auth&#39;;
import { getFirestore } from &#39;firebase-admin/firestore&#39;;

const firebase_admin_config = JSON.parse(PRIVATE_FIREBASE_ADMIN_CONFIG);

// initialize admin firebase only once
export const adminApp = getApps().length
    ? getApp()
    : initializeApp({
        credential: cert(firebase_admin_config),
        projectId: firebase_admin_config.project_id
    });

export const adminAuth = getAuth(adminApp);
export const adminDB = getFirestore(adminApp, &#39;firestore-testing&#39;);
</code></pre>
<p>Notice I am using <code>firestore-testing</code> as a secondary database. We import the key from the <code>.env</code> file created before.</p>
<h2>Embeddings</h2>
<p>The key to Vector Search is generating embeddings before storing data and searching for data. Unfortunately, this means you must make two API calls every time you search. One call will translate the data from your query text, and another will be used to search the Firestore database.</p>
<h3>Vertex AI Model</h3>
<p>You can use OpenAI or any other LLM, but Google already has a model ready for text searching.</p>
<table>
<thead>
<tr>
<th>English models</th>
<th>Multilingual models</th>
</tr>
</thead>
<tbody><tr>
<td><code>textembedding-gecko@001</code></td>
<td><code>textembedding-gecko-multilingual@001</code></td>
</tr>
<tr>
<td><code>textembedding-gecko@003</code></td>
<td><code>text-multilingual-embedding-002</code></td>
</tr>
<tr>
<td><code>text-embedding-004</code></td>
<td></td>
</tr>
<tr>
<td><code>text-embedding-preview-0815</code></td>
<td></td>
</tr>
</tbody></table>
<p>For English, the recommended model is <code>text-embedding-004</code>. </p>
<h3>Task Type</h3>
<p>You also must select how you’re storing the data in the embedding. </p>
<table>
<thead>
<tr>
<th>Task Type</th>
<th>Description</th>
</tr>
</thead>
<tbody><tr>
<td><code>SEMANTIC_SIMILARITY</code></td>
<td>Used to generate embeddings that are optimized to assess text similarity</td>
</tr>
<tr>
<td><code>CLASSIFICATION</code></td>
<td>Used to generate embeddings that are optimized to classify texts according to preset labels</td>
</tr>
<tr>
<td><code>CLUSTERING</code></td>
<td>Used to generate embeddings that are optimized to cluster texts based on their similarities</td>
</tr>
<tr>
<td><code>RETRIEVAL_DOCUMENT</code>, <code>RETRIEVAL_QUERY</code>, <code>QUESTION_ANSWERING</code>, and <code>FACT_VERIFICATION</code></td>
<td>Used to generate embeddings that are optimized for document search or information retrieval</td>
</tr>
</tbody></table>
<p>Because we want to build a full-text search, we will use <code>SEMANTIC_SIMILARITY</code>. However, <code>RETRIEVAL_DOCUMENT</code> or <code>RETRIEVAL_QUERY</code> could also work for this use case. I tested those as well, and there is not much difference.</p>
<h3>Dimensionality</h3>
<p>What is the quality of the Vector you want to store? The higher the quality, the more space you will use in Firestore. While the docs say a number between 1 and 2048, The maximum allowable dimensionality for the best quality is <code>768</code>.</p>
<h2>Instances</h2>
<p>Both API versions take an array of <code>instances</code> to search for. You could ultimately use this search for more than one term at a time. However, this example only uses one instance and task type.</p>
<h3>Vertex AI Package</h3>
<p>We need to generate the embeddings from our text. This version needs to use our Firebase Admin keys directly.</p>
<pre><code class="language-jsx">import { PRIVATE_FIREBASE_ADMIN_CONFIG } from &#39;$env/static/private&#39;;
import { adminAuth } from &#39;$lib/firebase-admin&#39;;
import { PredictionServiceClient, helpers } from &#39;@google-cloud/aiplatform&#39;;
import type { google } from &#39;@google-cloud/aiplatform/build/protos/protos&#39;;

const firebase_admin_config = JSON.parse(PRIVATE_FIREBASE_ADMIN_CONFIG);

const model = &#39;text-embedding-004&#39;;
const task = &#39;SEMANTIC_SIMILARITY&#39;;
const location = &#39;us-central1&#39;;
const apiEndpoint = &#39;us-central1-aiplatform.googleapis.com&#39;;
const dimensionality = 768;

export const getEmbedding = async (content: string) =&gt; {

    const project = adminAuth.app.options.projectId;

    const client = new PredictionServiceClient({
        apiEndpoint,
        credentials: {
            client_email: firebase_admin_config.client_email,
            private_key: firebase_admin_config.private_key
        }
    });

    const [response] = await client.predict({
        endpoint: `projects/${project}/locations/${location}/publishers/google/models/${model}`,
        instances: [
            helpers.toValue({ content, task }) as google.protobuf.IValue
        ],
        parameters: helpers.toValue({
            outputDimensionality: dimensionality
        })
    });

    const predictions = response.predictions;

    if (!predictions) {
        throw &#39;No predictions!&#39;;
    }

    const embeddings = predictions.map(p =&gt; {
        const embeddingsProto = p.structValue!.fields!.embeddings;
        const valuesProto = embeddingsProto.structValue!.fields!.values;
        return valuesProto.listValue!.values!.map(v =&gt; v.numberValue);
    });

    return embeddings[0] as number[];
};
</code></pre>
<p>Packages like <code>firebase-admin</code> usually automatically handle mapping the vector data, but this package was just as difficult to use as basic REST API. You have to map the fields manually and return the complex array correctly. Google Packages will automatically log errors to the console.</p>
<h3>REST API Method</h3>
<p>Alternatively, call the REST API directly. Here, we can use our Firebase Access Token.</p>
<pre><code class="language-jsx">export const getEmbedding2 = async (content: string) =&gt; {

    const project = adminAuth.app.options.projectId;
    const token = await adminAuth.app.options.credential!.getAccessToken();

    const url = `https://us-central1-aiplatform.googleapis.com/v1/projects/${project}/locations/us-central1/publishers/google/models/text-embedding-004:predict`;

    const r = await fetch(url, {
        method: &#39;POST&#39;,
        headers: {
            &#39;Authorization&#39;: `Bearer ${token.access_token}`,
            &#39;Content-Type&#39;: &#39;application/json&#39;
        },
        body: JSON.stringify({
            instances: [
                {
                    &quot;task_type&quot;: task,
                    // you can also use &quot;title&quot; for RETRIEVAL tasks 
                    &quot;content&quot;: content
                },
            ],
            parameters: {
                outputDimensionality: dimensionality
            }
        })
    });

    if (!r.ok) {
        const error = await r.json();
        throw error;
    }

    const response = await r.json();

    const predictions = response.predictions;

    if (!predictions) {
        throw &#39;No predictions!&#39;;
    }

    // @ts-expect-error: You should make your own types in reality here
    const embeddings = predictions.map(p =&gt; p.embeddings.values);

    return embeddings[0] as number[];
};
</code></pre>
<h2>Save Data to Firestore</h2>
<p> In this example, we save the content to the <code>text</code> field and the vector to the <code>search</code> field.</p>
<pre><code class="language-jsx">const embeddings = await getEmbedding(text);

await adminDB.collection(&#39;posts&#39;).add({
    text,
    search: FieldValue.vector(embeddings)
});
</code></pre>
<h2>Retrieving Data</h2>
<p>When retrieving our documents normally, without searching, we want to exclude the Vector field, as it will not be serializable from the server, and we don’t need to download extraneous information.</p>
<pre><code class="language-jsx">  const data = await adminDB.collection(&#39;posts&#39;).get();

  // don&#39;t return actual vector, won&#39;t serialize
  const docs = data.docs.map((doc) =&gt; {
      const data = doc.data();
      return {
          id: doc.id,
          text: data[&#39;text&#39;]
      };
  });

  return {
      docs
  };
</code></pre>
<p>📝 Using the <em>Firebase REST API</em>, you can choose the fields you want to download, thus avoiding the vector altogether and saving on data transfer.</p>
<h2>Create an Index</h2>
<p>Before searching, you must add a Vector Index. Currently, there is no way to do this outside of using the <code>gcloud</code> CLI. I could not even get that to work on my local machine. Luckily, it works as expected using the <a href="https://cloud.google.com/shell">Google Cloud Shell</a>.  Hopefully, this will be fixed soon when it is out of alpha.</p>
<pre><code class="language-jsx">gcloud alpha firestore indexes composite create
 --collection-group=YOUR-COLLECTION
 --query-scope=COLLECTION
 --field-config field-path=search,vector-config=&#39;{&quot;dimension&quot;:&quot;768&quot;,&quot;flat&quot;: &quot;{}&quot;}&#39;
 --project=YOUR-PROJECT-ID
 --database=&quot;YOUR-DATABASE&quot;
</code></pre>
<p>Replace <code>YOUR-COLLECTION</code>, <code>YOUR-PROJECT-ID</code>, and <code>YOUR-DATABASE</code> with the respected information. </p>
<p>⚠️ <code>COLLECTION</code> is not to be replaced or confused with <code>YOUR-COLLECTION</code>. </p>
<p>📝 You can also edit the <code>dimension</code> here for indexing if you want to change the default quality.</p>
<p>⚠️ Get rid of all new lines before using this command!</p>
<h2>Searching</h2>
<p>To search, we use the <code>findNearest</code> method. Unfortunately, we must get an embedding even for the data to be searched.</p>
<pre><code class="language-jsx">const embeddings = await getEmbedding(text);

const data = await adminDB
    .collection(&#39;posts&#39;)
    .findNearest(
        &#39;search&#39;,
        FieldValue.vector(embeddings), {
        limit: 5,
        distanceMeasure: &#39;EUCLIDEAN&#39;
    })
    .get();

// don&#39;t return actual vector, won&#39;t serialize
const docs = data.docs.map((doc) =&gt; {
    const data = doc.data();
    return {
        id: doc.id,
        text: data[&#39;text&#39;]
    };
});

return {
    docs
};
</code></pre>
<ul>
<li><code>search</code> is our Vector Field</li>
<li>Since all vector data is ultimately related, we limit our results to 5. Otherwise, we could potentially return all documents. This is required for a reason. Vector Search ultimately just sorts the data. You can only return a maximum of 1000 documents.</li>
<li>We want to use <code>EUCLIDEAN</code> to measure the shortest distance for semantic search. However, <code>COSINE</code> and <code>DOT_PRODUCT</code> are also available for more <a href="https://firebase.google.com/docs/firestore/vector-search">advanced cases</a>.</li>
</ul>
<h2>Pricing</h2>
<ul>
<li><a href="https://cloud.google.com/vertex-ai/generative-ai/pricing#embedding-models">Embeddings</a> are charged $0.000025 per 1000 characters.</li>
<li><a href="https://cloud.google.com/firestore/pricing#firestore-pricing">Vector Search</a> is charged one read operation for each batch of up to 100 kNN vector index entries read by the query.</li>
</ul>
<h2>Other Filtering</h2>
<p>You can also add any <a href="https://firebase.google.com/docs/firestore/vector-search">other filters</a> except for inequalities. However, each filter will require a separate index.</p>
<h2>Vector Limitations</h2>
<ul>
<li>Vectors only work on the backend. If you need frontend support, write a <code>WRITE</code> <a href="https://code.build/p/inner-joins-with-firebase-functions-teQxZV">trigger function</a> that creates the vector anytime a new document is added to your collection. However, you would still need a backend to convert the search text to a vector before searching.</li>
<li>For this reason, I’m not sure if it will ever be available on the frontend JS package. This is the worst part of the search. This also limits the ability to use real-time listeners.</li>
</ul>
<p>Google needs to use TypeScript for its examples instead of pure JavaScript. This was one of my many issues, and the documentation was terrible. Nevertheless, <em>we got er goin</em>!</p>
<h2>Demo</h2>
<ul>
<li>The demo uses SvelteKit with Form Actions.</li>
<li>Searching for something may take 1-20 seconds. This is not ideal, but it varies consistently and is quick enough most of the time.</li>
<li>I disabled adding new data to the production version to reduce spam.</li>
</ul>
<p><strong>Demo</strong>: <a href="https://firestore-vector-search.vercel.app/">Vercel Serverless</a></p>
<p><strong>Repo:</strong> <a href="https://github.com/jdgamble555/firestore-vector-search">GitHub</a></p>
<p>J</p>
<h3>Note</h3>
<p>I highly recommend you use the <a href="https://code.build/p/firestore-fuzzy-full-text-search-Ut2Smh">Fuzzy Full-Text Search</a> instead of this one, as it is faster, smaller, and easier.</p>
]]></description><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category></item><item><title>Firebase Now Works on the Server</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Sun, 18 Aug 2024 13:00:00 GMT</pubDate><link>https://code.build/p/firebase-now-works-on-the-server-eFtYMt</link><guid>https://code.build/p/firebase-now-works-on-the-server-eFtYMt</guid><description><![CDATA[<p>When Firebase was released, before Google even owned the company, you didn’t need to fetch anything on the server. Over 10 years later, you can pre-fetch your data to Firebase on the server before loading it on the client. This is faster, smaller, and less painful.</p>
<h2>TL;DR</h2>
<p>You can now use the <code>firebase-js-sdk</code> library directly on the server without needing the <code>firebase-admin</code> package. You can fetch Firestore data that requires user credentials and normal fetching. You can add service workers to your app that add the login token to every <code>fetch</code>. The fetch logins on the server with <code>initializeServerApp</code>.</p>
<h2>Firestore Lite</h2>
<p>Firebase has technically worked on the server for a long time, but not without headaches. <a href="https://code.build/p/sveltekit-todo-app-with-firebase-admin-tqdc5j">Firebase Admin</a> can load your data, but only on Node servers. Firebase Lite allows you to run Firebase on Edge Servers, including NextJS Middleware, Cloudflare, Bun, and Vercel.  </p>
<p>Remember to use <code>firebase/firestore/lite</code> if you’re on the edge for importing Firestore.</p>
<h2>Firebase Authentication on the Server</h2>
<p>The problem has been using authentication. There has not been a way to authenticate your data on the server that works across platforms. Since Firebase 10.12.3, everything works as expected, even on the Edge.</p>
<h2>SvelteKit</h2>
<p>This example uses SvelteKit, but I will update all framework examples over time. First, make sure to follow the <a href="https://code.build/p/sveltekit-todo-app-with-firebase-C73xaa">SvelteKit Todo App with Firebase</a> to understand the Svelte setup.</p>
<h3>Firebase Lite</h3>
<p>Here, we create a separate server file to handle the server request. This will be identical in all frameworks except loading the <code>.env</code> config and using error handling.</p>
<pre><code class="language-jsx">// lib/firebase-lite.ts

import { PUBLIC_FIREBASE_CONFIG } from &quot;$env/static/public&quot;;
import { error } from &quot;@sveltejs/kit&quot;;
import { initializeServerApp } from &quot;firebase/app&quot;;
import { getFirestore } from &quot;firebase/firestore/lite&quot;;
import { getAuth } from &quot;firebase/auth&quot;;

const firebase_config = JSON.parse(PUBLIC_FIREBASE_CONFIG);

export const firebaseServer = async (request: Request) =&gt; {

    const authIdToken = request.headers.get(&#39;Authorization&#39;)?.split(&#39;Bearer &#39;)[1];

    if (!authIdToken) {
        error(401, &#39;Not Logged In!&#39;);
    }

    const serverApp = initializeServerApp(firebase_config, {
        authIdToken
    });

    const auth = getAuth(serverApp);

    const db = getFirestore(serverApp);

    await auth.authStateReady();

    if (auth.currentUser === null) {
        error(401, &#39;Invalid Token&#39;);
    }

    return {
        auth,
        db
    };
};
</code></pre>
<p>The <code>initializeServerApp</code> is key. This allows us to create a temporary server app from an ID Token. We must get it from the request first. Then we await the authorization state before getting the current user.</p>
<p>The request object is passed to our <code>firebaseServer</code> function.</p>
<pre><code class="language-jsx">// routes/about/+page.server.ts

import { getAbout } from &#39;$lib/about&#39;;
import { firebaseServer } from &#39;$lib/firebase-lite&#39;;

import type { PageServerLoad } from &#39;./$types&#39;;

export const load = (async ({ request }) =&gt; {

    const { db } = await firebaseServer(request);

    return {
        about: await getAbout(db)
    };

}) satisfies PageServerLoad;
</code></pre>
<h2>Service Worker</h2>
<p>The key to making this work is a Service Worker. The SW intercepts all fetch requests and adds the ID token to it. This is beautiful and doesn’t require complex session cookies.</p>
<pre><code class="language-jsx">// service-worker/index.ts

/// &lt;reference lib=&quot;webworker&quot; /&gt;

import { requestProcessor } from &quot;./utils&quot;;

declare const self: ServiceWorkerGlobalScope;

self.addEventListener(&#39;activate&#39;, (event) =&gt; {

    const evt = event as ExtendableEvent;

    evt.waitUntil(self.clients.claim())
});

self.addEventListener(&#39;fetch&#39;, (event) =&gt; {

    const evt = event as FetchEvent;

    evt.respondWith(requestProcessor(evt));
});
</code></pre>
<p>We want to activate our service worker as soon as it is available, and then we listen to fetch requests.</p>
<h3>Process Request</h3>
<p>This is simplified code from <a href="https://firebase.google.com/docs/auth/web/service-worker-sessions">Firebase</a>. The Authorization Token is added to the request. I had to translate it to TypeScript. It should have been written that way first 🤓</p>
<pre><code class="language-jsx">// service-worker/utils.ts

import { getIdTokenPromise } from &quot;$lib/firebase-worker&quot;;

export const getOriginFromUrl = (url: string): string =&gt; {
    const [protocol, , host] = url.split(&#39;/&#39;);
    return `${protocol}//${host}`;
};

// Get underlying body if available. Works for text and json bodies.
export const getBodyContent = async (req: Request): Promise&lt;BodyInit | null | undefined&gt; =&gt; {

    if (req.method === &#39;GET&#39;) {
        return null;
    }

    try {
        if (req.headers.get(&#39;Content-Type&#39;)?.includes(&#39;json&#39;)) {
            const json = await req.json();
            return JSON.stringify(json);
        }
        return await req.text();
        
    } catch {
        return null;
    }
};

export const requestProcessor = async (event: FetchEvent) =&gt; {

    let req = event.request;

    const idToken = await getIdTokenPromise();

    if (
        self.location.origin === getOriginFromUrl(event.request.url) &amp;&amp;
        (self.location.protocol === &#39;https:&#39; || self.location.hostname === &#39;localhost&#39;) &amp;&amp;
        idToken
    ) {
        const headers = new Headers(req.headers);
        headers.append(&#39;Authorization&#39;, &#39;Bearer &#39; + idToken);
        const body = await getBodyContent(req);

        try {
            req = new Request(req.url, {
                method: req.method,
                headers: headers,
                mode: &#39;same-origin&#39;,
                credentials: req.credentials,
                cache: req.cache,
                redirect: req.redirect,
                referrer: req.referrer,
                body,
            });
        } catch {
            // This will fail for CORS requests. We just continue with the fetch logic without passing the ID token.
        }
    }

    return await fetch(req);
};
</code></pre>
<h3>Firebase in the Worker</h3>
<p>We need to get the ID token to pass it. <code>onAuthStateChanged</code> will always contain the latest token, so we convert it to a Promise.</p>
<pre><code class="language-jsx">import { PUBLIC_FIREBASE_CONFIG } from &quot;$env/static/public&quot;;
import { getApp, getApps, initializeApp } from &quot;firebase/app&quot;;
import { getAuth, getIdToken, onAuthStateChanged } from &quot;firebase/auth&quot;;

const firebase_config = JSON.parse(PUBLIC_FIREBASE_CONFIG);

const workerApp = getApps().length
    ? getApp()
    : initializeApp(firebase_config);

const auth = getAuth(workerApp);

export const getIdTokenPromise = (): Promise&lt;string | null&gt; =&gt; {
    return new Promise((resolve, reject) =&gt; {
        const unsubscribe = onAuthStateChanged(auth, async (user) =&gt; {
            unsubscribe();
            if (!user) {
                return resolve(null);
            }
            try {
                const idToken = await getIdToken(user);
                resolve(idToken);
            } catch (e) {
                reject(e);
            }
        }, reject);
    });
};
</code></pre>
<h2>Hosting Caveat</h2>
<p>While this works on <em>Vercel Edge,</em> which uses Cloudflare, there is a small bug with direct Cloudflare hosting that we are still waiting for the PR. See <a href="https://github.com/firebase/firebase-js-sdk/issues/8355">Server App Not Logging in</a>. It is one line of code, so hopefully, this will be repaired soon. It should work with Vercel Edge, Vercel Serverless, Netlify, etc.</p>
<p><strong>Demo</strong>: <a href="https://sveltekit-firebase-deploy.vercel.app/">Vercel Edge</a></p>
<p><strong>Code</strong>: <a href="https://github.com/jdgamble555/sveltekit-firebase-deploy">GitHub</a></p>
]]></description><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category></item><item><title>Firebase Storage User Profile Image</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Sun, 23 Jun 2024 13:00:00 GMT</pubDate><link>https://code.build/p/firebase-storage-user-profile-image-7r4PUK</link><guid>https://code.build/p/firebase-storage-user-profile-image-7r4PUK</guid><description><![CDATA[<p>Many database platforms handle authentication with the database functions, but not many have built-in storage capabilities. Firebase Storage allows you to securely and easily upload files in buckets, similar to folders.</p>
<p><img src="https://code.build/images/path/posts/0fda96ee-c023-468e-8957-ba2e79215127/ksqzlycpcytnxhkifek01d433.png" title="null" alt="Firebase Storage Profile Image" /></p>
<h2>TL;DR</h2>
<p>We use Svelte to update a user’s <code>photoURL</code> in the Firebase Authentication database. The image is stored in Firebase Storage, the old image is deleted automatically, and only image types are allowable.</p>
<h2>Svelte Example</h2>
<p>If you read many articles on this site, you should know now that I use Svelte for easy prototyping. This app is no exception. However, the concept is the same in any framework.</p>
<h2>Edit Image Component</h2>
<p>I use a custom image hook that returns a <code>status</code> and a <code>files</code> writable. In other frameworks, this would use signals instead of stores. You may also use a form and submit this to the backend.</p>
<pre><code class="language-html">&lt;script lang=&quot;ts&quot;&gt;
	import { useUploadImage } from &#39;$lib/use-upload-image&#39;;
	import Card from &#39;@components/elements/card.svelte&#39;;

	const { status, files } = useUploadImage();
&lt;/script&gt;

&lt;div class=&quot;my-5 flex items-center justify-center&quot;&gt;
	&lt;div class=&quot;w-3/4 max-w-3xl&quot;&gt;
		&lt;Card class=&quot;flex flex-col gap-5&quot;&gt;
			&lt;h1 class=&quot;text-3xl font-bold&quot;&gt;Upload Image&lt;/h1&gt;
			&lt;input type=&quot;file&quot; bind:files={$files} accept=&quot;image/*&quot; /&gt;
			{#if $status.progress}
				&lt;p&gt;Percent: {$status.progress.toString()}&lt;/p&gt;
			{/if}
			{#if $status.error}
				&lt;p class=&quot;text-red-600&quot;&gt;
					&lt;span class=&quot;font-bold&quot;&gt;Error: &lt;/span&gt;
					{$status.error}
				&lt;/p&gt;
			{/if}
		&lt;/Card&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
<h3>Image Type</h3>
<p>You can tell an HTML Input Element what you want to accept. For any image type:</p>
<pre><code class="language-tsx">&lt;input type=&quot;file&quot; bind:files={$files} accept=&quot;image/*&quot; /&gt;
</code></pre>
<p>For specific MIME types:</p>
<pre><code class="language-tsx">&lt;input type=&quot;file&quot; accept=&quot;image/png, image/webp, image/jpeg, image/jpg, image/gif&quot; /&gt;
</code></pre>
<p>Or by extension:</p>
<pre><code class="language-tsx">&lt;input type=&quot;file&quot; accept=&quot;.png, .webp, .jpg, .jpeg, .gif&quot;&gt;
</code></pre>
<p>We will need to secure this later in Firebase Storage Rules.</p>
<h2>Upload Image Hook</h2>
<p>The hook looks complicated.</p>
<pre><code class="language-tsx">import { auth, storage } from &quot;./firebase&quot;;
import {
    derived,
    writable,
    type Writable
} from &quot;svelte/store&quot;;
import {
    deleteObject,
    getDownloadURL,
    ref,
    uploadBytesResumable,
    type TaskState
} from &quot;firebase/storage&quot;;
import { updateProfile } from &quot;firebase/auth&quot;;

type UploadState = {
    progress: number | null;
    state: TaskState | null;
    error: string | null;
    downloadURL: string | null;
}

export const useUploadImage = () =&gt; {

    const files = writable&lt;FileList&gt;();

    const status = derived&lt;
        Writable&lt;FileList&gt;,
        UploadState
    &gt;(files, ($files, set) =&gt; {

        if (!$files) {
            set({
                progress: null,
                state: null,
                error: null,
                downloadURL: null
            });
            return;
        }

        if (!auth.currentUser) {
            throw &#39;Not logged in!&#39;;
        }

        const uid = auth.currentUser.uid;

        const new_file = $files[0];

        if (new_file.size &gt;= 1 * 1024 * 1024) {
            set({
                progress: null,
                state: null,
                error: &#39;Image size must be less than 1MB!&#39;,
                downloadURL: null
            })
            return;
        }

        const uploadTask = uploadBytesResumable(
            ref(storage, `profiles/${uid}/${new_file.name}`),
            new_file
        );

        uploadTask.on(&#39;state_changed&#39;,
            (snapshot) =&gt; {

                // handle upload progress
                const progress = (
                    snapshot.bytesTransferred / snapshot.totalBytes
                ) * 100;
                set({
                    progress,
                    state: snapshot.state,
                    error: null,
                    downloadURL: null
                });

            },
            (error) =&gt; {

                // error handling
                set({
                    progress: null,
                    state: &#39;error&#39;,
                    error: error.message,
                    downloadURL: null
                })
            },
            () =&gt; {

                // success, get download URL
                getDownloadURL(uploadTask.snapshot.ref)
                    .then((downloadURL) =&gt; {

                        // delete current image
                        deleteImage().then(() =&gt; {
                            if (!auth.currentUser) {
                                throw &#39;No User!&#39;;
                            }
                            // update profile with new image
                            updateProfile(auth.currentUser, {
                                displayName: auth.currentUser.displayName,
                                photoURL: downloadURL
                            }).then(() =&gt; {

                                // set progress to 100%
                                set({
                                    progress: 100,
                                    state: &#39;success&#39;,
                                    error: null,
                                    downloadURL
                                });
                            });
                        })

                    });
            }
        );

    });

    return {
        files,
        status
    };
};
</code></pre>
<h3>File Type</h3>
<p>With an input file element, you can keep track of the file being uploaded with a <code>FileList</code> type. Since we are only uploading one file, the file will be the first in the array.</p>
<pre><code class="language-tsx">const new_file = $files[0];
</code></pre>
<p>We can limit the file size to 1MB for our client. This should be matched on the backend as well.</p>
<pre><code class="language-tsx">new_file.size &gt;= 1 * 1024 * 1024
</code></pre>
<h3>Upload Part</h3>
<p>Let’s break down the upload part first.</p>
<pre><code class="language-tsx">const uploadTask = uploadBytesResumable(
    ref(storage, `profiles/${uid}/${new_file.name}`),
    new_file
);
</code></pre>
<p>We upload to the <code>profiles/{userId}/{filename}</code> folder here. The file name can be anything as long as it is in the user’s folder.</p>
<h3>Progress</h3>
<p>We can keep track of the upload in real-time using <code>uploadTask.on</code>.</p>
<pre><code class="language-tsx">uploadTask.on(&#39;state_changed&#39;,
    (snapshot) =&gt; {

        // handle upload progress
        const progress = (
            snapshot.bytesTransferred / snapshot.totalBytes
        ) * 100;
        // handle progress

    },
    (error) =&gt; {

        // error handling
        
    },
    () =&gt; {

        // success, get download URL
        getDownloadURL(uploadTask.snapshot.ref)
            .then((downloadURL) =&gt; {

                // handle download URL
                // set progress to 100%

            });
    }
);
</code></pre>
<p>We can translate this to an observable, a signal, or a store (in the case of Svelte). </p>
<h3>Delete Image</h3>
<p>In some cases, we may want to allow the user to have only one profile image at a time. Here, we remove the old profile image.</p>
<pre><code class="language-tsx">deleteObject(
    ref(storage, photoURL)
);
</code></pre>
<h3>Update the User Profile</h3>
<p>We also need to <a href="https://code.build/p/how-to-update-a-user-profile-in-firebase-jp6P3K">update the user’s profile</a> once we receive an image, upload a new image, or delete an image. The linked article explains this more fully.</p>
<h2>Storage Rules</h2>
<p>Of course, we should secure our app on the backend as well.</p>
<pre><code class="language-tsx">rules_version = &#39;2&#39;;
service firebase.storage {
  match /b/{bucket}/o {
  	match /profiles/{userId}/{image} {
    	allow read;
      allow create: if isLoggedIn() 
      	&amp;&amp; isOwner()
        &amp;&amp; isImageType()
        &amp;&amp; oneMBLimit();
      allow delete: if isLoggedIn()
        &amp;&amp; isOwner();
    }
  }
  
  function isOwner() {
  	return request.path[4] == request.auth.uid;
  }
  
  function isLoggedIn() {
  	return request.auth != null;
	}
  
  function isImageType() {
  	return request.resource.contentType.matches(&#39;image/.*&#39;);
	}
  
  function oneMBLimit() {
    return request.resource.size &lt; 1 * 1024 * 1024;
  }
  
}
</code></pre>
<p>Our <code>path</code> will be the storage file path instead of the collection path. For a more in-depth discussion of rules, see the <a href="https://code.build/p/firestore-security-rules-example-guide-eyfhvI">Firestore Security Rules Example Guide</a>.  You may need some advanced rules. See <a href="https://firebase.google.com/docs/reference/security/storage">Security Rules for Cloud Storage Reference</a>.</p>
<h3>Image Best Practices</h3>
<ul>
<li>If your images could be used in multiple places, you may not want to ever delete them. Images are cheap to store and are hosted on a CDN.</li>
<li>However, if an image could be an orphan (with no links to it), it is best practice to delete it automatically to save yourself in the long run.</li>
<li>Your image should have good keywords in the file name for best SEO practices. I don’t do that here, but it is good to know.</li>
<li>⚠️ Always use a new name when replacing an image. Images are cached by default, and it is extremely hard to reset the browser, server, and CDN caches when you update an image.<ul>
<li>Images belonging to a user should go in that user’s folder. Ex: <code>profiles/{userId}</code></li>
<li>You could generate a random UUID for every image or use a date timestamp.</li>
<li>You could keep the keywords with the random string. Ex: <code>profiles/{userId}/firebase-functions-image-x2030s0eksl.jpg</code>.</li>
</ul>
</li>
</ul>
<h2>Firebase Function Storage Triggers</h2>
<p>There are Cloud Storage Trigger Functions that allow you to handle different events. I will only cover Generation 2, which you should use for speed and security.</p>
<ul>
<li><code>onObjectArchived</code> - Used when versioning is enabled to archive old versions of files.</li>
<li><code>onObjectDeleted</code> - Used when a file is deleted.</li>
<li><code>onObjectFinalized</code> - Used when a file has finished uploading, even when replacing old files.</li>
<li><code>onMetadataUpdated</code> - Used when metadata changes.</li>
</ul>
<h3>Usage</h3>
<p>Relating to this app, you could use a storage trigger function to guarantee that the old user profile image is deleted or that the <code>photoURL</code> is updated.</p>
<ul>
<li>⚠️ <em>CAVEAT</em>: When the user profile image is updated on the server (in a server function or Cloud Function), it will not get updated until the active user logs out and back in. You could redundantly update it, but in a real app you would probably use Cloud Firestore to store the profile image. See <a href="https://code.build/p/how-to-update-a-user-profile-in-firebase-jp6P3K">Update a User Profile in Firebase</a>.</li>
</ul>
<p>In a production app, the best bet is to update Cloud Firestore inside the trigger function with the new download URL. </p>
<p><strong>Demo:</strong> <a href="https://firebase-storage-trigger.vercel.app/">Vercel Serverless</a></p>
<p><strong>Repo:</strong> <a href="https://github.com/jdgamble555/firebase-storage-trigger">GitHub</a></p>
<p>J</p>
]]></description><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category></item><item><title>Inner Joins with Firebase Functions</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Sun, 02 Jun 2024 13:00:00 GMT</pubDate><link>https://code.build/p/inner-joins-with-firebase-functions-teQxZV</link><guid>https://code.build/p/inner-joins-with-firebase-functions-teQxZV</guid><description><![CDATA[<p>Because Firestore is a NoSQL database, you can’t use joins like in SQL. You must plan your queries when you write your data, or you must over-fetch. There are no other options unless you use a third-party database alongside Firestore.</p>
<p><img src="https://code.build/images/path/posts/dc2433a1-9a93-42e3-a030-90be31c2ab13/ivlrzgzpmrmykdflfea5cgf2a.png" title="null" alt="Inner Joins Demo" /></p>
<h2>TL;DR</h2>
<p>You can’t have a serious Firebase app without needing Firebase Functions to perform joins for you. Luckily, it can make these aggregations very simple with <code>BulkWriter</code>.</p>
<h2>Connections</h2>
<p>In SQL, you use joins to get nested data. You may have to use a <code>jsonb</code> type, and return the data as a nested JSON object.</p>
<h3>One-to-One</h3>
<p>A 1:1 example would be a <code>user</code> and a <code>profile</code>. The <code>user</code> data may be secure, while the profile data is visible to everyone. This is similar to <a href="https://code.build/p/how-to-update-a-user-profile-in-firebase-jp6P3K">linking a user’s profile</a> data from the Firebase Auth database to the Firestore <code>users</code> or <code>profiles</code> collection.</p>
<h3>One-to-Many and Many-to-One</h3>
<p>One-to-many is the most common example in Firestore. If there are many authors, and each author only writes one book, that is one-to-one. However, in reality, each author could write several books. An author or user could write several <code>posts</code>, <code>comments</code>, or have many <code>bookmarks</code>. From the author&#39;s perspective, this is a one-to-many relationship. Each user can have multiple items. From the item perspective, this is a many-to-one. Each item can only have one user.</p>
<p>🔍 If you query a list of posts, each post needs to query a user. You have 2x the Firestore read cost. This means for every many-to-one query, you get charged double.</p>
<h3>Many-to-Many</h3>
<p>For completeness, many-to-many relationships are like a book with many authors. Or, a student can belong to many classes, and a class can have many students. I have other posts on these.</p>
<ul>
<li><a href="https://code.build/p/firestore-many-to-many-arrays-X9mf6s">Many-to-Many Arrays</a></li>
<li><a href="https://code.build/p/firestore-many-to-many-maps-gm3B5X">Many-to-Many Maps</a></li>
</ul>
<h2>Join with Reference Type</h2>
<p>If you don’t care about reads or the extra latency of several queries in one, you may want to look into the <a href="https://code.build/p/firestore-reference-type-fxhopT">Reference Type</a> for Joins. This has advantages when you need a quick mock-up or don’t want to deal with Firebase Functions.</p>
<h2>Firebase Functions</h2>
<p>Before deploying any functions during the creation and testing phases, I suggest you test with <a href="https://code.build/p/local-development-with-firebase-emulators-PV6xq8">Firebase Emulators</a> on your local machine. This will save you time trying to redeploy for every change and can keep you from managing a secondary cloud database.</p>
<h3>Initialization</h3>
<p>First, update your Firebase CLI to the latest version.</p>
<pre><code class="language-tsx"> npm i -g firebase-tools
</code></pre>
<p>Next, add Firebase to your project.</p>
<pre><code class="language-tsx">firebase init
</code></pre>
<p>You will be asked to log in and link to a current Firebase project. I <em>highly</em> suggest you use TypeScript and avoid ESLint unless you’re a pro at it. The default configuration is out-of-date and too strict.</p>
<h3>Generation 2</h3>
<p>I will use only <em>Generation 2</em> Functions for this example, as they are faster and allow you to select more than one database.</p>
<h2>Inner Joins</h2>
<p>To perform joins, you must copy data into the document you want to query. You also need to know your queries when you write your data, before any query occurs. Joins will take the form of nested JSON objects in a Firestore document.</p>
<h3>User Operations</h3>
<p>Let’s say you want to include the user’s (author’s) information on each post. While the join involves 6 operations, depending on your application, you may need only 3 triggers. It is common to use only two of them.</p>
<ol>
<li>Add a User<ol>
<li>You can’t add a post without a user, so there is nothing to do.</li>
</ol>
</li>
<li>Modify a User<ol>
<li>❗This is <em>the</em> expensive operation. You must update every post by that user with the new user data only if the user’s data has changed. We use an <code>onDocumentUpdated</code> trigger on the <code>users</code> collection for this.</li>
</ol>
</li>
<li>Delete a User<ol>
<li>Most modern apps use a soft delete, which requires marking every user&#39;s post as hidden. You could add a filter or copy it to an archive collection. Use an <code>onDocumentDeleted</code> trigger on the <code>users</code> collection, then grab all posts and mark them as private or use some other name for a filter.</li>
<li>You could also emulate a <em>Cascade Delete</em>, and delete every post by that user. Every post could have comments, and you could delete every comment with a separate <em>Cascade Delete</em> emulation. Use an <code>onDocumentDeleted</code> trigger on the <code>users</code> collection, then grab all posts and delete them individually. You could have the <code>onDocumentDeleted</code> trigger on the <code>posts</code> collection to do the same for the <code>comments</code>.</li>
</ol>
</li>
<li>Create a Post<ol>
<li>You can add the user information directly when the post is created, and this can be enforced with Firestore Rules. There may be edge cases where you have to add an <code>onCreate</code> trigger to your <code>posts</code> collection to enforce the correct data, but I am only mentioning it for awareness.</li>
</ol>
</li>
<li>Update a Post<ol>
<li>Again, using <a href="https://code.build/p/firestore-security-rules-example-guide-eyfhvI">Firestore Security Rules</a> you can easily prevent the nested user data from being edited.</li>
</ol>
</li>
<li>Delete a Post<ol>
<li>Everything gets deleted, so there is nothing to do.</li>
</ol>
</li>
</ol>
<h2>Comment Page Example</h2>
<p>I made an example app where users can add comments to a page. I am using <code>comments</code> and <code>profiles</code> collections. We will need 3 Triggers.</p>
<ol>
<li>Update a Profile<ol>
<li>Make sure all comments by that user get updated</li>
</ol>
</li>
<li>Create a Comment<ol>
<li>Add nested user data automatically</li>
</ol>
</li>
<li>Delete a Profile<ol>
<li>Cascade Delete all comments</li>
</ol>
</li>
</ol>
<h3>Update a Profile</h3>
<p>We update all comments with the latest user information. This app displays the <code>photoURL</code> and the <code>displayName</code>. Although not strictly necessary, I also update the <a href="https://code.build/p/how-to-update-a-user-profile-in-firebase-jp6P3K#1-use-firebase-auth-as-the-main-database">Firebase Auth database</a>. </p>
<p>⚠️ Remember that the Firebase Auth database must be updated on the client to be updated immediately in the session. There are lots of moving parts.</p>
<pre><code class="language-tsx">export const updateComments = onDocumentUpdated(
    &#39;profiles/{docId}&#39;,
    async (event) =&gt; {

        const eventData = event.data;

        if (!eventData) {
            return null;
        }

        const userId = event.params.docId;

        // Update user in Firebase Auth
        const { displayName, photoURL } = eventData.after.data();

        await adminAuth.updateUser(userId, {
            displayName,
            photoURL
        });

        // Update all comments
        const db = eventData.after.ref.firestore;

        const bulkWriter = db.bulkWriter();

        const comments = await db.collection(&#39;comments&#39;)
            .where(&#39;createdBy.uid&#39;, &#39;==&#39;, userId)
            .get();

        // Bulk Update by looping
        comments.forEach((comment) =&gt; {
            bulkWriter.set(comment.ref, {
                createdBy: {
                    displayName,
                    photoURL
                }
            }, { merge: true });
        });

        bulkWriter.onWriteError((error) =&gt; {
            const MAX_RETRIES = 3;
            if (error.failedAttempts &lt; MAX_RETRIES) {
                return true;
            }
            // Handle errors here
            return false;
        });

        await bulkWriter.close();

        return event;
    });
</code></pre>
<h3>Bulk Writer</h3>
<p>Using <a href="https://code.build/p/firestore-secure-batch-increment-IKO13Z">batch</a> is faster than consecutive writes but not faster than simultaneous writes. For that, we use <code>bulkwriter</code>. We can retry up to 10 times but are still stuck with the Firebase Function limits of 9 minutes per function call. This should not be a problem. This is known as the <code>fan-out</code> method or aggregating the data. If we need more processing power than Firebase Functions can provide, we should use the trigger function to offload the bulk updates to a faster server.</p>
<h3>Create a Comment</h3>
<p>We trigger the comments function, but only re-update the comment document if the data has not been properly added. Normally, we want to add the data on the client.</p>
<p>You can pick and choose any field(s). Since it costs money to read a <code>users</code> document, I am getting the data from the <a href="https://code.build/p/how-to-update-a-user-profile-in-firebase-jp6P3K#1-use-firebase-auth-as-the-main-database">Firebase Auth database</a>. You must <em>ALWAYS</em> think about reads. However, most of the time, you should read another document to join data.</p>
<pre><code class="language-tsx">export const createComment = onDocumentCreated(
    &#39;comments/{commentId}&#39;,
    async (event) =&gt; {

        const eventData = event.data;

        if (!eventData) {
            return null;
        }

        // createdBy should be enforced in Security Rules
        const docData = eventData.data();

        const {
            displayName,
            photoURL
        } = await adminAuth.getUser(docData.createdBy.uid);

        if (
            docData.displayName !== displayName
            || docData.photoURL !== photoURL
        ) {
            return eventData.ref.set({
                createdBy: {
                    displayName,
                    photoURL
                }
            }, { merge: true });
        }

        return event;
    });
</code></pre>
<h3>Cascade Delete</h3>
<p>This is just for example. The concept for deleting is exactly the same.</p>
<pre><code class="language-tsx">export const deleteProfile = onDocumentDeleted(
    { document: &#39;profiles/{docId}&#39; },
    async (event) =&gt; {

        const eventData = event.data;

        if (!eventData) {
            return null;
        }

        const userId = event.params.docId;

        // Delete in Firebase Auth
        await adminAuth.deleteUser(userId);

        const db = eventData.ref.firestore;

        // Delete all comments...

        const bulkWriter = db.bulkWriter();

        const comments = await db.collection(&#39;comments&#39;)
            .where(&#39;createdBy.uid&#39;, &#39;==&#39;, userId)
            .get();

        // Bulk Delete by looping
        comments.forEach((comment) =&gt; {
            bulkWriter.delete(comment.ref);
        });

        bulkWriter.onWriteError((error) =&gt; {
            const MAX_RETRIES = 3;
            if (error.failedAttempts &lt; MAX_RETRIES) {
                return true;
            }
            // Handle errors here
            return false;
        });

        await bulkWriter.close();

        // Delete all posts...

        // ...

        return event;
    });
</code></pre>
<h3>Trigger from a Different Database</h3>
<p>Firebase Functions Generation 2 allows you to select a different database. Instead of using a string for the first input of the trigger, you could use an object with a database key.</p>
<pre><code class="language-tsx">export const deleteProfile = onDocumentDeleted(
    { document: &#39;profiles/{docId}&#39;, database: &#39;cars&#39; },
    async (event) =&gt; {
</code></pre>
<h3>Update Only Changed</h3>
<p>There could be situations where a user is updated, and some of the nested data has already been updated. You could save writes by checking for the updates first, although each conditional could slow down write time. This probably won’t be significant.</p>
<pre><code class="language-tsx">// Bulk Update by looping
comments.forEach((comment) =&gt; {

    const { createdBy } = comment.data();

    // only update if data has changed
    if (
        createdBy.displayName !== displayName
        || createdBy.photoURL !== photoURL
    ) {

        bulkWriter.set(comment.ref, {
            createdBy: {
                displayName,
                photoURL
            }
        }, { merge: true });
    }
});
</code></pre>
<h3>Even Faster Reads</h3>
<p>The Firebase REST API allows you to query only the document IDs for a query. This still costs you one document read per document, but it could save you time and money by not downloading all the document data. However, the setup time will be longer since converting a query to the REST API is cumbersome.</p>
<h3>How to Avoid the Joins</h3>
<ol>
<li>Update a Profile<ol>
<li>Don’t display the user information.</li>
<li>Don’t allow user information to be changed (Ex. use a username). This is why you can’t edit a tweet on Twitter; it is an expensive operation.</li>
<li>Grab the data on the client, and cache it.</li>
</ol>
</li>
<li>Create a Comment<ol>
<li>Add the User information on the client, and enforce it with Firestore Rules (you should do this anyway)</li>
</ol>
</li>
<li>Delete a Profile<ol>
<li>Don’t allow a profile to be deleted.</li>
<li>Disable the profile but not the posts or comments.</li>
</ol>
</li>
</ol>
<h2>More on One-to-Many</h2>
<p>This method could also save you read charges and give you a better developer experience when reading several documents. You don’t want to make 10 API calls if you have one post with 9 comments. You could aggregate the latest 10 comments to your post document and use an infinite scroll or a load button to load the rest of the comments.</p>
<p><strong>Demo:</strong> <a href="https://firebase-inner-join.vercel.app/">Vercel Serverless</a></p>
<p><strong>Repo:</strong> <a href="https://github.com/jdgamble555/firebase-inner-join">GitHub</a></p>
<p>For more, see <a href="https://code.build/t/cloud-functions">Cloud Functions</a>.</p>
<p>J</p>
]]></description><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category></item><item><title>Update Firebase Email with Reauthentication</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Wed, 29 May 2024 13:00:00 GMT</pubDate><link>https://code.build/p/update-firebase-email-with-reauthentication-XjVwnr</link><guid>https://code.build/p/update-firebase-email-with-reauthentication-XjVwnr</guid><description><![CDATA[<p>Updating an email or password is handled differently in Firebase than updating a user’s profile name or photo URL. There is a separate function, you may have to reauthenticate the user, and the email must be confirmed.</p>
<h2>TL;DR</h2>
<p>After 5 minutes of logging in with Firebase, you must reauthenticate your login session to change a password or update an email. This requires a different login function than authenticating for the first time. You must check for an error code and direct the user correctly.</p>
<h2>Other Methods</h2>
<p>You could technically store the user’s email in Firestore, and use a Trigger Function to update the email in the Firebase Auth database like you might use when <a href="https://code.build/p/how-to-update-a-user-profile-in-firebase-jp6P3K">updating the profile information</a>, but this would be less secure and could cause conflicts. You want to update the email directly in the Firebase Auth database to ensure integrity and security.</p>
<h2>Shared State</h2>
<p>You need some version of shared state. This app uses Svelte because I can prototype a demo very quickly. The <a href="https://code.build/p/sveltekit-todo-app-with-firebase-C73xaa#shared-context">Svelte Shared Store</a> allows you to use a writable context anywhere in your app.</p>
<pre><code class="language-tsx">// writable store context
export const useWritable = &lt;T&gt;(name: string, value?: T) =&gt;
    useSharedStore(name, writable, value);
    
// use reLogin
export const useRelogin = () =&gt; useWritable(&#39;relogin&#39;, false);
</code></pre>
<h2>Relogin</h2>
<p>Along with the login, we need to be able to <em>relogin</em> when necessary.</p>
<h3>Functions</h3>
<pre><code class="language-tsx">export const loginWithGoogle = async () =&gt;
    await signInWithPopup(
        auth, new GoogleAuthProvider()
    );

// requires the user
export const reLoginWithGoogle = async () =&gt; {
    if (auth.currentUser) {
        await reauthenticateWithPopup(
            auth.currentUser,
            new GoogleAuthProvider()
        );
    }
}
</code></pre>
<h3>Login Component</h3>
<p>Here, we check for the re-login state. Otherwise, we display a regular login component.</p>
<pre><code class="language-tsx">&lt;script lang=&quot;ts&quot;&gt;
	import { loginWithGoogle, reLoginWithGoogle, useRelogin } from &#39;$lib/user-user&#39;;

	const relogin = useRelogin();

	const login = async () =&gt; {
		if ($relogin) {
			await reLoginWithGoogle();
			relogin.set(false);
			return;
		}
		await loginWithGoogle();
	};
&lt;/script&gt;

&lt;form method=&quot;POST&quot; on:submit|preventDefault={login}&gt;
	&lt;button type=&quot;submit&quot; class=&quot;bg-red-600 p-2 font-semibold text-white&quot;&gt;
		Signin with Google
	&lt;/button&gt;
&lt;/form&gt;
{#if $relogin}
	&lt;p&gt;You must re-signin to change your email.&lt;/p&gt;
{/if}
</code></pre>
<h2>Update Email</h2>
<p>The function returns an error or nothing.</p>
<pre><code class="language-tsx">export const updateProfileEmail = async (
    email: string
) =&gt; {

    if (!auth.currentUser) {
        return {
            error: &#39;Not Logged In!&#39;
        };
    }
    try {
        await updateEmail(auth.currentUser, email);
    } catch (e) {
        if (e instanceof FirebaseError) {
            return {
                error: e.message
            };
        }
    }
    return {};
};
</code></pre>
<h3>Reauthenticate</h3>
<p>In Firebase Auth, some actions require reauthentication. If it is necessary, you will be thrown an error.</p>
<pre><code class="language-tsx">// error.code === &#39;auth/requires-recent-login&#39;

or

// error.message === &#39;Firebase: Error (auth/requires-recent-login).&#39;
</code></pre>
<p>Because I’m only dealing with messages in my app for simplicity, I check for the error message.</p>
<pre><code class="language-tsx">const user = useUser();
const toast = useToast();
const relogin = useRelogin();

let email: HTMLInputElement;

const updateProfile = async () =&gt; {
	const { error } = await updateProfileEmail(email.value);
	if (error) {
		if (error === &#39;Firebase: Error (auth/requires-recent-login).&#39;) {
			relogin.set(true);
			return;
		}
		toast.error(error);
		return;
	}

	toast.open(&#39;Email Updated!&#39;);
};
</code></pre>
<p>This sets the relogin state dynamically, which will display the relogin button.</p>
<pre><code class="language-tsx">{#if $user}
	&lt;main class=&quot;mt-5 flex flex-col items-center justify-center gap-5&quot;&gt;
		&lt;form class=&quot;flex flex-col items-center gap-5&quot; on:submit|preventDefault={updateProfile}&gt;
			{#if $relogin}
				&lt;LoginWithGoogle /&gt;
			{:else}
				&lt;div&gt;
					&lt;label for=&quot;email&quot; class=&quot;mb-2 block text-sm font-medium text-gray-900 dark:text-white&quot;&gt;
						Email
					&lt;/label&gt;
					&lt;input
						bind:this={email}
						type=&quot;text&quot;
						id=&quot;email&quot;
						name=&quot;email&quot;
						value={$user.email}
						class=&quot;block w-full rounded-lg border border-gray-300 bg-gray-50 p-2.5 text-sm text-gray-900 focus:border-blue-500 focus:ring-blue-500&quot;
						required
					/&gt;
				&lt;/div&gt;
				&lt;button
					type=&quot;submit&quot;
					class=&quot;w-fit rounded-lg border bg-blue-600 p-3 font-semibold text-white&quot;
				&gt;
					Update
				&lt;/button&gt;
			{/if}
		&lt;/form&gt;
	&lt;/main&gt;
{:else}
	&lt;LoginWithGoogle /&gt;
{/if}
</code></pre>
<p>After a relogin, the state will return to editing an email. This could be done in a million different ways, but I tried to show you a simple one.</p>
<h2>When Is Reauthentication Necessary?</h2>
<p>After <em>5 minutes</em> you must reauthenticate to change your password or change your email. You will not see me showing any password authentication states on this website, as I do not believe it is good practice or secure.</p>
<p><img src="https://code.build/images/path/posts/74fb903f-851f-4ac0-b83a-4f3ef91827c1/nlvvzyabwbjdjgukfe89g0cd5.png" title="null" alt="Reauthenticate with Firebase" /></p>
<p><strong>Demo</strong>: <a href="https://svelte-edit-email.vercel.app/email">Vercel Serverless</a></p>
<p><strong>Repo:</strong> <a href="https://github.com/jdgamble555/svelte-edit-email">GitHub</a></p>
<p>See more on <a href="https://code.build/p/sveltekit-todo-app-with-firebase-C73xaa">Svelte with Firebase</a></p>
]]></description><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category></item><item><title>How to Update a User Profile in Firebase</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Sun, 12 May 2024 13:00:00 GMT</pubDate><link>https://code.build/p/how-to-update-a-user-profile-in-firebase-jp6P3K</link><guid>https://code.build/p/how-to-update-a-user-profile-in-firebase-jp6P3K</guid><description><![CDATA[<p>Firebase Authentication is its own Database, separate from Firestore. You can use it securely with any database you like to handle your login needs. However, this database has no filterable queries and is only available to the client for the logged-in user. How you store your user&#39;s profile information depends on your app&#39;s complexity.</p>
<h2>TL;DR</h2>
<p>A user’s profile in Firebase specifically refers to the <code>photoURL</code> and the <code>displayName</code>. These two items can be stored and updated in Firebase without using Firestore or Firebase Realtime Database. You can update them by calling the <code>updateProfile</code> function in Firebase Auth SDK. You must choose how to sync your data with other databases.</p>
<h2>Where to Store the User?</h2>
<p>If you want the best of both worlds, you can store your user profile information inside Firestore and Firebase Authentication. <a href="https://code.build/p/creating-a-firestore-user-document-on-sign-up-jSWukw#server-trigger-method">Copying the data on sign-up</a> is easy. However, updating a user can be trickier.</p>
<ol>
<li>While there is a <code>user().onCreate</code> trigger, there is <em>NOT</em> a <code>user().onUpdate</code> trigger.</li>
<li>There is no way to prevent users from updating their own record in Firebase Authentication once logged in. </li>
<li>Firebase Authentication is free, but Firestore charges per read.</li>
</ol>
<h2>Handling Synchronization</h2>
<p>There are trade-offs in everything we do.</p>
<h3>1. Use Firebase Auth as the Main Database</h3>
<p>This is the easiest and best way for small apps. There is no reason to have a separate <code>users</code> collection when no joins are involved, or you don&#39;t need to have a complicated user profile. This can be the best route when dealing only with the user profile information.</p>
<p>Here, we have an <code>updateUser</code> function that can update the <code>displayName</code> and the <code>photoURL</code>.</p>
<pre><code class="language-tsx">export const updateUser = async (
    displayName: string,
    photoURL: string
) =&gt; {

    if (!auth.currentUser) {
        return {
            error: &#39;Not Logged In!&#39;
        };
    }
    try {
        await updateProfile(auth.currentUser, {
            displayName,
            photoURL
        });
    } catch (e) {
        if (e instanceof FirebaseError) {
            return {
                error: e.message
            };
        }
    }
    return {};
};
</code></pre>
<p>📝 Get the <code>auth</code> object using your <a href="https://code.build/c/firebase/framework-setup">Framework Setup</a>.</p>
<p>This puts the boilerplate in one place. You can call it in your component while only returning an <code>error</code> if there is one.</p>
<pre><code class="language-tsx">const { error } = await updateUser(displayName, photoURL);
if (error) {
	// handle error
	...
	return;
}
// handle success
...
</code></pre>
<p>If you need to get the user information for a public profile on a different user than is logged in, you would have to use Firebase Admin on the server.</p>
<pre><code class="language-tsx">// Get user from Firebase Authentication database
export const getProfile = async (uid: string) {

	const { displayName, photoURL } = await adminAuth.getUser(uid);
	
	// Only return the profile values for security
	return {
	    displayName,
	    photoURL
	};

};
</code></pre>
<p>📝 Get the <code>adminAuth</code> object using your <a href="https://code.build/p/sveltekit-todo-app-with-firebase-admin-tqdc5j">Firebase Admin Setup</a>.</p>
<h3>2 - Update Both Databases on the Client</h3>
<p>This version is more obvious but won’t be as safe.</p>
<pre><code class="language-jsx">export const updateUser = async (
    displayName: string,
    photoURL: string
) =&gt; {

    // Update Firebase Authentication Database
    ...
    
    // Update Firestore Database
    try {
        await updateDoc(
            doc(db, &#39;users&#39;, auth.currentUser!.uid),
            {
                displayName,
                photoURL
            }
        )
    } catch(e) {
        if (e instanceof FirebaseError) {
            return {
                error: e.message
            }
        }
    } 
    return {};
};
</code></pre>
<h3>3 - Update Both Databases with a Firestore Trigger</h3>
<p>If you want to align both databases, you can trigger Firestore to automatically update the Firebase Authentication database when a change occurs.</p>
<pre><code class="language-jsx">import { firestore, https } from &#39;firebase-functions&#39;;
import { auth } from &#39;firebase-admin&#39;;

const adminAuth = auth();

export const updateAuthProfile = firestore
    .document(&#39;users/{userId}&#39;)
    .onUpdate(async (change, context) =&gt; {

        const beforeData = change.before.data();
        const afterData = change.after.data();
        const userId = context.params.userId;

        // Initialize an object to hold updates
        const updates: Partial&lt;{
            displayName: string,
            photoURL: string
        }&gt; = {};

        // Check if displayName has been modified
        if (beforeData.displayName !== afterData.displayName) {
            updates.displayName = afterData.displayName;
        }

        // Check if photoURL has been modified
        if (beforeData.photoURL !== afterData.photoURL) {
            updates.photoURL = afterData.photoURL;
        }

        if (!Object.keys(updates).length) {
            console.log(&#39;No relevant changes detected in user profile&#39;);
            return;
        }

        // Attempt to update the user&#39;s Authentication profile
        try {
            await adminAuth.updateUser(userId, updates);
            console.log(`Updated user profile for user ${userId}:`, updates);
        } catch (error) {
            console.error(&#39;Error updating user profile:&#39;, error);
            throw new https.HttpsError(
                &#39;internal&#39;,
                &#39;Failed to update user profile in Auth&#39;,
                error
            );
        }
    });
</code></pre>
<h3>Generation 2</h3>
<p>In case you’re using Generation 2.</p>
<pre><code class="language-jsx">
import { onDocumentUpdated } from &#39;firebase-functions/v2/firestore&#39;;
import { auth } from &#39;firebase-admin&#39;;
import { https } from &#39;firebase-functions/v2&#39;;

const adminAuth = auth();

export const updateAuthProfile = onDocumentUpdated(&#39;users/{userId}&#39;,
    async (event) =&gt; {

        const beforeData = event.data!.before.data();
        const afterData = event.data!.after.data();
        const userId = event.params.userId;

        // Initialize an object to hold updates
        const updates: Partial&lt;{
            displayName: string,
            photoURL: string
        }&gt; = {};

        // Check if displayName has been modified
        if (beforeData.displayName !== afterData.displayName) {
            updates.displayName = afterData.displayName;
        }

        // Check if photoURL has been modified
        if (beforeData.photoURL !== afterData.photoURL) {
            updates.photoURL = afterData.photoURL;
        }

        if (!Object.keys(updates).length) {
            console.log(&#39;No relevant changes detected in user profile&#39;);
            return;
        }

        // Attempt to update the user&#39;s Authentication profile
        try {
            await adminAuth.updateUser(userId, updates);
            console.log(`Updated user profile for user ${userId}:`, updates);
        } catch (error) {
            console.error(&#39;Error updating user profile:&#39;, error);
            throw new https.HttpsError(
                &#39;internal&#39;,
                &#39;Failed to update user profile in Auth&#39;,
                error
            );
        }
    });
</code></pre>
<p>📝 You could equally use an SQL trigger function or a trigger function in your database of choice.</p>
<h2>Caveats</h2>
<p>Because there is no guarantee that both databases will stay in sync if you have any client Firebase features, you must choose your <em>principal</em> database; your client API key will be easily available to any programmer.</p>
<ol>
<li>Use the <em>Firebase Authentication</em> database if you have no need for complex profile features and you want to save on reads.</li>
<li>Use <em>Firestore</em> or any external database if you have any complex data and don’t care about keeping <em>Firebase Authentication</em> database in sync.</li>
<li>Use both databases if keeping the data in sync is not mandatory.</li>
</ol>
<p><img src="https://code.build/images/path/posts/d52edc79-6f4f-4dc7-b21b-1a5b901f623a/oxagnjvmcwsyaespfdmm625km.png" title="null" alt="Update Profile Demo App" /></p>
<p><strong>Repo:</strong> <a href="https://github.com/jdgamble555/firebase-edit-profile">GitHub</a></p>
<p><strong>Demo:</strong> <a href="https://firebase-edit-profile.vercel.app/">Vercel Serverless</a></p>
<p>J</p>
]]></description><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category></item><item><title>Svelte 5 Todo App with Firebase</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Wed, 08 May 2024 13:00:00 GMT</pubDate><link>https://code.build/p/svelte-5-todo-app-with-firebase-X1Tr3J</link><guid>https://code.build/p/svelte-5-todo-app-with-firebase-X1Tr3J</guid><description><![CDATA[<p>Svelte 5 is coming very soon since it has hit the <em>Release Candidate</em> stage. Svelte 5 now has “runes,” Svelte’s version of signals. These are amazing, and I don’t want to go back. Translating my Todo App was extremely easy, as I have built it many times with signals.</p>
<h2>TL;DR</h2>
<p>Using a Svelte rune outside of a component requires getters and setters. Once you set up your signals correctly, everything works extremely similarly to other Frameworks. <code>$state()</code> works great in a component, but using the <em>Single Responsibility Principle</em> will force smart programmers to create their own sharable runes.</p>
<h2>Setup</h2>
<p>The setup is the same as in most other Firebase applications. We export the <code>auth</code> and <code>db</code> handlers.</p>
<pre><code class="language-tsx">// lib/firebase.ts

import { PUBLIC_FIREBASE_CONFIG } from &#39;$env/static/public&#39;;
import { getApp, getApps, initializeApp } from &#39;firebase/app&#39;;
import { getAuth } from &#39;firebase/auth&#39;;
import { getFirestore } from &#39;firebase/firestore&#39;;

const firebase_config = JSON.parse(PUBLIC_FIREBASE_CONFIG);

// initialize and login
export const app = getApps().length
    ? getApp()
    : initializeApp(firebase_config);

export const auth = getAuth(app);
export const db = getFirestore(app);
</code></pre>
<h3>Types</h3>
<p>Add the types to the <code>global</code> namespace in <code>app.d.ts</code>.</p>
<pre><code class="language-tsx">type UserType = {
    displayName: string | null
    photoURL: string | null;
    uid: string;
    email: string | null;
};

type Todo = {
    id: string;
    uid: string;
    text: string;
    complete: boolean;
    createdAt: Date;
};
</code></pre>
<h3>Runes</h3>
<p>I’m not sure why Svelte was built like this, as it is a compiler, but you can’t export <code>$state</code> variables from component to component. I made a cheat version similar to <code>Vue</code> and <code>Qwik</code>&#39;s signals.</p>
<pre><code class="language-jsx">// lib/rune.svelte.ts

export const rune = &lt;T&gt;(initialValue: T) =&gt; {

    let _rune = $state(initialValue);

    return {
        get value() {
            return _rune;
        },
        set value(v: T) {
            _rune = v;
        }
    };
};
</code></pre>
<p>Notice that you must use the <code>.svelte.ts</code> file type to work correctly with the <code>$state</code>. Again, this could have been the perfect reason to allow <code>$state</code> to work everywhere, but we are stuck making “getters and setters” manually. I don’t want to do it everywhere, so I made a reusable template function.</p>
<h3>Shared</h3>
<p>The runes and custom runes need to be sharable everywhere, so I reused my shared contexts to work with signals (runes) and stores.</p>
<pre><code class="language-tsx">import { getContext, hasContext, setContext } from &quot;svelte&quot;;
import { readable, writable } from &quot;svelte/store&quot;;
import { rune } from &quot;./rune.svelte&quot;;

export const useSharedStore = &lt;T, A&gt;(
    name: string,
    fn: (value?: A) =&gt; T,
    defaultValue?: A,
) =&gt; {
    if (hasContext(name)) {
        return getContext&lt;T&gt;(name);
    }
    const _value = fn(defaultValue);
    setContext(name, _value);
    return _value;
};

// writable store context
export const useWritable = &lt;T&gt;(name: string, value?: T) =&gt;
    useSharedStore(name, writable, value);

// readable store context
export const useReadable = &lt;T&gt;(name: string, value: T) =&gt;
    useSharedStore(name, readable, value);

// shared rune
export const useRune = &lt;T&gt;(name: string, value: T) =&gt;
    useSharedStore(name, rune, value);
</code></pre>
<h2>User Hook</h2>
<p>We need to get the <code>user</code> from <code>onIdTokenStateChanged</code> and share it across our app. We must be careful not to call it more than once, so we use our <code>useShared</code> hook above. </p>
<pre><code class="language-tsx">import {
    GoogleAuthProvider,
    onIdTokenChanged,
    signInWithPopup,
    signOut,
    type User
} from &quot;firebase/auth&quot;;
import { auth } from &quot;./firebase&quot;;
import { useSharedStore } from &quot;./use-shared.svelte&quot;;
import { onDestroy } from &quot;svelte&quot;;
import { rune } from &quot;./rune.svelte&quot;;

export const loginWithGoogle = async () =&gt;
    await signInWithPopup(auth, new GoogleAuthProvider());

export const logout = async () =&gt;
    await signOut(auth);

const _useUser = () =&gt; {

    const user = rune&lt;{
        loading: boolean,
        data: UserType | null,
        error: Error | null
    }&gt;({
        loading: true,
        data: null,
        error: null
    });

    const unsubscribe = onIdTokenChanged(
        auth,
        (_user: User | null) =&gt; {

            // not logged in
            if (!_user) {
                user.value = {
                    loading: false,
                    data: null,
                    error: null
                };
                return;
            }

            // logged in
            const { displayName, photoURL, uid, email } = _user;
            user.value = {
                loading: false,
                data: { displayName, photoURL, uid, email },
                error: null
            };
        }, (error) =&gt; {

            // error
            user.value = {
                loading: false,
                data: null,
                error
            };
        });

    onDestroy(unsubscribe);

    return user;
};

export const useUser = (defaultUser: UserType | null = null) =&gt;
    useSharedStore(&#39;user&#39;, _useUser, defaultUser);
</code></pre>
<h3>📝Notes</h3>
<ul>
<li>I import the <code>rune</code> function and call it exactly like <code>Qwik</code> and <code>Vue</code>.</li>
<li>We handle the <code>error</code> and <code>loading</code> states in the signal.</li>
<li>ALWAYS <code>unsubscribe</code>. Here, we use <code>onDestroy</code> as we would in a component.</li>
</ul>
<h2>Todos Hook</h2>
<p>We need a hook to get our <code>todos</code> and subscribe to the changes.</p>
<pre><code class="language-tsx">import {
    collection,
    deleteDoc,
    doc,
    onSnapshot,
    orderBy,
    query,
    serverTimestamp,
    setDoc,
    where,
    QueryDocumentSnapshot,
    type SnapshotOptions,
    Timestamp,
    type PartialWithFieldValue,
    type SetOptions
} from &quot;firebase/firestore&quot;;
import { auth, db } from &quot;./firebase&quot;;
import { useUser } from &quot;./user.svelte&quot;;
import { FirebaseError } from &quot;firebase/app&quot;;
import { untrack } from &quot;svelte&quot;;
import { rune } from &quot;./rune.svelte&quot;;
import { dev } from &quot;$app/environment&quot;;

export const genText = () =&gt;
    Math.random().toString(36).substring(2, 15);

const todoConverter = {
    toFirestore(
        value: PartialWithFieldValue&lt;Todo&gt;,
        options?: SetOptions
    ) {
        const isMerge = options &amp;&amp; &#39;merge&#39; in options;
        if (!auth.currentUser) {
            throw &#39;User not logged in!&#39;;
        }
        return {
            ...value,
            uid: auth.currentUser.uid,
            [isMerge
                ? &#39;updatedAt&#39;
                : &#39;createdAt&#39;
            ]: serverTimestamp()
        };
    },
    fromFirestore(
        snapshot: QueryDocumentSnapshot,
        options: SnapshotOptions
    ) {
        const data = snapshot.data(options);
        const createdAt = data[&#39;createdAt&#39;] as Timestamp;
        return {
            ...data,
            id: snapshot.id,
            createdAt: createdAt.toDate()
        } as Todo;
    }
};

export const useTodos = () =&gt; {

    const user = useUser();

    const _todos = rune&lt;{
        data: Todo[],
        loading: boolean,
        error: FirebaseError | null
    }&gt;({
        data: [],
        loading: true,
        error: null
    });

    $effect(() =&gt; {

        const _user = user.value.data;

        // filtering todos depend on user
        if (!_user) {
            untrack(() =&gt; {
                _todos.value = {
                    loading: false,
                    data: [],
                    error: null
                };
            });
            return;
        }

        return onSnapshot(
            query(
                collection(db, &#39;todos&#39;),
                where(&#39;uid&#39;, &#39;==&#39;, _user.uid),
                orderBy(&#39;createdAt&#39;)
            ).withConverter&lt;Todo&gt;(todoConverter), (q) =&gt; {

                if (q.empty) {
                    _todos.value = {
                        loading: false,
                        data: [],
                        error: null
                    };
                }

                const data = q.docs.map(doc =&gt; doc.data({
                    serverTimestamps: &#39;estimate&#39;
                }));

                if (dev) {
                    console.log(data);
                }

                _todos.value = {
                    loading: false,
                    data,
                    error: null
                };

            }, (error) =&gt; {

                // Handle error
                _todos.value = {
                    loading: false,
                    data: [],
                    error
                };
            });
    });
    return _todos;
};

export const addTodo = async (text: string) =&gt; {

    setDoc(doc(collection(db, &#39;todos&#39;))
        .withConverter(todoConverter), {
        text,
        complete: false
    }).catch((e) =&gt; {
        if (e instanceof FirebaseError) {
            console.error(e.code)
        }
    });
}

export const updateTodo = async (
    id: string,
    newStatus: boolean
) =&gt; {

    try {
        await setDoc(
            doc(db, &#39;todos&#39;, id),
            { complete: newStatus },
            { merge: true }
        );
    } catch (e) {
        if (e instanceof FirebaseError) {
            console.error(e.code);
        }
    }
}

export const deleteTodo = (id: string) =&gt; {
    deleteDoc(doc(db, &#39;todos&#39;, id));
}
</code></pre>
<h3>📝 Notes</h3>
<ul>
<li>This hook is only called once in one component, so we don&#39;t need <code>useShared</code>.</li>
<li>I’m using <a href="https://code.build/p/does-firestore-need-data-converters-sSveEL">Data Converters</a>, but they are not necessary.</li>
<li>The <code>createdAt</code> date is added, but you could also add the <code>updatedAt</code> date by adding the data converter to the <code>updateTodo</code> function. See <a href="https://code.build/p/does-firestore-need-data-converters-sSveEL">Data Converters</a>.</li>
<li>The <code>todos</code> collection subscription is dependent on the <code>user</code>. If there is no user, we need to automatically unsubscribe and resubscribe when there is one.  The <code>$effect()</code> function handles this.</li>
<li>To prevent looping, we generally should not set signals inside <code>$effect()</code>. However, we can get around this by setting our <code>todos</code> signal inside the <code>untrack()</code> function.</li>
<li>We don’t need to use <code>untrack()</code> inside the <code>onSnapshot</code> because it is an <code>async</code> function. This means the signal is not tracked.</li>
<li>When we get our Firebase data with a date type, we need to use <code>estimate</code> with <code>serverTimestamps</code>. This will keep our optimistic updates working well for the user. See <a href="https://code.build/p/firestore-dates-and-timestamps-WjGiiQ">Dates and Timestamps</a>.</li>
</ul>
<h2>User Components</h2>
<p>You can use the user hook in any component, and it will only be called once. </p>
<pre><code class="language-tsx">&lt;script lang=&quot;ts&quot;&gt;
	import { loginWithGoogle, logout } from &#39;$lib/user.svelte&#39;;
	import Todos from &#39;@components/todos.svelte&#39;;
	import Profile from &#39;@components/profile.svelte&#39;;
	import { useUser } from &#39;$lib/user.svelte&#39;;

	const _user = useUser();
	const user = $derived(_user.value);
&lt;/script&gt;

&lt;h1 class=&quot;my-3 text-3xl font-semibold text-center&quot;&gt;Svelte 5 Firebase Todo App&lt;/h1&gt;

&lt;section class=&quot;flex flex-col items-center gap-3 p-5&quot;&gt;
	{#if user.data}
		&lt;Profile /&gt;
		&lt;button
			class=&quot;p-3 font-semibold text-white bg-blue-600 border rounded-lg w-fit&quot;
			onclick={logout}
		&gt;
			Logout
		&lt;/button&gt;
		&lt;hr /&gt;
		&lt;Todos /&gt;
	{:else if user.loading}
		&lt;p&gt;Loading...&lt;/p&gt;
	{:else if user.error}
		&lt;p class=&quot;text-red-500&quot;&gt;Error: {user.error}&lt;/p&gt;
	{:else}
		&lt;button class=&quot;p-2 font-semibold text-white bg-red-600&quot; onclick={loginWithGoogle}&gt;
			Signin with Google
		&lt;/button&gt;
	{/if}
&lt;/section&gt;
</code></pre>
<p>Here we handle the user state with <code>data</code>, <code>loading</code>, and <code>error</code>. However, we cannot destructure signals.</p>
<pre><code class="language-tsx">// ⚠️ THIS WILL NOT WORK!
const { value } = useUser();

// ⚠️ NEITHER WILL THIS
const { data, loading, error } = useUser().value;
</code></pre>
<p>If we want to have a reactive user value that is just one variable, we need to redefine it as a new signal using <code>$derived</code>. This will work with any long nested value in a signal.</p>
<pre><code class="language-tsx">const user = $derived(_user.value);
</code></pre>
<h2>Todos Component</h2>
<p>Notice you will see the same setup for the todos component. Use the <code>$derived</code> to shorten your variable after you get the signal.</p>
<pre><code class="language-tsx">&lt;script lang=&quot;ts&quot;&gt;
	import { fly } from &#39;svelte/transition&#39;;
	import { useTodos } from &#39;$lib/todos.svelte&#39;;
	import TodoItem from &#39;@components/todo-item.svelte&#39;;
	import TodoForm from &#39;./todo-form.svelte&#39;;

	const _todos = useTodos();
	const todos = $derived(_todos.value);
&lt;/script&gt;

{#if todos.data?.length}
	&lt;div
		class=&quot;grid grid-cols-[auto,auto,auto,auto] gap-3 justify-items-start&quot;
		in:fly={{ x: 900, duration: 500 }}
	&gt;
		{#each todos.data || [] as todo (todo.id)}
			&lt;TodoItem {todo} /&gt;
		{/each}
	&lt;/div&gt;
{:else if todos.loading}
	&lt;p&gt;Loading...&lt;/p&gt;
{:else if todos.error}
	&lt;p class=&quot;text-red-500&quot;&gt;{todos.error}&lt;/p&gt;
{:else}
	&lt;p&gt;&lt;b&gt;Add your first todo item!&lt;/b&gt;&lt;/p&gt;
{/if}

&lt;TodoForm /&gt;
</code></pre>
<h2>Server Fetching</h2>
<p>Nothing has changed in SvelteKit when it comes to getting data from the server. </p>
<h3>About Library</h3>
<p>Remember to use <code>firestore/lite</code> if you want to fetch on an <a href="https://code.build/p/firestore-query-and-mutation-patterns-w5vhl7#edge-functions">Edge Function</a>.</p>
<pre><code class="language-tsx">// lib/about.ts
import { doc, getDoc, getFirestore } from &quot;firebase/firestore/lite&quot;;
import { app } from &quot;./firebase&quot;;

const db = getFirestore(app);

type AboutDoc = {
    name: string;
    description: string;
};

export const getAbout = async () =&gt; {

    const aboutSnap = await getDoc(
        doc(db, &#39;/about/ZlNJrKd6LcATycPRmBPA&#39;)
    );

    if (!aboutSnap.exists()) {
        throw &#39;Document does not exist!&#39;;
    }

    return aboutSnap.data() as AboutDoc;
};
</code></pre>
<h3>About Server</h3>
<p>Call the about function.</p>
<pre><code class="language-tsx">// routes/about/+page.server.ts
import { getAbout } from &#39;$lib/about&#39;;
import type { PageServerLoad } from &#39;./$types&#39;;

export const load = (async () =&gt; {

    return {
        about: await getAbout()
    };

}) satisfies PageServerLoad;
</code></pre>
<h3>About Component</h3>
<p>You can display the data with <code>$page</code> or <code>data</code> from the <code>routes</code> directory.</p>
<pre><code class="language-tsx">&lt;script lang=&quot;ts&quot;&gt;
	import type { PageData } from &#39;./$types&#39;;

	export let data: PageData;
&lt;/script&gt;

&lt;div class=&quot;flex items-center justify-center my-5&quot;&gt;
	&lt;div class=&quot;border w-[400px] p-5 flex flex-col gap-3&quot;&gt;
		&lt;h1 class=&quot;text-3xl font-semibold&quot;&gt;{data.about.name}&lt;/h1&gt;
		&lt;p&gt;{data.about.description}&lt;/p&gt;
	&lt;/div&gt;
&lt;/div&gt;
</code></pre>
<p>Using <em>Runes</em> in Svelte 5 is much easier than Svelte 4 <em>Stores</em> in every scenario.</p>
<p><strong>Repo</strong>: <a href="https://github.com/jdgamble555/svelte5-firebase-todo">GitHub</a></p>
<p><strong>Demo</strong>: <a href="https://svelte5-firebase-todo.vercel.app/">Vercel Edge</a> </p>
<p>↪️ <a href="https://code.build/p/sveltekit-todo-app-with-firebase-C73xaa">See the Svelte 4 Version</a></p>
]]></description><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category></item><item><title>Firestore Security Rules Example Guide</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Sun, 05 May 2024 13:00:00 GMT</pubDate><link>https://code.build/p/firestore-security-rules-example-guide-eyfhvI</link><guid>https://code.build/p/firestore-security-rules-example-guide-eyfhvI</guid><description><![CDATA[<p>I want you to learn “how to write Firebase Rules” rather than what specific rules you need to write. Once you understand what kinds of operations are available and how to write clean code, you should be able to write reusable functions for any Firebase app.</p>
<h2>TL;DR</h2>
<p>Using Firestore Rules can be cumbersome. However, simplifying your rules into reusable functions can make writing complex, secure apps much easier. Remember the Security Rules Limitations, when to use Trigger Functions instead, and how you are charged per document read.</p>
<h2>Firestore Rules</h2>
<pre><code class="language-jsx">// firestore.rules
rules_version = &#39;2&#39;;

service cloud.firestore {

  match /databases/{database}/documents {
  
    // MATCHES

    match /users/{document} {
    
		allow read, write: if false;
			
    }
  }
}
</code></pre>
<ul>
<li>Use version 2 by default</li>
<li>Lock your data by default using <code>if false</code></li>
</ul>
<h2>Permission</h2>
<ul>
<li><code>allow read</code> - single and multiple documents<ul>
<li><code>allow get</code> - a single document with <code>getDoc</code> or <code>onSnapshot</code></li>
<li><code>allow list</code> - multiple documents with <code>getDocs</code> or <code>onSnapshot</code></li>
</ul>
</li>
<li><code>allow write</code> - create, update, delete<ul>
<li><code>allow create</code> - create a document with <code>addDoc</code> or <code>setDoc</code></li>
<li><code>allow update</code> - update a document with <code>updateDoc</code> or <code>setDoc</code> using <code>merge</code></li>
<li><code>allow delete</code> - delete a document with <code>deleteDoc</code></li>
</ul>
</li>
</ul>
<h2>Resource</h2>
<ul>
<li><code>resource</code> - document being read or written<ul>
<li><code>resource.data</code> - map of document data</li>
<li><code>resource.id</code> - resource document id as string</li>
<li><code>resource[&#39;__name__&#39;]</code> or <code>request.path</code> - the document name as path</li>
</ul>
</li>
</ul>
<h2>Request</h2>
<ul>
<li><code>request</code> - the incoming request context<ul>
<li><code>request.auth</code> - request authentication context<ul>
<li><code>uid</code> - user ID</li>
<li><code>token</code> - token map</li>
</ul>
</li>
<li><code>request.method</code> - request method<ul>
<li><code>get</code></li>
<li><code>list</code></li>
<li><code>create</code></li>
<li><code>update</code></li>
<li><code>delete</code></li>
</ul>
</li>
<li><code>request.path</code> - path of affected resource</li>
<li><code>request.query</code> - map of query properties when present<ul>
<li><code>limit</code></li>
<li><code>offset</code></li>
<li><code>orderBy</code></li>
</ul>
</li>
<li><code>request.time</code> - time request was received</li>
<li><code>request.resource</code> - new resource value, present on write requests only</li>
</ul>
</li>
</ul>
<h3>Request Resource</h3>
<ul>
<li><code>request.resource</code> - resource document <em>AFTER</em> write<ul>
<li><code>request.resource</code> - map of document data</li>
<li><code>resource.id</code> - resource document id as string</li>
<li><code>resource[&#39;__name__&#39;]</code> or <code>request.path</code> - the document name as path</li>
</ul>
</li>
</ul>
<h3>Request token</h3>
<ul>
<li><code>request.auth.token</code><ul>
<li><code>email</code></li>
<li><code>email_verified</code></li>
<li><code>phone_number</code></li>
<li><code>name</code></li>
<li><code>sub</code></li>
<li><code>firebase.identities</code></li>
<li><code>firebase.sign_in_provider</code></li>
<li><code>firebase.tenant</code></li>
</ul>
</li>
</ul>
<h3>Request Patterns</h3>
<ul>
<li><code>request.path[3]</code> - request collection</li>
<li><code>request.path[4]</code> - request document id or <code>request.resource.id</code></li>
<li><code>request.path[N]</code> - request sub-collection and sub-collection document ID</li>
</ul>
<p>There is currently no way to check if a request is for a <code>collection</code> or <code>sub-collection</code> document inside a function. See <a href="https://stackoverflow.com/questions/49193264/size-of-firestore-rules-path">StackOverflow</a>.</p>
<h2>Counters and Timestamp Rules</h2>
<ul>
<li><a href="https://code.build/p/firestore-secure-batch-increment-IKO13Z">Batch Increment Counters</a>
You can verify increments with rules.</li>
<li><a href="https://code.build/p/firestore-dates-and-timestamps-WjGiiQ">Dates and Timestamps</a>
You can use <code>request.time</code> to verify <code>serverTimestamp()</code></li>
</ul>
<h2>Ownership</h2>
<ul>
<li>Create<ul>
<li>Check for ownership after creation</li>
</ul>
</li>
<li>Delete<ul>
<li>Check for ownership before deletion</li>
</ul>
</li>
<li>Update<ul>
<li>Check for ownership both before and after updating</li>
</ul>
</li>
</ul>
<pre><code class="language-jsx">// assumes you have a `createdBy` set to uid

function isOwnerBefore() {
  return request.auth.uid == resource.data.createdBy
   || resource == null;
}

function isOwnerAfter() {
  return request.auth.uid == request.resource.data.createdBy 
    || request.resource == null;
}
</code></pre>
<p>📝 Checking for <code>null</code> before and after makes the function usable in more places with <code>write</code> instead of a separate <code>create</code> and <code>update</code>.</p>
<h2>Logged In</h2>
<pre><code class="language-jsx">function isLoggedIn() {
  return request.auth != null;
}
</code></pre>
<h2>Verifying Keys</h2>
<p>You can verify a document only contains certain fields or that only certain fields can be updated. First, you must get the <code>keys()</code> from the <code>map</code> type.</p>
<pre><code class="language-jsx">function hasAll(data, fields) {
  // data has all fields
  return data.keys().hasAll(fields);
}

function hasOnly(data, fields) {
  // data has only fields
  return data.keys().hasOnly(fields);
}

function isValidUserDocument() {
  let fields = [&#39;title&#39;, &#39;slug&#39;, &#39;minutes&#39;, &#39;content&#39;];
  return hasAll(request.resource.data, fields);
}
</code></pre>
<p>📝 Arrays are <code>lists</code> in Firebase Rules.</p>
<h3>List Methods</h3>
<ul>
<li><code>concat</code></li>
<li><code>hasAll</code></li>
<li><code>hasAny</code></li>
<li><code>hasOnly</code></li>
<li><code>join</code></li>
<li><code>removeAll</code></li>
<li><code>size</code></li>
<li><code>toSet</code> - a unique set of lists</li>
</ul>
<h3>Map Methods</h3>
<ul>
<li><code>diff(map_to_compare)</code></li>
<li><code>get(key, default_value)</code></li>
<li><code>keys()</code></li>
<li><code>size()</code></li>
<li><code>values()</code></li>
</ul>
<h2>String Verification Example</h2>
<p>Strings should be verified as well.</p>
<pre><code class="language-jsx">// verify the content is string type and at least 2 characters
&amp;&amp; request.resource.data.content is string
&amp;&amp; request.resource.data.content.size() &gt; 2
</code></pre>
<h3>String Methods</h3>
<ul>
<li><code>lower()</code></li>
<li><code>matches(re)</code></li>
<li><code>replace(re, sub)</code></li>
<li><code>size()</code></li>
<li><code>split(re)</code></li>
<li><code>toUtf8()</code></li>
<li><code>trim()</code></li>
<li><code>upper()</code></li>
</ul>
<h3>Slug Example</h3>
<p>You could verify a slug, although this might be better handled in a trigger write function.</p>
<pre><code class="language-jsx">// verify slug = a slugify(title)
// should be updated for utf8 characters
request.resource.data.slug == request.resource.data.title
.lower().replace(&#39;[^a-z0-9]+&#39;, &#39;-&#39;).replace(&#39;^-|-$&#39;,&#39;&#39;)
</code></pre>
<h2>Other Types</h2>
<ul>
<li>You can view the <a href="https://firebase.google.com/docs/reference/rules/index-all">Firestore Rules Index</a> of all functions and operators for all types.</li>
</ul>
<h2>Reading is Expensive</h2>
<p>⚠️ Always use the current document from the <code>request.resource</code>, and <code>resource</code> variables. If you need to compare other documents, it will cost one read for each document. You should also consider custom claims instead of reading the user document from <code>request.auth.token</code>. These are all free, as you are already reading the document outside Firestore Rules.</p>
<h3>Reading Firestore External Documents</h3>
<p>You can compare other documents with four functions when it can&#39;t be avoided.</p>
<ul>
<li><code>get(path)</code> - document before write</li>
<li><code>getAfter(path)</code> - document after write</li>
<li><code>exists(path)</code> - or <code>get(path) != null</code> whether document exists before write</li>
<li><code>existsAfter(path)</code> - or <code>getAfter(path) != null</code> whether document exists after write</li>
</ul>
<pre><code class="language-jsx">allow write: if exists(/databases/${database)/documents/collectionName/documentID)
</code></pre>
<h3>Pricing</h3>
<ul>
<li>You are only charged one read for multiple requests to the same document.</li>
<li>You are only charged one read for the same document in a transaction or batch.<ul>
<li>See <a href="https://stackoverflow.com/a/60676273/271450">StackOverFlow Answer</a> by Firebase Staff</li>
</ul>
</li>
<li>Some document access calls may be cached and don’t count against you</li>
</ul>
<h2>Clean Code and Limitations</h2>
<p>You need to model your data correctly to avoid the least amount of reads, keep your data consistent, and not have too much hassle. This requires you to know Firestore well, when to use Functions or the Server Environment, and how to write clean code.</p>
<h3>Reusable Functions</h3>
<p>I personally avoid using variables in the match path. You can get the data you need from the <code>request</code> and <code>resource</code> variables anywhere in your code; there is no reason to create extra function parameters for global variables. </p>
<h3>Notable Function Limitations</h3>
<ul>
<li>Maximum of 7 function arguments</li>
<li>Maximum of 10 let variables per function</li>
<li>Maximum of 20 function call depth</li>
<li>Maximum of 1000 expressions evaluated per request</li>
</ul>
<h3>No Loops</h3>
<p>You cannot do loops or recursion in Firestore. This means you must set verifiable limits to verify a data set like <code>tags</code>. Your function would need to run a string test 1-5 times, for example. Otherwise, you would use Firebase Functions to prevent problematic documents (after the write).</p>
<h3>Match</h3>
<p>Put all your <code>match</code> statements in one place, usually at the top or bottom of the document. The top will allow you less scrolling to see what you need. I like to use one function for every match call to keep in line with the <em>Single Responsibilty Principle</em> and to have cleaner code. However, you must keep track of the function dependencies to not reach the Firestore Rules Limitations.</p>
<pre><code class="language-jsx">
  match /databases/{database}/documents {
  
    // MATCHES

    match /users/{document} {
    
			allow read, write: if allowUser();
			
    }
    
    // FUNCTIONS
    
    function allowUser() {
    
      return isLoggedIn() &amp;&amp; isOwner()...
      
    }
    
    ...
    
  }
</code></pre>
<p>Writing Firebase Security Rules is an art. </p>
<p>J</p>
]]></description><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category></item><item><title>SolidJS Todo App with Firebase</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Wed, 01 May 2024 13:00:00 GMT</pubDate><link>https://code.build/p/solidjs-todo-app-with-firebase-p0UcKV</link><guid>https://code.build/p/solidjs-todo-app-with-firebase-p0UcKV</guid><description><![CDATA[<p>I figured SolidJS would be just like React, but it’s not. It’s way better. I had a few learning curves but got my app up and running. My biggest problem is the lack of good documentation for Solid Start techniques, but the framework itself is solid. See what I did there?</p>
<h2>TL;DR</h2>
<p>Solid Start is similar to <a href="https://code.build/p/nextjs-todo-app-with-firebase-sWvbf7">NextJS</a> but works differently under the hood. Using helper functions like <code>Show</code> and <code>For</code> makes JSX more bearable and the underlying signals quick. The loaders require a few extra boilerplate steps, but <code>Suspense</code> can be powerful if you need streaming.</p>
<h2>Setup</h2>
<p>First, set up your <code>.env</code> file with the Firebase credentials as usual but with the <code>VITE_</code> prefix for client recognition.</p>
<pre><code class="language-tsx">VITE_PUBLIC_FIREBASE_CONFIG={&quot;apiKey&quot;:&quot;...&quot;,&quot;authDomain&quot;:&quot;...&quot;...}
</code></pre>
<p>📝 Make sure your keys have quotes.</p>
<h3>Firebase Library</h3>
<p>Create your <code>lib/firebase.ts</code> file, sharing your <code>app</code> for the server version.</p>
<pre><code class="language-tsx">
import { getApp, getApps, initializeApp } from &#39;firebase/app&#39;;
import { getAuth } from &#39;firebase/auth&#39;;
import { getFirestore } from &#39;firebase/firestore&#39;;

const firebase_config = JSON.parse(
    import.meta.env.VITE_PUBLIC_FIREBASE_CONFIG
);

// initialize and login

export const app = getApps().length
    ? getApp()
    : initializeApp(firebase_config);

export const auth = getAuth(app);
export const db = getFirestore(app);
</code></pre>
<h2>Shared Context</h2>
<p>I created a universal <a href="https://code.build/p/nextjs-todo-app-with-firebase-sWvbf7#accessing-the-variable">Shared Context Library for React</a>. Since this uses almost identical context code as React, it was easy to translate. This allows you to have one provider and import your reusable hooks wherever you want.</p>
<pre><code class="language-tsx">import {
    Component,
    JSX,
    createContext,
    useContext,
    type Context
} from &quot;solid-js&quot;;

const _Map = &lt;T,&gt;() =&gt; new Map&lt;string, T&gt;();
const Context = createContext(_Map());

export const Provider: Component&lt;{ children: JSX.Element }&gt; = ({
    children
}) =&gt;
    &lt;Context.Provider value={_Map()}&gt;{children}&lt;/Context.Provider&gt;;

const useContextProvider = &lt;T,&gt;(key: string) =&gt; {
    const context = useContext(Context);
    return {
        set value(v: T) { context.set(key, v); },
        get value() {
            if (!context.has(key)) {
                throw Error(`Context key &#39;${key}&#39; Not Found!`);
            }
            return context.get(key) as T;
        }
    }
};

export const useShared = &lt;T, A&gt;(
    key: string,
    fn: (value?: A) =&gt; T,
    initialValue?: A
) =&gt; {
    const provider = useContextProvider&lt;Context&lt;T&gt;&gt;(key);
    if (initialValue !== undefined) {
        const state = fn(initialValue);
        const Context = createContext&lt;T&gt;(state);
        provider.value = Context;
    }
    return useContext(provider.value);
};
</code></pre>
<p>Put the <code>Provider</code> inside the <code>routes/index.ts</code> file so they can be used for our main route. Normally, you would share this everywhere.</p>
<pre><code class="language-tsx">import { Title } from &quot;@solidjs/meta&quot;;
import Home from &quot;~/components/home&quot;;
import { Provider } from &quot;~/lib/use-shared&quot;;

export default function Index() {
  return (
    &lt;main&gt;
      &lt;Provider&gt;
        &lt;Title&gt;SolidStart - Firebase&lt;/Title&gt;
        &lt;Home /&gt;
      &lt;/Provider&gt;
    &lt;/main&gt;
  );
}
</code></pre>
<p>🖇️ Notice how easy it is to use the <code>Title</code> tag anywhere in your code edit the meta tag.</p>
<h2>User Hook</h2>
<p>The signals in Solid work almost identically to <code>useState</code> and <code>useEffect</code> in React; they are just faster and actually use signals. No more <code>useMemo</code> malarky for basic rendering. That being said, you can use <code>createMemo</code> to memoize expensive computations. For this example, we are using <code>createStore</code>, but we could have used <code>createSignal</code>.</p>
<pre><code class="language-tsx">import { useShared } from &quot;./use-shared&quot;;
import {
    User,
    onIdTokenChanged,
    signOut,
    signInWithPopup,
    GoogleAuthProvider
} from &quot;firebase/auth&quot;;
import { auth } from &quot;./firebase&quot;;
import { createStore } from &quot;solid-js/store&quot;;
import { onCleanup } from &quot;solid-js&quot;;

export interface userData {
    photoURL: string | null;
    uid: string;
    displayName: string | null;
    email: string | null;
};

type UserState = {
    loading: boolean;
    data: userData | null;
};

export function _useUser(initialValue: UserState = {
    loading: true,
    data: null
}) {

    const _store = createStore&lt;UserState&gt;(initialValue);

    const setUser = _store[1];

    setUser(v =&gt; ({ ...v, loading: true }));

    // subscribe to user changes
    const unsubscribe = onIdTokenChanged(auth, (_user: User | null) =&gt; {

        if (!_user) {
            setUser({ data: null, loading: false });
            return;
        }

        // map data to user data type
        const { photoURL, uid, displayName, email } = _user;
        const data = { photoURL, uid, displayName, email };

        // print data in dev mode
        if (process.env.NODE_ENV === &#39;development&#39;) {
            console.log(data);
        }

        // set store
        setUser({ loading: false, data });
    });

    onCleanup(unsubscribe);

    return _store;
}

export const useUser = (initialValue?: UserState) =&gt;
    useShared(&#39;user&#39;, _useUser, initialValue);

export const loginWithGoogle = () =&gt;
    signInWithPopup(auth, new GoogleAuthProvider());

export const logout = () =&gt; signOut(auth);
</code></pre>
<h3>📌 User Notice</h3>
<ul>
<li>I’m converting the <code>onIdTokenChanged</code> event handler to a signal.</li>
<li>Always unsubscribe. In SolidJS, we use <code>onCleanup</code>.</li>
<li>Export the hook with <code>useShared</code>, we only want to subscribe once when we share our user state.</li>
</ul>
<h2>Todos Hook</h2>
<p>Fetching <code>todos</code> is similar but relies on the <code>user</code> signal.</p>
<pre><code class="language-tsx">import {
    type DocumentData,
    onSnapshot,
    type QuerySnapshot,
    Timestamp
} from &#39;firebase/firestore&#39;;
import {
    addDoc,
    collection,
    deleteDoc,
    doc,
    orderBy,
    query,
    serverTimestamp,
    updateDoc,
    where
} from &#39;firebase/firestore&#39;;

import { db } from &#39;./firebase&#39;;
import { useUser } from &#39;./use-user&#39;;
import { createStore } from &#39;solid-js/store&#39;;
import { createComputed, onCleanup } from &#39;solid-js&#39;;

export interface TodoItem {
    id: string;
    text: string;
    complete: boolean;
    created: Date;
    uid: string;
};

export const snapToData = (
    q: QuerySnapshot&lt;DocumentData, DocumentData&gt;
) =&gt; {

    // creates todo data from snapshot
    if (q.empty) {
        return [];
    }
    return q.docs.map((doc) =&gt; {
        const data = doc.data({
            serverTimestamps: &#39;estimate&#39;
        });
        const created = data.created as Timestamp;
        return {
            ...data,
            created: created.toDate(),
            id: doc.id
        }
    }) as TodoItem[];
}

export function useTodos() {

    const _user = useUser();

    const _store = createStore&lt;{
        todos: TodoItem[],
        loading: boolean
    }&gt;({
        todos: [],
        loading: true
    });

    const user = _user[0];

    const setTodos = _store[1];

    setTodos(v =&gt; ({
        ...v,
        loading: true
    }));

    createComputed(() =&gt; {

        if (!user.data) {
            setTodos({
                loading: false,
                todos: []
            });
            return _store[0];
        }

        const unsubscribe = onSnapshot(

            // query realtime todo list
            query(
                collection(db, &#39;todos&#39;),
                where(&#39;uid&#39;, &#39;==&#39;, user.data.uid),
                orderBy(&#39;created&#39;)
            ), (q) =&gt; {

                // get data, map to todo type
                const data = snapToData(q);

                /**
                 * Note: Will get triggered 2x on add 
                 * 1 - for optimistic update
                 * 2 - update real date from server date
                 */

                // print data in dev mode
                if (process.env.NODE_ENV === &#39;development&#39;) {
                    console.log(data);
                }

                // add to store
                setTodos({
                    loading: false,
                    todos: data
                });

            });

        onCleanup(unsubscribe);
    });

    return _store[0];
};

export const addTodo = (
    e: SubmitEvent,
    uid: string
) =&gt; {

    e.preventDefault();

    // get and reset form
    const target = e.target as HTMLFormElement;
    const form = new FormData(target);
    const { task } = Object.fromEntries(form);

    if (typeof task !== &#39;string&#39;) {
        return;
    }

    // reset form
    target.reset();

    addDoc(collection(db, &#39;todos&#39;), {
        uid,
        text: task,
        complete: false,
        created: serverTimestamp()
    });
}

export const updateTodo = (id: string, complete: boolean) =&gt; {
    updateDoc(doc(db, &#39;todos&#39;, id), { complete });
}

export const deleteTodo = (id: string) =&gt; {
    deleteDoc(doc(db, &#39;todos&#39;, id));
}
</code></pre>
<h3>📌 Todos Notice</h3>
<ul>
<li>I’m using <code>createComputed</code> to derive the <code>onSnapshot</code> event handler to a signal from the <code>user</code> signal.</li>
<li><code>createComputed</code> is unique to SolidJS and solves the reactivity problem with <code>effect()</code>.</li>
<li>Again, always unsubscribe. In SolidJS, we use <code>onCleanup</code>.</li>
<li>We don’t need <code>useShared</code> since this is only called once in one component.</li>
<li>The <code>snapToData</code> converts the collection array of metadata, to actual data. See <a href="https://code.build/p/firestore-query-and-mutation-patterns-w5vhl7#snapshot">Data Patterns</a>.</li>
</ul>
<h2>Profile Component</h2>
<p>The first big change you will notice with Solid is the <code>Show</code> component. It makes handling JSX much easier. However, when dealing with a possible <code>null</code> or <code>undefined</code> check, you have to wrap the internal child component inside this awkward <code>{() =&gt; ( ... )}</code>. I understand this is necessary for proper reactive handling, but it is really a hack for JSX’s shortcomings. Either way, it is better than React.</p>
<pre><code class="language-tsx">import { Logout } from &quot;~/lib/helpers&quot;;
import { useUser } from &quot;~/lib/use-user&quot;;
import Todos from &quot;./todos&quot;;
import { Show } from &quot;solid-js&quot;;

export default function Profile() {

    const [user] = useUser();

    return (
        &lt;Show when={user.data}&gt;
            {(user) =&gt; (
                &lt;div class=&quot;flex flex-col gap-3 items-center&quot;&gt;
                    &lt;h3 class=&quot;font-bold&quot;&gt;Hi {user().displayName}!&lt;/h3&gt;
                    &lt;Show when={user().photoURL}&gt;
                        {(data) =&gt; (
                            &lt;img src={data()} width=&quot;100&quot; height=&quot;100&quot; alt=&quot;user avatar&quot; /&gt;
                        )}
                    &lt;/Show&gt;
                    &lt;p&gt;Your userID is {user().uid}&lt;/p&gt;
                    &lt;Logout /&gt;
                    &lt;Todos /&gt;
                &lt;/div&gt;
            )}
        &lt;/Show&gt;
    );
}
</code></pre>
<h2>Todos Component</h2>
<p>The <code>todos</code> require a similar pattern. I must check for the <code>user</code> again since I use a shared hook. If you deal with prop-drilling, you won’t have this problem, but you will have to deal with prop-drilling.</p>
<pre><code class="language-tsx">import { addTodo, useTodos } from &quot;~/lib/use-todos&quot;;
import { useUser, userData } from &quot;~/lib/use-user&quot;;
import { Todo } from &quot;./todo-item&quot;;
import { For, Show } from &quot;solid-js&quot;;

export default function Todos() {

    const _user = useUser();

    const todoStore = useTodos();

    const user = _user[0];

    return (
        &lt;Show when={user.data}&gt;
            {(user) =&gt; (
                &lt;&gt;
                    &lt;div class=&quot;grid grid-cols-[auto,auto,auto,auto] gap-3 justify-items-start&quot;&gt;
                        &lt;For each={todoStore.todos} fallback={&lt;p&gt;&lt;b&gt;Add your first todo item!&lt;/b&gt;&lt;/p&gt;}&gt;
                            {(todo, _index) =&gt; (
                                &lt;Todo {...{ todo }} /&gt;
                            )}
                        &lt;/For&gt;
                    &lt;/div&gt;
                    &lt;TodoForm {...user()} /&gt;
                &lt;/&gt;
            )}
        &lt;/Show&gt;
    );
}

export const TodoForm = (user: userData) =&gt; {
    return (
        &lt;form class=&quot;flex gap-3 items-center justify-center mt-5&quot; onSubmit={(e) =&gt; addTodo(e, user.uid)}&gt;
            &lt;input class=&quot;border p-2&quot; name=&quot;task&quot; /&gt;
            &lt;button class=&quot;border p-2 rounded-md text-white bg-sky-700&quot; type=&quot;submit&quot;&gt;
                Add Task
            &lt;/button&gt;
        &lt;/form&gt;
    );
};
</code></pre>
<ul>
<li>The <code>fallback</code> is a simple way to handle the loading state of an empty array.</li>
<li>The items are tracked automatically, so you don’t need a <code>key</code>. There are no <code>Fragment</code> types in Solid for this.</li>
<li>You still need to pass the odd <code>{(item, index) =&gt; ()}</code> inside a <code>For</code> loop.</li>
<li>React uses <code>className</code>, but Solid is smarter and uses our friend <code>class</code>.</li>
</ul>
<h2>Todo Item</h2>
<p>The individual <code>todos</code> are cleaner with <code>Show</code> than with ternaries.</p>
<pre><code class="language-tsx">import { Show } from &quot;solid-js&quot;;
import { TodoItem, deleteTodo, updateTodo } from &quot;~/lib/use-todos&quot;;

// each todo item
export const Todo = ({ todo }: { todo: TodoItem }) =&gt; {
    return (
        &lt;&gt;
            &lt;span class={todo.complete ? &#39;line-through text-green-700&#39; : &#39;&#39;}&gt;
                {todo.text}
            &lt;/span&gt;
            &lt;span class={todo.complete ? &#39;line-through text-green-700&#39; : &#39;&#39;}&gt;
                {todo.id}
            &lt;/span&gt;
            &lt;Show when={todo.complete}&gt;
                &lt;button type=&quot;button&quot; onClick={() =&gt; updateTodo(todo.id, !todo.complete)}&gt;
                    ✔️
                &lt;/button&gt;
            &lt;/Show&gt;
            &lt;Show when={!todo.complete}&gt;
                &lt;button type=&quot;button&quot; onClick={() =&gt; updateTodo(todo.id, !todo.complete)}&gt;
                    ❌
                &lt;/button&gt;
            &lt;/Show&gt;
            &lt;button type=&quot;button&quot; onClick={() =&gt; deleteTodo(todo.id)}&gt;🗑&lt;/button&gt;
        &lt;/&gt;
    );
};
</code></pre>
<p>🖇️ I didn’t need the JSX <code>{() =&gt; ()}</code> because I wasn’t dealing with <code>null</code> or <code>undefined</code> inside the <code>Show</code> component.</p>
<h2>Home</h2>
<p>Finally, we can go home. This is the first component that gets rendered, but I saved it for last.</p>
<pre><code class="language-tsx">import { Loading, Login } from &quot;~/lib/helpers&quot;;
import { useUser } from &quot;~/lib/use-user&quot;;
import Profile from &quot;./profile&quot;;
import { Match, Switch } from &quot;solid-js&quot;;

export default function Home() {

    const [user] = useUser({ data: null, loading: true });

    return (
        &lt;div class=&quot;text-center&quot;&gt;
            &lt;h1 class=&quot;text-3xl font-semibold my-3&quot;&gt;
                Solid Start Firebase Todo App
            &lt;/h1&gt;
            &lt;Switch fallback={&lt;Login /&gt;}&gt;
                &lt;Match when={user.loading}&gt;
                    &lt;Loading /&gt;
                &lt;/Match&gt;
                &lt;Match when={user.data}&gt;
                    &lt;Profile /&gt;
                &lt;/Match&gt;
            &lt;/Switch&gt;
        &lt;/div&gt;
    );
}
</code></pre>
<p>We set the <code>user</code> for the first time before <code>onAuthIdTokenChanged</code>. I also used <code>Switch</code> and <code>Match</code> to show you other possibilities as well.</p>
<h2>About Page</h2>
<p>First, create an about library at <code>lib/about.ts</code> that uses Firestore Lite if you want to host on <a href="https://code.build/p/firestore-query-and-mutation-patterns-w5vhl7#edge-functions">the Edge</a>.</p>
<pre><code class="language-tsx">import { doc, getDoc, getFirestore } from &quot;firebase/firestore/lite&quot;;
import { app } from &quot;./firebase&quot;;

const db = getFirestore(app);

type AboutDoc = {
    name: string;
    description: string;
};

export const getAbout = async () =&gt; {

    const aboutSnap = await getDoc(
        doc(db, &#39;/about/ZlNJrKd6LcATycPRmBPA&#39;)
    );

    if (!aboutSnap.exists()) {
        throw &#39;Document does not exist!&#39;;
    }

    return aboutSnap.data() as AboutDoc;
};
</code></pre>
<h3>About Route</h3>
<p>Loading the data from the server required a lot of extra boilerplate compared to <a href="https://code.build/p/nextjs-todo-app-with-firebase-sWvbf7">NextJS</a>, <a href="https://code.build/p/qwik-todo-app-with-firebase-P2rX5i">Qwik</a>, or <a href="https://code.build/p/remix-todo-app-with-firebase-p4HFXN">Remix</a>. There were no good, complete examples, so I had to hit up Discord to figure this out.</p>
<pre><code class="language-tsx">import { Title } from &quot;@solidjs/meta&quot;;
import { RouteDefinition, cache, createAsync } from &quot;@solidjs/router&quot;;
import { Show } from &quot;solid-js&quot;;
import { getAbout } from &quot;~/lib/about&quot;;

// load data once and cache it
const getAboutPage = cache(async () =&gt; {
  &#39;use server&#39;;
  return await getAbout();
}, &#39;about&#39;);

// preload the data
export const route = {
  load: () =&gt; getAboutPage(),
} satisfies RouteDefinition;

export default function About() {

  // get your preloaded data
  const about = createAsync(() =&gt; getAboutPage(), { deferStream: true });

  return (
    &lt;Show when={about()}&gt;
      {(data) =&gt; (
        &lt;&gt;
          &lt;Title&gt;About&lt;/Title&gt;
          &lt;div class=&quot;flex items-center justify-center my-5&quot;&gt;
            &lt;div class=&quot;border w-[400px] p-5 flex flex-col gap-3&quot;&gt;
              &lt;h1 class=&quot;text-3xl font-semibold&quot;&gt;{data().name}&lt;/h1&gt;
              &lt;p&gt;{data().description}&lt;/p&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/&gt;
      )}
    &lt;/Show&gt;
  );
};
</code></pre>
<p>Solid Start uses <code>Suspense</code> by default to stream your data to the client as it becomes available. This is awesome, but it gives a bad user experience for this particular example. I don’t want the page to ever be blank. Waiting those few extra milliseconds is worth the wait. Adding <code>deferStream</code> accomplished this.</p>
<h2>Final Thoughts</h2>
<p>I hate JSX, but SolidJS makes it a bit more bearable if you learn its patterns. While the documentation needs more clear examples, building with it was fun.</p>
<p><strong>Demo:</strong> <a href="https://solid-start-firebase.vercel.app/">Vercel Edge</a></p>
<p><strong>Repo:</strong> <a href="https://github.com/jdgamble555/solid-start-firebase">GitHub</a></p>
]]></description><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category></item><item><title>Local Development with Firebase Emulators</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Sun, 28 Apr 2024 13:00:00 GMT</pubDate><link>https://code.build/p/local-development-with-firebase-emulators-PV6xq8</link><guid>https://code.build/p/local-development-with-firebase-emulators-PV6xq8</guid><description><![CDATA[<p>I noticed there are many articles about creating unit tests with Firestore Emulators, but I find them most useful for building apps. You can run the full Firebase Local Suite without touching your live data in the cloud or creating an extra development database.</p>
<h2>TL;DR</h2>
<p>Always remember to install Java JDK. Follow the <code>firebase init</code> instructions. Connect the emulators to your app in development mode only, and use the authentication to generate a random fake user for you.</p>
<h2>Emulator Setup</h2>
<ol>
<li>Install Java JDK</li>
</ol>
<p>The Firebase Emulator Suite is built upon the <a href="https://jdk.java.net/">Java JDK</a>. It must be installed first.</p>
<ol start="2">
<li>Make sure you have <em>Firebase Tools</em> installed globally.</li>
</ol>
<pre><code class="language-tsx">npm install -g firebase-tools
</code></pre>
<ol start="3">
<li>Initialize Firebase inside your project.</li>
</ol>
<pre><code class="language-tsx">firebase init
</code></pre>
<ol start="4">
<li><p>Select <code>Emulators</code> and whatever features you want to test locally.
<img src="https://code.build/images/path/posts/9387aa8c-f9fd-4f61-b85c-8b8a33265911/notlsrsxqlunuyfufdhm5g5a7.png" title="null" alt="Emulators Installation" /></p>
</li>
<li><p>Set up the <code>default</code> project.</p>
</li>
</ol>
<p>The full set of <em>Firebase Emulators</em> requires you to link with an existing project. However, you do not have to modify your cloud version if you’re spinning up a project for local testing.</p>
<ol start="6">
<li>Use the defaults for <code>firestore.rules</code> and <code>firestore.indexes.json</code> files. Install <code>TypeScript</code> because you’re a professional with standards who writes quality code. Use <code>eslint</code> for testing, and go ahead and install dependencies.</li>
<li>Finally, select the default ports and add the emulators you want to use, like <code>Authentication</code>, <code>Functions</code>, and <code>Firestore</code>.</li>
</ol>
<p>🗈 If you’re using any of my test projects, use <code>firebase use</code> to connect your local instance to an active database in your Firebase Cloud.</p>
<h3>Emulator Helper</h3>
<p>I like to add a helper in <code>package.json</code> so that the emulators can be run without switching directories. This is particularly useful with Firebase Functions Emulators.</p>
<pre><code class="language-tsx">&quot;emulators&quot;: &quot;npm run build --prefix functions &amp; firebase emulators:start&quot;
</code></pre>
<h3>Functions with Eslint</h3>
<p>Edit <code>functions/.eslintrc.js</code> to include <code>.eslintrc.js</code> in the ignore file. </p>
<pre><code class="language-tsx">  ignorePatterns: [
    &quot;/lib/**/*&quot;, // Ignore built files.
    &quot;.eslintrc.js&quot;
  ],
</code></pre>
<p>Also, change your rules to fit your style of writing code.</p>
<pre><code class="language-tsx">
module.exports = {
  root: true,
  env: {
    es6: true,
    node: true,
  },
  extends: [
    &quot;plugin:import/errors&quot;,
    &quot;plugin:import/warnings&quot;,
    &quot;plugin:import/typescript&quot;,
    &quot;google&quot;,
    &quot;plugin:@typescript-eslint/recommended&quot;,
  ],
  parser: &quot;@typescript-eslint/parser&quot;,
  parserOptions: {
    project: [&quot;tsconfig.json&quot;, &quot;tsconfig.dev.json&quot;],
    sourceType: &quot;module&quot;,
    tsconfigRootDir: __dirname,
  },
  ignorePatterns: [
    &quot;/lib/**/*&quot;, // Ignore built files.
    &quot;.eslintrc.js&quot;
  ],
  plugins: [
    &quot;@typescript-eslint&quot;,
    &quot;import&quot;,
  ],
  rules: {
    &quot;quotes&quot;: [&quot;error&quot;, &quot;single&quot;],
    &#39;object-curly-spacing&#39;: [&#39;error&#39;, &#39;always&#39;],
    &quot;import/no-unresolved&quot;: 0,
    &quot;indent&quot;: [&quot;error&quot;, 4],
  },
};
</code></pre>
<h2>Firebase Setup</h2>
<p>Create a <code>lib/firebase.ts</code> file and add emulator support. Your <a href="https://code.build/c/firebase/framework-setup">framework implementation</a> may vary slightly.</p>
<pre><code class="language-tsx">// emulators
if (dev) {
    connectFirestoreEmulator(db, &#39;localhost&#39;, 8080);
    connectAuthEmulator(auth, &#39;http://localhost:9099&#39;);
    connectFunctionsEmulator(functions, &quot;http://localhost&quot;, 5001);
}
</code></pre>
<p>Here is a full example file.</p>
<pre><code class="language-tsx">import { PUBLIC_FIREBASE_CONFIG } from &#39;$env/static/public&#39;;
import { dev } from &#39;$app/environment&#39;;

import { getApps, initializeApp } from &#39;firebase/app&#39;;
import { connectAuthEmulator, getAuth } from &#39;firebase/auth&#39;;
import { connectFirestoreEmulator, getFirestore } from &#39;firebase/firestore&#39;;
import { connectFunctionsEmulator, getFunctions } from &#39;firebase/functions&#39;;

const firebase_config = JSON.parse(
    PUBLIC_FIREBASE_CONFIG
);

// initialize and login

if (!getApps().length) {
    initializeApp(firebase_config);
}

export const auth = getAuth();
export const db = getFirestore();
export const functions = getFunctions();

// emulators
if (dev) {
    connectFirestoreEmulator(db, &#39;localhost&#39;, 8080);
    connectAuthEmulator(auth, &#39;http://localhost:9099&#39;);
    connectFunctionsEmulator(functions, &quot;http://localhost&quot;, 5001);
}
</code></pre>
<p>📝 The ports are imported differently depending on the service.</p>
<h3>Angular</h3>
<p>If you’re using Angular, you must import it into each of the <em>providers</em> array.</p>
<pre><code class="language-tsx">export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideClientHydration(),
    // Angular Providers
    importProvidersFrom(
      provideFirebaseApp(() =&gt; initializeApp(firebaseConfig)),
      provideFirestore(() =&gt; {
            const firestore = getFirestore();
            connectFirestoreEmulator(firestore, &#39;localhost&#39;, 8080);
            return firestore;
        }),
      provideAuth(() =&gt; getAuth()),
      // provideStorage(() =&gt; getStorage()),
      // provideAnalytics(() =&gt; getAnalytics()),
    )
  ]
};
</code></pre>
<h2>Running the Emulators</h2>
<p>Thanks to our emulators helper, we can run the emulators in our root directory with <code>npm run emulators</code>. We should get a second window that opens. Minimize that. Our main terminal will have the emulator links. We can click <code>ctrl+</code> and the link to open a new window with our emulators.</p>
<p><img src="https://code.build/images/path/posts/9387aa8c-f9fd-4f61-b85c-8b8a33265911/waoqscfdpbwxludyfdhm5mgjk.png" title="null" alt="Running Emulators" /></p>
<h2>Authentication</h2>
<p>When we want to login to our app, we must create a fake profile.</p>
<p><img src="https://code.build/images/path/posts/9387aa8c-f9fd-4f61-b85c-8b8a33265911/flibvtwgmlrghpfzfdhm63de1.png" title="null" alt="Firebase Emulators Login" /></p>
<p>Clicking on <code>add a new account</code> allows us to emulate a real user.</p>
<p><img src="https://code.build/images/path/posts/9387aa8c-f9fd-4f61-b85c-8b8a33265911/tltrbgcfgpajixplfdhm67m7d.png" title="null" alt="Firebase Emulator Fake Profile" /></p>
<p>We can then sign in with this fake user to test our app.</p>
<h2>Firestore Emulator</h2>
<p>The Firestore Emulator will be in purple to clarify that it is a development environment. </p>
<p><img src="https://code.build/images/path/posts/9387aa8c-f9fd-4f61-b85c-8b8a33265911/sofimbgltfngkhkofdhm6a418.png" title="null" alt="Firestore Emulators" /></p>
<p>Everything else should work as expected. You can now test your app on your local machine without modifying your live data.</p>
<h3>Seeding Data</h3>
<p>To seed your data, you must write your own functions. You must connect to <a href="https://code.build/p/sveltekit-todo-app-with-firebase-admin-tqdc5j">Firebase Admin</a>, run the function only in dev mode, and call it in your <code>package.json</code> file with a command or in your app only in development mode. </p>
<p>⚠️ All data is lost when you stop the emulators.</p>
<h2>Test Projects</h2>
<p>You can view some of the projects in my <a href="https://code.build/c/firebase/cloud-firestore/counting-documents">Counters</a> posts for Firebase Emulator examples.</p>
<ul>
<li><a href="https://code.build/p/firestore-cloud-functions-counter-JuC0Pw">Firestore Cloud Function Counters</a></li>
<li><a href="https://code.build/p/firestore-server-side-counter-Kj5PZ5">Firestore Server Side Counter</a></li>
</ul>
<p>Now go test locally!</p>
<p>J</p>
]]></description><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category></item><item><title>Remix Todo App with Firebase</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Wed, 24 Apr 2024 13:00:00 GMT</pubDate><link>https://code.build/p/remix-todo-app-with-firebase-p4HFXN</link><guid>https://code.build/p/remix-todo-app-with-firebase-p4HFXN</guid><description><![CDATA[<p>After creating my Todo App multiple times for other Frameworks, I figured creating it for Remix would be easy. After all, I had already built the components in React for <a href="https://code.build/p/nextjs-todo-app-with-firebase-sWvbf7">NextJS</a>. I was wrong. Remix has its own eccentricities. </p>
<h2>TL;DR</h2>
<p>Remix requires a few hacks to get the environment variables to work, but it is not too different from NextJS. Loaders are beautiful, and the idea has been copied into other Frameworks like Qwik and SolidJS. This article shows the pitfalls and configuration settings for a good Remix app with Firebase.</p>
<h2>Firebase Config</h2>
<p>First, put your Firebase configuration data inside the <code>.env</code> file as usual.</p>
<pre><code class="language-tsx">PUBLIC_FIREBASE_CONFIG={&quot;apiKey&quot;:&quot;...&quot;,&quot;authDomain&quot;:&quot;...&quot;...}
</code></pre>
<p>📝 Make sure your keys have quotes.</p>
<h3>Using Environmental Variables</h3>
<p>Remix does not make this easy, as this was by far one of my biggest hurdles. In your <code>root.ts</code> file, create a loader for your <code>env</code> file.</p>
<pre><code class="language-tsx">export async function loader() {
  return json({
    ENV: {
      PUBLIC_FIREBASE_CONFIG: process.env.PUBLIC_FIREBASE_CONFIG
    },
  });
}
</code></pre>
<p>Next, you have to save it to the <code>Window</code> object. This is not <em>my</em> recommended code, but the recommended way by Remix. This should be placed in your <code>Layout</code> component.</p>
<pre><code class="language-tsx">&lt;script
  dangerouslySetInnerHTML={{
    __html: `window.ENV = ${JSON.stringify(
      data.ENV
    )}`,
  }}
/&gt;
&lt;Scripts /&gt;
</code></pre>
<h3>Accessing the Variable</h3>
<p>I created a <code>utils.ts</code> file to get the Firebase variable.</p>
<pre><code class="language-tsx">function isBrowser() {
    return typeof window !== &#39;undefined&#39;;
}

export function getFirebaseConfig() {
    const env = isBrowser()
        ? window.ENV
        : process.env;
    return JSON.parse(env.PUBLIC_FIREBASE_CONFIG);
}
</code></pre>
<p>It needs to be available in any environment.</p>
<p>📝 Interestingly enough, the <code>process.env.NODE_ENV</code> works everywhere as expected.</p>
<h3>Types</h3>
<p>I also found it useful to create a <code>app/global.d.ts</code> file for storing the config types. You need to export something for it to show up throughout your app.</p>
<pre><code class="language-tsx">declare global {
    interface Window {
        ENV: {
            PUBLIC_FIREBASE_CONFIG: string;
        }
    }
    namespace NodeJS {
        interface ProcessEnv {
            PUBLIC_FIREBASE_CONFIG: string;
        }
    }
}

export { }
</code></pre>
<h2>Setup</h2>
<p>I created a reusable Firebase hook. You don’t have to do this; you can export and import the variables wherever you want. Firebase is a singleton by default, so you will always call the same instance. However, it is good practice to do this as most classes won’t use singletons.</p>
<pre><code class="language-tsx">import { getApp, getApps, initializeApp } from &#39;firebase/app&#39;;
import { getAuth } from &#39;firebase/auth&#39;;
import { getFirestore } from &#39;firebase/firestore&#39;;
import { useShared } from &#39;./use-shared&#39;;
import { getFirebaseConfig } from &#39;./utils&#39;;

const firebase_config = getFirebaseConfig();

export const app = getApps().length
    ? getApp()
    : initializeApp(firebase_config);

const _useFirebase = () =&gt; ({
    auth: getAuth(),
    db: getFirestore()
});

export const useFirebase = (init?: boolean) =&gt;
    useShared(&#39;firebase&#39;, _useFirebase, init);
</code></pre>
<p>We could have put the <code>initializeApp</code> in the <code>use firebase</code> hook. However, it must be later exported and reused in the server <em>About</em> component. </p>
<h3>Shared Component</h3>
<p>Notice I also use my reusable single provider <code>useShared</code>.  We only need to call <code>useFirebase</code> once. We need access to them everywhere. This uses contexts under the hood in my <code>use-shared-tsx</code> file.</p>
<pre><code class="language-tsx">import {
    FC,
    ReactNode,
    createContext,
    useContext,
    type Context
} from &quot;react&quot;;

const _Map = &lt;T,&gt;() =&gt; new Map&lt;string, T&gt;();
const Context = createContext(_Map());

export const Provider: FC&lt;{ children: ReactNode }&gt; = ({ children }) =&gt;
    &lt;Context.Provider value={_Map()}&gt;{children}&lt;/Context.Provider&gt;;

const useContextProvider = &lt;T,&gt;(key: string) =&gt; {
    const context = useContext(Context);
    return {
        set value(v: T) { context.set(key, v); },
        get value() {
            if (!context.has(key)) {
                throw Error(`Context key &#39;${key}&#39; Not Found!`);
            }
            return context.get(key) as T;
        }
    }
};

export const useShared = &lt;T, A&gt;(
    key: string,
    fn: (value?: A) =&gt; T,
    initialValue?: A
) =&gt; {
    const provider = useContextProvider&lt;Context&lt;T&gt;&gt;(key);
    if (initialValue !== undefined) {
        const state = fn(initialValue);
        const Context = createContext&lt;T&gt;(state);
        provider.value = Context;
    }
    return useContext(provider.value);
};
</code></pre>
<p>You also need to configure the provider in your <code>entry.client.ts</code> file.</p>
<pre><code class="language-tsx">startTransition(() =&gt; {
  hydrateRoot(
    document,
    &lt;Provider&gt;
      &lt;StrictMode&gt;
        &lt;RemixBrowser /&gt;
      &lt;/StrictMode&gt;
    &lt;/Provider&gt;
  );
});
</code></pre>
<p>I talk more about my universal provider in my <a href="https://dev.to/jdgamble555/easy-shared-reactive-state-in-react-without-external-libraries-36cc">dev.to article.</a></p>
<h2>User Hook</h2>
<p>My user hook is exactly the same as my NextJS Todo App, except you need to import the Firebase hook for the <code>auth</code> and add it to the <code>useEffect</code> dependency array.</p>
<pre><code class="language-tsx">import { useEffect, useState } from &quot;react&quot;;
import { useShared } from &quot;./use-shared&quot;;
import {
    User,
    onIdTokenChanged,
    signOut,
    signInWithPopup,
    GoogleAuthProvider,
    Auth
} from &quot;firebase/auth&quot;;
import { useFirebase } from &quot;./firebase&quot;;

export type userData = {
    photoURL: string | null;
    uid: string;
    displayName: string | null;
    email: string | null;
};

type UserState = {
    loading: boolean;
    data: userData | null;
};

export function _useUser(
    initialValue: UserState = {
        loading: true,
        data: null
    }
) {

    const { auth } = useFirebase();

    const _store = useState&lt;UserState&gt;(initialValue);

    const setUser = _store[1];

    useEffect(() =&gt; {

        if (!auth) {
            return;
        }

        setUser(v =&gt; ({ ...v, loading: true }));

        // subscribe to user changes
        return onIdTokenChanged(auth, (_user: User | null) =&gt; {

            if (!_user) {
                setUser({ data: null, loading: false });
                return;
            }

            // map data to user data type
            const { photoURL, uid, displayName, email } = _user;
            const data = { photoURL, uid, displayName, email };

            // print data in dev mode
            if (process.env.NODE_ENV === &#39;development&#39;) {
                console.log(data);
            }

            // set store
            setUser({ loading: false, data });
        });

    }, [setUser, auth]);

    return _store;
}

export const useUser = (initialValue?: UserState) =&gt;
    useShared(&#39;user&#39;, _useUser, initialValue);

export const loginWithGoogle = (auth: Auth | null) =&gt; {
    if (!auth) {
        return;
    }
    signInWithPopup(auth, new GoogleAuthProvider());
}

export const logout = (auth: Auth | null) =&gt; {
    if (!auth) {
        return;
    }
    signOut(auth);
};
</code></pre>
<p>Unfortunately, the same goes for the functions. You have to pass the <code>auth</code> to your functions. You cannot use a custom hook inside a function in React, only inside another custom hook.</p>
<pre><code class="language-tsx">export const Login = () =&gt; {
    const { auth } = useFirebase();
    return &lt;button type=&quot;button&quot; className=&quot;...&quot;
        onClick={() =&gt; loginWithGoogle(auth)}&gt;
        Signin with Google
    &lt;/button&gt;
};
</code></pre>
<p>The same goes for the <code>logout</code>.</p>
<h2>Todos Hook</h2>
<p>The <code>todos</code> work similarly, except you must import the <code>db</code> hook.</p>
<pre><code class="language-tsx"> export function useTodos(
    _user: ReturnType&lt;typeof useUser&gt;
) {

    const { db } = useFirebase();

    const _store = useState&lt;{
        todos: TodoItem[],
        loading: boolean
    }&gt;({
        todos: [],
        loading: true
    });

    const user = _user[0];

    const setTodos = _store[1];

    useEffect(() =&gt; {

        setTodos(v =&gt; ({ ...v, loading: true }));

        if (!user.data) {
            setTodos({ loading: false, todos: [] });
            return;
        }

        if (!db) {
            return;
        }

        return onSnapshot(

            // query realtime todo list
            query(
                collection(db, &#39;todos&#39;),
                where(&#39;uid&#39;, &#39;==&#39;, user.data.uid),
                orderBy(&#39;created&#39;)
            ), (q) =&gt; {

                // get data, map to todo type
                const data = snapToData(q);

                /**
                 * Note: Will get triggered 2x on add 
                 * 1 - for optimistic update
                 * 2 - update real date from server date
                 */

                // print data in dev mode
                if (process.env.NODE_ENV === &#39;development&#39;) {
                    console.log(data);
                }

                // add to store
                setTodos({ loading: false, todos: data });

            });

    }, [setTodos, user.data, db]);

    return _store[0];
}
</code></pre>
<h2>User and Todos Component</h2>
<p>The rest of the app is exactly the same as my <a href="https://code.build/p/nextjs-todo-app-with-firebase-sWvbf7">NextJS Todo App with Firebase</a>. Read that article for more in-depth explanations of the React part.</p>
<h2>About Page</h2>
<p>My About page shows one document from the server.</p>
<pre><code class="language-tsx">import { doc, getDoc, getFirestore } from &quot;firebase/firestore&quot;;
import { app } from &quot;./firebase&quot;;

type AboutDoc = {
    name: string;
    description: string;
};

const db = getFirestore(app);

export const getAbout = async () =&gt; {

    const aboutSnap = await getDoc(
        doc(db, &#39;/about/ZlNJrKd6LcATycPRmBPA&#39;)
    );

    if (!aboutSnap.exists()) {
        throw &#39;Document does not exist!&#39;;
    }

    return aboutSnap.data() as AboutDoc;
};
</code></pre>
<p>Notice I import the <code>app</code> only from the <code>firebase.ts</code> file. This is for a specific reason. If we want to use Remix on a <a href="https://code.build/p/firestore-query-and-mutation-patterns-w5vhl7#modifying-data">Cloudflare or Vercel Edge Function</a>, we must use the Firestore Lite package.</p>
<pre><code class="language-tsx">import { doc, getDoc, getFirestore } from &quot;firebase/firestore/lite&quot;;
</code></pre>
<p>This will call the Firebase Rest API instead of using gRPC functions. Cloudflare only has certain NodeJS packages available and does not support Firebase on the server; this is the one exception. </p>
<p>☢️ You must add the native fetch option to your <code>vite.config.ts</code> in order for <em>Firestore Lite</em> to work.</p>
<pre><code class="language-ts">installGlobals({ nativeFetch: true });
</code></pre>
<h3>Loader</h3>
<p>To load the About page on your route, create a <code>routes/about.tsx</code> file.</p>
<pre><code class="language-tsx">import { json } from &quot;@remix-run/node&quot;;
import { useLoaderData } from &quot;@remix-run/react&quot;;
import { getAbout } from &quot;~/lib/about.server&quot;;

export const loader = async () =&gt; {
    
    const about = await getAbout();
    
    return json(about);
};

export default function AboutPage() {

    const about = useLoaderData&lt;typeof loader&gt;();

    return (
        &lt;div className=&quot;flex items-center justify-center my-5&quot;&gt;
            &lt;div className=&quot;border w-[400px] p-5 flex flex-col gap-3&quot;&gt;
                &lt;h1 className=&quot;text-3xl font-semibold&quot;&gt;{about.name}&lt;/h1&gt;
                &lt;p&gt;{about.description}&lt;/p&gt;
            &lt;/div&gt;
        &lt;/div&gt;
    );
}
</code></pre>
<p>Remix is known for its easy-of-use <code>loader</code>. When you see <code>server$</code> in other frameworks, they copied Remix. Import the <code>useLoaderData</code> hook, and it loads your data as expected.</p>
<h2>Bugs</h2>
<p>I had some other issues besides the pain of having to manually check for a <code>window</code> object, the terrible environment variable techniques, or the lack of server components; <em>Nuxt</em> had server components before Remix. However, these were not Remix&#39;s fault.</p>
<h3>Rollup</h3>
<p>There was a <a href="https://github.com/remix-run/remix/issues/9276">bug</a> in Rollup. I had to <code>override</code> to the fixed version. This is not a Remix problem but Vite’s problem. </p>
<pre><code class="language-tsx">&quot;overrides&quot;: {
  &quot;rollup&quot;: &quot;4.15.0&quot;
}
</code></pre>
<p>I have to add Remix was very quick to respond to the bug and give me a solution.</p>
<h2>Remix Thoughts</h2>
<p>This was my first experience with Remix, and I had some issues. However, if you’re used to NextJS before Server Components, you might like Remix. If I had to pick, I would go with NextJS, but I know they plan on adding Server Components in the future. I think a lot of my bad experience was a fluke, but time will tell.</p>
<p>I want to build Firebase apps for everyone in all flavors.</p>
<p><strong>Demo:</strong> <a href="https://remix-firebase-todo.vercel.app/">Vercel Serverless</a>
<strong>Repo:</strong> <a href="https://github.com/jdgamble555/remix-firebase-todo">GitHub</a></p>
<p>J</p>
]]></description><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category></item><item><title>Firestore Query and Mutation Patterns</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Sun, 21 Apr 2024 13:02:15 GMT</pubDate><link>https://code.build/p/firestore-query-and-mutation-patterns-w5vhl7</link><guid>https://code.build/p/firestore-query-and-mutation-patterns-w5vhl7</guid><description><![CDATA[<p>Since Firebase released the Web Modular API in Firebase 9, queries have been handled faster, bundles are smaller due to <em>Tree-Shaking</em>, and the API is growing with new features. However, the entire API is less intuitive and requires more practice than its predecessor.</p>
<h2>TL;DR</h2>
<p>There are simple patterns to querying and modifying data effectively in Firestore. Always be aware of your environment, use error handling, and <em>ALWAYS</em> unsubscribe if you’re using <code>onSnapshot</code>. </p>
<h2>Setup</h2>
<p>First, you must securely store your Firebase configuration information in a <code>.env</code> file. While this information is technically available to any novice hacker on the client, you still don’t want to upload it to GitHub or make it easily accessible. If you have secure Firestore Rules, there should be no worries.</p>
<pre><code class="language-tsx">const firebase_config = JSON.parse(
    import.meta.env.PUBLIC_FIREBASE_CONFIG
);
</code></pre>
<p>Follow the <a href="https://code.build/c/firebase/framework-setup">Framework Guide</a> for the best way to do this.</p>
<h3>Initialize</h3>
<p>Next, you need to initialize Firebase in the client. If you’re only running one instance of Firebase, you only need to initialize Firebase when there is not an existing instance.</p>
<pre><code class="language-tsx">if (!getApps().length) {
    initializeApp(firebase_config);
}
</code></pre>
<p>Firebase is a <em>singleton</em> class, meaning it can only be run once, but the SDK will still throw a warning if re-initialized. You can set a variable for the application if you like, but it is not strictly necessary. Firebase will get the default initialized app automatically.</p>
<pre><code class="language-tsx">export const app = getApps().length
    ? getApp()
    : initializeApp(firebase_config);
</code></pre>
<p>If you run more than one Firebase instance in a single app, you have to pass the application instance to your functions.</p>
<h3>Firebase Products</h3>
<p>You should return a variable for each product you want to use.</p>
<pre><code class="language-tsx">export const db = getFirestore();
export const auth = getAuth();
export const storable = getStorage();
export const functions = getFunctions();
</code></pre>
<p>Remember to pass the app only if you need to.</p>
<pre><code class="language-tsx">export const db = getFirestore(app);
</code></pre>
<h2>Server and Client Environments</h2>
<p>Firebase is limited in server environments.</p>
<h3>NodeJS</h3>
<p>When dealing with a Node environment, like <em>AWS Serverless Functions,</em> you can fetch unauthenticated data using the client SDK. You may have access to limited authentication functions, but you should not use them.</p>
<h3>Session and Cookies</h3>
<p>You do not want to store a user’s authentication information on the server. You should use the proper cookie and session techniques and the client API. See <a href="https://code.build/p/sveltekit-todo-app-with-firebase-admin-tqdc5j">SvelteKit Todo App with Firebase Admin</a>.</p>
<h3>Edge Functions</h3>
<p>You can fetch unauthenticated data in <em>Edge Functions</em> like <em>Cloudflare</em> and <em>Vercel Edge</em> (which uses Cloudflare) by importing <em>Firestore Lite</em>. This just uses the Firebase REST API under the hood but does not work with any authentication.</p>
<pre><code class="language-tsx">import { doc, getDoc, getFirestore } from &quot;firebase/firestore/lite&quot;;
</code></pre>
<p>However, you will not have access to any session or cookie functions. You must call a <em>Firebase Callable Function</em> to get authenticated data and handle sessions and cookies. You could equally call an API in any NodeJS environment. Either way, your authenticated data will probably be retrieved slower. The only other method is to directly query the REST API, which will require a lot of boilerplate. This hopefully changes in the future. See <a href="https://code.build/p/firebase-wish-list-94kMYC">Firebase Wish List</a>.</p>
<p>Tradeoffs. </p>
<h3>Importing</h3>
<p>Be careful importing functions in environments that do not support them. You do not need to create a shared hook for Firebase on the client, but it is a good idea on the server.</p>
<h2>Queries</h2>
<p>This is how you return documents with promises. For all examples I will be using the <code>posts</code> collection.</p>
<h3>Get a Document</h3>
<pre><code class="language-tsx">const snap = await getDoc(
  doc(db, &#39;posts/some-document-id&#39;)
);
</code></pre>
<h3>Get All Documents</h3>
<pre><code class="language-tsx">const snaps = await getDocs(
    collection(db, &#39;posts&#39;)
);
</code></pre>
<h3>Get Some Documents</h3>
<pre><code class="language-tsx">const snaps = await getDocs(
  query(
     collection(db, &#39;posts&#39;),
     limit(3)
  )        
);
</code></pre>
<h3>Options</h3>
<pre><code class="language-tsx">// filters
where(field, comparison, value)
orderBy(field, &quot;desc|asc&quot;)
limit(1)
or(query)

// cursors using field or docSnap
startAt()
startAFter()
endAt()
endBefore()

// comparisons
&quot;&lt;&quot;, &quot;&lt;=&quot;, &quot;==&quot;, &quot;&gt;&quot;, &quot;&gt;=&quot;, &quot;!=&quot;

// array comparisons
&quot;array-contains&quot;, &quot;array-contains-any&quot;, &quot;in&quot;, &quot;not-in&quot;
</code></pre>
<h3>Snapshot</h3>
<p>When you get document data, you get the snapshot data. You then need to decide what to do with it. You may want to use <a href="https://code.build/p/does-firestore-need-data-converters-sSveEL">Data Converters</a> as well. You should check for existence and combine the document ID with the data. </p>
<pre><code class="language-tsx">const snapToData = &lt;T&gt;(
    snap: DocumentSnapshot&lt;DocumentData, DocumentData&gt;
) =&gt; {

    if (!snap.exists()) {
        throw &#39;Document DNE!&#39;;
    }

    return {
        data: snap.data() as T,
        id: snap.id
    };
}
</code></pre>
<p>You would call it after getting the snapshot.</p>
<pre><code class="language-tsx">const snap = await getDoc(
    doc(db, &#39;posts&#39;, &#39;post-document-id&#39;)
);

const data = snapToData&lt;PostType&gt;(snap);
</code></pre>
<p>Or for multiple documents.</p>
<pre><code class="language-tsx">const snaps = await getDocs(
    collection(db, &#39;posts&#39;)
);

if (snap.empty) {
    throw &#39;No Documents&#39;;
}

const data = snaps.docs.map(snap =&gt; snapToData&lt;PostType&gt;(snap))
</code></pre>
<h2>Realtime</h2>
<p>To make any query real-time, use <code>onSnapshot</code> instead of <code>getDoc</code> or <code>getDocs</code>. This subscribes to changes to the document snapshot.</p>
<pre><code class="language-tsx">const unsubscribe = onSnapshot(
  doc(db, &#39;posts&#39;, postID),
  (doc) =&gt; {
    // handle document snapshot(s)
  }
);  
</code></pre>
<h3>Convert to Observable</h3>
<p>It is often more useful to convert the <code>onSnapshot</code> to an <code>RxJS</code> Observable.</p>
<pre><code class="language-tsx">const postSnap = new Observable&lt;DocumentSnapshot&lt;PostType&gt;&gt;(
	(subscriber) =&gt; onSnapshot(
	    doc(db, &#39;posts&#39;, &#39;x1s3&#39;) as any,
	    subscriber
	));
</code></pre>
<p>You could also use a <a href="https://code.build/p/does-firestore-need-data-converters-sSveEL">data converter</a> here for better typing. Here is the query version.</p>
<pre><code class="language-tsx">const postSnaps = new Observable&lt;QuerySnapshot&lt;PostType&gt;&gt;(
  (subscriber) =&gt; onSnapshot(
      collection(db, &#39;posts&#39;).withConverter&lt;PostType&gt;(dataConverter),
      subscriber
  ));
</code></pre>
<h3>Unsubscribe</h3>
<p>⚠️ ALWAYS unsubscribe. No excuses, or you will have memory leaks in your app. This is usually handled in an <em>onDestroy</em> function.</p>
<pre><code class="language-tsx">onDestroy(unsubscribe);
</code></pre>
<h2>Mutations</h2>
<p>You also need to <em>CRUD</em> documents.</p>
<h3>Add a Document</h3>
<pre><code class="language-tsx">await addDoc(
    collection(db, &#39;posts&#39;), {
        title: &#39;new post&#39;,
        ...
    }
);
</code></pre>
<p>OR</p>
<pre><code class="language-tsx">// create a new document ID
const newID = collection(db, &#39;posts&#39;).id;

await setDoc(
    doc(db, &#39;posts&#39;, newID), {
        title: &#39;new post&#39;,
        ...
    }
);
</code></pre>
<h3>Update a Document</h3>
<pre><code class="language-tsx">await updateDoc(
  doc(db, &#39;posts&#39;, postID), {
    title: &#39;new post title&#39;,
    ...
  }
);
</code></pre>
<h3>Upsert a Document</h3>
<p>This updates the document or creates it if it doesn’t exist.</p>
<pre><code class="language-tsx">await setDoc(
  doc(db, &#39;posts&#39;, postID), {
    title: &#39;new post title,
    createdAt: &#39;...&#39;,
  },
  { merge: true }
);
</code></pre>
<h3>Update an Array</h3>
<p>For more on arrays see <a href="https://code.build/p/firestore-many-to-many-arrays-X9mf6s">Firestore Many-to-Many: Arrays</a>.</p>
<pre><code class="language-tsx">await updateDoc(
  doc(db, &#39;posts&#39;, postID), {
    arrayname: arrayUnion(&#39;new value&#39;) // or arrayRemove(&#39;old value&#39;)...
  }
);
</code></pre>
<p>📝 You could equally use <code>setDoc</code> with <code>merge</code> set to <code>true</code> for updating arrays or incrementing below.</p>
<h3>Increment a Counter</h3>
<pre><code class="language-tsx">await updateDoc(
  doc(db, &#39;posts&#39;, postID), {
    counter: increment(1), // or increment(-1) for decrement
    ...
  }
);
</code></pre>
<h3>Delete a Document</h3>
<pre><code class="language-tsx">await deleteDoc(
  doc(db, &#39;posts&#39;, postID)
);
</code></pre>
<h2>Transactions</h2>
<p>You want to use transactions to get and update data simultaneously. You can’t modify the data until the transaction is complete. If one mutation fails, they all do. </p>
<pre><code class="language-tsx"> const txData = await runTransaction(db, async (transaction) =&gt; {
 
  // get some data
  const docRef = doc(db, &#39;posts&#39;, postID);
  
  const docSnap = await transaction.get(docRef);
  
  if (!docSnap.exists()) {
    throw &#39;Document DNE&#39;;
  }
    
  const data =  { 
    ...doc.data(),
    id: doc.id
  };
  
  // modify some data, great for increments  
  const new_data = { ... };

  // await not necessary due to tx
  transaction.update(docRef, new_data);

  // return new data
  return { ... };
});
</code></pre>
<p>See <a href="https://code.build/p/firestore-cloud-functions-counter-JuC0Pw#transaction">Firestore Cloud Functions Counter</a> for an example.</p>
<h2>Batches</h2>
<pre><code class="language-tsx">// batch writes
const batch = writeBatch(db);
const docRef = doc(db, &#39;posts&#39;, postID);
const docRef2 = doc(db, &#39;posts&#39;, postID2);
const docRef3 = doc(db, &#39;posts&#39;, postID3);

batch.set(docRef, { ... });
batch.update(docRef2, { ... });
batch.delete(docRef3);

await batch.commit();
</code></pre>
<p>You can securely modify multiple documents at once, or they all fail. See <a href="https://code.build/p/firestore-secure-batch-increment-IKO13Z#add-a-todo">Secure Batch Count</a> for an example.</p>
<h2>Error Handling</h2>
<p>The errors occur in promises. </p>
<h3>Catch Method</h3>
<p>This will only get called whenever the promise resolves or rejects.</p>
<pre><code class="language-tsx">deleteDoc(
  doc(db, &#39;posts&#39;, postID)
).catch((e) =&gt; {
    if (e instanceof FirebaseError) {
        console.error(e.message);
    }
});
</code></pre>
<h3>Try / Catch</h3>
<p>You must have <code>await</code> here in an async function for this to work.</p>
<pre><code class="language-tsx">try {
	
	// any firebase promise
	await deleteDoc(
	  doc(db, &#39;posts&#39;, postID)
	);
	
} catch (e) {
    if (e instanceof FirebaseError) {
        console.error(e.code);
    }
}
</code></pre>
<h3>Error Pattern</h3>
<p>When creating larger apps, I like to return an error in my function and handle it there.</p>
<pre><code class="language-tsx">const getData = (id: string) =&gt; {

  try {
  
    // get document
    const snap = await getDoc(
      doc(db, &#39;posts&#39;, id)
    );
    
    if (!snap.exists()) {
      throw new Error(&#39;Document DNE!&#39;);
    }
    const data = snap.data();
    
    // return data
    return {
      data: {
        ...data,
        id: snap.id
      }
    };
    
  } catch (_e) {
  
    // get typing correct, or check for instanceof error class
	  const e = _e as Error;
    return {
        error: e.message
    };
  }
</code></pre>
<p>Then you could call the function easily. This will make error handling easier throughout your code.</p>
<pre><code class="language-tsx">const { error, data } = await getPost(&#39;post-id-here&#39;);
</code></pre>
<p>I admit I need to handle errors more in my example apps 😎</p>
<p>J</p>
]]></description><category>[object Object]</category><category>[object Object]</category></item><item><title>Creating a Firestore User Document on Sign-Up</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Wed, 17 Apr 2024 13:00:37 GMT</pubDate><link>https://code.build/p/creating-a-firestore-user-document-on-sign-up-jSWukw</link><guid>https://code.build/p/creating-a-firestore-user-document-on-sign-up-jSWukw</guid><description><![CDATA[<p>When a user logs in to most Firebase apps, the first thing your app needs to do is create a user document. There are ways to do this automatically, so your app can always function without bugs.</p>
<h2>TL;DR</h2>
<p>You can create a <code>user</code> document automatically when a user signs up by using a <em>Trigger Function</em>, creating a user on the server, or listing for the user login, and then creating it. If you don’t use a server to handle user authentication, you should check for a user document on all sign-ins and create a user document if necessary.</p>
<h2>Creating a User Collection</h2>
<p>Once your app needs some features, you will inevitably need to store the user data in a <code>users</code> collection. This should be done on <code>signUp</code> and verified on <code>signIn</code> methods. </p>
<h3>User Rules</h3>
<p>Ensure the correct <em>Firestore Rules are</em> in place to verify writability in the <code>users</code> collection. You could write this a million different ways.</p>
<pre><code class="language-tsx">match /users/{document} {
  allow write: if requestUser() == requestDoc();
  allow read;
}

function requestDoc() {
  // 4th path is doc id
  return request.path[4];
}

function requestUser() {
  return request.auth.uid;
}
</code></pre>
<h3>🚫 Bad Method</h3>
<p>This is the method that may come to mind for some of you.</p>
<pre><code class="language-tsx">const credential = await signInWithPopup(auth, provider);
const additionalInfo = getAdditionalUserInfo(credential);
if (additionalInfo?.isNewUser) {
    await setDoc(doc(db, &#39;users&#39;, user.uid), {
        displayName: user.displayName,
        photoURL: user.photoURL,
        email: user.email,
        createdAt: serverTimestamp()
    });
}
</code></pre>
<p>The problem with this method is that there are some edge cases where clients fail, and a user needs to be created on the first sign-in. Then your app fails when the <code>isNewUser</code> flag is not set. If you want to do everything on the client, you should always check for a user document (you need to get it anyway) and create one if is not there.</p>
<h3>Client Method</h3>
<p>First, we need a function to create a user document. We store the name, photo, and email here in Firestore, but this could be any database. Firebase Authentication is built to work with any database you want.</p>
<pre><code class="language-tsx"> const createUserDoc = async (user: UserType) =&gt; {

    // get user doc
    const userDoc = await getDoc(
        doc(db, &#39;users&#39;, user.uid)
    );

    // create one if DNE
    if (!userDoc.exists()) {
        await setDoc(doc(db, &#39;users&#39;, user.uid), {
            displayName: user.displayName,
            photoURL: user.photoURL,
            email: user.email,
            createdAt: serverTimestamp()
        });
    }

    // return user
    return user;
};
</code></pre>
<p>We can then call this function inside <code>onIdStateChanged</code>.</p>
<pre><code class="language-tsx">const user = (
    defaultUser: UserType | null = null
) =&gt; readable&lt;UserType | null&gt;(
    defaultUser,
    (set: Subscriber&lt;UserType | null&gt;) =&gt; {
        return onIdTokenChanged(auth, (user: User | null) =&gt; {
            if (!user) {
                set(null);
                return;
            }
            const { displayName, photoURL, uid, email } = user;

            // create user doc if DNE
            createUserDoc({
                displayName,
                photoURL,
                uid,
                email
            }).then((_user) =&gt; {
                set(_user);
            });
        });
    }
);
</code></pre>
<p>📌 Do not use <code>await</code> inside observables, effects, or stores. Instead, call the results inside the <code>then</code> function. Some implementations can have undesired effects even with <code>async</code>.</p>
<h3>Other Frameworks</h3>
<p>You can follow the same pattern in <a href="https://code.build/c/firebase/framework-setup">any Framework</a> by putting the <code>createUserDoc</code> function inside <code>onIdStateChanged</code>.</p>
<h3>Server Trigger Method</h3>
<p>The Firebase Trigger Function is extremely simple. Add the user on user creation.</p>
<pre><code class="language-tsx">import { auth } from &#39;firebase-functions&#39;;
import { FieldValue, getFirestore } from &#39;firebase-admin/firestore&#39;;

const db = getFirestore();

export const createUser = auth.user().onCreate((user) =&gt; {

    const { displayName, phoneNumber, email } = user;
    
    return db.collection(&#39;users&#39;).doc(user.uid).create({
        displayName,
        phoneNumber, 
        email,
        createdAt: FieldValue.serverTimestamp()
    });

});
</code></pre>
<p>Feel free to add the <code>createdAt</code> in any method you choose, but you can also use <code>user.metadata.creationTime</code> to get the creation time anywhere in your app.</p>
<p>⭐ Firestore Functions Generation 2 do <em>NOT</em> provide support for Authentication Event Triggers.</p>
<h3>Delete Trigger</h3>
<p>You could also delete a user from your collection with <code>onDelete</code>, but I wouldn’t recommend using this. You would need to make sure you have <code>cascade</code> delete that removes all the user’s references in other collections (posts, tweets, comments, reviews, etc). Generally, to delete a user, you would want to <em>soft delete</em> or disable the user on the client side. </p>
<h3>Server Only Method</h3>
<p>You could also manually create a user on the server using <code>firebase-admin</code>. However, you will not have access to user methods like <code>signInWithPopup</code>, as this would need to be done on the client. You would pass in the user information and create the user on the server. This could be a callable Firebase function or directly in your SSR Framework.</p>
<pre><code class="language-tsx">import { getAuth } from &#39;firebase-admin/auth&#39;;
...
const adminAuth = getAuth();

const userData = {
    email: &#39;bob@test.com&#39;,
    emailVerified: true,
    displayName: &#39;Jonathan Gamble&#39;,
    photoURL: &#39;https://code.build/images/code.build-1x1.webp&#39;
};

const user = await adminAuth.createUser(userData);

createUserDoc({ ...user, uid: user.uid });
</code></pre>
<h3>Considerations</h3>
<ul>
<li>You could also consider protecting your routes by checking for a user collection on the server, but you would have to be cognizant of document reads.</li>
<li>If you have the <code>Firebase</code> SDK present on the client, a beginner hacker could create a new <code>user</code> without access to your server. This could potentially create a problem with your app, as the <code>user</code> would not be guaranteed to have a <code>user</code> document unless you take precautions.</li>
<li>While the <code>onCreate</code> trigger method is the safest method, the user document will not be created immediately, so your app needs to account for the delay.</li>
<li>You cannot secure fields in a document, only documents themselves. A user document is great for <code>profile</code> information, but you should create a separate collection or subcollection for private user data.</li>
</ul>
<p>J</p>
]]></description><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category></item><item><title>Angular Todo App with Firebase</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Sun, 14 Apr 2024 13:01:58 GMT</pubDate><link>https://code.build/p/angular-todo-app-with-firebase-fmLJ37</link><guid>https://code.build/p/angular-todo-app-with-firebase-fmLJ37</guid><description><![CDATA[<p>Using Angular with Firebase has changed slightly since version 16. While we navigate closer to standalone components, the library is evolving. It will soon merge with Google’s internal Framework, <em>Wiz</em>. There are cleaner ways to do things in Angular, but you still have options depending on your needs. This is the same old Todo app but in Angular.</p>
<h2>TL;DR</h2>
<p>This post teaches you how to set up Angular with <em>AngularFire</em> so you can use Firebase with your Angular app. This app shows you how to use Firebase with Angular <em>WITHOUT SIGNALS</em>. There are too many bad articles where an observable is not unsubscribed to, or the integration is half-baked.</p>
<h3>🗝️ The Traditional Approach (Observables)</h3>
<p>I rewrote this article to focus on the old way of doing things. They are still applicable today, and older code bases should be refactored to use them correctly if they can’t use the modern version with Signals. This version covers Observables, Services, Zone.js Change Detection, and the traditional conditional directives.</p>
<h3>🔑 The Modern Approach (Signals)</h3>
<p>For the modern approach, see the sister article for <a href="https://code.build/p/analog-todo-app-with-firebase-U3HGQL">Analog Todo App with Firebase</a>. That version uses Signals, Injectable Tokens, and Custom Hooks. No Zone.js worries.</p>
<h3>📗 The Realistic Approach (Mix-and-Match)</h3>
<p>Your app will probably use a combination of techniques, so I wrote this article to be opinionated one way and the Analog version to be bullish on Modern Techniques.</p>
<h2>Environment Variables</h2>
<p>The old Angular way is to use an <code>environment.ts</code> file to configure your app. We now know this is bad practice. We don’t want our keys, even public keys, easily accessible on GitHub. The easiest way to allow <code>.env</code> file support in Angular 16 and up is to use <strong>@ngx-env/builder</strong>. You could also consider using <a href="https://gist.github.com/quinnjr/6ef72d6c3ba755125ac64da2677c5c52">Webpack</a>, but it is not as easy to configure.</p>
<pre><code class="language-html">ng add @ngx-env/builder
</code></pre>
<p>You need to add the <code>NG_APP_FIREBASE_CONFIG</code> variable to your <code>.env</code> file. You can use your variables anywhere in your project from your <code>.env</code> files if the prefix is <code>NG_APP_</code>.</p>
<pre><code class="language-tsx">NG_APP_FIREBASE_CONFIG={&quot;apiKey&quot;:&quot;...&quot;,&quot;authDomain&quot;:&quot;...&quot;...}
</code></pre>
<p><strong>Note:</strong> The keys must be in double quotes as well.</p>
<pre><code class="language-tsx">const firebaseConfig = JSON.parse(
  import.meta.env[&#39;NG_APP_FIREBASE_CONFIG&#39;]
);
</code></pre>
<p>You can parse the data at the top of your <code>app.config.ts</code> file for later use.</p>
<h2>AngularFire</h2>
<p>First, you need to install <em>AngularFire</em>. This is necessary in Angular due to the big JavaScript wrapper called <em>Zone.js</em>. It is considered optional in future versions of Angular and possibly removed. It creates a large overhead that will be unnecessary now that we have Signals.</p>
<p>Add the <em>AngularFire</em> package.</p>
<pre><code class="language-html">ng add @angular/fire
</code></pre>
<p>Because we use standalone components, we need to put our Firebase providers in <code>importProvidersFrom</code> in our <code>app.config.ts</code> file so that they work correctly. Your app will have all of them in separate functions, but you can delete the generated version and simplify it to look pretty.</p>
<pre><code class="language-tsx">export const appConfig: ApplicationConfig = {
  providers: [
    provideRouter(routes),
    provideClientHydration(),
    // Angular Providers
    importProvidersFrom(
      provideFirebaseApp(() =&gt; initializeApp(firebaseConfig)),
      provideFirestore(() =&gt; getFirestore()),
      provideAuth(() =&gt; getAuth()),
      // provideStorage(() =&gt; getStorage()),
      // provideAnalytics(() =&gt; getAnalytics()),
    )
  ]
};
</code></pre>
<h2>User Service</h2>
<p>We must first handle the user state, as we can’t view our <code>todos</code> without a logged-in user. To do this, we must create a user service.</p>
<pre><code class="language-tsx">ng g s services/user
</code></pre>
<p>We will match the <code>User</code> type in Firebase but only need a few variables. We must make a type in TypeScript.</p>
<pre><code class="language-tsx">export interface UserData {
  photoURL: string | null;
  uid: string;
  displayName: string | null;
  email: string | null;
};

type UserType = {
  loading: boolean;
  data: UserData | null;
};
</code></pre>
<p>I also created a <code>UserType</code> because I like to handle loading states.</p>
<h3>Behavior Subjects</h3>
<p>We must share the user state among several components. As our app gets bigger, this could be many components. We use <code>onIdTokenChanged</code>, the better version of <code>onAuthStateChanged</code>, to get the user state in real time. It gets called like an observable gets called, but we can’t use the data in our template directly without subscribing to it. We can either convert it to an observable or a <em>BehaviorSubject</em>. An observable does not share state well with all subscribers, while a <em>BehaviorSubject</em> does this well.</p>
<pre><code class="language-tsx">  private _user = new BehaviorSubject&lt;UserType&gt;({
    loading: true,
    data: null
  });

  user = this._user.asObservable();
</code></pre>
<p>The Subject is private, but shared to your template(s) as an observable so it can be subscribed to with the async pipe.</p>
<h3>⚠️ Note on Observables</h3>
<p>If you want to use an actual Observable, use the RxJS <code>shareReplay</code> pipe to share the state correctly without any problems.</p>
<pre><code class="language-tsx">user.pipe(
  shareReplay({
    bufferSize: 1,
    refCount: true
   })
);
</code></pre>
<p>The <code>refCount</code> flag will ensure your observable gets unsubscribed when there are no subscribers, and the <code>bufferSize</code> will ensure you get the last version to all your subscribers. This version does not use that, but you need to know it is an option and <em>SHOULD</em> be used if you do not use <em>BehaviorSubject</em>.</p>
<h3>Getting User Data</h3>
<p>We create a <code>getUser()</code> function to handle the subscription and use the <code>this._user.next()</code> function to handle the data changes to the Behavior Subject. The function gets called by setting it to the <code>_subscription</code> variable, which will get unsubscribed to in the <code>ngOnDestroy()</code> function.</p>
<pre><code class="language-tsx">  private _user = new BehaviorSubject&lt;UserType&gt;({
    loading: true,
    data: null
  });

  constructor(
    private auth: Auth
  ) { }

  user = this._user.asObservable();

  private _subscription = this._getUser();

  private _getUser() {
    return onIdTokenChanged(
      this.auth,
      (_user: User | null) =&gt; {

        if (!_user) {
          this._user.next({
            loading: false,
            data: null
          });
          return;
        }

        // map data to user data type
        const {
          photoURL,
          uid,
          displayName,
          email
        } = _user;
        const data = {
          photoURL,
          uid,
          displayName,
          email
        };

        // print data in dev mode
        if (isDevMode()) {
          console.log(data);
        }

        // set store
        this._user.next({
          data,
          loading: false
        });
      });
  }

  // ⚠️ ALWAYS HANDLE UNSUBSCRIBE!!!
  ngOnDestroy(): void {
    this._subscription();
  }
</code></pre>
<h3>Declarative vs Imperative Programming</h3>
<p>When we handle subscribing and unsubscribing manually, it is called <em>imperative programming</em>. When we let the async pipe do this for us, it is called declarative programming. Generally, declarative programming is better because you can make fewer mistakes when handling unsubscribing. Everything is also in one place. However, declarative programming can take more work to learn. While I normally prefer declarative programming techniques, I like how easily a <em>BehaviorSubject</em> can handle your sharing. Technically we are using both here.</p>
<h3>Login</h3>
<p>Finally, we need to add our login methods to the service. This app uses <strong>Login With Google</strong> to keep it simple.</p>
<pre><code class="language-tsx">  login() {
    signInWithPopup(this.auth, new GoogleAuthProvider());
  }

  logout() {
    signOut(this.auth);
  }
</code></pre>
<h2>Todo Service</h2>
<p>First, we need the types.</p>
<pre><code class="language-tsx">export interface TodoItem {
  id: string;
  text: string;
  complete: boolean;
  createdAt: Date;
  uid: string;
};

export type TodoType = {
  loading: boolean;
  data: TodoItem[];
};
</code></pre>
<p>Next, generate the service.</p>
<pre><code class="language-tsx">ng g s services/todos
</code></pre>
<p>This service follows the same pattern as our user service, except that we require the <strong>user</strong> to subscribe to the <code>todos</code> collection by adding the user service.</p>
<pre><code class="language-tsx">private _todos = new BehaviorSubject&lt;TodoType&gt;({
  loading: true,
  data: []
});

constructor(
  private zone: NgZone, // keep reading for why this is here
  private db: Firestore,
  private us: UserService
) {}

todos = this._todos.asObservable();
</code></pre>
<h3>getTodosFromUser()</h3>
<p>Subscribing to the <code>todos</code> collection is more complicated. We need the user ID to subscribe, so we can’t subscribe unless a user is logged in.</p>
<pre><code class="language-tsx">private _getTodosFromUser(uid: string) {
    // query realtime todo list
    return new Observable&lt;QuerySnapshot&lt;TodoItem&gt;&gt;(
      (subscriber) =&gt; onSnapshot(
        query(
          collection(this.db, &#39;todos&#39;),
          where(&#39;uid&#39;, &#39;==&#39;, uid),
          orderBy(&#39;createdAt&#39;)
        ).withConverter(todoConverter),
        subscriber // will be changed... keep reading
      )
    )
      .pipe(
        map((arr) =&gt; {

          /**
           * Note: Will get triggered 2x on add
           * 1 - for optimistic update
           * 2 - update real date from server date
          */

          if (arr.empty) {
            return {
              loading: false,
              data: []
            };
          }
          const data = arr.docs
            .map((snap) =&gt; snap.data());

          // print data in dev mode
          if (isDevMode()) {
            console.log(data);
          }
          return {
            loading: false,
            data
          };
        })
      );
  }
</code></pre>
<h3>Observable</h3>
<p>We want to be able to subscribe and unsubscribe to the collection on-demand, and RxJS has an operator <code>switchMap</code> for that. However, we have to convert the <code>onSnapshot</code> to an observable. We use <code>pipe</code> and <code>map</code> to modify the data.</p>
<h3>Data Converter</h3>
<p>While I don’t always like data converters, they simplify typing with complex observables like this.</p>
<pre><code class="language-tsx">const todoConverter = {
  toFirestore(value: WithFieldValue&lt;TodoItem&gt;) {
    return value;
  },
  fromFirestore(
    snapshot: QueryDocumentSnapshot
  ) {
    const data = snapshot.data({
      serverTimestamps: &#39;estimate&#39;
    });
    const createdAt = data[&#39;createdAt&#39;] as Timestamp;
    return {
      ...data,
      createdAt: createdAt.toDate(),
      id: snapshot.id
    } as TodoItem;
  }
};
</code></pre>
<p>I need to convert the timestamp to a date, handle ID fields, and handle the optimistic updates using <code>serverTimestamp()</code>. See <a href="https://code.build/p/firestore-dates-and-timestamps-WjGiiQ">Firestore Dates and Timestamps</a> and <a href="https://code.build/p/does-firestore-need-data-converters-sSveEL">Does Firestore Need Data Converters</a>.</p>
<h3>⚠️ Zone.js</h3>
<p>Perhaps the most complicated thing about Angular, which will soon be optional, is using Zone.js. In a nutshell, Angular puts all tasks in a Zone, while other tasks run <em>outside</em> of Angular. <em>AngularFire</em> converts all Firebase functions to run inside the main Zone. When we create the Observable, Angular does not keep it inside the Angular Zone in this case. We have to manually declare it, so that the <code>next</code> function of the <em>BehaviorSubject</em> triggers change detection. If you have no idea what I’m talking about, don’t worry. Instead, use <code>effect()</code> with <code>signals()</code> in the <a href="https://code.build/p/analog-todo-app-with-firebase-U3HGQL">Analog Todo With Firebase</a> version. This is for advanced users who are stuck using old techniques. In order to combat the issue, we bring back the change detection by running the <code>next()</code> function inside the zone using <code>this.zone.run()</code> function.</p>
<pre><code class="language-tsx">  private _getTodosFromUser(uid: string) {
    // query realtime todo list
    return new Observable&lt;QuerySnapshot&lt;TodoItem&gt;&gt;(
      (subscriber) =&gt; onSnapshot(
        query(
          collection(this.db, &#39;todos&#39;),
          where(&#39;uid&#39;, &#39;==&#39;, uid),
          orderBy(&#39;createdAt&#39;)
        ).withConverter(todoConverter),

        // there is no complete function as real-time is app-lived
        snapshot =&gt; this.zone.run(() =&gt; subscriber.next(snapshot)),
        error =&gt; this.zone.run(() =&gt; subscriber.error(error))
      )
    )
</code></pre>
<p>This alone is a reason enough <em>NOT</em> to use this version.</p>
<h3>Getting Todos</h3>
<p>The <code>getTodosFromUser()</code> function is automatically subscribed to <em>WHEN</em> there is a user. The RxJS <code>switchMap</code> pipe makes this easy. It returns a new observable to subscribe to. For cases without a user, we can make an observable from any data using <code>of()</code>. We subscribe manually to this function and pass the <code>this._todos</code> subscriber, which is the <em>BehaviorSubject</em>. Keep the functions we don’t need access to outside the class <code>private</code>.</p>
<pre><code class="language-tsx">  private _subscription = this._getTodos();

  private _getTodos() {
    // get todos from user observable
    return this.us.user.pipe(
      switchMap((_user) =&gt; {

        // get todos if user
        if (_user.data) {
          return this._getTodosFromUser(
            _user.data.uid
          );
        }
        // otherwise return empty
        return of({
          loading: false,
          data: []
        });
      })
    ).subscribe(this._todos);
  }

  ngOnDestroy(): void {
    this._subscription.unsubscribe();
  }
</code></pre>
<p>Always handle unsubscribe. Always.</p>
<h3>Todo CRUD</h3>
<p>My example app doesn’t use <em>Angular Reactive Forms</em>, but you would probably want to if you have anything but simplicity.</p>
<pre><code class="language-tsx">  addTodo = (e: SubmitEvent) =&gt; {

    e.preventDefault();

    const userData = await firstValueFrom(
      this.us.user
    );

    if (!userData.data) {
      throw &#39;No User!&#39;;
    }

    // get and reset form
    const target = e.target as HTMLFormElement;
    const form = new FormData(target);
    const { task } = Object.fromEntries(form);

    if (typeof task !== &#39;string&#39;) {
        return;
    }

    // reset form
    target.reset();

    addDoc(collection(this.db, &#39;todos&#39;), {
        uid,
        text: task,
        complete: false,
        createdAt: serverTimestamp()
    });
  }
</code></pre>
<p>You must prevent the todo from being added if there is no user, but this should be impossible as you can’t view the form without a logged-in user. However, errors can happen. We convert the user observable to a promise and wait on it with <code>firstValueFrom</code> to get the latest value.</p>
<p>For the sake of completeness, you could also use the <code>currentUser</code> variable in the Firebase <code>Auth</code>.</p>
<pre><code class="language-tsx">
  constructor(
    private zone: NgZone,
    private db: Firestore,
    private us: UserService,
    private auth: Auth
  ) {}

    ...

    const user = this.auth.currentUser;

    if (!user) {
      throw &#39;No User!&#39;;
    }
</code></pre>
<p>The update and delete functions are as expected.</p>
<pre><code class="language-tsx">  updateTodo = (id: string, complete: boolean) =&gt; {
    updateDoc(doc(this.db, &#39;todos&#39;, id), { complete });
  }

  deleteTodo = (id: string) =&gt; {
    deleteDoc(doc(this.db, &#39;todos&#39;, id));
  }
</code></pre>
<h2>Components</h2>
<p>You can use your services in any component by adding them to the constructor.</p>
<pre><code class="language-tsx">constructor(public us: UserService) { }
</code></pre>
<p><strong>Note</strong>: The modern way of injectors is covered in the <a href="https://code.build/p/analog-todo-app-with-firebase-U3HGQL">Analog version</a>.</p>
<pre><code class="language-tsx">user = inject(UserService).user$;
</code></pre>
<h3>Subscribing</h3>
<p>You can get the data in your components by using the <code>async</code> pipe. You have to add <code>AsyncPipe</code> to your imports in the components that use them.</p>
<pre><code class="language-tsx">@Component({
  selector: &#39;app-home&#39;,
  standalone: true,
  imports: [
    ProfileComponent,
    AsyncPipe,
    CommonModule
  ],
  templateUrl: &#39;./home.component.html&#39;
})
</code></pre>
<p>Then, you check for the state here using the <em>ngIf</em> directives. Modern versions use control <code>@if</code> loops.</p>
<pre><code class="language-tsx">&lt;div class=&quot;text-center&quot;&gt;
    &lt;h1 class=&quot;text-3xl font-semibold my-3&quot;&gt;Angular Firebase Todo App&lt;/h1&gt;
    &lt;ng-container *ngIf=&quot;us.user | async as user&quot;&gt;
        &lt;ng-container *ngIf=&quot;user.loading; else data&quot;&gt;
            &lt;p&gt;Loading...&lt;/p&gt;
        &lt;/ng-container&gt;
        &lt;ng-template #data&gt;
            &lt;ng-container *ngIf=&quot;user.data; else no_user&quot;&gt;
                &lt;app-profile /&gt;
            &lt;/ng-container&gt;
        &lt;/ng-template&gt;
    &lt;/ng-container&gt;
    &lt;ng-template #no_user&gt;
        &lt;button type=&quot;button&quot; class=&quot;border p-2 rounded-md text-white bg-red-600&quot; (click)=&quot;us.login()&quot;&gt;
            Signin with Google
        &lt;/button&gt;
    &lt;/ng-template&gt;
&lt;/div&gt;
</code></pre>
<p>There is nothing fun about <em>ngIf</em>, but they are still usable in Angular Apps and have their purpose. The <code>async</code> pipe is a declarative approach.</p>
<h2>About Page</h2>
<p>I also created an About page to show you how to load Firebase from the Server. The <a href="https://code.build/p/analog-todo-app-with-firebase-U3HGQL">Analog Version</a> will cover this.</p>
<p>The rest of the app is standard Angular techniques. I will not be covering them, as this post is about Firebase.</p>
<h2>Deployment</h2>
<p>Deploying an Angular app is not easy, but deploying an Angular Fire app is even more brutal. Google obviously wants you to use Firebase Functions. You can use <code>firebase init hosting</code> or <code>ng deploy</code> and follow the instructions. You may need to set <code>prerender</code>, to <code>false</code> in <code>angular.json</code>, and comment out the port in <code>server.ts</code>, as this is a known issue.</p>
<pre><code class="language-tsx">// const port = process.env[&#39;PORT&#39;] || 4000;
const port = 4000;
</code></pre>
<h3>Edge Deployment</h3>
<p>Currently, there is no efficient way to deploy Angular on Edge Network Functions without using Analog or Vite directly. Hopefully, this will change soon.</p>
<h2>Should I Use This?</h2>
<p>Nope. I wouldn’t. I updated this Article for learning reference and for advanced users who want to use <code>RxJS</code> correctly. There are also optional ways of doing things you may find useful here, as Angular keeps evolving.</p>
<p><strong>Repo:</strong> <a href="https://github.com/jdgamble555/angular-17-todo">GitHub</a></p>
<p><strong>Serverless Demo</strong>: <a href="https://angular-17-todo.vercel.app/">Vercel Demo</a></p>
<p>For modern techniques, read <a href="https://code.build/p/analog-todo-app-with-firebase-U3HGQL">Analog Todo App with Firebase</a>.</p>
]]></description><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category></item><item><title>Analog Todo App with Firebase</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Sun, 14 Apr 2024 13:00:00 GMT</pubDate><link>https://code.build/p/analog-todo-app-with-firebase-U3HGQL</link><guid>https://code.build/p/analog-todo-app-with-firebase-U3HGQL</guid><description><![CDATA[<p>AnalogJS is what Angular should be. It definitely makes handling server-side applications easier. It fixes all the deployment problems with Angular. I rebuilt my Todo app again, this time for Analog. I actually rebuilt the old Angular Version using Observables, Classes, and Injectables… Yuck! I decided to incorporate all the modern Angular techniques in a modern Angular Framework.</p>
<h2>TL;DR</h2>
<p>Building this app with Analog was a breeze. I realized how important Signals are after rebuilding the Angular Version using older techniques. Zoneless Angular will be amazing.</p>
<h3>🔑 The Modern Approach (Signals)</h3>
<p>We will see a page router, injector tokens, transfer state utilities, new <code>@if</code> conditional controls, built-in <code>.env</code> support, easy deployment, and the possibility of deploying to Edge Functions.</p>
<h3>🗝️ The Traditional Approach (Observables)</h3>
<p>I rewrote my old article to focus on the old way of doing things. While still applicable today, older code bases should be refactored to use them correctly if they can’t use the modern version with Signals. This version covers Observables, Services, Zone.js Change Detection, and the traditional conditional directives. See <a href="https://code.build/p/angular-todo-app-with-firebase-fmLJ37">Angular Todo App with Firebase.</a></p>
<h3>📗 The Realistic Approach (Mix-and-Match)</h3>
<p>Your app will probably use a combination of techniques. This article explains the new Angular techniques for using Firebase, but you will probably use both techniques in a complex application.</p>
<h2>Setup</h2>
<p>Add <em>AngularFire</em>.</p>
<pre><code class="language-tsx">ng add @angular/fire
</code></pre>
<p>Add your Firebase configuration to your <code>.env</code> file. Make sure the keys have double quotes.</p>
<pre><code class="language-tsx">VITE_FIREBASE_CONFIG={&quot;apiKey&quot;:&quot;...&quot;,&quot;authDomain&quot;:&quot;...&quot;...}
</code></pre>
<p>Add the correct providers to your application in <code>app.config.ts</code>.</p>
<pre><code class="language-tsx">const firebaseConfig = JSON.parse(
  import.meta.env[&#39;VITE_FIREBASE_CONFIG&#39;]
);

export const appConfig: ApplicationConfig = {
  providers: [
    provideFileRouter(),
    provideHttpClient(withFetch()),
    provideClientHydration(),
    importProvidersFrom(
      provideFirebaseApp(() =&gt; initializeApp(firebaseConfig)),
      provideFirestore(() =&gt; getFirestore()),
      provideAuth(() =&gt; getAuth()),
      // provideStorage(() =&gt; getStorage()),
      // provideAnalytics(() =&gt; getAnalytics()),
    )
  ],
};
</code></pre>
<h2>Generate the Pages</h2>
<p>Generate a home component and an about component.</p>
<pre><code class="language-tsx">ng g c components/about
// or
ng g c components/home -t -s
</code></pre>
<p>✏️ Use <code>-s</code> for inline styles (good for tailwind) and—<code>t</code> for inline templates.</p>
<p>We will have two pages, the <em>Todo Page</em>, and the <em>About Page</em>. Create <code>pages/index.page.ts</code> and <code>pages/about.page.ts</code>. I like to link them to the components I use to keep my features in one place.</p>
<pre><code class="language-tsx">// index.page.ts
import { Component } from &#39;@angular/core&#39;;
import { HomeComponent } from &#39;@components/home/home.component&#39;;

@Component({
  selector: &#39;app-index&#39;,
  standalone: true,
  imports: [HomeComponent],
  template: ` &lt;app-home /&gt; `
})
export default class IndexComponent { }
</code></pre>
<p>You should also create a resolver for the About page.</p>
<pre><code class="language-tsx">ng g r components/about
</code></pre>
<p>And you can import it into the About route with the <code>routeMeta</code> keyword.</p>
<pre><code class="language-tsx">import { RouteMeta } from &#39;@analogjs/router&#39;;
import { Component } from &#39;@angular/core&#39;;
import AboutComponent from &#39;@components/about/about.component&#39;;
import { aboutResolver } from &#39;@components/about/about.resolver&#39;;

export const routeMeta: RouteMeta = {
  resolve: { data: aboutResolver }
};

@Component({
  selector: &#39;app-route&#39;,
  standalone: true,
  imports: [AboutComponent],
  template: ` &lt;app-about /&gt; `
})
export default class AboutRoute { }
</code></pre>
<p>If you’re not using the Page Directory, you should import the router in your <code>app.routes.ts</code> file instead.</p>
<pre><code class="language-tsx">export const routes: Routes = [
    { path: &#39;&#39;, component: HomeComponent },
    { path: &#39;about&#39;, component: AboutComponent, resolve: { data: aboutResolver } }
];
</code></pre>
<h2>User Service</h2>
<p>Create the service. If you don’t want the spec file, add <code>--skip-tests</code>.</p>
<pre><code class="language-tsx">ng g s user --skip-tests
</code></pre>
<p>However, we will not use the classes; we will use injection tokens. Delete the contents of the file and add the user type.</p>
<pre><code class="language-tsx">export interface userData {
  photoURL: string | null;
  uid: string;
  displayName: string | null;
  email: string | null;
};
</code></pre>
<h3>Injection Token</h3>
<p>An injection token is a functional version of a service. A class cannot use <em>Tree Shaking</em>, so all methods are always imported. A function can be imported where and only where it is needed. Injection tokens are similar to hooks in other frameworks, except they can be shared anywhere. They have three arguments (technically two, one, and an object):</p>
<ol>
<li>The name of the token. This is like the name of the context.</li>
<li>The provider. Use <code>root</code>, and it can be shared everywhere.</li>
<li>The <code>factory()</code> function runs only once.</li>
</ol>
<h3>Auth Token</h3>
<p>Our first token is the <code>auth</code> token. We don’t want this to load on the browser, so we can check for that with <code>PLATFORM_ID</code>.</p>
<pre><code class="language-tsx">export const FIREBASE_AUTH = new InjectionToken&lt;Auth | null&gt;(
  &#39;firebase-auth&#39;,
  {
    providedIn: &#39;root&#39;,
    factory() {
      const platformID = inject(PLATFORM_ID);
      if (isPlatformBrowser(platformID)) {
        return inject(Auth);
      }
      return null;
    }
  }
);
</code></pre>
<h3>User Token</h3>
<p>We need to share the user state from <code>onIdTokenChanged</code>, better than <code>onAuthStateChanged</code> because it handles token changes as well, in more than one component.</p>
<pre><code class="language-tsx">export const USER = new InjectionToken(
  &#39;user&#39;,
  {
    providedIn: &#39;root&#39;,
    factory() {

      const auth = inject(FIREBASE_AUTH);
      const destroy = inject(DestroyRef);

      const user = signal&lt;{
        loading: boolean,
        data: userData | null,
        error: Error | null
      }&gt;({
        loading: true,
        data: null,
        error: null
      });

      // server environment
      if (!auth) {
        user.set({
          data: null,
          loading: false,
          error: null
        });
        return user;
      }

      // toggle loading
      user.update(_user =&gt; ({
        ..._user,
        loading: true
      }));

      const unsubscribe = onIdTokenChanged(auth,
        (_user: User | null) =&gt; {

          if (!_user) {
            user.set({
              data: null,
              loading: false,
              error: null
            });
            return;
          }

          // map data to user data type
          const {
            photoURL,
            uid,
            displayName,
            email
          } = _user;
          const data = {
            photoURL,
            uid,
            displayName,
            email
          };

          // print data in dev mode
          if (isDevMode()) {
            console.log(data);
          }

          // set store
          user.set({
            data,
            loading: false,
            error: null
          });
        }, (error) =&gt; {

          // handle error
          user.set({
            data: null,
            loading: false,
            error
          });

        });

      destroy.onDestroy(unsubscribe);

      return user;
    }
  }
);
</code></pre>
<p>We create and return a signal with the loading data; it updates every time the user state changes. I also handle <code>error</code> cases. Notice we use a <code>DestroyRef</code> to unsubscribe. ALWAYS HANDLE UNSUBSCRIBE! Injection Tokens are pure magic. <em>No other framework can do this out of the box!</em></p>
<h3>Inject</h3>
<p>Notice we can inject the <code>FIREBASE_AUTH</code> or the <code>USER</code> token anywhere we like and use it. So much better than putting it into the constructor. Beautiful!</p>
<pre><code class="language-tsx">const auth = inject(FIREBASE_AUTH);
</code></pre>
<h3>Login and Logout</h3>
<p>The login and logout functions can be used anywhere in the same manner.</p>
<pre><code class="language-tsx">export const LOGIN = new InjectionToken(
  &#39;LOGIN&#39;,
  {
    providedIn: &#39;root&#39;,
    factory() {
      const auth = inject(FIREBASE_AUTH);
      return () =&gt; {
        if (auth) {
          signInWithPopup(
            auth,
            new GoogleAuthProvider()
          );
          return;
        }
        throw &#39;Can\\\\&#39;t run Auth on Server&#39;;
      };
    }
  }
);

export const LOGOUT = new InjectionToken(
  &#39;LOGOUT&#39;,
  {
    providedIn: &#39;root&#39;,
    factory() {
      const auth = inject(FIREBASE_AUTH);
      return () =&gt; {
        if (auth) {
          signOut(auth);
          return;
        }
        throw &#39;Can\\\\&#39;t run Auth on Server&#39;;
      };
    }
  }
);
</code></pre>
<h2>Todo Service</h2>
<p>The Todo service is similar, except that it depends on the user service. You can’t subscribe to a user’s collection unless the user is logged in.</p>
<pre><code class="language-tsx">export const TODOS = new InjectionToken(
  &#39;TODOS&#39;,
  {
    providedIn: &#39;root&#39;,
    factory() {
      const db = inject(Firestore);
      const user = inject(USER);

      const todos = signal&lt;{
        data: TodoItem[],
        loading: boolean,
        error: FirebaseError | null
      }&gt;({
        data: [],
        loading: true,
        error: null
      });

      effect((onCleanup) =&gt; {

        const userData = user().data;

        if (!userData) {
          untracked(() =&gt; {
            todos.set({
              loading: false,
              data: [],
              error: null
            });
          });
          return;
        }

        const unsubscribe = onSnapshot(

          // query realtime todo list
          query(
            collection(db, &#39;todos&#39;),
            where(&#39;uid&#39;, &#39;==&#39;, userData.uid),
            orderBy(&#39;createdAt&#39;)
          ), (q) =&gt; {

            // get data, map to todo type
            const data = snapToData(q);

            /**
             * Note: Will get triggered 2x on add 
             * 1 - for optimistic update
             * 2 - update real date from server date
             */

            // print data in dev mode
            if (isDevMode()) {
              console.log(data);
            }

            // add to store            
            todos.set({
              data,
              loading: false,
              error: null
            });
          }, (error) =&gt; {

            // handle errors
            todos.set({
              loading: false,
              data: [],
              error
            });
            
          });

        onCleanup(unsubscribe);
      });

      return todos;
    }
  }
);
</code></pre>
<p>The <code>onSnapshot</code> will not get subscribed to unless there is a user, and it will be automatically unsubscribed when a user logs out. Again, we handle unsubscribing with <code>DestroyRef</code>.</p>
<h3>Snapshot Data</h3>
<p>This version does not use <a href="https://code.build/p/does-firestore-need-data-converters-sSveEL">Data Converters</a>, but you could equally build one. Here we just handle the data from the collection manually. We also need to handle the <a href="https://code.build/p/firestore-dates-and-timestamps-WjGiiQ">Dates and Timestamps.</a></p>
<pre><code class="language-tsx">export const snapToData = (
  q: QuerySnapshot&lt;DocumentData, DocumentData&gt;
) =&gt; {

  // creates todo data from snapshot
  if (q.empty) {
    return [];
  }
  return q.docs.map((doc) =&gt; {
    const data = doc.data({
      serverTimestamps: &#39;estimate&#39;
    });
    const createdAt = data[&#39;createdAt&#39;] as Timestamp;
    return {
      ...data,
      createdAt: createdAt.toDate(),
      id: doc.id
    }
  }) as TodoItem[];
}
</code></pre>
<h3>Add, Delete, and Update</h3>
<p>The CRUD operations work as expected too. Signals get the data in the factory functions.</p>
<pre><code class="language-tsx">export const ADD_TODO = new InjectionToken(
  &#39;ADD_TODO&#39;,
  {
    providedIn: &#39;root&#39;,
    factory() {

      const user = inject(USER);
      const db = inject(Firestore)

      return (e: SubmitEvent) =&gt; {

        e.preventDefault();

        const userData = user().data;

        if (!userData) {
          throw &#39;No User!&#39;;
        }

        // get and reset form
        const target = e.target as HTMLFormElement;
        const form = new FormData(target);
        const { task } = Object.fromEntries(form);

        if (typeof task !== &#39;string&#39;) {
          return;
        }

        // reset form
        target.reset();

        addDoc(collection(db, &#39;todos&#39;), {
          uid: userData.uid,
          text: task,
          complete: false,
          createdAt: serverTimestamp()
        });
      };
    }
  }
);

export const UPDATE_TODO = new InjectionToken(
  &#39;UPDATE_TODO&#39;,
  {
    providedIn: &#39;root&#39;,
    factory() {
      const db = inject(Firestore);
      return (id: string, complete: boolean) =&gt; {
        updateDoc(doc(db, &#39;todos&#39;, id), { complete });
      };
    }
  }
);

export const DELETE_TODO = new InjectionToken(
  &#39;DELETE_TODO&#39;,
  {
    providedIn: &#39;root&#39;,
    factory() {
      const db = inject(Firestore);
      return (id: string) =&gt; {
        deleteDoc(doc(db, &#39;todos&#39;, id));
      };
    }
  }
);
</code></pre>
<p>It would be impossible to do this cleanly in any Framework except Angular (and Analog, of course).</p>
<h3>Todo Component</h3>
<p>Showing the todos is much better than using the <code>ngIf</code> directive.</p>
<pre><code class="language-tsx">&lt;div&gt;
    @if (!todos().loading) {
    &lt;div class=&quot;grid grid-cols-[auto,auto,auto,auto] gap-3 justify-items-start&quot;&gt;
        @for (todo of todos().data; track todo.id) {
        &lt;app-todo-item class=&quot;contents&quot; [todo]=&quot;todo&quot; /&gt;
        } @empty {
        &lt;p&gt;&lt;b&gt;Add your first todo item!&lt;/b&gt;&lt;/p&gt;
        }
    &lt;/div&gt;
    &lt;app-todo-form /&gt;
    } @else {
    &lt;p&gt;Loading...&lt;/p&gt;
    }
&lt;/div&gt;
</code></pre>
<p>We can load and track the data easily without creating unnecessary templates.</p>
<h2>About Page</h2>
<p>We also need to get data server-side only. We do this with a resolver.</p>
<h3>Resolver Utilities</h3>
<p>I’m hoping to get these utility functions implemented into Analog.</p>
<pre><code class="language-tsx">export const useAsyncTransferState = async &lt;T&gt;(
    name: string,
    fn: () =&gt; T
) =&gt; {
    const state = inject(TransferState);
    const key = makeStateKey&lt;T&gt;(name);
    const cache = state.get(key, null);
    if (cache) {
        return cache;
    }
    const data = await fn() as T;
    state.set(key, data);
    return data;
};

export const useTransferState = &lt;T&gt;(
    name: string,
    fn: () =&gt; T
) =&gt; {
    const state = inject(TransferState);
    const key = makeStateKey&lt;T&gt;(name);
    const cache = state.get(key, null);
    if (cache) {
        return cache;
    }
    const data = fn() as T;
    state.set(key, data);
    return data;
};

export const injectResolver = &lt;T&gt;(name: string) =&gt;
    inject(ActivatedRoute).data.pipe&lt;T&gt;(map(r =&gt; r[name]));

export const injectSnapResolver = &lt;T&gt;(name: string) =&gt;
    inject(ActivatedRoute).snapshot.data[name] as T;
</code></pre>
<h3>About Resolver</h3>
<p>The about resolver uses the <code>useAsyncTransferState</code> utility function to automatically grab the function on the server and and hydrate the data to the browser. This is done automatically in a component but not in a resolver.</p>
<pre><code class="language-tsx">import { inject, isDevMode } from &#39;@angular/core&#39;;
import { ResolveFn } from &#39;@angular/router&#39;;
import { Firestore, doc, getDoc } from &#39;@angular/fire/firestore&#39;;
import { useAsyncTransferState } from &#39;@lib/utils&#39;;

export type AboutDoc = {
  name: string;
  description: string;
};

export const aboutResolver: ResolveFn&lt;AboutDoc&gt; = async () =&gt;

  useAsyncTransferState(&#39;about&#39;, async () =&gt; {

    const db = inject(Firestore);

    const aboutSnap = await getDoc(
      doc(db, &#39;/about/ZlNJrKd6LcATycPRmBPA&#39;)
    );

    if (!aboutSnap.exists()) {
      throw &#39;Document does not exist!&#39;;
    }

    const data = aboutSnap.data() as AboutDoc;

    if (isDevMode()) {
      console.log(data);
    }

    return data;

  });
</code></pre>
<p>It returns a resolved promise before the component is rendered.</p>
<h3>About Component</h3>
<p>We can easily inject the resolver into our component using the utility resolver function.</p>
<pre><code class="language-tsx">about = injectResolver&lt;AboutDoc&gt;(&#39;data&#39;);
</code></pre>
<p>This is the <code>async</code> version which requires a pipe. <code>AsyncPipe</code> must also be imported. However, if we don’t expect our data to change in the component, we could use the snapshot version, which doesn’t require an observable and <code>AsyncPipe</code>.</p>
<pre><code class="language-tsx">about = injectSnapResolver&lt;AboutDoc&gt;(&#39;data&#39;);
</code></pre>
<p>The rest of the data is shown as expected.</p>
<pre><code class="language-tsx">import { Component } from &#39;@angular/core&#39;;
import { AboutDoc } from &#39;./about.resolver&#39;;
import { injectResolver } from &#39;@lib/utils&#39;;
import { AsyncPipe } from &#39;@angular/common&#39;;

@Component({
    selector: &#39;app-about&#39;,
    standalone: true,
    imports: [AsyncPipe],
    template: `
    @if (about | async; as data) {
    &lt;div class=&quot;flex items-center justify-center my-5&quot;&gt;
        &lt;div class=&quot;border w-[400px] p-5 flex flex-col gap-3&quot;&gt;
            &lt;h1 class=&quot;text-3xl font-semibold&quot;&gt;{{ data.name }}&lt;/h1&gt;
            &lt;p&gt;{{ data.description }}&lt;/p&gt;
        &lt;/div&gt;
    &lt;/div&gt;
    }
    `
})
export default class AboutComponent {
    about = injectResolver&lt;AboutDoc&gt;(&#39;data&#39;);
}
</code></pre>
<p>Importing components from the server to the browser in Angular has never been so easy!</p>
<p>Building an app in Angular has become fun again! No more worrying about Zone.js problems or complex observables.</p>
<p>🎩 Signals with Analog are awesome!</p>
<p><strong>Repo:</strong> <a href="https://github.com/jdgamble555/analog-firebase">GitHub</a></p>
<p><strong>Demo:</strong> <a href="https://analog-firebase.netlify.app/">Netlify Edge</a></p>
<p>J</p>
]]></description><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category></item><item><title>Firestore Dates and Timestamps</title><dc:creator>Jonathan Gamble</dc:creator><pubDate>Wed, 10 Apr 2024 13:07:24 GMT</pubDate><link>https://code.build/p/firestore-dates-and-timestamps-WjGiiQ</link><guid>https://code.build/p/firestore-dates-and-timestamps-WjGiiQ</guid><description><![CDATA[<p>Storing dates in Cloud Firestore is extremely easy. Convert the date to a Timestamp type, and add it to your JSON. However, storing the current date in a timestamp is a little more complicated. You don’t want to store the client’s date and time, as this could differ from client to client. Always use Firestore’s server as the baseline.</p>
<h2>TL;DR</h2>
<p>The <code>serverTimestamp()</code> function will allow you to accurately update the current date and time in your Firestore documents. You always want to use the server time, not the client time. You should use this function securely by adding Firestore Rules to ensure the time comes from the server. You could also do this with a Cloud Function, but it will be more expensive and not immediate. Finally, you should configure what happens with optimistic updates when using real-time data to ensure your user has the best experience.</p>
<h2>Timestamp</h2>
<p>All dates are stored in Firestore as a Timestamp. To convert the JavaScript dates properly, use <code>Timestamp.fromDate(date: Date)</code>.</p>
<pre><code class="language-tsx">const publishDate = new Date(&#39;January 10, 2030&#39;);

updateDoc(doc(db, &#39;posts&#39;, ID), {
    publishedAt: Timestamp.fromDate(publishDate)
});
</code></pre>
<h2>Current Date</h2>
<p>When you update a document, you usually want to store the updated date and the created date. The usual two field names for these are <code>updatedAt</code> and <code>createdAt</code>. The updated timestamp will reflect the current time the document is updated, while the created timestamp reflects the current time the document is created. Both of these will need to use the current date, but we need to base this on the server time.</p>
<h2>Cloud Functions Method</h2>
<p>Using a Firebase Trigger function, we can set the date after a new document has been created or updated. This will be based on the <em>Firebase Functions</em>’s timestamp, keeping the date on the server, not the client.</p>
<p>⚠️ I do not recommend this method for most use cases.</p>
<h3>createdAtTrigger</h3>
<p>The first trigger will handle what to do when a document is created. First, we must import our packages and set our <code>PATH</code> for both functions.</p>
<pre><code class="language-tsx">import { FieldValue, Timestamp } from &#39;firebase-admin/firestore&#39;;
import { firestore } from &#39;firebase-functions&#39;;

const PATH = &#39;todos/{docID}&#39;
</code></pre>
<p>Our created trigger function is extremely simple. </p>
<pre><code class="language-tsx">exports.createdAtTrigger = firestore
    .document(PATH)
    .onCreate(async (change) =&gt; {
	      
	      // update createdAt timestamp
        change.ref.update({
            createdAt: FieldValue.serverTimestamp()
        });
    });
</code></pre>
<p>We could use <code>set</code> with <code>merge</code> or an <code>update</code> function.</p>
<pre><code class="language-tsx">exports.createdAtTrigger = firestore
    .document(PATH)
    .onCreate(async (change) =&gt; {

        // update createdAt timestamp
        change.ref.set({
            createdAt: FieldValue.serverTimestamp()
        }, { merge: true });
    });
</code></pre>
<p>Here is how this works.</p>
<ol>
<li>A new document is created.</li>
<li>The <code>createdAtTrigger</code> function gets called.</li>
<li>The trigger function updates the new document again with the server timestamp field.</li>
</ol>
<p>See the problem with this? We have 2 writes and a function call just for one update. This could cost us more money depending on our use case. However, there are technically more steps here, as you will see.</p>
<h3>updatedAtTrigger</h3>
<p>Our updated trigger function is more complicated. </p>
<pre><code class="language-tsx">exports.updatedAtTrigger = firestore
    .document(PATH)
    .onUpdate(async (change) =&gt; {

        const after = change.after.exists ? change.after.data() : null;
        const before = change.before.exists ? change.before.data() : null;

        // don&#39;t update if creating createdAt
        if (!before?.createdAt &amp;&amp; after?.createdAt) {
            return;
        }

        // don&#39;t update if creating updatedAt
        if (!before?.updatedAt &amp;&amp; after?.updatedAt) {
            return;
        }

        // don&#39;t update if updating updatedAt
        if (before?.updatedAt &amp;&amp; after?.updatedAt) {
            if (!(before.updatedAt as Timestamp).isEqual(after.updatedAt)) {
                return;
            }
        }

        // update updatedAt timestamp
        change.after.ref.update({
            updatedAt: FieldValue.serverTimestamp()
        });
    });
</code></pre>
<p>Let’s first relook at the created function once we implement this function. The steps will really look like this.</p>
<ol>
<li>A new document is created.</li>
<li>The <code>createdAtTrigger</code> function gets called.</li>
<li>The trigger function updates the new document again with the server timestamp field.</li>
<li>The <code>updatedAtTrigger</code> function gets called. It detects a newly created date, and nothing further happens.</li>
</ol>
<p>So we are really looking at <em>2 document writes</em> and <em>2 function calls</em> for a created timestamp.</p>
<p>Updates, on the other hand, have to be handled prudently. Here is what happens.</p>
<ol>
<li>An existing document gets updated.</li>
<li>The <code>updatedAtTrigger</code> function gets called.</li>
<li>The trigger function updates the existing document again with the current timestamp field.</li>
<li>The <code>updatedAtTrigger</code> function gets called again. It detects a newly updated date, and nothing further happens.</li>
</ol>
<p>You must compare the old and new values to detect a new updated date. When dealing with any Timestamp type in Firestore, you usually need to cast the data using <code>as Timestamp</code>. This will show you the options on the field you can use. Here we use the <code>isEqual()</code> property to compare the dates. </p>
<h3>Firebase Functions Generation 2</h3>
<p>For the sake of completeness, here are the second-generation counterparts.</p>
<pre><code class="language-tsx">import { FieldValue, Timestamp } from &#39;firebase-admin/firestore&#39;;
import {
 onDocumentCreated,
 onDocumentUpdated
} from &#39;firebase-functions/v2/firestore&#39;;

const PATH = &#39;todos/{docID}&#39;

exports.createdAtTrigger = onDocumentCreated(PATH, (event) =&gt; {

    // update createdAt timestamp
    event.data?.ref.update({
        createdAt: FieldValue.serverTimestamp()
    });

});

exports.updatedAtTrigger = onDocumentUpdated(PATH, (event) =&gt; {

    const after = event.data?.after.exists ? event.data.after.data() : null;
    const before = event.data?.before.exists ? event.data.before.data() : null;

    // don&#39;t update if creating createdAt
    if (!before?.createdAt &amp;&amp; after?.createdAt) {
        return;
    }

    // don&#39;t update if creating updatedAt
    if (!before?.updatedAt &amp;&amp; after?.updatedAt) {
        return;
    }

    // don&#39;t update if updating updatedAt
    if (before?.updatedAt &amp;&amp; after?.updatedAt) {
        if (!(before.updatedAt as Timestamp).isEqual(after.updatedAt)) {
            return;
        }
    }

    // update updatedAt timestamp
    event.data?.after.ref.update({
        updatedAt: FieldValue.serverTimestamp()
    });

});
</code></pre>
<p>But remember, we should not use these cloud function triggers unless we have a specific use case for them. I honestly don’t know of a time when using them is better. If you do, please <a href="https://code.build/contact">get in touch with me</a> and let me know.</p>
<h2>The Secure Client Version</h2>
<p>The better version is to use <code>server timestamp ()</code> on your front and secure it. It doesn’t require any complex Firebase Functions, and it doesn’t need to perform extra reads. It just needs one line of code for each Date type in your Firestore Rules.</p>
<h3>Create Timestamp</h3>
<p>Here we use the server timestamp on the client when creating using <code>addDoc</code> or <code>setDoc</code>.</p>
<pre><code class="language-tsx">addDoc(collection(db, &#39;posts&#39;), {
    title: &#39;some thing&#39;,
    createdAt: serverTimestamp()
});
</code></pre>
<p>And the server timestamp on the client when updating using <code>updateDoc</code> or <code>setDoc</code> with <code>merge</code>.</p>
<pre><code class="language-tsx">updateDoc(doc(db, &#39;posts&#39;, ID), {
    title: &#39;new title rename&#39;,
    updatedAt: serverTimestamp()
});
</code></pre>
<p>And we secure it in Firestore Security Rules.</p>
<pre><code class="language-tsx">    match /todos/{document} {
      allow read;
      allow create: if request.time == request.resource.data.createdAt;
      allow update: if request.time == request.resource.data.updatedAt;
      allow delete;
    }
</code></pre>
<p>This means the mutation will fail if the <code>createdAt</code> or <code>updatedAt</code> dates are not added. We do not get extra function calls or writes. It works out of the box. This is your best bet. I won’t say “always,” as there may be exceptions. However, I have never seen one. Save money. Use this version.</p>
<h3>Real-time Considerations</h3>
<p>When using <code>serverTimestamp()</code> with real-time data, you must consider what happens when adding or modifying a document.</p>
<ol>
<li>A document gets updated or created with the <code>serverTimestamp()</code> on a field.</li>
<li>The client cache will get updated immediately for optimistic updates.</li>
<li>The client cache will then be re-updated with the real server timestamp.</li>
</ol>
<p>By default, the server timestamp is null. To prevent JavaScript errors, you should set the <code>serverTimestamps</code> setting when retrieving your client data using <code>onSnapShot</code>. Your options are <code>estimate | previous | none</code>. This applies to collections, queries, and individual documents.</p>
<pre><code class="language-tsx">const documentData = doc.data({
  serverTimestamps: &#39;estimate&#39;
});
</code></pre>
<p>I suggest using an <code>estimate</code>, as it will be your closest value. For a breakdown, see <a href="https://code.build/p/does-firestore-need-data-converters-sSveEL">Data Converters</a>.</p>
<p><strong>Repo</strong>: <a href="https://github.com/jdgamble555/svelte-firestore-timestamps/tree/master">GitHub</a></p>
<p><strong>Functions File</strong>: <a href="https://github.com/jdgamble555/svelte-firestore-timestamps/blob/master/functions/src/index.ts">Index.ts</a></p>
<p><strong>Timestamp Client</strong>: <a href="https://github.com/jdgamble555/svelte-firestore-timestamps/blob/master/src/lib/todos.ts">todos.ts</a></p>
<p><strong>Firestore Rules</strong>: <a href="https://github.com/jdgamble555/svelte-firestore-timestamps/blob/master/firestore.rules">firestore.rules</a></p>
<p>J</p>
]]></description><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category><category>[object Object]</category></item></channel></rss>