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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
create extension if not exists pg_stat_statements with schema extensions;
create extension if not exists pg_trgm with schema extensions;
create extension if not exists citext with schema extensions;
create extension if not exists pg_cron;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
create schema app;

grant usage on schema app to authenticated, anon;

alter default privileges in schema app grant select on tables to authenticated, anon;
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
-- https://semver.org/#backusnaur-form-grammar-for-valid-semver-versions
create type app.semver_struct as (
major smallint,
minor smallint,
patch smallint
);

create or replace function app.is_valid(app.semver_struct)
returns boolean
immutable
language sql
as $$
select (
($1).major is not null
and ($1).minor is not null
and ($1).patch is not null
)
$$;

create domain app.semver
as app.semver_struct
check (
app.is_valid(value)
);

create function app.semver_exception(version text)
returns app.semver_struct
immutable
language plpgsql
as $$
begin
raise exception using errcode='22000', message=format('Invalid semver %L', version);
end;
$$;


-- Cast from Text
create function app.text_to_semver(text)
returns app.semver_struct
immutable
strict
language sql
as $$
with s(version) as (
select (
split_part($1, '.', 1),
split_part($1, '.', 2),
split_part(split_part(split_part($1, '.', 3), '-', 1), '+', 1)
)::app.semver_struct
)
select
case app.is_valid(s.version)
when true then s.version
else app.semver_exception($1)
end
from
s
$$;


create or replace function app.semver_to_text(app.semver)
returns text
immutable
language sql
as $$
select
format('%s.%s.%s', $1.major, $1.minor, $1.patch)
$$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
create extension if not exists citext with schema extensions;

create domain app.valid_name
as extensions.citext
check (
-- 3 to 15 chars, A-z with underscores
value ~ '^[A-z][A-z0-9\_]{2,32}$'
);

create or replace function app.exception(message text)
returns text
language plpgsql
as $$
begin
raise exception using errcode='22000', message=message;
end;
$$;

/*
create domain app.valid_name
as extensions.citext
check (
-- 3 to 15 chars, A-z with underscores
case
when value ~ '^[A-z][A-z0-9\_]{2,14}$' then True
else app.exception('Bad name ' || value)::bool
end
);
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
create domain app.email_address
-- https://html.spec.whatwg.org/multipage/input.html#email-state-(type=email)
AS citext
check ( value ~ '^[a-zA-Z0-9.!#$%&''*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$'
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
insert into storage.buckets ("id", "name")
values ('avatars', 'avatars');

create table app.handle_registry(
/*
Enforces uniqueness of handles across orgs and accounts
e.g. jsmith or supabase
*/
handle app.valid_name primary key not null,
is_organization boolean not null,
created_at timestamptz not null default now(),
unique (handle, is_organization)
);

create table app.accounts(
-- 1:1 with auth.users
id uuid primary key references auth.users(id),
handle app.valid_name not null unique,
is_organization boolean generated always as (false) stored,
avatar_id uuid references storage.objects(id),
display_name text check (length(display_name) <= 128),
bio text check (length(bio) <= 512),
contact_email app.email_address,
created_at timestamptz not null default now(),

constraint fk_handle_registry
foreign key (handle, is_organization)
references app.handle_registry(handle, is_organization)
);

create or replace function app.register_account()
returns trigger
language plpgsql
security definer
as $$
begin
insert into app.handle_registry (handle, is_organization)
values (
new.raw_user_meta_data ->> 'handle',
false
);

insert into app.accounts (id, handle, display_name, bio, contact_email)
values (
new.id,
new.raw_user_meta_data ->> 'handle',
new.raw_user_meta_data ->> 'display_name',
new.raw_user_meta_data ->> 'bio',
new.raw_user_meta_data ->> 'contact_email'
);
return new;
end;
$$;

create or replace trigger on_auth_user_created
after insert on auth.users
for each row execute procedure app.register_account();

create table app.organizations(
id uuid primary key default gen_random_uuid(),
handle app.valid_name not null unique,
is_organization boolean generated always as (true) stored,
avatar_id uuid references storage.objects(id),
display_name text check (length(display_name) <= 128),
bio text check (length(bio) <= 512),
contact_email app.email_address,
-- enforced so organization always have at least 1 admin member
created_at timestamptz not null default now(),

constraint fk_handle_registry
foreign key (handle, is_organization)
references app.handle_registry(handle, is_organization)
);

