refactor(approvals/t-paliad-160 slice3 / M2): drop required_role column
Cleanup of the t-paliad-160 dual-read shim. After slice 1+2 every writer
hits both `required_role` and the new `(requires_approval, min_role)`
columns, and every reader prefers the new ones. M2 (migration 065) drops
the legacy column from `paliad.approval_policies` and rewrites
`paliad.approval_policy_effective()` to a 4-column return shape.
`paliad.approval_requests.required_role` is intentionally untouched —
that's the in-flight policy snapshot at submission time, a separate
concern from the policy authoring grammar.
Go side:
- models.ApprovalPolicy and models.EffectivePolicy lose RequiredRole.
The MinRole pointer is now the only seniority-threshold surface.
- LookupPolicy / GetEffectivePolicyOne / List* / snapshotProjectRows
drop the required_role SELECT projection.
- UpsertProjectPolicySplit / UpsertUnitPolicySplit /
DeleteProjectPolicy / DeleteUnitPolicy / ApplyMatrixToDescendants
drop the required_role write. The audit-log row still uses the
legacy string format ('partner|...|none'); composed via
legacyFromSplit() from the new columns so the audit table layout
keeps working without a parallel migration.
- submit() reads policy.MinRole directly (LookupPolicy guarantees
non-nil when a non-nil policy is returned).
- nullToPtr helper retired (no remaining callers).
Frontend side:
- admin-approval-policies.ts UnitPolicy / EffectivePolicy lose the
legacy required_role optional. The 2-control UI was already on the
split-grammar path.
- deadlines-new.ts + appointments-new.ts form-time hint readers prefer
requires_approval+min_role. They keep a soft-fall back to the
legacy required_role for one cycle in case any cached pre-M2 server
is still serving the old shape — that path is dead-code post-deploy
and can be dropped later.
Test:
- TestApprovalService_PolicyCRUD asserts MinRole instead of
RequiredRole after re-upsert.
Build: bun build OK, go build ./... OK, go test ./... OK.
Deploy ordering: this slice MUST land after slice 2 is merged so the
pre-deploy code paths that still reference required_role have already
been retired.
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
-- Reverse t-paliad-160 M2: re-add the required_role column on
|
||||
-- paliad.approval_policies and re-introduce the dual-read function shape
|
||||
-- from migration 064.
|
||||
|
||||
ALTER TABLE paliad.approval_policies
|
||||
ADD COLUMN required_role text;
|
||||
|
||||
UPDATE paliad.approval_policies
|
||||
SET required_role = CASE
|
||||
WHEN requires_approval = false OR min_role IS NULL THEN 'none'
|
||||
ELSE min_role
|
||||
END;
|
||||
|
||||
ALTER TABLE paliad.approval_policies
|
||||
ALTER COLUMN required_role SET NOT NULL;
|
||||
|
||||
ALTER TABLE paliad.approval_policies
|
||||
ADD CONSTRAINT approval_policies_required_role_check CHECK (
|
||||
required_role IN ('partner', 'of_counsel', 'associate', 'senior_pa', 'pa', 'none')
|
||||
);
|
||||
|
||||
DROP FUNCTION IF EXISTS paliad.approval_policy_effective(uuid, text, text);
|
||||
|
||||
CREATE FUNCTION paliad.approval_policy_effective(
|
||||
p_project_id uuid,
|
||||
p_entity_type text,
|
||||
p_lifecycle text
|
||||
) RETURNS TABLE (
|
||||
requires_approval boolean,
|
||||
min_role text,
|
||||
required_role text,
|
||||
source text,
|
||||
source_id uuid
|
||||
)
|
||||
LANGUAGE plpgsql STABLE AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
WITH path AS (
|
||||
SELECT string_to_array(p.path, '.')::uuid[] AS ids
|
||||
FROM paliad.projects p WHERE p.id = p_project_id
|
||||
),
|
||||
project_rows AS (
|
||||
SELECT ap.requires_approval, ap.min_role,
|
||||
'project'::text AS src, ap.project_id AS sid, 1 AS src_priority
|
||||
FROM paliad.approval_policies ap
|
||||
WHERE ap.project_id = p_project_id
|
||||
AND ap.entity_type = p_entity_type
|
||||
AND ap.lifecycle_event = p_lifecycle
|
||||
),
|
||||
ancestor_rows AS (
|
||||
SELECT ap.requires_approval, ap.min_role,
|
||||
'ancestor'::text AS src, ap.project_id AS sid, 2 AS src_priority
|
||||
FROM paliad.approval_policies ap, path
|
||||
WHERE ap.project_id = ANY(path.ids)
|
||||
AND ap.project_id <> p_project_id
|
||||
AND ap.entity_type = p_entity_type
|
||||
AND ap.lifecycle_event = p_lifecycle
|
||||
),
|
||||
unit_rows AS (
|
||||
SELECT ap.requires_approval, ap.min_role,
|
||||
'unit_default'::text AS src, ap.partner_unit_id AS sid, 3 AS src_priority
|
||||
FROM paliad.approval_policies ap
|
||||
JOIN paliad.project_partner_units ppu
|
||||
ON ppu.partner_unit_id = ap.partner_unit_id
|
||||
WHERE ppu.project_id = p_project_id
|
||||
AND ap.entity_type = p_entity_type
|
||||
AND ap.lifecycle_event = p_lifecycle
|
||||
),
|
||||
candidates AS (
|
||||
SELECT * FROM project_rows UNION ALL
|
||||
SELECT * FROM ancestor_rows UNION ALL
|
||||
SELECT * FROM unit_rows
|
||||
),
|
||||
strictest_role AS (
|
||||
SELECT c.min_role, c.src AS source, c.sid AS source_id
|
||||
FROM candidates c
|
||||
WHERE c.requires_approval = true AND c.min_role IS NOT NULL
|
||||
ORDER BY paliad.approval_role_level(c.min_role) DESC, c.src_priority ASC
|
||||
LIMIT 1
|
||||
),
|
||||
no_approval_attribution AS (
|
||||
SELECT c.src AS source, c.sid AS source_id
|
||||
FROM candidates c
|
||||
WHERE c.requires_approval = false
|
||||
ORDER BY c.src_priority ASC
|
||||
LIMIT 1
|
||||
),
|
||||
summary AS (
|
||||
SELECT bool_or(c.requires_approval) AS req FROM candidates c
|
||||
)
|
||||
SELECT
|
||||
COALESCE(s.req, false) AS requires_approval,
|
||||
sr.min_role AS min_role,
|
||||
sr.min_role AS required_role,
|
||||
COALESCE(sr.source, na.source) AS source,
|
||||
COALESCE(sr.source_id, na.source_id) AS source_id
|
||||
FROM summary s
|
||||
LEFT JOIN strictest_role sr ON true
|
||||
LEFT JOIN no_approval_attribution na ON true
|
||||
WHERE EXISTS (SELECT 1 FROM candidates);
|
||||
END;
|
||||
$$;
|
||||
@@ -0,0 +1,139 @@
|
||||
-- t-paliad-160 (M2): drop the legacy `required_role` column from
|
||||
-- paliad.approval_policies. Migration 064 (M1) introduced the
|
||||
-- requires_approval + min_role split-grammar columns and kept
|
||||
-- required_role as a dual-read mirror so a rollback to pre-deploy
|
||||
-- code would still work. M2 retires the mirror once all writers have
|
||||
-- cut over.
|
||||
--
|
||||
-- Deploy ordering — IMPORTANT:
|
||||
--
|
||||
-- 1. Migration 064 must already be applied (introduces the new
|
||||
-- columns and rewrites approval_policy_effective() to the
|
||||
-- new shape). The Go service layer in slice 1+2 reads the new
|
||||
-- columns AND writes both old + new on every Upsert*. With
|
||||
-- this migration the writes drop the legacy column path.
|
||||
-- 2. After 065, NO code path may reference
|
||||
-- paliad.approval_policies.required_role.
|
||||
-- 3. paliad.approval_requests.required_role is a different column
|
||||
-- (the in-flight snapshot of the policy at submission time) and
|
||||
-- is intentionally untouched here.
|
||||
--
|
||||
-- The function paliad.approval_policy_effective() is also updated to
|
||||
-- stop returning the redundant required_role column.
|
||||
|
||||
-- ============================================================================
|
||||
-- 1. Replace approval_policy_effective() with a 4-column return that no
|
||||
-- longer mirrors required_role.
|
||||
-- ============================================================================
|
||||
|
||||
DROP FUNCTION IF EXISTS paliad.approval_policy_effective(uuid, text, text);
|
||||
|
||||
CREATE FUNCTION paliad.approval_policy_effective(
|
||||
p_project_id uuid,
|
||||
p_entity_type text,
|
||||
p_lifecycle text
|
||||
) RETURNS TABLE (
|
||||
requires_approval boolean,
|
||||
min_role text,
|
||||
source text,
|
||||
source_id uuid
|
||||
)
|
||||
LANGUAGE plpgsql STABLE AS $$
|
||||
BEGIN
|
||||
RETURN QUERY
|
||||
WITH path AS (
|
||||
SELECT string_to_array(p.path, '.')::uuid[] AS ids
|
||||
FROM paliad.projects p WHERE p.id = p_project_id
|
||||
),
|
||||
project_rows AS (
|
||||
SELECT ap.requires_approval,
|
||||
ap.min_role,
|
||||
'project'::text AS src,
|
||||
ap.project_id AS sid,
|
||||
1 AS src_priority
|
||||
FROM paliad.approval_policies ap
|
||||
WHERE ap.project_id = p_project_id
|
||||
AND ap.entity_type = p_entity_type
|
||||
AND ap.lifecycle_event = p_lifecycle
|
||||
),
|
||||
ancestor_rows AS (
|
||||
SELECT ap.requires_approval,
|
||||
ap.min_role,
|
||||
'ancestor'::text AS src,
|
||||
ap.project_id AS sid,
|
||||
2 AS src_priority
|
||||
FROM paliad.approval_policies ap, path
|
||||
WHERE ap.project_id = ANY(path.ids)
|
||||
AND ap.project_id <> p_project_id
|
||||
AND ap.entity_type = p_entity_type
|
||||
AND ap.lifecycle_event = p_lifecycle
|
||||
),
|
||||
unit_rows AS (
|
||||
SELECT ap.requires_approval,
|
||||
ap.min_role,
|
||||
'unit_default'::text AS src,
|
||||
ap.partner_unit_id AS sid,
|
||||
3 AS src_priority
|
||||
FROM paliad.approval_policies ap
|
||||
JOIN paliad.project_partner_units ppu
|
||||
ON ppu.partner_unit_id = ap.partner_unit_id
|
||||
WHERE ppu.project_id = p_project_id
|
||||
AND ap.entity_type = p_entity_type
|
||||
AND ap.lifecycle_event = p_lifecycle
|
||||
),
|
||||
candidates AS (
|
||||
SELECT * FROM project_rows
|
||||
UNION ALL
|
||||
SELECT * FROM ancestor_rows
|
||||
UNION ALL
|
||||
SELECT * FROM unit_rows
|
||||
),
|
||||
strictest_role AS (
|
||||
SELECT c.min_role,
|
||||
c.src AS source,
|
||||
c.sid AS source_id
|
||||
FROM candidates c
|
||||
WHERE c.requires_approval = true
|
||||
AND c.min_role IS NOT NULL
|
||||
ORDER BY paliad.approval_role_level(c.min_role) DESC,
|
||||
c.src_priority ASC
|
||||
LIMIT 1
|
||||
),
|
||||
no_approval_attribution AS (
|
||||
SELECT c.src AS source, c.sid AS source_id
|
||||
FROM candidates c
|
||||
WHERE c.requires_approval = false
|
||||
ORDER BY c.src_priority ASC
|
||||
LIMIT 1
|
||||
),
|
||||
summary AS (
|
||||
SELECT bool_or(c.requires_approval) AS req
|
||||
FROM candidates c
|
||||
)
|
||||
SELECT
|
||||
COALESCE(s.req, false) AS requires_approval,
|
||||
sr.min_role AS min_role,
|
||||
COALESCE(sr.source, na.source) AS source,
|
||||
COALESCE(sr.source_id, na.source_id) AS source_id
|
||||
FROM summary s
|
||||
LEFT JOIN strictest_role sr ON true
|
||||
LEFT JOIN no_approval_attribution na ON true
|
||||
WHERE EXISTS (SELECT 1 FROM candidates);
|
||||
END;
|
||||
$$;
|
||||
|
||||
COMMENT ON FUNCTION paliad.approval_policy_effective(uuid, text, text) IS
|
||||
'Effective approval policy resolver (t-paliad-160 M2). Returns '
|
||||
'requires_approval (OR across candidates) + min_role (MAX along the '
|
||||
'role ladder among requires_approval=true candidates) + source '
|
||||
'attribution. Zero rows when no policy candidates exist.';
|
||||
|
||||
-- ============================================================================
|
||||
-- 2. Drop the legacy column.
|
||||
-- ============================================================================
|
||||
|
||||
ALTER TABLE paliad.approval_policies
|
||||
DROP CONSTRAINT IF EXISTS approval_policies_required_role_check;
|
||||
|
||||
ALTER TABLE paliad.approval_policies
|
||||
DROP COLUMN required_role;
|
||||
Reference in New Issue
Block a user