mirror of
https://origin.cursor.com/mrdevmx/panels.git
synced 2026-10-09 14:33:17 +00:00
<!-- CURSOR_AGENT_PR_BODY_BEGIN --> ## Causa El error `duplicate key value violates unique constraint "projects_code_key"` aparece porque: 1. La columna `projects.code` es **UNIQUE en toda la base** (todos los tenants). 2. Al dar de alta, `fn_project_create` llama a `fn_next_project_code`, que hacía `MAX(PRY…)+1` **solo sobre las filas que el tenant ve** (RLS). 3. Si en la base ya existe `PRY-0001` (seed de dev, otra empresa cliente, u otra obra que este tenant no ve), el sistema vuelve a proponer `PRY-0001` → el `INSERT` falla con CONFLICT. No es el nombre del proyecto ni el presupuesto adjunto: es el **código autogenerado**. ## Cambio (Liquibase 039) - `fn_next_project_code`: `SECURITY DEFINER` para leer la secuencia **global** de códigos PRY/OBR. - `fn_project_create`: `pg_advisory_xact_lock` al asignar código (evita duplicado si envían dos veces seguidas). ## Cómo probar 1. Aplicar migraciones (039). 2. Con un tenant que aún no tenga obras (o en un ambiente donde ya exista `PRY-0001` de otro tenant), dar de alta un proyecto nuevo. 3. Debe crearse con `PRY-0002` (o el siguiente libre), sin toast de CONFLICT. Script de demostración RLS: `scripts/verify-project-code-next.sql` + `verify-project-code-next-run.sql`. <!-- CURSOR_AGENT_PR_BODY_END --> <div><a href="https://cursor.com/agents/bc-d8a5d34a-ead9-4912-8296-322cff799783?cursor_ref=pr_footer&cursor_cta=open_in_web"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-d8a5d34a-ead9-4912-8296-322cff799783&cursor_ref=pr_footer&cursor_cta=open_in_cursor"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>
44 lines
2.2 KiB
PL/PgSQL
44 lines
2.2 KiB
PL/PgSQL
-- Simula RLS: tenant B no ve PRY-0001 de tenant A pero el código debe ser global.
|
|
DROP SCHEMA IF EXISTS core CASCADE;
|
|
CREATE SCHEMA core;
|
|
SET search_path = core;
|
|
|
|
CREATE FUNCTION rpc_assert_message(p_message text) RETURNS void LANGUAGE plpgsql IMMUTABLE AS $$
|
|
BEGIN IF p_message IS NULL OR btrim(p_message) = '' THEN RAISE EXCEPTION 'empty'; END IF; END; $$;
|
|
CREATE FUNCTION rpc_ok(p_data jsonb, p_message text, p_ctx jsonb DEFAULT '{}'::jsonb) RETURNS jsonb
|
|
LANGUAGE plpgsql IMMUTABLE AS $$
|
|
BEGIN PERFORM rpc_assert_message(p_message);
|
|
RETURN jsonb_build_object('ok', true, 'data', p_data); END; $$;
|
|
CREATE FUNCTION rpc_created(p_data jsonb, p_message text, p_ctx jsonb DEFAULT '{}'::jsonb) RETURNS jsonb
|
|
LANGUAGE plpgsql IMMUTABLE AS $$
|
|
BEGIN PERFORM rpc_assert_message(p_message);
|
|
RETURN jsonb_build_object('ok', true, 'data', p_data); END; $$;
|
|
CREATE FUNCTION rpc_err(p_code text, p_message text, p_ctx jsonb DEFAULT '{}'::jsonb, p_errors jsonb DEFAULT NULL) RETURNS jsonb
|
|
LANGUAGE plpgsql IMMUTABLE AS $$
|
|
BEGIN PERFORM rpc_assert_message(p_message);
|
|
RETURN jsonb_build_object('ok', false, 'message', p_message); END; $$;
|
|
CREATE FUNCTION rpc_from_exception(p_fn text, p_sqlstate text, p_message text, p_detail text DEFAULT NULL) RETURNS jsonb
|
|
LANGUAGE plpgsql IMMUTABLE AS $$
|
|
BEGIN RETURN rpc_err('INTERNAL', format('%s %s', p_fn, p_message), '{}'::jsonb); END; $$;
|
|
|
|
CREATE TABLE companies (id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text);
|
|
CREATE TABLE projects (
|
|
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
|
|
code text NOT NULL UNIQUE,
|
|
name text NOT NULL,
|
|
address text DEFAULT '',
|
|
theme_id text NOT NULL DEFAULT 'arctec-dos-logos-fold',
|
|
status text DEFAULT 'activo',
|
|
company_id bigint,
|
|
tenant_id int
|
|
);
|
|
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;
|
|
CREATE POLICY tenant_isolation ON projects
|
|
USING (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer)
|
|
WITH CHECK (tenant_id = NULLIF(current_setting('app.tenant_id', true), '')::integer);
|
|
|
|
INSERT INTO companies (name) VALUES ('Co A'), ('Co B');
|
|
|
|
-- PRY-0001 ya existe (otro tenant / seed)
|
|
INSERT INTO projects (code, name, theme_id, company_id, tenant_id)
|
|
VALUES ('PRY-0001', 'Obra seed', 'arctec-dos-logos-fold', 1, 1);
|