Implementa o MVP do Adaptador de Planilhas no Elo Pro.
O módulo usa a identidade Supabase, isola cada workspace e registra storage, TTL, runner e biblioteca XLSX como decisões ainda abertas. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
commit
6e23f1533b
144 files changed
+19060
No files matched your search
@@ -0,0 +1,52 @@
|
||||
-- Compatibilidade para desenvolvimento isolado.
|
||||
-- Em produção estas tabelas pertencem ao Elo Pro e NÃO devem ser recriadas se já existirem.
|
||||
-- Não há coluna de senha, hash, refresh token ou sessão paralela.
|
||||
-- gen_random_uuid() faz parte do núcleo do PostgreSQL 13+.
|
||||
|
||||
do $$ begin
|
||||
create role app_user nologin nosuperuser nobypassrls;
|
||||
exception when duplicate_object then null;
|
||||
end $$;
|
||||
|
||||
create table if not exists public.profiles (
|
||||
id uuid primary key,
|
||||
display_name text not null,
|
||||
is_superadmin boolean not null default false,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table if not exists public.workspaces (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
name text not null,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table if not exists public.workspace_members (
|
||||
workspace_id uuid not null references public.workspaces (id) on delete cascade,
|
||||
user_id uuid not null references public.profiles (id) on delete cascade,
|
||||
role text not null check (role in ('owner', 'admin', 'gestor', 'agent')),
|
||||
created_at timestamptz not null default now(),
|
||||
primary key (workspace_id, user_id)
|
||||
);
|
||||
|
||||
create table if not exists public.workspace_module_flags (
|
||||
workspace_id uuid not null references public.workspaces (id) on delete cascade,
|
||||
module_key text not null,
|
||||
enabled boolean not null default false,
|
||||
updated_at timestamptz not null default now(),
|
||||
primary key (workspace_id, module_key)
|
||||
);
|
||||
|
||||
create table if not exists public.audit_log (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
workspace_id uuid references public.workspaces (id) on delete cascade,
|
||||
actor_id uuid references public.profiles (id),
|
||||
action text not null,
|
||||
resource_type text,
|
||||
resource_id text,
|
||||
result text not null check (result in ('success', 'denied', 'error')),
|
||||
metadata jsonb not null default '{}'::jsonb,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists audit_log_workspace_idx on public.audit_log (workspace_id, created_at desc);
|
||||
@@ -0,0 +1,166 @@
|
||||
-- Schema do Adaptador. Soft delete via status para não tornar a exclusão física irreversível.
|
||||
|
||||
create table if not exists public.spreadsheet_adapter_settings (
|
||||
workspace_id uuid primary key references public.workspaces (id) on delete cascade,
|
||||
allow_non_admin_model_upload boolean not null default false,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create table if not exists public.spreadsheet_models (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
workspace_id uuid not null references public.workspaces (id) on delete cascade,
|
||||
name text not null,
|
||||
original_filename text not null,
|
||||
format text not null check (format in ('xlsx', 'csv')),
|
||||
storage_key text not null,
|
||||
content_sha256 text not null,
|
||||
sheet_name text,
|
||||
header_row integer not null default 1 check (header_row >= 1),
|
||||
columns_schema jsonb not null,
|
||||
created_by uuid not null references public.profiles (id),
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now(),
|
||||
deleted_at timestamptz,
|
||||
status text not null default 'active' check (status in ('active', 'deleted'))
|
||||
);
|
||||
|
||||
create unique index if not exists spreadsheet_models_active_name_idx
|
||||
on public.spreadsheet_models (workspace_id, lower(name))
|
||||
where status = 'active';
|
||||
|
||||
create index if not exists spreadsheet_models_workspace_idx
|
||||
on public.spreadsheet_models (workspace_id, status);
|
||||
|
||||
create table if not exists public.spreadsheet_jobs (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
workspace_id uuid not null references public.workspaces (id) on delete cascade,
|
||||
user_id uuid not null references public.profiles (id),
|
||||
model_id uuid not null references public.spreadsheet_models (id) on delete restrict,
|
||||
status text not null check (status in (
|
||||
'draft', 'validating', 'ready', 'queued', 'processing', 'completed', 'failed', 'expired', 'cancelled'
|
||||
)),
|
||||
source_count integer not null default 0 check (source_count >= 0 and source_count <= 10),
|
||||
current_source integer not null default 0,
|
||||
output_format text check (output_format in ('xlsx', 'csv', 'pdf')),
|
||||
progress jsonb not null default '{}'::jsonb,
|
||||
error_code text,
|
||||
idempotency_key text,
|
||||
started_at timestamptz,
|
||||
finished_at timestamptz,
|
||||
created_at timestamptz not null default now(),
|
||||
updated_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create unique index if not exists spreadsheet_jobs_idempotency_idx
|
||||
on public.spreadsheet_jobs (user_id, idempotency_key)
|
||||
where idempotency_key is not null;
|
||||
|
||||
create index if not exists spreadsheet_jobs_workspace_user_idx
|
||||
on public.spreadsheet_jobs (workspace_id, user_id, status);
|
||||
|
||||
create table if not exists public.spreadsheet_job_sources (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
job_id uuid not null references public.spreadsheet_jobs (id) on delete cascade,
|
||||
source_name text not null,
|
||||
temp_storage_key text not null,
|
||||
selected_sheet text,
|
||||
header_row integer not null default 1 check (header_row >= 1),
|
||||
row_count integer not null default 0,
|
||||
size_bytes integer not null check (size_bytes >= 0),
|
||||
status text not null check (status in (
|
||||
'pending', 'validated', 'invalid', 'mapped', 'processing', 'completed', 'failed', 'expired'
|
||||
)),
|
||||
columns_schema jsonb not null default '[]'::jsonb,
|
||||
error_code text,
|
||||
output_storage_key text,
|
||||
output_csv_key text,
|
||||
output_xlsx_key text,
|
||||
process_idempotency_key text,
|
||||
expires_at timestamptz not null,
|
||||
processed_at timestamptz,
|
||||
created_at timestamptz not null default now()
|
||||
);
|
||||
|
||||
create index if not exists spreadsheet_job_sources_job_idx on public.spreadsheet_job_sources (job_id);
|
||||
create index if not exists spreadsheet_job_sources_expires_idx on public.spreadsheet_job_sources (expires_at);
|
||||
|
||||
create table if not exists public.spreadsheet_job_mappings (
|
||||
id uuid primary key default gen_random_uuid(),
|
||||
job_source_id uuid not null references public.spreadsheet_job_sources (id) on delete cascade,
|
||||
source_column text not null check (char_length(source_column) > 0),
|
||||
model_column text not null check (char_length(model_column) > 0),
|
||||
is_required boolean not null default false,
|
||||
position integer not null check (position >= 0),
|
||||
unique (job_source_id, source_column),
|
||||
unique (job_source_id, model_column)
|
||||
);
|
||||
|
||||
create or replace function public.jobs_model_same_workspace()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
model_ws uuid;
|
||||
begin
|
||||
select workspace_id into model_ws from public.spreadsheet_models where id = new.model_id;
|
||||
if model_ws is null or model_ws <> new.workspace_id then
|
||||
raise exception 'MODEL_WORKSPACE_MISMATCH' using errcode = '23514';
|
||||
end if;
|
||||
if new.user_id is null then
|
||||
raise exception 'JOB_USER_REQUIRED' using errcode = '23514';
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists spreadsheet_jobs_workspace_guard on public.spreadsheet_jobs;
|
||||
create trigger spreadsheet_jobs_workspace_guard
|
||||
before insert or update of model_id, workspace_id on public.spreadsheet_jobs
|
||||
for each row execute function public.jobs_model_same_workspace();
|
||||
|
||||
create or replace function public.enforce_active_model_limit()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
total integer;
|
||||
begin
|
||||
if new.status = 'active' then
|
||||
select count(*) into total
|
||||
from public.spreadsheet_models
|
||||
where workspace_id = new.workspace_id
|
||||
and status = 'active'
|
||||
and id <> new.id;
|
||||
if total >= 10 then
|
||||
raise exception 'MODEL_LIMIT' using errcode = '23514';
|
||||
end if;
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists spreadsheet_models_limit on public.spreadsheet_models;
|
||||
create trigger spreadsheet_models_limit
|
||||
before insert or update of status, workspace_id on public.spreadsheet_models
|
||||
for each row execute function public.enforce_active_model_limit();
|
||||
|
||||
create or replace function public.enforce_source_limit()
|
||||
returns trigger
|
||||
language plpgsql
|
||||
as $$
|
||||
declare
|
||||
total integer;
|
||||
begin
|
||||
select count(*) into total from public.spreadsheet_job_sources where job_id = new.job_id;
|
||||
if total >= 10 then
|
||||
raise exception 'SOURCE_LIMIT' using errcode = '23514';
|
||||
end if;
|
||||
return new;
|
||||
end;
|
||||
$$;
|
||||
|
||||
drop trigger if exists spreadsheet_sources_limit on public.spreadsheet_job_sources;
|
||||
create trigger spreadsheet_sources_limit
|
||||
before insert on public.spreadsheet_job_sources
|
||||
for each row execute function public.enforce_source_limit();
|
||||
@@ -0,0 +1,284 @@
|
||||
create or replace function public.auth_uid()
|
||||
returns uuid
|
||||
language sql
|
||||
stable
|
||||
as $$
|
||||
select nullif(current_setting('app.user_id', true), '')::uuid
|
||||
$$;
|
||||
|
||||
create or replace function public.is_member(ws uuid)
|
||||
returns boolean
|
||||
language sql
|
||||
stable
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
select exists (
|
||||
select 1 from public.workspace_members m
|
||||
where m.workspace_id = ws and m.user_id = public.auth_uid()
|
||||
)
|
||||
$$;
|
||||
|
||||
create or replace function public.member_role(ws uuid)
|
||||
returns text
|
||||
language sql
|
||||
stable
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
select m.role from public.workspace_members m
|
||||
where m.workspace_id = ws and m.user_id = public.auth_uid()
|
||||
$$;
|
||||
|
||||
create or replace function public.module_enabled(ws uuid)
|
||||
returns boolean
|
||||
language sql
|
||||
stable
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
select coalesce((
|
||||
select f.enabled from public.workspace_module_flags f
|
||||
where f.workspace_id = ws and f.module_key = 'spreadsheet_adapter'
|
||||
), false)
|
||||
$$;
|
||||
|
||||
create or replace function public.can_manage_adapter(ws uuid)
|
||||
returns boolean
|
||||
language sql
|
||||
stable
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
select public.module_enabled(ws) and public.member_role(ws) in ('owner', 'admin')
|
||||
$$;
|
||||
|
||||
create or replace function public.can_upload_model(ws uuid)
|
||||
returns boolean
|
||||
language sql
|
||||
stable
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
select public.module_enabled(ws) and (
|
||||
public.member_role(ws) in ('owner', 'admin')
|
||||
or (
|
||||
public.member_role(ws) in ('gestor', 'agent')
|
||||
and coalesce((
|
||||
select s.allow_non_admin_model_upload
|
||||
from public.spreadsheet_adapter_settings s
|
||||
where s.workspace_id = ws
|
||||
), false)
|
||||
)
|
||||
)
|
||||
$$;
|
||||
|
||||
create or replace function public.shares_workspace(other_user uuid)
|
||||
returns boolean
|
||||
language sql
|
||||
stable
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
select exists (
|
||||
select 1
|
||||
from public.workspace_members a
|
||||
join public.workspace_members b on a.workspace_id = b.workspace_id
|
||||
where a.user_id = public.auth_uid() and b.user_id = other_user
|
||||
)
|
||||
$$;
|
||||
|
||||
create or replace function public.model_has_active_job(model uuid)
|
||||
returns boolean
|
||||
language sql
|
||||
stable
|
||||
security definer
|
||||
set search_path = public
|
||||
as $$
|
||||
select exists (
|
||||
select 1 from public.spreadsheet_jobs j
|
||||
where j.model_id = model
|
||||
and j.status in ('validating', 'ready', 'queued', 'processing')
|
||||
)
|
||||
$$;
|
||||
|
||||
revoke all on function public.auth_uid() from public;
|
||||
revoke all on function public.is_member(uuid) from public;
|
||||
revoke all on function public.member_role(uuid) from public;
|
||||
revoke all on function public.module_enabled(uuid) from public;
|
||||
revoke all on function public.can_manage_adapter(uuid) from public;
|
||||
revoke all on function public.can_upload_model(uuid) from public;
|
||||
revoke all on function public.shares_workspace(uuid) from public;
|
||||
revoke all on function public.model_has_active_job(uuid) from public;
|
||||
|
||||
grant execute on function public.auth_uid() to app_user;
|
||||
grant execute on function public.is_member(uuid) to app_user;
|
||||
grant execute on function public.member_role(uuid) to app_user;
|
||||
grant execute on function public.module_enabled(uuid) to app_user;
|
||||
grant execute on function public.can_manage_adapter(uuid) to app_user;
|
||||
grant execute on function public.can_upload_model(uuid) to app_user;
|
||||
grant execute on function public.shares_workspace(uuid) to app_user;
|
||||
grant execute on function public.model_has_active_job(uuid) to app_user;
|
||||
|
||||
alter table public.profiles enable row level security;
|
||||
alter table public.workspaces enable row level security;
|
||||
alter table public.workspace_members enable row level security;
|
||||
alter table public.workspace_module_flags enable row level security;
|
||||
alter table public.audit_log enable row level security;
|
||||
alter table public.spreadsheet_adapter_settings enable row level security;
|
||||
alter table public.spreadsheet_models enable row level security;
|
||||
alter table public.spreadsheet_jobs enable row level security;
|
||||
alter table public.spreadsheet_job_sources enable row level security;
|
||||
alter table public.spreadsheet_job_mappings enable row level security;
|
||||
|
||||
alter table public.profiles force row level security;
|
||||
alter table public.workspaces force row level security;
|
||||
alter table public.workspace_members force row level security;
|
||||
alter table public.workspace_module_flags force row level security;
|
||||
alter table public.audit_log force row level security;
|
||||
alter table public.spreadsheet_adapter_settings force row level security;
|
||||
alter table public.spreadsheet_models force row level security;
|
||||
alter table public.spreadsheet_jobs force row level security;
|
||||
alter table public.spreadsheet_job_sources force row level security;
|
||||
alter table public.spreadsheet_job_mappings force row level security;
|
||||
|
||||
drop policy if exists profiles_select on public.profiles;
|
||||
create policy profiles_select on public.profiles
|
||||
for select to app_user
|
||||
using (id = public.auth_uid() or public.shares_workspace(id));
|
||||
|
||||
drop policy if exists workspaces_select on public.workspaces;
|
||||
create policy workspaces_select on public.workspaces
|
||||
for select to app_user
|
||||
using (public.is_member(id));
|
||||
|
||||
drop policy if exists members_select on public.workspace_members;
|
||||
create policy members_select on public.workspace_members
|
||||
for select to app_user
|
||||
using (user_id = public.auth_uid() or public.is_member(workspace_id));
|
||||
|
||||
drop policy if exists flags_select on public.workspace_module_flags;
|
||||
create policy flags_select on public.workspace_module_flags
|
||||
for select to app_user
|
||||
using (public.is_member(workspace_id));
|
||||
|
||||
drop policy if exists audit_insert on public.audit_log;
|
||||
create policy audit_insert on public.audit_log
|
||||
for insert to app_user
|
||||
with check (actor_id = public.auth_uid() and (workspace_id is null or public.is_member(workspace_id)));
|
||||
|
||||
drop policy if exists audit_select on public.audit_log;
|
||||
create policy audit_select on public.audit_log
|
||||
for select to app_user
|
||||
using (public.can_manage_adapter(workspace_id));
|
||||
|
||||
drop policy if exists settings_select on public.spreadsheet_adapter_settings;
|
||||
create policy settings_select on public.spreadsheet_adapter_settings
|
||||
for select to app_user
|
||||
using (public.is_member(workspace_id) and public.module_enabled(workspace_id));
|
||||
|
||||
drop policy if exists settings_insert on public.spreadsheet_adapter_settings;
|
||||
create policy settings_insert on public.spreadsheet_adapter_settings
|
||||
for insert to app_user
|
||||
with check (public.can_manage_adapter(workspace_id));
|
||||
|
||||
drop policy if exists settings_update on public.spreadsheet_adapter_settings;
|
||||
create policy settings_update on public.spreadsheet_adapter_settings
|
||||
for update to app_user
|
||||
using (public.can_manage_adapter(workspace_id))
|
||||
with check (public.can_manage_adapter(workspace_id));
|
||||
|
||||
drop policy if exists models_select on public.spreadsheet_models;
|
||||
create policy models_select on public.spreadsheet_models
|
||||
for select to app_user
|
||||
using (public.is_member(workspace_id) and public.module_enabled(workspace_id));
|
||||
|
||||
drop policy if exists models_insert on public.spreadsheet_models;
|
||||
create policy models_insert on public.spreadsheet_models
|
||||
for insert to app_user
|
||||
with check (
|
||||
public.can_upload_model(workspace_id)
|
||||
and created_by = public.auth_uid()
|
||||
);
|
||||
|
||||
drop policy if exists models_update on public.spreadsheet_models;
|
||||
create policy models_update on public.spreadsheet_models
|
||||
for update to app_user
|
||||
using (public.can_manage_adapter(workspace_id))
|
||||
with check (public.can_manage_adapter(workspace_id));
|
||||
|
||||
drop policy if exists jobs_all on public.spreadsheet_jobs;
|
||||
create policy jobs_select on public.spreadsheet_jobs
|
||||
for select to app_user
|
||||
using (user_id = public.auth_uid() and public.is_member(workspace_id) and public.module_enabled(workspace_id));
|
||||
|
||||
create policy jobs_insert on public.spreadsheet_jobs
|
||||
for insert to app_user
|
||||
with check (user_id = public.auth_uid() and public.is_member(workspace_id) and public.module_enabled(workspace_id));
|
||||
|
||||
create policy jobs_update on public.spreadsheet_jobs
|
||||
for update to app_user
|
||||
using (user_id = public.auth_uid() and public.is_member(workspace_id) and public.module_enabled(workspace_id))
|
||||
with check (user_id = public.auth_uid() and public.is_member(workspace_id) and public.module_enabled(workspace_id));
|
||||
|
||||
drop policy if exists sources_select on public.spreadsheet_job_sources;
|
||||
create policy sources_select on public.spreadsheet_job_sources
|
||||
for select to app_user
|
||||
using (exists (
|
||||
select 1 from public.spreadsheet_jobs j
|
||||
where j.id = job_id and j.user_id = public.auth_uid() and public.module_enabled(j.workspace_id)
|
||||
));
|
||||
|
||||
create policy sources_insert on public.spreadsheet_job_sources
|
||||
for insert to app_user
|
||||
with check (exists (
|
||||
select 1 from public.spreadsheet_jobs j
|
||||
where j.id = job_id and j.user_id = public.auth_uid() and public.module_enabled(j.workspace_id)
|
||||
));
|
||||
|
||||
create policy sources_update on public.spreadsheet_job_sources
|
||||
for update to app_user
|
||||
using (exists (
|
||||
select 1 from public.spreadsheet_jobs j
|
||||
where j.id = job_id and j.user_id = public.auth_uid() and public.module_enabled(j.workspace_id)
|
||||
))
|
||||
with check (exists (
|
||||
select 1 from public.spreadsheet_jobs j
|
||||
where j.id = job_id and j.user_id = public.auth_uid() and public.module_enabled(j.workspace_id)
|
||||
));
|
||||
|
||||
create policy sources_delete on public.spreadsheet_job_sources
|
||||
for delete to app_user
|
||||
using (exists (
|
||||
select 1 from public.spreadsheet_jobs j
|
||||
where j.id = job_id and j.user_id = public.auth_uid() and public.module_enabled(j.workspace_id)
|
||||
));
|
||||
|
||||
drop policy if exists mappings_select on public.spreadsheet_job_mappings;
|
||||
create policy mappings_select on public.spreadsheet_job_mappings
|
||||
for select to app_user
|
||||
using (exists (
|
||||
select 1 from public.spreadsheet_job_sources s
|
||||
join public.spreadsheet_jobs j on j.id = s.job_id
|
||||
where s.id = job_source_id and j.user_id = public.auth_uid()
|
||||
));
|
||||
|
||||
create policy mappings_insert on public.spreadsheet_job_mappings
|
||||
for insert to app_user
|
||||
with check (exists (
|
||||
select 1 from public.spreadsheet_job_sources s
|
||||
join public.spreadsheet_jobs j on j.id = s.job_id
|
||||
where s.id = job_source_id and j.user_id = public.auth_uid()
|
||||
));
|
||||
|
||||
create policy mappings_delete on public.spreadsheet_job_mappings
|
||||
for delete to app_user
|
||||
using (exists (
|
||||
select 1 from public.spreadsheet_job_sources s
|
||||
join public.spreadsheet_jobs j on j.id = s.job_id
|
||||
where s.id = job_source_id and j.user_id = public.auth_uid()
|
||||
));
|
||||
|
||||
grant usage on schema public to app_user;
|
||||
grant select, insert, update, delete on all tables in schema public to app_user;
|
||||
grant usage, select on all sequences in schema public to app_user;
|
||||
@@ -0,0 +1,15 @@
|
||||
-- Rollback do módulo Adaptador.
|
||||
-- Não remove profiles, workspaces, members, flags ou audit_log do Elo Pro.
|
||||
-- Funções de autorização usadas também pelo audit_log permanecem.
|
||||
|
||||
drop table if exists public.spreadsheet_job_mappings;
|
||||
drop table if exists public.spreadsheet_job_sources;
|
||||
drop table if exists public.spreadsheet_jobs;
|
||||
drop table if exists public.spreadsheet_models;
|
||||
drop table if exists public.spreadsheet_adapter_settings;
|
||||
|
||||
drop function if exists public.enforce_source_limit();
|
||||
drop function if exists public.enforce_active_model_limit();
|
||||
drop function if exists public.jobs_model_same_workspace();
|
||||
drop function if exists public.model_has_active_job(uuid);
|
||||
drop function if exists public.can_upload_model(uuid);
|
||||
Reference in new issue
Block a user