diff --git a/packages/db-store/README.md b/packages/db-store/README.md index 7c9570c66..35aad9147 100644 --- a/packages/db-store/README.md +++ b/packages/db-store/README.md @@ -29,6 +29,7 @@ pnpm add @solid-primitives/db-store const [dbStore, setDbStore] = createDbStore({ adapter: supabaseAdapter(client), table: "todos", + defaultFields: ['id', 'userid'] filter: ({ userid }) => userid === user.id, onError: handleErrors, }); @@ -42,9 +43,11 @@ The store is automatically initialized and optimistically updated both ways. Due > [!NOTE] > It can take some time for the database editor to show updates. They are processed a lot faster. -### Setting preliminary IDs +### Handling default fields -The `id` field needs to be set by the database, so even if you set it, it needs to be overwritten in any case. It is not required to set it for your fields manually; one can also treat its absence as a sign that an insertion is not yet done in the database. +The `id` field needs to be set by the database, so even if you set it, it needs to be overwritten in any case. There might be other fields that the server sets by default, e.g. a user ID. It is not required to set those for your rows manually; one can also treat its absence as a sign that an insertion is not yet done in the database. + +By default, only 'id' is handled as default field. If you have additional default fields, you need to use the `defaultField` option to convey them; otherwise default fields not set by the client might break the reconciliation of newly added fields. ### Handling errors diff --git a/packages/db-store/dev/README.md b/packages/db-store/dev/README.md new file mode 100644 index 000000000..6e2887444 --- /dev/null +++ b/packages/db-store/dev/README.md @@ -0,0 +1,42 @@ +# Database setup + +To create and set up the database for your own version of the todo list, use the following SQL statements in your supabase SQL editor: + +```sql +create table todos ( + id serial primary key, + task text, + user_id uuid references auth.users default auth.uid() +); + +alter publication supabase_realtime add table todos; + +create policy "realtime updates only for authenticated users" +on "realtime"."messages" +for select +to authenticated +using (true); + +alter table "public"."todos" enable row level security; + +create policy "Select only own tasks" on "public"."todos" +for select +to authenticated +using (((SELECT auth.uid() AS uid) = user_id)); + +create policy "Insert only own tasks" on "public"."todos" +for insert +to authenticated +with check (((SELECT auth.uid() AS uid) = user_id)); + +create policy "Delete only own tasks" on "public"."todos" +for delete +to authenticated +using (((SELECT auth.uid() AS uid) = user_id)); + +create policy "Update only own tasks" on "public"."todos" +for update +to authenticated +using (((SELECT auth.uid() AS uid) = user_id)) +with check (((SELECT auth.uid() AS uid) = user_id)); +``` diff --git a/packages/db-store/dev/index.tsx b/packages/db-store/dev/index.tsx index d313a0ac4..18920afe0 100644 --- a/packages/db-store/dev/index.tsx +++ b/packages/db-store/dev/index.tsx @@ -1,19 +1,22 @@ -import { Component, createSignal, For, Show } from "solid-js"; +import { Component, createSignal, onMount, For, Show } from "solid-js"; import { createDbStore, supabaseAdapter, DbRow, DbStoreError } from "../src/index.js"; -import { createClient, SupabaseClient } from "@supabase/supabase-js"; +import { AuthResponse, createClient, Session, SupabaseClient } from "@supabase/supabase-js"; import { reconcile } from "solid-js/store"; -const TodoList = (props: { client: SupabaseClient }) => { +const TodoList = (props: { client: SupabaseClient, logout: () => void }) => { const [error, setError] = createSignal>(); + (globalThis as any).supabaseClient = props.client; const [todos, setTodos] = createDbStore({ adapter: supabaseAdapter({ client: props.client, table: "todos" }), + defaultFields: ['id', 'user_id'], onError: setError, }); const [edit, setEdit] = createSignal(); const done = (task: DbRow) => setTodos(reconcile(todos.filter(todo => todo !== task))); const add = (task: string) => setTodos(todos.length, { task }); return ( - <> +
+ {err => (

@@ -70,85 +73,74 @@ const TodoList = (props: { client: SupabaseClient }) => { - +

); }; const App: Component = () => { - const [client, setClient] = createSignal>(); - const connect = () => { - const url = (document.querySelector('[type="url"]') as HTMLInputElement | null)?.value; - const key = (document.querySelector('[type="password"]') as HTMLInputElement | null)?.value; - url && key && setClient(createClient(url, key)); + // these are public keys that will end up in the client in any case: + const client = + createClient( + import.meta.env.VITE_SUPABASE_URL, + import.meta.env.VITE_SUPABASE_KEY + ); + const [session, setSession] = createSignal(); + const [error, setError] = createSignal(''); + const handleAuthPromise = ({ error, data }: AuthResponse) => { + if (error) { + setError(error.toString()) + } else { + setSession(data.session ?? undefined); + } + }; + onMount(() => client.auth.refreshSession().then(handleAuthPromise)); + const login = () => { + const email = (document.querySelector('[type="email"]') as HTMLInputElement | null)?.value; + const password = (document.querySelector('[type="password"]') as HTMLInputElement | null)?.value; + if (!email || !password) { + setError('please provide an email and password'); + return; + } + client.auth.signInWithPassword({ email, password }).then(handleAuthPromise); }; + const register = () => { + const email = (document.querySelector('[type="email"]') as HTMLInputElement | null)?.value; + const password = (document.querySelector('[type="password"]') as HTMLInputElement | null)?.value; + if (!email || !password) { + setError('please provide an email and password'); + return; + } + client.auth.signUp({ email, password }).then(handleAuthPromise); + } return (

db-store-backed to-do list

-
- To configure your own database, -
    -
  • - Register with Supabase. -
  • -
  • - Create a new database and note down the url and the key (that usually go into an - environment) -
  • -
  • - Within the database, create a table and configure it to be public, promote - changes in realtime and has no row protection: -
    -                      {`-- Create table
    -create table todos (
    -  id serial primary key,
    -  task text
    -);
    --- Turn off row-level security
    -alter table "todos"
    -disable row level security;
    --- Allow anonymous access
    -create policy "Allow anonymous access"
    -on todos
    -for select
    -to anon
    -using (true);`}
    -                    
    -
  • -
  • Fill in the url and key in the fields below and press "connect".
  • -
-
-

- -

+

{error()}

- + + } > - {(client: SupabaseClient) => } + { setSession(undefined); client.auth.signOut(); }} + />
diff --git a/packages/db-store/src/index.ts b/packages/db-store/src/index.ts index 940eeda43..0c962143a 100644 --- a/packages/db-store/src/index.ts +++ b/packages/db-store/src/index.ts @@ -11,7 +11,7 @@ import { import { createStore, reconcile, SetStoreFunction, Store, unwrap } from "solid-js/store"; import { RealtimePostgresChangesPayload, SupabaseClient } from "@supabase/supabase-js"; -export type DbRow = Record & { id: number | string }; +export type DbRow = Record; export type DbAdapterUpdate = { old?: Partial; new?: Partial }; @@ -21,11 +21,11 @@ export type DbAdapterFilter = ( ev: { action: DbAdapterAction } & DbAdapterUpdate, ) => boolean; -export type DbAdapterOptions = { +export type DbAdapterOptions = {}> = { client: SupabaseClient; filter?: DbAdapterFilter; table: string; -}; +} & Extras; export type DbAdapter = { insertSignal: () => DbAdapterUpdate | undefined; @@ -69,7 +69,7 @@ const supabaseHandleError = ) : Promise.resolve(); -export const supabaseAdapter = (opts: DbAdapterOptions): DbAdapter => { +export const supabaseAdapter = (opts: DbAdapterOptions): DbAdapter => { const [insertSignal, setInsertSignal] = createSignal>(); const [updateSignal, setUpdateSignal] = createSignal>(); const [deleteSignal, setDeleteSignal] = createSignal>(); @@ -84,7 +84,7 @@ export const supabaseAdapter = (opts: DbAdapterOptions): }; const channel = opts.client .channel("schema-db-changes") - .on("postgres_changes", { event: "*", schema: "public" }, updateHandler) + .on("postgres_changes", { event: "*", schema: opts.schema || "public" }, updateHandler) .subscribe(); onCleanup(() => channel.unsubscribe()); return { @@ -119,6 +119,7 @@ export const supabaseAdapter = (opts: DbAdapterOptions): export type DbStoreOptions = { adapter: DbAdapter; init?: Row[]; + defaultFields?: readonly string[]; equals?: (a: unknown, b: unknown) => boolean; onError?: (err: DbStoreError) => void; }; @@ -130,6 +131,7 @@ export const createDbStore = ( const [dbStore, setDbStore] = createStore(opts.init || []); const [dbInit, { refetch }] = createResource(opts.adapter.init); const equals = opts.equals || ((a, b) => a === b); + const defaultFields = opts.defaultFields || ['id']; const onError = (error: DbStoreError) => { if (typeof opts.onError === "function") { opts.onError(error); @@ -150,16 +152,16 @@ export const createDbStore = ( on(opts.adapter.insertSignal, inserted => { if (!inserted?.new?.id) return; for (const row of insertions.values()) { - if (Object.entries(inserted.new).some(([key, value]) => key !== "id" && row[key] !== value)) + if (Object.entries(inserted.new).some(([key, value]) => !defaultFields.includes(key) && row[key] !== value)) continue; const index = untrack(() => dbStore.findIndex(cand => - Object.entries(cand).every(([key, value]) => key === "id" || row[key] == value), + Object.entries(cand).every(([key, value]) => defaultFields.includes(key) || row[key] == value), ), ); if (index !== -1) { // @ts-ignore - setDbStore(index, "id", inserted.new.id); + setDbStore(index, inserted.new); insertions.delete(row); return; } diff --git a/site/.env b/site/.env index ac257f987..9e4ed2c09 100644 --- a/site/.env +++ b/site/.env @@ -2,4 +2,6 @@ # These variables are available to the public and can be accessed by anyone. VITE_SITE_URL=https://solid-primitives.netlify.app +VITE_SUPABASE_URL=https://hpinwklbtszwebhaqjxr.supabase.co +VITE_SUPABASE_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhwaW53a2xidHN6d2ViaGFxanhyIiwicm9sZSI6ImFub24iLCJpYXQiOjE3Mzc1NTc1MTUsImV4cCI6MjA1MzEzMzUxNX0._D88mlVMZvkvipFkSwQBA8QZ9i0cl1tutWdYpM02cdI NODE_VERSION=18.15.0 diff --git a/site/src/env.d.ts b/site/src/env.d.ts index 020dac55e..950a0eed3 100644 --- a/site/src/env.d.ts +++ b/site/src/env.d.ts @@ -3,6 +3,8 @@ // declare import.meta.env interface ImportMetaEnv { readonly VITE_SITE_URL: string; + readonly VITE_SUPABASE_URL: string; + readonly VITE_SUPABASE_KEY: string; } interface ImportMeta {