All blocks
Content
Imports
Import runs. Parsing, mapping and the dry run live in TypeScript. The job records import.completed or import.failed.
npx better-supabase sql add importsimport { defineConfig } from "better-supabase/config";
export default defineConfig({
sql: {
modules: {
// Required by imports; sql add installs them too.
"updated-at": {},
tenant: {},
jobs: {},
access: {},
// Modes: "managed", "adopt", "custom". The default is "managed".
imports: { mode: "managed" },
},
},
});npx better-supabase sql add imports writes these files into your declarative schema with the default layout. See SQL modules for the migration step and the layout options.
supabase/schemas/900_better_supabase_01_updated_at.sqlschemaupdated-at100 linessupabase/better-supabase-data/900_better_supabase_01_updated_at.sqldataupdated-at12 linessupabase/schemas/900_better_supabase_04_tenant.sqlschematenant156 linessupabase/better-supabase-data/900_better_supabase_04_tenant.sqldatatenant12 linessupabase/schemas/900_better_supabase_07_jobs.sqlschemajobs534 linessupabase/better-supabase-data/900_better_supabase_07_jobs.sqldatajobs17 linessupabase/schemas/900_better_supabase_19_access.sqlschemaaccess374 linessupabase/better-supabase-data/900_better_supabase_19_access.sqldataaccess12 linessupabase/schemas/900_better_supabase_64_imports.sqlschema122 linessupabase/better-supabase-data/900_better_supabase_64_imports.sqldata12 lines
supabase/schemas/900_better_supabase_64_imports.sql
-- better-supabase module: imports
-- @bs-module imports@1 managed
-- Import runs. Parsing, mapping and the dry run live in TypeScript. The job records import.completed or import.failed.
-- Managed by `better-supabase sql add`; re-running it overwrites this file.
-- Change it through `sql.modules` in better-supabase.config.ts and the module's SQL hooks.
create or replace function better_supabase.actor_may_write()
returns boolean
language sql
stable
security invoker
set search_path = ''
as $$
select case
when auth.jwt() is null then true
when coalesce(auth.jwt() -> 'act' ->> 'read_only', '') = 'false' then true
when auth.jwt() -> 'act' ->> 'kind' = 'agent'
then coalesce(auth.jwt() -> 'act' ->> 'read_only', '') <> 'true'
when auth.jwt() -> 'act' ->> 'kind' in ('support', 'impersonation') then false
when auth.jwt() -> 'act' ->> 'session_id' is not null
and auth.jwt() -> 'act' ->> 'kind' is null then false
else true
end
$$;
revoke execute on function better_supabase.actor_may_write() from public;
grant execute on function better_supabase.actor_may_write() to anon, authenticated, service_role;
create or replace function better_supabase.raise_actor_readonly()
returns void
language plpgsql
volatile
set search_path = ''
as $$
begin
raise exception 'This session is read-only' using errcode = '42501', hint = 'ACTOR_READ_ONLY';
end;
$$;
revoke execute on function better_supabase.raise_actor_readonly() from public;
grant execute on function better_supabase.raise_actor_readonly() to anon, authenticated, service_role;
create schema if not exists better_supabase;
grant usage on schema better_supabase to anon, authenticated, service_role;
create table if not exists "better_supabase"."import_runs" (
"id" uuid primary key default gen_random_uuid(),
"tenant_id" uuid not null,
"kind" text not null,
"status" text not null default 'pending' check ("status" in ('pending', 'running', 'completed', 'failed')),
"mapping" jsonb not null default '{}'::jsonb,
"counts" jsonb not null default '{}'::jsonb,
"errors" jsonb not null default '[]'::jsonb,
"created_by" uuid,
"created_at" timestamptz not null default now()
);
alter table "better_supabase"."import_runs" enable row level security;
revoke all on "better_supabase"."import_runs" from anon, authenticated;
grant select on "better_supabase"."import_runs" to authenticated;
grant all on "better_supabase"."import_runs" to service_role;
create or replace function "better_supabase"."start_import"(tenant uuid, kind text, mapping jsonb)
returns jsonb
language plpgsql
security definer
set search_path = ''
as $$
declare
v_id uuid;
begin
if not (better_supabase.actor_may_write()) then
raise exception 'This session is read-only' using errcode = '42501', hint = 'ACTOR_READ_ONLY';
end if;
if not (auth.role() = 'service_role' or coalesce(better_supabase.can('tenant', start_import.tenant, 'imports.manage'), false)) then
raise exception 'not allowed to import' using errcode = '42501', hint = 'IMPORT_FORBIDDEN';
end if;
insert into "better_supabase"."import_runs" ("tenant_id", "kind", "mapping", "created_by")
values (start_import.tenant, start_import.kind, coalesce(start_import.mapping, '{}'::jsonb), auth.uid())
returning "id" into v_id;
return jsonb_build_object('id', v_id);
end;
$$;
revoke execute on function "better_supabase"."start_import"(uuid, text, jsonb) from public, anon;
grant execute on function "better_supabase"."start_import"(uuid, text, jsonb) to authenticated, service_role;
create or replace function "better_supabase"."finish_import"(id uuid, status text, count integer, error text)
returns boolean
language plpgsql
security definer
set search_path = ''
as $$
#variable_conflict use_column
declare
v_tenant uuid;
begin
update "better_supabase"."import_runs" set "status" = finish_import.status,
"counts" = jsonb_build_object('imported', finish_import.count),
"errors" = case when finish_import.error is null then '[]'::jsonb else jsonb_build_array(finish_import.error) end
where "id" = finish_import.id
returning "tenant_id" into v_tenant;
if not found then
raise exception 'import not found' using errcode = 'P0002', hint = 'IMPORT_NOT_FOUND';
end if;
if finish_import.status = 'failed' then
null;
else
null;
end if;
return true;
end;
$$;
revoke execute on function "better_supabase"."finish_import"(uuid, text, integer, text) from public, anon, authenticated;
grant execute on function "better_supabase"."finish_import"(uuid, text, integer, text) to service_role;
create schema if not exists better_supabase;
create table if not exists better_supabase.modules (
name text primary key,
version integer not null,
mode text not null,
installed_at timestamptz not null default now(),
updated_at timestamptz not null default now()
);
alter table better_supabase.modules enable row level security;
revoke all on better_supabase.modules from anon, authenticated;
grant select on better_supabase.modules to service_role;
supabase/better-supabase-data/900_better_supabase_64_imports.sql
-- better-supabase module: imports
-- @bs-module-data imports
-- Rows and settings a schema diff doesn't capture. Run `better-supabase sql data`
-- after the schema migration to put them in a migration.
-- Managed by `better-supabase sql add`; re-running it overwrites this file.
-- Change it through `sql.modules` in better-supabase.config.ts and the module's SQL hooks.
insert into better_supabase.modules (name, version, mode)
values ('imports', 1, 'managed')
on conflict (name) do update
set version = excluded.version, mode = excluded.mode, updated_at = now();
This block has no React entry yet. Its TypeScript side is in better-supabase/blocks/imports, which runs on the server.
With the outbox installed, the module writes these events in the same transaction as the change.
- import.completed
- import.failed