Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions packages/vecstore-sdk/sql/supabase.sql
Original file line number Diff line number Diff line change
@@ -1,11 +1,3 @@
-- vecstore-sdk: server side for the Supabase adapter.
-- Run once per project, with a role that can create functions.
-- psql "$SUPABASE_DB_URL" -f node_modules/vecstore-sdk/sql/supabase.sql
-- Every function runs with the privileges of the caller. Tables that
-- vecstore_create_index creates have row level security enabled and no
-- policies, so the anon and authenticated roles read and write nothing until
-- you add one. The service role bypasses row level security.

create extension if not exists vector;

create or replace function vecstore_table(index_name text, index_schema text)
Expand Down
56 changes: 51 additions & 5 deletions packages/vecstore-sdk/test/postgres/supabase.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,8 +174,11 @@ describe("a cosine vecstore_query that meets a zero vector on PGlite", () => {
describe("supabase install script", () => {
const db = usePostgres(installSupabaseSql);

const asAnon = async <T>(fn: () => PromiseLike<T>): Promise<T> => {
await db().exec("set role anon");
const asRole = async <T>(
role: string,
fn: () => PromiseLike<T>
): Promise<T> => {
await db().exec(`set role ${role}`);
try {
return await fn();
} finally {
Expand Down Expand Up @@ -215,19 +218,62 @@ describe("supabase install script", () => {

await ensureRoles(db(), ["anon"]);
await db().exec("grant select on secured_anon to anon");
expect(await asAnon(() => countRows("secured_anon"))).toBe(0);
expect(await asRole("anon", () => countRows("secured_anon"))).toBe(0);

await db().exec(
"create policy secured_read on secured_anon for select to anon using (true)"
);
expect(await asAnon(() => countRows("secured_anon"))).toBe(1);
expect(await asRole("anon", () => countRows("secured_anon"))).toBe(1);
});

test("every vecstore function runs with the privileges of its caller", async () => {
const functions = await db().query<{ definer: boolean; name: string }>(
"select proname as name, prosecdef as definer from pg_proc where proname like 'vecstore\\_%'"
);
expect(functions.rows.length).toBeGreaterThan(0);
expect(functions.rows.filter((row) => row.definer)).toStrictEqual([]);
});

test.each(["anon", "authenticated"])(
"%s reads and changes nothing through the adapter until a policy exists",
async (role) => {
const name = `secured_${role}_verbs`;
await createSecured(name);
const index = createSupabaseStore({ client: rpcClient(db()) }).index(
name
);
await index.upsert([{ id: "a", vector: [0, 0, 1] }]);

await ensureRoles(db(), [role]);
await db().exec(
`grant select, insert, update, delete on ${name} to ${role}`
);

const queried = await asRole(role, () =>
index.query({ topK: 1, vector: [0, 0, 1] })
);
const fetched = await asRole(role, () => index.fetch(["a"]));
const upserted = await asRole(role, () =>
index.upsert([{ id: "b", vector: [1, 0, 0] }])
);
const deleted = await asRole(role, () => index.delete({ ids: ["a"] }));

expect(queried.ok && queried.value).toStrictEqual([]);
expect(fetched.ok && fetched.value).toStrictEqual([]);
expect(upserted.ok || upserted.error.kind).toBe("unauthorized");
expect(deleted.ok).toBe(true);
const remaining = await db().query<{ id: string }>(
`select id from ${name}`
);
expect(remaining.rows.map((row) => row.id)).toStrictEqual(["a"]);
}
);

test("anon cannot call the ddl functions once the supabase roles exist", async () => {
await ensureRoles(db(), ["anon", "authenticated", "service_role"]);
await installSupabaseSql(db());

const response = await asAnon(() =>
const response = await asRole("anon", () =>
rpcClient(db()).rpc("vecstore_create_index", {
dimension: 3,
index_name: "x",
Expand Down
Loading