freightdesk/supabase/migrations/003_soft_delete.sql

134 lines
7.2 KiB
PL/PgSQL

-- ============================================================
-- SOFT DELETE & AUDIT LOG EXTENSION
-- ============================================================
-- Add soft delete columns to core tables
-- ============================================================
ALTER TABLE loads ADD COLUMN deleted_at TIMESTAMPTZ;
ALTER TABLE shippers ADD COLUMN deleted_at TIMESTAMPTZ;
ALTER TABLE vehicles ADD COLUMN deleted_at TIMESTAMPTZ;
ALTER TABLE payments ADD COLUMN deleted_at TIMESTAMPTZ;
ALTER TABLE portal_users ADD COLUMN deleted_at TIMESTAMPTZ;
-- ============================================================
-- AUDIT LOG TABLE
-- ============================================================
CREATE TABLE audit_logs (
id BIGSERIAL PRIMARY KEY,
action TEXT NOT NULL, -- 'INSERT', 'UPDATE', 'DELETE'
table_name TEXT NOT NULL, -- e.g., 'loads', 'shippers'
row_id UUID, -- UUID of the affected row (if applicable)
before_json JSONB, -- state before change
after_json JSONB, -- state after change
created_at TIMESTAMPTZ DEFAULT NOW(),
user_id UUID, -- references admin who made change (nullable)
notes TEXT
);
-- Indexes for fast lookup
CREATE INDEX idx_audit_logs_table ON audit_logs(table_name);
CREATE INDEX idx_audit_logs_user ON audit_logs(user_id);
CREATE INDEX idx_audit_logs_time ON audit_logs(created_at DESC);
-- ============================================================
-- TRIGGERS FOR SOFT DELETE & AUDIT LOGGING
-- ============================================================
-- Function to perform soft delete and log action
CREATE OR REPLACE FUNCTION soft_delete_and_audit()
RETURNS TRIGGER AS $$
DECLARE
v_user_id UUID;
BEGIN
-- Determine which table triggered the trigger
IF TG_TABLE_NAME = 'loads' THEN
IF OLD.deleted_at IS NOT NULL THEN
RETURN OLD; -- already soft-deleted
END IF;
UPDATE loads SET deleted_at = NOW() WHERE id = OLD.id;
IF TG_OP = 'DELETE' THEN
INSERT INTO audit_logs(action, table_name, row_id, before_json, created_at, user_id)
VALUES ('DELETE', TG_TABLE_NAME, OLD.id::UUID, to_jsonb(OLD), NOW(), current_setting('user_id')::UUID);
ELSIF TG_OP = 'UPDATE' THEN
INSERT INTO audit_logs(action, table_name, row_id, before_json, after_json, created_at, user_id)
VALUES ('UPDATE', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(OLD), to_jsonb(NEW), NOW(), current_setting('user_id')::UUID);
ELSIF TG_OP = 'INSERT' THEN
INSERT INTO audit_logs(action, table_name, row_id, after_json, created_at, user_id)
VALUES ('INSERT', TG_TABLE_NAME, NEW.id::UUID, NULL, NOW(), current_setting('user_id')::UUID);
END IF;
ELSIF TG_TABLE_NAME = 'shippers' THEN
IF OLD.deleted_at IS NOT NULL THEN
RETURN OLD;
END IF;
UPDATE shippers SET deleted_at = NOW() WHERE id = OLD.id;
IF TG_OP = 'DELETE' THEN
INSERT INTO audit_logs(action, table_name, row_id, before_json, created_at, user_id)
VALUES ('DELETE', TG_TABLE_NAME, OLD.id::UUID, to_jsonb(OLD), NOW(), current_setting('user_id')::UUID);
ELSIF TG_OP = 'UPDATE' THEN
INSERT INTO audit_logs(action, table_name, row_id, before_json, after_json, created_at, user_id)
VALUES ('UPDATE', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(OLD), to_jsonb(NEW), NOW(), current_setting('user_id')::UUID);
ELSIF TG_OP = 'INSERT' THEN
INSERT INTO audit_logs(action, table_name, row_id, after_json, created_at, user_id)
VALUES ('INSERT', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(NEW), NOW(), current_setting('user_id')::UUID);
END IF;
ELSIF TG_TABLE_NAME = 'vehicles' THEN
IF OLD.deleted_at IS NOT NULL THEN
RETURN OLD;
END IF;
UPDATE vehicles SET deleted_at = NOW() WHERE id = OLD.id;
IF TG_OP = 'DELETE' THEN
INSERT INTO audit_logs(action, table_name, row_id, before_json, created_at, user_id)
VALUES ('DELETE', TG_TABLE_NAME, OLD.id::UUID, to_jsonb(OLD), NOW(), current_setting('user_id')::UUID);
ELSIF TG_OP = 'UPDATE' THEN
INSERT INTO audit_logs(action, table_name, row_id, before_json, after_json, created_at, user_id)
VALUES ('UPDATE', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(OLD), to_jsonb(NEW), NOW(), current_setting('user_id')::UUID);
ELSIF TG_OP = 'INSERT' THEN
INSERT INTO audit_logs(action, table_name, row_id, after_json, created_at, user_id)
VALUES ('INSERT', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(NEW), NOW(), current_setting('user_id')::UUID);
END IF;
ELSIF TG_TABLE_NAME = 'payments' THEN
IF OLD.deleted_at IS NOT NULL THEN
RETURN OLD;
END IF;
UPDATE payments SET deleted_at = NOW() WHERE id = OLD.id;
IF TG_OP = 'DELETE' THEN
INSERT INTO audit_logs(action, table_name, row_id, before_json, created_at, user_id)
VALUES ('DELETE', TG_TABLE_NAME, OLD.id::UUID, to_jsonb(OLD), NOW(), current_setting('user_id')::UUID);
ELSIF TG_OP = 'UPDATE' THEN
INSERT INTO audit_logs(action, table_name, row_id, before_json, after_json, created_at, user_id)
VALUES ('UPDATE', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(OLD), to_jsonb(NEW), NOW(), current_setting('user_id')::UUID);
ELSIF TG_OP = 'INSERT' THEN
INSERT INTO audit_logs(action, table_name, row_id, after_json, created_at, user_id)
VALUES ('INSERT', TG_TABLE_NAME, NEW.id::UUID, to_jsonb(NEW), NOW(), current_setting('user_id')::UUID);
END IF;
END IF;
RETURN COALESCE(NEW, OLD);
END;
$$ LANGUAGE plpgsql;
-- Create triggers for soft delete and audit logging on each table
CREATE TRIGGER trg_loads_soft_delete BEFORE UPDATE OR DELETE ON loads
FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit();
CREATE TRIGGER trg_loads_soft_delete_INSERT BEFORE INSERT ON loads
FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit();
CREATE TRIGGER trg_shippers_soft_delete BEFORE UPDATE OR DELETE ON shippers
FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit();
CREATE TRIGGER trg_shippers_soft_delete_INSERT BEFORE INSERT ON shippers
FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit();
CREATE TRIGGER trg_vehicles_soft_delete BEFORE UPDATE OR DELETE ON vehicles
FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit();
CREATE TRIGGER trg_vehicles_soft_delete_INSERT BEFORE INSERT ON vehicles
FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit();
CREATE TRIGGER trg_payments_soft_delete BEFORE UPDATE OR DELETE ON payments
FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit();
CREATE TRIGGER trg_payments_soft_delete_INSERT BEFORE INSERT ON payments
FOR EACH ROW EXECUTE FUNCTION soft_delete_and_audit();
-- ============================================================
-- AUDIT SETUP FUNCTION TO CAPTURE USER ID FROM SESSION CONTEXT
-- ============================================================
-- Note: PostgreSQL cannot directly access Express session, so we pass user_id
-- via SET user_id = '<session_user_id>' before triggering audits.
-- Application must set this variable before performing DB operations.