create type app.membership_role as enum ('maintainer');

create table app.members(
id uuid primary key default uuid_generate_v4(),
organization_id uuid not null references app.organizations(id),
account_id uuid not null references app.accounts(id),
role app.membership_role not null,
created_at timestamptz not null default now(),
unique (organization_id, account_id)
);

create or replace function app.register_organization_creator_as_member()
returns trigger
language plpgsql
security definer
as $$
begin
insert into app.members(organization_id, account_id, role)
values (new.id, auth.uid(), 'maintainer');

return new;
end;
$$;

create or replace trigger on_app_organization_created
after insert on app.organizations
for each row execute procedure app.register_organization_creator_as_member();

create or replace function app.update_avatar_id()
returns trigger
language plpgsql
security definer
as $$
declare
v_handle app.valid_name;
v_affected_account app.accounts := null;
begin
select (string_to_array(new.name, '-'::text))[1]::app.valid_name into v_handle;

update app.accounts
set avatar_id = new.id
where handle = v_handle
returning * into v_affected_account;

if not v_affected_account is null then
update auth.users u
set
"raw_user_meta_data" = u.raw_user_meta_data || jsonb_build_object(
'avatar_path', new.name
)
where u.id = v_affected_account.id;
else
update app.organizations
set avatar_id = new.id
where handle = v_handle;
end if;

return new;
end;
$$;

create or replace trigger on_storage_object_created
after insert on storage.objects
for each row when(new.bucket_id = 'avatars') execute procedure app.update_avatar_id();
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
insert into storage.buckets (id, name)
values
('package_versions', 'package_versions'),
('package_upgrades', 'package_upgrades');

create function app.to_package_name(handle app.valid_name, partial_name app.valid_name)
returns text
immutable
language sql
as $$
select format('%s-%s', $1, $2)
$$;

create table app.packages(
id uuid primary key default gen_random_uuid(),
package_name text not null generated always as (app.to_package_name(handle, partial_name)) stored,
handle app.valid_name not null references app.handle_registry(handle),
partial_name app.valid_name not null, -- ex: math
control_description varchar(1000),
control_relocatable bool not null default false,
control_requires varchar(128)[] default '{}'::varchar(128)[],
created_at timestamptz not null default now(),
unique (handle, partial_name)
);
create index packages_partial_name_search_idx on app.packages using gin (partial_name extensions.gin_trgm_ops);
create index packages_handle_search_idx on app.packages using gin (handle extensions.gin_trgm_ops);

create table app.package_versions(
id uuid primary key default gen_random_uuid(),
package_id uuid not null references app.packages(id),
version_struct app.semver not null,
version text not null generated always as (app.semver_to_text(version_struct)) stored,
sql varchar(250000),
description_md varchar(250000),
created_at timestamptz not null default now(),
unique(package_id, version_struct)
);

create table app.package_upgrades(
id uuid primary key default gen_random_uuid(),
package_id uuid not null references app.packages(id),
from_version_struct app.semver not null,
from_version text not null generated always as (app.semver_to_text(from_version_struct)) stored,
to_version_struct app.semver not null,
to_version text not null generated always as (app.semver_to_text(to_version_struct)) stored,
sql varchar(250000),
created_at timestamptz not null default now(),
unique(package_id, from_version_struct, to_version_struct)
);

create function app.version_text_to_handle(version text)
returns app.valid_name
immutable
language sql
as $$
select split_part($1, '-', 1)
$$;

create function app.version_text_to_package_partial_name(version text)
returns app.valid_name
immutable
language sql
as $$
select split_part($1, '--', 2)
$$;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
create or replace function app.simulate_login(email citext)
returns void
language sql
as $$
/*
Simulated JWT of logged in user
*/

select
set_config(
'request.jwt.claims',
(
select
json_build_object(
'sub',
id,
'role',
'authenticated'
)::text
from
auth.users
where
email = $1
),
true
),
set_config('role', 'authenticated', true)
$$;
Loading
Loading