@selvajs/supabase-provider 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -14
- package/dist/cli/sync-migrations.d.ts +3 -0
- package/dist/cli/sync-migrations.d.ts.map +1 -0
- package/dist/cli/sync-migrations.js +108 -0
- package/dist/cli/sync-migrations.js.map +1 -0
- package/dist/data/__tests__/test-helpers.d.ts +2 -3
- package/dist/data/__tests__/test-helpers.d.ts.map +1 -1
- package/dist/data/__tests__/test-helpers.js +1 -0
- package/dist/data/__tests__/test-helpers.js.map +1 -1
- package/dist/data/client.d.ts +10 -2
- package/dist/data/client.d.ts.map +1 -1
- package/dist/data/client.js +3 -0
- package/dist/data/client.js.map +1 -1
- package/package.json +4 -1
- package/src/cli/sync-migrations.ts +133 -0
- package/src/data/__tests__/test-helpers.ts +5 -4
- package/src/data/client.ts +16 -4
- package/supabase/config.toml +7 -2
- package/supabase/migrations/{0001_initial.sql → 20260425155514_selva_initial.sql} +314 -233
- package/supabase/migrations/{0002_definition_version_schema.sql → 20260529202220_selva_definition_version_schema.sql} +1 -1
- package/supabase/migrations/20260531120000_set_updated_at_search_path.sql +11 -0
|
@@ -1,3 +1,48 @@
|
|
|
1
|
+
-- ============================================================================
|
|
2
|
+
-- 0. Dedicated `selva` schema
|
|
3
|
+
-- All engine objects live here, never in `public`, so a consuming app that
|
|
4
|
+
-- shares the database keeps `public` entirely for its own tables. Cross-schema
|
|
5
|
+
-- refs from the app (FKs into selva.orgs, calls to selva.is_org_member) are
|
|
6
|
+
-- supported. The provider's data client targets it (db: { schema: 'selva' }).
|
|
7
|
+
--
|
|
8
|
+
-- PostgREST must expose `selva` to serve it over REST. We do that HERE, from the
|
|
9
|
+
-- migration, rather than from config.toml's static `[api] schemas`: that config
|
|
10
|
+
-- is read at stack boot, before migrations run, so naming a not-yet-created
|
|
11
|
+
-- schema there fails the health check and aborts `supabase start`. Setting it on
|
|
12
|
+
-- the `authenticator` role after the schema exists, then signalling a reload,
|
|
13
|
+
-- avoids the bootstrap race and works identically on hosted projects.
|
|
14
|
+
-- ============================================================================
|
|
15
|
+
create schema if not exists selva;
|
|
16
|
+
grant usage on schema selva to anon, authenticated, service_role;
|
|
17
|
+
alter default privileges in schema selva grant all on tables to anon, authenticated, service_role;
|
|
18
|
+
alter default privileges in schema selva grant all on functions to anon, authenticated, service_role;
|
|
19
|
+
alter default privileges in schema selva grant all on sequences to anon, authenticated, service_role;
|
|
20
|
+
|
|
21
|
+
-- Expose `selva` over PostgREST (additive to whatever is already exposed).
|
|
22
|
+
do $$
|
|
23
|
+
declare
|
|
24
|
+
current_schemas text;
|
|
25
|
+
begin
|
|
26
|
+
select coalesce(
|
|
27
|
+
(select setting from pg_db_role_setting s
|
|
28
|
+
join pg_roles r on r.oid = s.setrole
|
|
29
|
+
cross join lateral unnest(s.setconfig) as setting
|
|
30
|
+
where r.rolname = 'authenticator' and setting like 'pgrst.db_schemas=%'),
|
|
31
|
+
'pgrst.db_schemas=public, graphql_public'
|
|
32
|
+
) into current_schemas;
|
|
33
|
+
|
|
34
|
+
if current_schemas not like '%selva%' then
|
|
35
|
+
execute format(
|
|
36
|
+
'alter role authenticator set pgrst.db_schemas = %L',
|
|
37
|
+
replace(current_schemas, 'pgrst.db_schemas=', '') || ', selva'
|
|
38
|
+
);
|
|
39
|
+
end if;
|
|
40
|
+
end $$;
|
|
41
|
+
|
|
42
|
+
notify pgrst, 'reload config';
|
|
43
|
+
notify pgrst, 'reload schema';
|
|
44
|
+
|
|
45
|
+
|
|
1
46
|
-- ============================================================================
|
|
2
47
|
-- Selva — initial schema (consolidated bootstrap).
|
|
3
48
|
--
|
|
@@ -27,7 +72,7 @@
|
|
|
27
72
|
-- Maintains updated_at on every UPDATE without adapter involvement.
|
|
28
73
|
-- ============================================================================
|
|
29
74
|
|
|
30
|
-
create or replace function
|
|
75
|
+
create or replace function selva.set_updated_at()
|
|
31
76
|
returns trigger
|
|
32
77
|
language plpgsql
|
|
33
78
|
as $$
|
|
@@ -44,7 +89,7 @@ $$;
|
|
|
44
89
|
-- starred definitions, recent runs). Auto-seeded by trigger on signup.
|
|
45
90
|
-- ============================================================================
|
|
46
91
|
|
|
47
|
-
create table if not exists
|
|
92
|
+
create table if not exists selva.user_profiles (
|
|
48
93
|
user_id uuid primary key references auth.users(id) on delete cascade,
|
|
49
94
|
display_name text,
|
|
50
95
|
platform_permissions text[] not null default '{}',
|
|
@@ -54,14 +99,14 @@ create table if not exists public.user_profiles (
|
|
|
54
99
|
last_login_at timestamptz
|
|
55
100
|
);
|
|
56
101
|
|
|
57
|
-
create or replace function
|
|
102
|
+
create or replace function selva.handle_new_auth_user()
|
|
58
103
|
returns trigger
|
|
59
104
|
language plpgsql
|
|
60
105
|
security definer
|
|
61
106
|
set search_path = public
|
|
62
107
|
as $$
|
|
63
108
|
begin
|
|
64
|
-
insert into
|
|
109
|
+
insert into selva.user_profiles (user_id)
|
|
65
110
|
values (new.id)
|
|
66
111
|
on conflict (user_id) do nothing;
|
|
67
112
|
return new;
|
|
@@ -71,17 +116,17 @@ $$;
|
|
|
71
116
|
drop trigger if exists trg_auth_user_created on auth.users;
|
|
72
117
|
create trigger trg_auth_user_created
|
|
73
118
|
after insert on auth.users
|
|
74
|
-
for each row execute function
|
|
119
|
+
for each row execute function selva.handle_new_auth_user();
|
|
75
120
|
|
|
76
|
-
alter table
|
|
121
|
+
alter table selva.user_profiles enable row level security;
|
|
77
122
|
|
|
78
123
|
create policy "user_profiles: any authenticated can read"
|
|
79
|
-
on
|
|
124
|
+
on selva.user_profiles for select
|
|
80
125
|
to authenticated
|
|
81
126
|
using (true);
|
|
82
127
|
|
|
83
128
|
create policy "user_profiles: users can update own"
|
|
84
|
-
on
|
|
129
|
+
on selva.user_profiles for update
|
|
85
130
|
to authenticated
|
|
86
131
|
using (user_id = auth.uid())
|
|
87
132
|
with check (
|
|
@@ -89,7 +134,7 @@ with check (
|
|
|
89
134
|
-- Users cannot escalate their own platform permissions; only service-role
|
|
90
135
|
-- writes (which bypass RLS) may change that column.
|
|
91
136
|
and platform_permissions is not distinct from (
|
|
92
|
-
select platform_permissions from
|
|
137
|
+
select platform_permissions from selva.user_profiles where user_id = auth.uid()
|
|
93
138
|
)
|
|
94
139
|
);
|
|
95
140
|
|
|
@@ -100,7 +145,7 @@ with check (
|
|
|
100
145
|
-- reads filter deleted_at IS NULL.
|
|
101
146
|
-- ============================================================================
|
|
102
147
|
|
|
103
|
-
create table if not exists
|
|
148
|
+
create table if not exists selva.orgs (
|
|
104
149
|
id uuid primary key,
|
|
105
150
|
name text not null,
|
|
106
151
|
slug text not null unique,
|
|
@@ -111,10 +156,10 @@ create table if not exists public.orgs (
|
|
|
111
156
|
updated_at timestamptz not null default now(),
|
|
112
157
|
deleted_at timestamptz
|
|
113
158
|
);
|
|
114
|
-
create index if not exists idx_orgs_live on
|
|
159
|
+
create index if not exists idx_orgs_live on selva.orgs(id) where deleted_at is null;
|
|
115
160
|
|
|
116
|
-
create table if not exists
|
|
117
|
-
org_id uuid not null references
|
|
161
|
+
create table if not exists selva.org_members (
|
|
162
|
+
org_id uuid not null references selva.orgs(id) on delete cascade,
|
|
118
163
|
user_id uuid not null references auth.users(id) on delete cascade,
|
|
119
164
|
role text not null check (role in ('owner', 'admin', 'member')),
|
|
120
165
|
permissions text[] not null default '{}',
|
|
@@ -125,11 +170,11 @@ create table if not exists public.org_members (
|
|
|
125
170
|
primary key (org_id, user_id)
|
|
126
171
|
);
|
|
127
172
|
create index if not exists idx_org_members_user
|
|
128
|
-
on
|
|
173
|
+
on selva.org_members(user_id) where deleted_at is null;
|
|
129
174
|
|
|
130
|
-
create table if not exists
|
|
175
|
+
create table if not exists selva.projects (
|
|
131
176
|
id uuid primary key,
|
|
132
|
-
org_id uuid not null references
|
|
177
|
+
org_id uuid not null references selva.orgs(id) on delete cascade,
|
|
133
178
|
name text not null,
|
|
134
179
|
slug text not null,
|
|
135
180
|
description text,
|
|
@@ -142,19 +187,19 @@ create table if not exists public.projects (
|
|
|
142
187
|
updated_at timestamptz not null default now(),
|
|
143
188
|
deleted_at timestamptz,
|
|
144
189
|
unique (org_id, slug),
|
|
145
|
-
-- Spec §4: commons mode requires visibility=
|
|
190
|
+
-- Spec §4: commons mode requires visibility=selva.
|
|
146
191
|
constraint projects_commons_requires_public check (
|
|
147
192
|
auto_join_on_upload = false or visibility = 'public'
|
|
148
193
|
)
|
|
149
194
|
);
|
|
150
195
|
create index if not exists idx_projects_org
|
|
151
|
-
on
|
|
196
|
+
on selva.projects(org_id) where deleted_at is null;
|
|
152
197
|
-- Case-insensitive unique name per org (defense beyond slug uniqueness).
|
|
153
198
|
create unique index if not exists projects_org_name_unique
|
|
154
|
-
on
|
|
199
|
+
on selva.projects (org_id, lower(name));
|
|
155
200
|
|
|
156
|
-
create table if not exists
|
|
157
|
-
project_id uuid not null references
|
|
201
|
+
create table if not exists selva.project_members (
|
|
202
|
+
project_id uuid not null references selva.projects(id) on delete cascade,
|
|
158
203
|
user_id uuid not null references auth.users(id) on delete cascade,
|
|
159
204
|
role text not null check (role in ('owner', 'editor', 'viewer')),
|
|
160
205
|
joined_at timestamptz not null default now(),
|
|
@@ -164,21 +209,21 @@ create table if not exists public.project_members (
|
|
|
164
209
|
primary key (project_id, user_id)
|
|
165
210
|
);
|
|
166
211
|
create index if not exists idx_project_members_user
|
|
167
|
-
on
|
|
212
|
+
on selva.project_members(user_id) where deleted_at is null;
|
|
168
213
|
|
|
169
214
|
|
|
170
215
|
-- ============================================================================
|
|
171
216
|
-- 4. updated_at triggers
|
|
172
217
|
-- ============================================================================
|
|
173
218
|
|
|
174
|
-
create trigger trg_orgs_updated_at before update on
|
|
175
|
-
for each row execute function
|
|
176
|
-
create trigger trg_projects_updated_at before update on
|
|
177
|
-
for each row execute function
|
|
178
|
-
create trigger trg_org_members_updated_at before update on
|
|
179
|
-
for each row execute function
|
|
180
|
-
create trigger trg_project_members_updated_at before update on
|
|
181
|
-
for each row execute function
|
|
219
|
+
create trigger trg_orgs_updated_at before update on selva.orgs
|
|
220
|
+
for each row execute function selva.set_updated_at();
|
|
221
|
+
create trigger trg_projects_updated_at before update on selva.projects
|
|
222
|
+
for each row execute function selva.set_updated_at();
|
|
223
|
+
create trigger trg_org_members_updated_at before update on selva.org_members
|
|
224
|
+
for each row execute function selva.set_updated_at();
|
|
225
|
+
create trigger trg_project_members_updated_at before update on selva.project_members
|
|
226
|
+
for each row execute function selva.set_updated_at();
|
|
182
227
|
|
|
183
228
|
|
|
184
229
|
-- ============================================================================
|
|
@@ -187,83 +232,83 @@ create trigger trg_project_members_updated_at before update on public.project_me
|
|
|
187
232
|
-- recursion). All filter deleted_at IS NULL.
|
|
188
233
|
-- ============================================================================
|
|
189
234
|
|
|
190
|
-
create or replace function
|
|
235
|
+
create or replace function selva.is_instance_admin()
|
|
191
236
|
returns boolean
|
|
192
237
|
language sql stable security definer set search_path = public
|
|
193
238
|
as $$
|
|
194
239
|
select exists (
|
|
195
|
-
select 1 from
|
|
240
|
+
select 1 from selva.user_profiles
|
|
196
241
|
where user_id = auth.uid() and 'instance_admin' = any(platform_permissions)
|
|
197
242
|
);
|
|
198
243
|
$$;
|
|
199
244
|
|
|
200
|
-
create or replace function
|
|
245
|
+
create or replace function selva.is_org_member(o uuid)
|
|
201
246
|
returns boolean
|
|
202
247
|
language sql stable security definer set search_path = public
|
|
203
248
|
as $$
|
|
204
249
|
select exists (
|
|
205
|
-
select 1 from
|
|
250
|
+
select 1 from selva.org_members
|
|
206
251
|
where org_id = o and user_id = auth.uid() and deleted_at is null
|
|
207
252
|
);
|
|
208
253
|
$$;
|
|
209
254
|
|
|
210
|
-
create or replace function
|
|
255
|
+
create or replace function selva.is_org_admin(o uuid)
|
|
211
256
|
returns boolean
|
|
212
257
|
language sql stable security definer set search_path = public
|
|
213
258
|
as $$
|
|
214
259
|
select exists (
|
|
215
|
-
select 1 from
|
|
260
|
+
select 1 from selva.org_members
|
|
216
261
|
where org_id = o and user_id = auth.uid() and deleted_at is null
|
|
217
262
|
and role in ('owner', 'admin')
|
|
218
263
|
);
|
|
219
264
|
$$;
|
|
220
265
|
|
|
221
|
-
create or replace function
|
|
266
|
+
create or replace function selva.is_org_owner(o uuid)
|
|
222
267
|
returns boolean
|
|
223
268
|
language sql stable security definer set search_path = public
|
|
224
269
|
as $$
|
|
225
270
|
select exists (
|
|
226
|
-
select 1 from
|
|
271
|
+
select 1 from selva.org_members
|
|
227
272
|
where org_id = o and user_id = auth.uid() and deleted_at is null
|
|
228
273
|
and role = 'owner'
|
|
229
274
|
);
|
|
230
275
|
$$;
|
|
231
276
|
|
|
232
|
-
create or replace function
|
|
277
|
+
create or replace function selva.has_org_permission(o uuid, perm text)
|
|
233
278
|
returns boolean
|
|
234
279
|
language sql stable security definer set search_path = public
|
|
235
280
|
as $$
|
|
236
|
-
select
|
|
237
|
-
select 1 from
|
|
281
|
+
select selva.is_instance_admin() or exists (
|
|
282
|
+
select 1 from selva.org_members
|
|
238
283
|
where org_id = o and user_id = auth.uid() and deleted_at is null
|
|
239
284
|
and perm = any(permissions)
|
|
240
285
|
);
|
|
241
286
|
$$;
|
|
242
287
|
|
|
243
|
-
create or replace function
|
|
288
|
+
create or replace function selva.is_project_member(p uuid)
|
|
244
289
|
returns boolean
|
|
245
290
|
language sql stable security definer set search_path = public
|
|
246
291
|
as $$
|
|
247
292
|
select exists (
|
|
248
|
-
select 1 from
|
|
293
|
+
select 1 from selva.project_members
|
|
249
294
|
where project_id = p and user_id = auth.uid() and deleted_at is null
|
|
250
295
|
);
|
|
251
296
|
$$;
|
|
252
297
|
|
|
253
298
|
-- Spec §5 canView: private → member, org → org member, public → everyone.
|
|
254
|
-
create or replace function
|
|
299
|
+
create or replace function selva.visible_project(p uuid)
|
|
255
300
|
returns boolean
|
|
256
301
|
language sql stable security definer set search_path = public
|
|
257
302
|
as $$
|
|
258
|
-
select
|
|
303
|
+
select selva.is_instance_admin() or exists (
|
|
259
304
|
select 1
|
|
260
|
-
from
|
|
305
|
+
from selva.projects
|
|
261
306
|
where id = p
|
|
262
307
|
and deleted_at is null
|
|
263
308
|
and (
|
|
264
309
|
visibility = 'public'
|
|
265
|
-
or (visibility = 'org' and
|
|
266
|
-
or
|
|
310
|
+
or (visibility = 'org' and selva.is_org_member(org_id))
|
|
311
|
+
or selva.is_project_member(id)
|
|
267
312
|
)
|
|
268
313
|
);
|
|
269
314
|
$$;
|
|
@@ -273,7 +318,7 @@ $$;
|
|
|
273
318
|
-- 6. Org / project RLS policies
|
|
274
319
|
-- ============================================================================
|
|
275
320
|
|
|
276
|
-
alter table
|
|
321
|
+
alter table selva.orgs enable row level security;
|
|
277
322
|
|
|
278
323
|
-- RLS gates "may this principal touch this row?", not "is the row in a state
|
|
279
324
|
-- worth showing?". Soft-delete filtering (`deleted_at is null`) belongs at the
|
|
@@ -284,118 +329,118 @@ alter table public.orgs enable row level security;
|
|
|
284
329
|
-- out the just-tombstoned row and surfaces as "new row violates RLS". Same
|
|
285
330
|
-- trap for `revoked_at` on share_links below.
|
|
286
331
|
create policy "orgs: members and instance admins can read"
|
|
287
|
-
on
|
|
332
|
+
on selva.orgs for select
|
|
288
333
|
to authenticated
|
|
289
|
-
using (
|
|
334
|
+
using (selva.is_instance_admin() or selva.is_org_member(id));
|
|
290
335
|
|
|
291
336
|
create policy "orgs: authenticated can create their own"
|
|
292
|
-
on
|
|
337
|
+
on selva.orgs for insert
|
|
293
338
|
to authenticated
|
|
294
|
-
with check (
|
|
339
|
+
with check (selva.is_instance_admin() or owner_id = auth.uid());
|
|
295
340
|
|
|
296
341
|
create policy "orgs: owners and instance admins can update"
|
|
297
|
-
on
|
|
342
|
+
on selva.orgs for update
|
|
298
343
|
to authenticated
|
|
299
|
-
using (
|
|
300
|
-
with check (
|
|
344
|
+
using (selva.is_instance_admin() or selva.is_org_owner(id))
|
|
345
|
+
with check (selva.is_instance_admin() or selva.is_org_owner(id));
|
|
301
346
|
|
|
302
347
|
create policy "orgs: owners and instance admins can delete"
|
|
303
|
-
on
|
|
348
|
+
on selva.orgs for delete
|
|
304
349
|
to authenticated
|
|
305
|
-
using (
|
|
350
|
+
using (selva.is_instance_admin() or selva.is_org_owner(id));
|
|
306
351
|
|
|
307
|
-
alter table
|
|
352
|
+
alter table selva.org_members enable row level security;
|
|
308
353
|
|
|
309
354
|
create policy "org_members: org members can read roster"
|
|
310
|
-
on
|
|
355
|
+
on selva.org_members for select
|
|
311
356
|
to authenticated
|
|
312
|
-
using (
|
|
357
|
+
using (selva.is_instance_admin() or selva.is_org_member(org_id));
|
|
313
358
|
|
|
314
359
|
create policy "org_members: admins can insert"
|
|
315
|
-
on
|
|
360
|
+
on selva.org_members for insert
|
|
316
361
|
to authenticated
|
|
317
|
-
with check (
|
|
362
|
+
with check (selva.is_instance_admin() or selva.is_org_admin(org_id));
|
|
318
363
|
|
|
319
364
|
create policy "org_members: admins can update"
|
|
320
|
-
on
|
|
365
|
+
on selva.org_members for update
|
|
321
366
|
to authenticated
|
|
322
|
-
using (
|
|
323
|
-
with check (
|
|
367
|
+
using (selva.is_instance_admin() or selva.is_org_admin(org_id))
|
|
368
|
+
with check (selva.is_instance_admin() or selva.is_org_admin(org_id));
|
|
324
369
|
|
|
325
370
|
create policy "org_members: admins can delete"
|
|
326
|
-
on
|
|
371
|
+
on selva.org_members for delete
|
|
327
372
|
to authenticated
|
|
328
|
-
using (
|
|
373
|
+
using (selva.is_instance_admin() or selva.is_org_admin(org_id));
|
|
329
374
|
|
|
330
|
-
alter table
|
|
375
|
+
alter table selva.projects enable row level security;
|
|
331
376
|
|
|
332
377
|
create policy "projects: visible to members"
|
|
333
|
-
on
|
|
378
|
+
on selva.projects for select
|
|
334
379
|
to authenticated
|
|
335
|
-
using (
|
|
380
|
+
using (selva.visible_project(id));
|
|
336
381
|
|
|
337
382
|
create policy "projects: manage_projects can create"
|
|
338
|
-
on
|
|
383
|
+
on selva.projects for insert
|
|
339
384
|
to authenticated
|
|
340
|
-
with check (
|
|
385
|
+
with check (selva.has_org_permission(org_id, 'manage_projects'));
|
|
341
386
|
|
|
342
387
|
-- Project settings (name, slug, description, visibility, flags) are owner-only.
|
|
343
388
|
create policy "projects: owners can update"
|
|
344
|
-
on
|
|
389
|
+
on selva.projects for update
|
|
345
390
|
to authenticated
|
|
346
|
-
using (
|
|
347
|
-
with check (
|
|
391
|
+
using (selva.is_instance_admin() or owner_id = auth.uid())
|
|
392
|
+
with check (selva.is_instance_admin() or owner_id = auth.uid());
|
|
348
393
|
|
|
349
394
|
create policy "projects: owners can delete"
|
|
350
|
-
on
|
|
395
|
+
on selva.projects for delete
|
|
351
396
|
to authenticated
|
|
352
|
-
using (
|
|
397
|
+
using (selva.is_instance_admin() or owner_id = auth.uid());
|
|
353
398
|
|
|
354
|
-
alter table
|
|
399
|
+
alter table selva.project_members enable row level security;
|
|
355
400
|
|
|
356
401
|
create policy "project_members: visible to project members"
|
|
357
|
-
on
|
|
402
|
+
on selva.project_members for select
|
|
358
403
|
to authenticated
|
|
359
|
-
using (
|
|
404
|
+
using (selva.is_instance_admin() or selva.visible_project(project_id));
|
|
360
405
|
|
|
361
406
|
-- canManage (§5) collapses to owner-only; org admins no longer silently
|
|
362
407
|
-- manage project members.
|
|
363
408
|
create policy "project_members: managers can insert"
|
|
364
|
-
on
|
|
409
|
+
on selva.project_members for insert
|
|
365
410
|
to authenticated
|
|
366
411
|
with check (
|
|
367
|
-
|
|
412
|
+
selva.is_instance_admin()
|
|
368
413
|
or exists (
|
|
369
|
-
select 1 from
|
|
414
|
+
select 1 from selva.projects p
|
|
370
415
|
where p.id = project_id and p.deleted_at is null and p.owner_id = auth.uid()
|
|
371
416
|
)
|
|
372
417
|
);
|
|
373
418
|
|
|
374
419
|
create policy "project_members: managers can update"
|
|
375
|
-
on
|
|
420
|
+
on selva.project_members for update
|
|
376
421
|
to authenticated
|
|
377
422
|
using (
|
|
378
|
-
|
|
423
|
+
selva.is_instance_admin()
|
|
379
424
|
or exists (
|
|
380
|
-
select 1 from
|
|
425
|
+
select 1 from selva.projects p
|
|
381
426
|
where p.id = project_id and p.deleted_at is null and p.owner_id = auth.uid()
|
|
382
427
|
)
|
|
383
428
|
)
|
|
384
429
|
with check (
|
|
385
|
-
|
|
430
|
+
selva.is_instance_admin()
|
|
386
431
|
or exists (
|
|
387
|
-
select 1 from
|
|
432
|
+
select 1 from selva.projects p
|
|
388
433
|
where p.id = project_id and p.deleted_at is null and p.owner_id = auth.uid()
|
|
389
434
|
)
|
|
390
435
|
);
|
|
391
436
|
|
|
392
437
|
create policy "project_members: managers can delete"
|
|
393
|
-
on
|
|
438
|
+
on selva.project_members for delete
|
|
394
439
|
to authenticated
|
|
395
440
|
using (
|
|
396
|
-
|
|
441
|
+
selva.is_instance_admin()
|
|
397
442
|
or exists (
|
|
398
|
-
select 1 from
|
|
443
|
+
select 1 from selva.projects p
|
|
399
444
|
where p.id = project_id and p.deleted_at is null and p.owner_id = auth.uid()
|
|
400
445
|
)
|
|
401
446
|
);
|
|
@@ -413,9 +458,9 @@ using (
|
|
|
413
458
|
-- not on definitions — different versions can carry different uploaded shapes.
|
|
414
459
|
-- ============================================================================
|
|
415
460
|
|
|
416
|
-
create table if not exists
|
|
461
|
+
create table if not exists selva.definitions (
|
|
417
462
|
guid uuid primary key,
|
|
418
|
-
project_id uuid not null references
|
|
463
|
+
project_id uuid not null references selva.projects(id) on delete cascade,
|
|
419
464
|
owner_id uuid not null references auth.users(id) on delete cascade,
|
|
420
465
|
created_by uuid references auth.users(id) on delete set null,
|
|
421
466
|
updated_by uuid references auth.users(id) on delete set null,
|
|
@@ -436,19 +481,19 @@ create table if not exists public.definitions (
|
|
|
436
481
|
);
|
|
437
482
|
|
|
438
483
|
create index if not exists idx_definitions_project
|
|
439
|
-
on
|
|
484
|
+
on selva.definitions(project_id) where deleted_at is null;
|
|
440
485
|
create index if not exists idx_definitions_status
|
|
441
|
-
on
|
|
486
|
+
on selva.definitions(status) where deleted_at is null;
|
|
442
487
|
create index if not exists idx_definitions_pending_updated
|
|
443
|
-
on
|
|
488
|
+
on selva.definitions(updated_at)
|
|
444
489
|
where status = 'pending' and deleted_at is null;
|
|
445
490
|
|
|
446
|
-
create trigger trg_definitions_updated_at before update on
|
|
447
|
-
for each row execute function
|
|
491
|
+
create trigger trg_definitions_updated_at before update on selva.definitions
|
|
492
|
+
for each row execute function selva.set_updated_at();
|
|
448
493
|
|
|
449
|
-
create table if not exists
|
|
494
|
+
create table if not exists selva.definition_versions (
|
|
450
495
|
id uuid primary key,
|
|
451
|
-
definition_guid uuid not null references
|
|
496
|
+
definition_guid uuid not null references selva.definitions(guid) on delete cascade,
|
|
452
497
|
version_number integer not null check (version_number >= 1),
|
|
453
498
|
file_ext text not null check (file_ext in ('gh', 'ghx')),
|
|
454
499
|
file_key text not null,
|
|
@@ -459,47 +504,47 @@ create table if not exists public.definition_versions (
|
|
|
459
504
|
);
|
|
460
505
|
|
|
461
506
|
create index if not exists idx_definition_versions_def
|
|
462
|
-
on
|
|
507
|
+
on selva.definition_versions(definition_guid, version_number desc);
|
|
463
508
|
|
|
464
509
|
-- Spec §6 deletion protection: cannot delete a version while it's serving
|
|
465
510
|
-- either channel. Postgres raises 23503; the store maps that to 409.
|
|
466
|
-
alter table
|
|
511
|
+
alter table selva.definitions
|
|
467
512
|
add constraint fk_definitions_live_version
|
|
468
|
-
foreign key (live_version_id) references
|
|
513
|
+
foreign key (live_version_id) references selva.definition_versions(id)
|
|
469
514
|
on delete restrict;
|
|
470
515
|
|
|
471
|
-
alter table
|
|
516
|
+
alter table selva.definitions
|
|
472
517
|
add constraint fk_definitions_draft_version
|
|
473
|
-
foreign key (draft_version_id) references
|
|
518
|
+
foreign key (draft_version_id) references selva.definition_versions(id)
|
|
474
519
|
on delete restrict;
|
|
475
520
|
|
|
476
521
|
-- Atomic run-count increment. Called after each successful solve. No-op on
|
|
477
522
|
-- missing or soft-deleted rows.
|
|
478
|
-
create or replace function
|
|
523
|
+
create or replace function selva.increment_run_count(g uuid)
|
|
479
524
|
returns void
|
|
480
525
|
language sql security definer set search_path = public
|
|
481
526
|
as $$
|
|
482
|
-
update
|
|
527
|
+
update selva.definitions
|
|
483
528
|
set run_count = run_count + 1
|
|
484
529
|
where guid = g and deleted_at is null;
|
|
485
530
|
$$;
|
|
486
|
-
grant execute on function
|
|
531
|
+
grant execute on function selva.increment_run_count(uuid) to authenticated, service_role;
|
|
487
532
|
|
|
488
|
-
alter table
|
|
533
|
+
alter table selva.definitions enable row level security;
|
|
489
534
|
|
|
490
535
|
create policy "definitions: visible via project"
|
|
491
|
-
on
|
|
536
|
+
on selva.definitions for select
|
|
492
537
|
to authenticated
|
|
493
|
-
using (
|
|
538
|
+
using (selva.visible_project(project_id));
|
|
494
539
|
|
|
495
540
|
create policy "definitions: editors can insert"
|
|
496
|
-
on
|
|
541
|
+
on selva.definitions for insert
|
|
497
542
|
to authenticated
|
|
498
543
|
with check (
|
|
499
|
-
|
|
544
|
+
selva.is_instance_admin()
|
|
500
545
|
-- Container mode: project editor/owner required.
|
|
501
546
|
or exists (
|
|
502
|
-
select 1 from
|
|
547
|
+
select 1 from selva.project_members m
|
|
503
548
|
where m.project_id = definitions.project_id
|
|
504
549
|
and m.user_id = auth.uid()
|
|
505
550
|
and m.deleted_at is null
|
|
@@ -508,7 +553,7 @@ with check (
|
|
|
508
553
|
-- Commons mode: any authenticated user may create on a public + commons
|
|
509
554
|
-- project; they become its owner_id.
|
|
510
555
|
or exists (
|
|
511
|
-
select 1 from
|
|
556
|
+
select 1 from selva.projects p
|
|
512
557
|
where p.id = definitions.project_id
|
|
513
558
|
and p.deleted_at is null
|
|
514
559
|
and p.visibility = 'public'
|
|
@@ -518,19 +563,19 @@ with check (
|
|
|
518
563
|
);
|
|
519
564
|
|
|
520
565
|
create policy "definitions: editors can update"
|
|
521
|
-
on
|
|
566
|
+
on selva.definitions for update
|
|
522
567
|
to authenticated
|
|
523
568
|
using (
|
|
524
|
-
|
|
569
|
+
selva.is_instance_admin()
|
|
525
570
|
or exists (
|
|
526
|
-
select 1 from
|
|
571
|
+
select 1 from selva.project_members m
|
|
527
572
|
where m.project_id = definitions.project_id
|
|
528
573
|
and m.user_id = auth.uid()
|
|
529
574
|
and m.deleted_at is null
|
|
530
575
|
and m.role in ('owner', 'editor')
|
|
531
576
|
)
|
|
532
577
|
or exists (
|
|
533
|
-
select 1 from
|
|
578
|
+
select 1 from selva.projects p
|
|
534
579
|
where p.id = definitions.project_id
|
|
535
580
|
and p.deleted_at is null
|
|
536
581
|
and p.auto_join_on_upload = true
|
|
@@ -538,16 +583,16 @@ using (
|
|
|
538
583
|
)
|
|
539
584
|
)
|
|
540
585
|
with check (
|
|
541
|
-
|
|
586
|
+
selva.is_instance_admin()
|
|
542
587
|
or exists (
|
|
543
|
-
select 1 from
|
|
588
|
+
select 1 from selva.project_members m
|
|
544
589
|
where m.project_id = definitions.project_id
|
|
545
590
|
and m.user_id = auth.uid()
|
|
546
591
|
and m.deleted_at is null
|
|
547
592
|
and m.role in ('owner', 'editor')
|
|
548
593
|
)
|
|
549
594
|
or exists (
|
|
550
|
-
select 1 from
|
|
595
|
+
select 1 from selva.projects p
|
|
551
596
|
where p.id = definitions.project_id
|
|
552
597
|
and p.deleted_at is null
|
|
553
598
|
and p.auto_join_on_upload = true
|
|
@@ -556,19 +601,19 @@ with check (
|
|
|
556
601
|
);
|
|
557
602
|
|
|
558
603
|
create policy "definitions: editors can delete"
|
|
559
|
-
on
|
|
604
|
+
on selva.definitions for delete
|
|
560
605
|
to authenticated
|
|
561
606
|
using (
|
|
562
|
-
|
|
607
|
+
selva.is_instance_admin()
|
|
563
608
|
or exists (
|
|
564
|
-
select 1 from
|
|
609
|
+
select 1 from selva.project_members m
|
|
565
610
|
where m.project_id = definitions.project_id
|
|
566
611
|
and m.user_id = auth.uid()
|
|
567
612
|
and m.deleted_at is null
|
|
568
613
|
and m.role in ('owner', 'editor')
|
|
569
614
|
)
|
|
570
615
|
or exists (
|
|
571
|
-
select 1 from
|
|
616
|
+
select 1 from selva.projects p
|
|
572
617
|
where p.id = definitions.project_id
|
|
573
618
|
and p.deleted_at is null
|
|
574
619
|
and p.auto_join_on_upload = true
|
|
@@ -576,31 +621,31 @@ using (
|
|
|
576
621
|
)
|
|
577
622
|
);
|
|
578
623
|
|
|
579
|
-
alter table
|
|
624
|
+
alter table selva.definition_versions enable row level security;
|
|
580
625
|
|
|
581
626
|
-- Versions inherit visibility from the parent definition (which inherits
|
|
582
627
|
-- from the parent project). Versions are immutable — no UPDATE policy.
|
|
583
628
|
create policy "definition_versions: visible via parent"
|
|
584
|
-
on
|
|
629
|
+
on selva.definition_versions for select
|
|
585
630
|
to authenticated
|
|
586
631
|
using (
|
|
587
632
|
exists (
|
|
588
|
-
select 1 from
|
|
633
|
+
select 1 from selva.definitions d
|
|
589
634
|
where d.guid = definition_versions.definition_guid
|
|
590
635
|
and d.deleted_at is null
|
|
591
|
-
and
|
|
636
|
+
and selva.visible_project(d.project_id)
|
|
592
637
|
)
|
|
593
638
|
);
|
|
594
639
|
|
|
595
640
|
create policy "definition_versions: editors can insert"
|
|
596
|
-
on
|
|
641
|
+
on selva.definition_versions for insert
|
|
597
642
|
to authenticated
|
|
598
643
|
with check (
|
|
599
|
-
|
|
644
|
+
selva.is_instance_admin()
|
|
600
645
|
or exists (
|
|
601
646
|
select 1
|
|
602
|
-
from
|
|
603
|
-
join
|
|
647
|
+
from selva.definitions d
|
|
648
|
+
join selva.project_members m on m.project_id = d.project_id
|
|
604
649
|
where d.guid = definition_guid
|
|
605
650
|
and d.deleted_at is null
|
|
606
651
|
and m.user_id = auth.uid()
|
|
@@ -609,8 +654,8 @@ with check (
|
|
|
609
654
|
)
|
|
610
655
|
or exists (
|
|
611
656
|
select 1
|
|
612
|
-
from
|
|
613
|
-
join
|
|
657
|
+
from selva.definitions d
|
|
658
|
+
join selva.projects p on p.id = d.project_id
|
|
614
659
|
where d.guid = definition_guid
|
|
615
660
|
and d.deleted_at is null
|
|
616
661
|
and p.deleted_at is null
|
|
@@ -626,14 +671,14 @@ with check (
|
|
|
626
671
|
-- `definitions.live_version_id` / `draft_version_id` never gets a chance to
|
|
627
672
|
-- raise the 23503 the store maps to a 409. Same authority as INSERT.
|
|
628
673
|
create policy "definition_versions: editors can delete"
|
|
629
|
-
on
|
|
674
|
+
on selva.definition_versions for delete
|
|
630
675
|
to authenticated
|
|
631
676
|
using (
|
|
632
|
-
|
|
677
|
+
selva.is_instance_admin()
|
|
633
678
|
or exists (
|
|
634
679
|
select 1
|
|
635
|
-
from
|
|
636
|
-
join
|
|
680
|
+
from selva.definitions d
|
|
681
|
+
join selva.project_members m on m.project_id = d.project_id
|
|
637
682
|
where d.guid = definition_versions.definition_guid
|
|
638
683
|
and d.deleted_at is null
|
|
639
684
|
and m.user_id = auth.uid()
|
|
@@ -642,8 +687,8 @@ using (
|
|
|
642
687
|
)
|
|
643
688
|
or exists (
|
|
644
689
|
select 1
|
|
645
|
-
from
|
|
646
|
-
join
|
|
690
|
+
from selva.definitions d
|
|
691
|
+
join selva.projects p on p.id = d.project_id
|
|
647
692
|
where d.guid = definition_versions.definition_guid
|
|
648
693
|
and d.deleted_at is null
|
|
649
694
|
and p.deleted_at is null
|
|
@@ -652,6 +697,41 @@ using (
|
|
|
652
697
|
)
|
|
653
698
|
);
|
|
654
699
|
|
|
700
|
+
-- Without an explicit UPDATE policy, RLS silently affects 0 rows on every
|
|
701
|
+
-- `update definition_versions` — `SupabaseDefinitionStore.setVersionSchema`
|
|
702
|
+
-- (which caches the compute-extracted UI schema, migration 0002) returns
|
|
703
|
+
-- success without persisting anything for a user-scoped caller. Same authority
|
|
704
|
+
-- as INSERT/DELETE: project owner/editor, or instance admin.
|
|
705
|
+
create policy "definition_versions: editors can update"
|
|
706
|
+
on selva.definition_versions for update
|
|
707
|
+
to authenticated
|
|
708
|
+
using (
|
|
709
|
+
selva.is_instance_admin()
|
|
710
|
+
or exists (
|
|
711
|
+
select 1
|
|
712
|
+
from selva.definitions d
|
|
713
|
+
join selva.project_members m on m.project_id = d.project_id
|
|
714
|
+
where d.guid = definition_versions.definition_guid
|
|
715
|
+
and d.deleted_at is null
|
|
716
|
+
and m.user_id = auth.uid()
|
|
717
|
+
and m.deleted_at is null
|
|
718
|
+
and m.role in ('owner', 'editor')
|
|
719
|
+
)
|
|
720
|
+
)
|
|
721
|
+
with check (
|
|
722
|
+
selva.is_instance_admin()
|
|
723
|
+
or exists (
|
|
724
|
+
select 1
|
|
725
|
+
from selva.definitions d
|
|
726
|
+
join selva.project_members m on m.project_id = d.project_id
|
|
727
|
+
where d.guid = definition_versions.definition_guid
|
|
728
|
+
and d.deleted_at is null
|
|
729
|
+
and m.user_id = auth.uid()
|
|
730
|
+
and m.deleted_at is null
|
|
731
|
+
and m.role in ('owner', 'editor')
|
|
732
|
+
)
|
|
733
|
+
);
|
|
734
|
+
|
|
655
735
|
|
|
656
736
|
-- ============================================================================
|
|
657
737
|
-- 8. invites
|
|
@@ -666,11 +746,11 @@ using (
|
|
|
666
746
|
-- token is shown to the admin once and never persisted. A DB-only leak
|
|
667
747
|
-- therefore can't be replayed without the instance's INVITE_TOKEN_SECRET.
|
|
668
748
|
-- Mirrors the share-link design (`share_links.token_hash`).
|
|
669
|
-
create table if not exists
|
|
749
|
+
create table if not exists selva.invites (
|
|
670
750
|
id uuid primary key,
|
|
671
751
|
token_hash text not null unique,
|
|
672
752
|
email text not null,
|
|
673
|
-
org_id uuid not null references
|
|
753
|
+
org_id uuid not null references selva.orgs(id) on delete cascade,
|
|
674
754
|
org_role text not null check (org_role in ('owner', 'admin', 'member')),
|
|
675
755
|
org_permissions text[] not null default '{}',
|
|
676
756
|
invited_by uuid not null references auth.users(id) on delete cascade,
|
|
@@ -680,47 +760,47 @@ create table if not exists public.invites (
|
|
|
680
760
|
accepted_by_user_id uuid references auth.users(id) on delete set null
|
|
681
761
|
);
|
|
682
762
|
|
|
683
|
-
create index if not exists idx_invites_org on
|
|
763
|
+
create index if not exists idx_invites_org on selva.invites(org_id, created_at desc);
|
|
684
764
|
|
|
685
|
-
alter table
|
|
765
|
+
alter table selva.invites enable row level security;
|
|
686
766
|
|
|
687
767
|
create policy "invites: manage_org_members can read org invites"
|
|
688
|
-
on
|
|
768
|
+
on selva.invites for select
|
|
689
769
|
to authenticated
|
|
690
|
-
using (
|
|
770
|
+
using (selva.has_org_permission(org_id, 'manage_org_members'));
|
|
691
771
|
|
|
692
772
|
create policy "invites: manage_org_members can insert"
|
|
693
|
-
on
|
|
773
|
+
on selva.invites for insert
|
|
694
774
|
to authenticated
|
|
695
|
-
with check (
|
|
775
|
+
with check (selva.has_org_permission(org_id, 'manage_org_members'));
|
|
696
776
|
|
|
697
777
|
create policy "invites: manage_org_members can update"
|
|
698
|
-
on
|
|
778
|
+
on selva.invites for update
|
|
699
779
|
to authenticated
|
|
700
|
-
using (
|
|
701
|
-
with check (
|
|
780
|
+
using (selva.has_org_permission(org_id, 'manage_org_members'))
|
|
781
|
+
with check (selva.has_org_permission(org_id, 'manage_org_members'));
|
|
702
782
|
|
|
703
783
|
create policy "invites: manage_org_members can delete"
|
|
704
|
-
on
|
|
784
|
+
on selva.invites for delete
|
|
705
785
|
to authenticated
|
|
706
|
-
using (
|
|
786
|
+
using (selva.has_org_permission(org_id, 'manage_org_members'));
|
|
707
787
|
|
|
708
788
|
-- Hash-gated read via SECURITY DEFINER so the token itself is the capability.
|
|
709
789
|
-- Caller hashes the raw URL token at the route layer and passes the digest;
|
|
710
790
|
-- the function just looks up the row. Returns SETOF so a missing match
|
|
711
791
|
-- becomes an empty array (not a composite of nulls).
|
|
712
|
-
create or replace function
|
|
713
|
-
returns setof
|
|
792
|
+
create or replace function selva.get_invite_by_token_hash(h text)
|
|
793
|
+
returns setof selva.invites
|
|
714
794
|
language sql stable security definer set search_path = public
|
|
715
795
|
as $$
|
|
716
796
|
select *
|
|
717
|
-
from
|
|
797
|
+
from selva.invites
|
|
718
798
|
where token_hash = h
|
|
719
799
|
and accepted_at is null
|
|
720
800
|
and expires_at > now()
|
|
721
801
|
limit 1;
|
|
722
802
|
$$;
|
|
723
|
-
grant execute on function
|
|
803
|
+
grant execute on function selva.get_invite_by_token_hash(text) to anon, authenticated, service_role;
|
|
724
804
|
|
|
725
805
|
|
|
726
806
|
-- ============================================================================
|
|
@@ -759,10 +839,10 @@ grant execute on function public.get_invite_by_token_hash(text) to anon, authent
|
|
|
759
839
|
-- `owner_org_id` is required and visibility is implicit (only members
|
|
760
840
|
-- of `owner_org_id` can see it). Gated by the platform flag
|
|
761
841
|
-- ALLOW_ORG_COMPUTE_OVERRIDE at the TS layer.
|
|
762
|
-
create table if not exists
|
|
842
|
+
create table if not exists selva.compute_servers (
|
|
763
843
|
id uuid primary key,
|
|
764
844
|
scope text not null check (scope in ('platform', 'org')),
|
|
765
|
-
owner_org_id uuid references
|
|
845
|
+
owner_org_id uuid references selva.orgs(id) on delete cascade,
|
|
766
846
|
shared_with_all boolean not null default false,
|
|
767
847
|
label text not null,
|
|
768
848
|
server_url text not null,
|
|
@@ -776,37 +856,37 @@ create table if not exists public.compute_servers (
|
|
|
776
856
|
or (scope = 'org' and owner_org_id is not null and shared_with_all = false)
|
|
777
857
|
)
|
|
778
858
|
);
|
|
779
|
-
create index if not exists idx_compute_servers_owner_org on
|
|
859
|
+
create index if not exists idx_compute_servers_owner_org on selva.compute_servers(owner_org_id);
|
|
780
860
|
|
|
781
861
|
-- Per-org share allowlist for platform servers. Empty rows = dormant
|
|
782
862
|
-- platform server (admin-only) unless it's the global default.
|
|
783
|
-
create table if not exists
|
|
784
|
-
server_id uuid not null references
|
|
785
|
-
org_id uuid not null references
|
|
863
|
+
create table if not exists selva.compute_server_shares (
|
|
864
|
+
server_id uuid not null references selva.compute_servers(id) on delete cascade,
|
|
865
|
+
org_id uuid not null references selva.orgs(id) on delete cascade,
|
|
786
866
|
primary key (server_id, org_id)
|
|
787
867
|
);
|
|
788
|
-
create index if not exists idx_compute_server_shares_org on
|
|
868
|
+
create index if not exists idx_compute_server_shares_org on selva.compute_server_shares(org_id);
|
|
789
869
|
|
|
790
870
|
-- Per-org default override (orgDefaults[orgId]).
|
|
791
|
-
create table if not exists
|
|
792
|
-
org_id uuid primary key references
|
|
793
|
-
default_server_id uuid references
|
|
871
|
+
create table if not exists selva.compute_server_org_defaults (
|
|
872
|
+
org_id uuid primary key references selva.orgs(id) on delete cascade,
|
|
873
|
+
default_server_id uuid references selva.compute_servers(id) on delete set null
|
|
794
874
|
);
|
|
795
875
|
|
|
796
876
|
-- Single-row sentinel for the global default.
|
|
797
|
-
create table if not exists
|
|
877
|
+
create table if not exists selva.compute_server_platform_default (
|
|
798
878
|
singleton boolean primary key default true,
|
|
799
|
-
default_server_id uuid references
|
|
879
|
+
default_server_id uuid references selva.compute_servers(id) on delete set null,
|
|
800
880
|
check (singleton)
|
|
801
881
|
);
|
|
802
|
-
insert into
|
|
882
|
+
insert into selva.compute_server_platform_default (singleton, default_server_id)
|
|
803
883
|
values (true, null)
|
|
804
884
|
on conflict (singleton) do nothing;
|
|
805
885
|
|
|
806
|
-
alter table
|
|
807
|
-
alter table
|
|
808
|
-
alter table
|
|
809
|
-
alter table
|
|
886
|
+
alter table selva.compute_servers enable row level security;
|
|
887
|
+
alter table selva.compute_server_shares enable row level security;
|
|
888
|
+
alter table selva.compute_server_org_defaults enable row level security;
|
|
889
|
+
alter table selva.compute_server_platform_default enable row level security;
|
|
810
890
|
|
|
811
891
|
-- Read: instance_admin sees everything. Otherwise visibility follows §3:
|
|
812
892
|
-- * platform server with shared_with_all = true → all authenticated users
|
|
@@ -815,70 +895,70 @@ alter table public.compute_server_platform_default enable row level security;
|
|
|
815
895
|
-- * platform server that is the current global default → all (floor)
|
|
816
896
|
-- * org server → only members of owner_org_id
|
|
817
897
|
create policy "compute_servers: visibility-scoped read"
|
|
818
|
-
on
|
|
898
|
+
on selva.compute_servers for select
|
|
819
899
|
to authenticated
|
|
820
900
|
using (
|
|
821
|
-
|
|
901
|
+
selva.is_instance_admin()
|
|
822
902
|
or (scope = 'platform' and shared_with_all)
|
|
823
903
|
or (scope = 'platform' and id = (
|
|
824
|
-
select default_server_id from
|
|
904
|
+
select default_server_id from selva.compute_server_platform_default where singleton
|
|
825
905
|
))
|
|
826
906
|
or (scope = 'platform' and exists (
|
|
827
|
-
select 1 from
|
|
828
|
-
where s.server_id = compute_servers.id and
|
|
907
|
+
select 1 from selva.compute_server_shares s
|
|
908
|
+
where s.server_id = compute_servers.id and selva.is_org_member(s.org_id)
|
|
829
909
|
))
|
|
830
|
-
or (scope = 'org' and
|
|
910
|
+
or (scope = 'org' and selva.is_org_member(owner_org_id))
|
|
831
911
|
);
|
|
832
912
|
|
|
833
913
|
-- Writes:
|
|
834
914
|
-- * scope = 'platform' → instance_admin only.
|
|
835
915
|
-- * scope = 'org' → manage_org_compute on owner_org_id.
|
|
836
916
|
create policy "compute_servers: scoped write"
|
|
837
|
-
on
|
|
917
|
+
on selva.compute_servers for all
|
|
838
918
|
to authenticated
|
|
839
919
|
using (
|
|
840
|
-
|
|
920
|
+
selva.is_instance_admin()
|
|
841
921
|
or (scope = 'org' and owner_org_id is not null
|
|
842
|
-
and
|
|
922
|
+
and selva.has_org_permission(owner_org_id, 'manage_org_compute'))
|
|
843
923
|
)
|
|
844
924
|
with check (
|
|
845
|
-
|
|
925
|
+
selva.is_instance_admin()
|
|
846
926
|
or (scope = 'org' and owner_org_id is not null
|
|
847
|
-
and
|
|
927
|
+
and selva.has_org_permission(owner_org_id, 'manage_org_compute'))
|
|
848
928
|
);
|
|
849
929
|
|
|
850
930
|
create policy "compute_server_shares: read"
|
|
851
|
-
on
|
|
931
|
+
on selva.compute_server_shares for select
|
|
852
932
|
to authenticated
|
|
853
|
-
using (
|
|
933
|
+
using (selva.is_instance_admin() or selva.is_org_member(org_id));
|
|
854
934
|
|
|
855
935
|
create policy "compute_server_shares: admin write"
|
|
856
|
-
on
|
|
936
|
+
on selva.compute_server_shares for all
|
|
857
937
|
to authenticated
|
|
858
|
-
using (
|
|
859
|
-
with check (
|
|
938
|
+
using (selva.is_instance_admin())
|
|
939
|
+
with check (selva.is_instance_admin());
|
|
860
940
|
|
|
861
941
|
create policy "compute_server_org_defaults: members can read"
|
|
862
|
-
on
|
|
942
|
+
on selva.compute_server_org_defaults for select
|
|
863
943
|
to authenticated
|
|
864
|
-
using (
|
|
944
|
+
using (selva.is_instance_admin() or selva.is_org_member(org_id));
|
|
865
945
|
|
|
866
946
|
create policy "compute_server_org_defaults: manage_org_compute can write"
|
|
867
|
-
on
|
|
947
|
+
on selva.compute_server_org_defaults for all
|
|
868
948
|
to authenticated
|
|
869
|
-
using (
|
|
870
|
-
with check (
|
|
949
|
+
using (selva.is_instance_admin() or selva.has_org_permission(org_id, 'manage_org_compute'))
|
|
950
|
+
with check (selva.is_instance_admin() or selva.has_org_permission(org_id, 'manage_org_compute'));
|
|
871
951
|
|
|
872
952
|
create policy "compute_server_platform_default: read all"
|
|
873
|
-
on
|
|
953
|
+
on selva.compute_server_platform_default for select
|
|
874
954
|
to authenticated
|
|
875
955
|
using (true);
|
|
876
956
|
|
|
877
957
|
create policy "compute_server_platform_default: admin write"
|
|
878
|
-
on
|
|
958
|
+
on selva.compute_server_platform_default for all
|
|
879
959
|
to authenticated
|
|
880
|
-
using (
|
|
881
|
-
with check (
|
|
960
|
+
using (selva.is_instance_admin())
|
|
961
|
+
with check (selva.is_instance_admin());
|
|
882
962
|
|
|
883
963
|
|
|
884
964
|
-- ============================================================================
|
|
@@ -889,9 +969,9 @@ with check (public.is_instance_admin());
|
|
|
889
969
|
-- raw token is shown to the minter once at creation.
|
|
890
970
|
-- ============================================================================
|
|
891
971
|
|
|
892
|
-
create table if not exists
|
|
972
|
+
create table if not exists selva.share_links (
|
|
893
973
|
id uuid primary key,
|
|
894
|
-
definition_guid uuid not null references
|
|
974
|
+
definition_guid uuid not null references selva.definitions(guid) on delete cascade,
|
|
895
975
|
channel text not null check (channel in ('live', 'draft')),
|
|
896
976
|
token_hash text not null unique,
|
|
897
977
|
name text,
|
|
@@ -905,17 +985,17 @@ create table if not exists public.share_links (
|
|
|
905
985
|
);
|
|
906
986
|
|
|
907
987
|
create index if not exists idx_share_links_definition
|
|
908
|
-
on
|
|
988
|
+
on selva.share_links(definition_guid, created_at desc)
|
|
909
989
|
where revoked_at is null;
|
|
910
990
|
|
|
911
991
|
-- Atomic check-and-increment. Returns the new solve_count on success, NULL
|
|
912
992
|
-- when the cap was reached (no row updated) or the link is revoked /
|
|
913
993
|
-- expired / missing. Single-statement = no race even under concurrent solves.
|
|
914
|
-
create or replace function
|
|
994
|
+
create or replace function selva.try_increment_share_link_solve_count(link_id uuid)
|
|
915
995
|
returns integer
|
|
916
996
|
language sql security definer set search_path = public
|
|
917
997
|
as $$
|
|
918
|
-
update
|
|
998
|
+
update selva.share_links
|
|
919
999
|
set solve_count = solve_count + 1
|
|
920
1000
|
where id = link_id
|
|
921
1001
|
and revoked_at is null
|
|
@@ -923,10 +1003,10 @@ as $$
|
|
|
923
1003
|
and (max_solves is null or solve_count < max_solves)
|
|
924
1004
|
returning solve_count;
|
|
925
1005
|
$$;
|
|
926
|
-
grant execute on function
|
|
1006
|
+
grant execute on function selva.try_increment_share_link_solve_count(uuid)
|
|
927
1007
|
to authenticated, anon, service_role;
|
|
928
1008
|
|
|
929
|
-
alter table
|
|
1009
|
+
alter table selva.share_links enable row level security;
|
|
930
1010
|
|
|
931
1011
|
-- Read/insert/revoke: same authority that can edit the parent definition
|
|
932
1012
|
-- (project owner/editor or commons-mode definition owner). Token resolution
|
|
@@ -934,14 +1014,14 @@ alter table public.share_links enable row level security;
|
|
|
934
1014
|
-- IS the credential there.
|
|
935
1015
|
|
|
936
1016
|
create policy "share_links: editors can read"
|
|
937
|
-
on
|
|
1017
|
+
on selva.share_links for select
|
|
938
1018
|
to authenticated
|
|
939
1019
|
using (
|
|
940
|
-
|
|
1020
|
+
selva.is_instance_admin()
|
|
941
1021
|
or exists (
|
|
942
1022
|
select 1
|
|
943
|
-
from
|
|
944
|
-
join
|
|
1023
|
+
from selva.definitions d
|
|
1024
|
+
join selva.project_members m on m.project_id = d.project_id
|
|
945
1025
|
where d.guid = share_links.definition_guid
|
|
946
1026
|
and d.deleted_at is null
|
|
947
1027
|
and m.user_id = auth.uid()
|
|
@@ -950,8 +1030,8 @@ using (
|
|
|
950
1030
|
)
|
|
951
1031
|
or exists (
|
|
952
1032
|
select 1
|
|
953
|
-
from
|
|
954
|
-
join
|
|
1033
|
+
from selva.definitions d
|
|
1034
|
+
join selva.projects p on p.id = d.project_id
|
|
955
1035
|
where d.guid = share_links.definition_guid
|
|
956
1036
|
and d.deleted_at is null
|
|
957
1037
|
and p.deleted_at is null
|
|
@@ -961,14 +1041,14 @@ using (
|
|
|
961
1041
|
);
|
|
962
1042
|
|
|
963
1043
|
create policy "share_links: editors can insert"
|
|
964
|
-
on
|
|
1044
|
+
on selva.share_links for insert
|
|
965
1045
|
to authenticated
|
|
966
1046
|
with check (
|
|
967
|
-
|
|
1047
|
+
selva.is_instance_admin()
|
|
968
1048
|
or exists (
|
|
969
1049
|
select 1
|
|
970
|
-
from
|
|
971
|
-
join
|
|
1050
|
+
from selva.definitions d
|
|
1051
|
+
join selva.project_members m on m.project_id = d.project_id
|
|
972
1052
|
where d.guid = share_links.definition_guid
|
|
973
1053
|
and d.deleted_at is null
|
|
974
1054
|
and m.user_id = auth.uid()
|
|
@@ -977,8 +1057,8 @@ with check (
|
|
|
977
1057
|
)
|
|
978
1058
|
or exists (
|
|
979
1059
|
select 1
|
|
980
|
-
from
|
|
981
|
-
join
|
|
1060
|
+
from selva.definitions d
|
|
1061
|
+
join selva.projects p on p.id = d.project_id
|
|
982
1062
|
where d.guid = share_links.definition_guid
|
|
983
1063
|
and d.deleted_at is null
|
|
984
1064
|
and p.deleted_at is null
|
|
@@ -989,14 +1069,14 @@ with check (
|
|
|
989
1069
|
|
|
990
1070
|
-- UPDATE policy covers `revoke` (set revoked_at). Same authority as insert.
|
|
991
1071
|
create policy "share_links: editors can revoke"
|
|
992
|
-
on
|
|
1072
|
+
on selva.share_links for update
|
|
993
1073
|
to authenticated
|
|
994
1074
|
using (
|
|
995
|
-
|
|
1075
|
+
selva.is_instance_admin()
|
|
996
1076
|
or exists (
|
|
997
1077
|
select 1
|
|
998
|
-
from
|
|
999
|
-
join
|
|
1078
|
+
from selva.definitions d
|
|
1079
|
+
join selva.project_members m on m.project_id = d.project_id
|
|
1000
1080
|
where d.guid = share_links.definition_guid
|
|
1001
1081
|
and d.deleted_at is null
|
|
1002
1082
|
and m.user_id = auth.uid()
|
|
@@ -1005,11 +1085,11 @@ using (
|
|
|
1005
1085
|
)
|
|
1006
1086
|
)
|
|
1007
1087
|
with check (
|
|
1008
|
-
|
|
1088
|
+
selva.is_instance_admin()
|
|
1009
1089
|
or exists (
|
|
1010
1090
|
select 1
|
|
1011
|
-
from
|
|
1012
|
-
join
|
|
1091
|
+
from selva.definitions d
|
|
1092
|
+
join selva.project_members m on m.project_id = d.project_id
|
|
1013
1093
|
where d.guid = share_links.definition_guid
|
|
1014
1094
|
and d.deleted_at is null
|
|
1015
1095
|
and m.user_id = auth.uid()
|
|
@@ -1036,6 +1116,7 @@ with check (
|
|
|
1036
1116
|
|
|
1037
1117
|
-- selva-public is served directly from the CDN (cover images, brand assets);
|
|
1038
1118
|
-- anonymous reads are required. Writes are service-role only.
|
|
1119
|
+
drop policy if exists "selva-public: anyone can read" on storage.objects;
|
|
1039
1120
|
create policy "selva-public: anyone can read"
|
|
1040
1121
|
on storage.objects for select
|
|
1041
1122
|
to anon, authenticated
|
|
@@ -1061,7 +1142,7 @@ using (bucket_id = 'selva-public');
|
|
|
1061
1142
|
-- exists today so audit data is captured from the moment the system goes live.
|
|
1062
1143
|
-- ============================================================================
|
|
1063
1144
|
|
|
1064
|
-
create table if not exists
|
|
1145
|
+
create table if not exists selva.audit_events (
|
|
1065
1146
|
id uuid primary key default gen_random_uuid(),
|
|
1066
1147
|
type text not null,
|
|
1067
1148
|
actor_id text not null,
|
|
@@ -1070,15 +1151,15 @@ create table if not exists public.audit_events (
|
|
|
1070
1151
|
);
|
|
1071
1152
|
|
|
1072
1153
|
create index if not exists audit_events_occurred_at_idx
|
|
1073
|
-
on
|
|
1154
|
+
on selva.audit_events (occurred_at desc);
|
|
1074
1155
|
|
|
1075
1156
|
create index if not exists audit_events_type_occurred_at_idx
|
|
1076
|
-
on
|
|
1157
|
+
on selva.audit_events (type, occurred_at desc);
|
|
1077
1158
|
|
|
1078
1159
|
create index if not exists audit_events_actor_occurred_at_idx
|
|
1079
|
-
on
|
|
1160
|
+
on selva.audit_events (actor_id, occurred_at desc);
|
|
1080
1161
|
|
|
1081
1162
|
-- RLS: writes always go through service-role (the sink uses the service
|
|
1082
1163
|
-- client). Authenticated users have NO read access today — the audit-log
|
|
1083
1164
|
-- viewer UI will land later with its own instance_admin-only policy.
|
|
1084
|
-
alter table
|
|
1165
|
+
alter table selva.audit_events enable row level security;
|