chore: perauset customer project
This commit is contained in:
88
sql/0002-audit-log.sql
Normal file
88
sql/0002-audit-log.sql
Normal file
@@ -0,0 +1,88 @@
|
||||
-- Audit log table
|
||||
CREATE TABLE app.audit_log (
|
||||
audit_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
table_name TEXT NOT NULL,
|
||||
record_id TEXT NOT NULL,
|
||||
action TEXT NOT NULL, -- 'INSERT', 'UPDATE', 'DELETE'
|
||||
user_id UUID, -- from session variable, nullable for system actions
|
||||
changed_fields JSONB, -- UPDATE: {field: {old, new}}; INSERT: full row; DELETE: full row
|
||||
occurred_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_audit_log_table_record ON app.audit_log(table_name, record_id);
|
||||
CREATE INDEX idx_audit_log_user ON app.audit_log(user_id);
|
||||
CREATE INDEX idx_audit_log_occurred ON app.audit_log(occurred_at);
|
||||
|
||||
-- Generic trigger function
|
||||
CREATE OR REPLACE FUNCTION app.audit_trigger_func() RETURNS TRIGGER AS $$
|
||||
DECLARE
|
||||
_user_id UUID;
|
||||
_record_id TEXT;
|
||||
_changed JSONB;
|
||||
_old JSONB;
|
||||
_new JSONB;
|
||||
_key TEXT;
|
||||
BEGIN
|
||||
-- Get current user from session variable (set per-request by app)
|
||||
BEGIN
|
||||
_user_id := current_setting('app.current_user_id', true)::UUID;
|
||||
EXCEPTION WHEN OTHERS THEN
|
||||
_user_id := NULL;
|
||||
END;
|
||||
|
||||
IF TG_OP = 'DELETE' THEN
|
||||
_record_id := (row_to_json(OLD) ->> TG_ARGV[0])::TEXT;
|
||||
INSERT INTO app.audit_log (table_name, record_id, action, user_id, changed_fields)
|
||||
VALUES (TG_TABLE_NAME, _record_id, 'DELETE', _user_id, row_to_json(OLD)::JSONB);
|
||||
RETURN OLD;
|
||||
|
||||
ELSIF TG_OP = 'INSERT' THEN
|
||||
_record_id := (row_to_json(NEW) ->> TG_ARGV[0])::TEXT;
|
||||
INSERT INTO app.audit_log (table_name, record_id, action, user_id, changed_fields)
|
||||
VALUES (TG_TABLE_NAME, _record_id, 'INSERT', _user_id, row_to_json(NEW)::JSONB);
|
||||
RETURN NEW;
|
||||
|
||||
ELSIF TG_OP = 'UPDATE' THEN
|
||||
_record_id := (row_to_json(NEW) ->> TG_ARGV[0])::TEXT;
|
||||
_old := row_to_json(OLD)::JSONB;
|
||||
_new := row_to_json(NEW)::JSONB;
|
||||
_changed := '{}'::JSONB;
|
||||
|
||||
FOR _key IN SELECT jsonb_object_keys(_new)
|
||||
LOOP
|
||||
IF _key IN ('updated_at', 'created_at') THEN CONTINUE; END IF;
|
||||
IF (_old ->> _key) IS DISTINCT FROM (_new ->> _key) THEN
|
||||
_changed := _changed || jsonb_build_object(
|
||||
_key, jsonb_build_object('old', _old -> _key, 'new', _new -> _key)
|
||||
);
|
||||
END IF;
|
||||
END LOOP;
|
||||
|
||||
-- Skip if nothing actually changed
|
||||
IF _changed = '{}'::JSONB THEN RETURN NEW; END IF;
|
||||
|
||||
INSERT INTO app.audit_log (table_name, record_id, action, user_id, changed_fields)
|
||||
VALUES (TG_TABLE_NAME, _record_id, 'UPDATE', _user_id, _changed);
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
|
||||
RETURN NULL;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Helper to attach audit trigger to a table
|
||||
CREATE OR REPLACE FUNCTION app.enable_audit(target_table TEXT, pk_column TEXT DEFAULT 'id') RETURNS VOID AS $$
|
||||
DECLARE
|
||||
trigger_name TEXT;
|
||||
BEGIN
|
||||
trigger_name := 'audit_' || replace(target_table, '"', '');
|
||||
EXECUTE format(
|
||||
'CREATE TRIGGER %I AFTER INSERT OR UPDATE OR DELETE ON app.%s FOR EACH ROW EXECUTE FUNCTION app.audit_trigger_func(%L)',
|
||||
trigger_name, target_table, pk_column
|
||||
);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- Enable audit on existing tables
|
||||
SELECT app.enable_audit('"user"', 'user_id');
|
||||
SELECT app.enable_audit('user_profile', 'profile_id');
|
||||
Reference in New Issue
Block a user