388 lines
12 KiB
PL/PgSQL
388 lines
12 KiB
PL/PgSQL
-- ============================================
|
|
-- Migration: V20260120_003
|
|
-- Description: Migrate audit.audit_logs structure
|
|
-- Changes:
|
|
-- - Rename entity_type -> resource_type
|
|
-- - Rename entity_id -> resource_id
|
|
-- - Add user_email, actor_type columns
|
|
-- - Add resource_name, severity columns
|
|
-- - Add request_id, session_id columns
|
|
-- - Add endpoint, http_method, response_status, duration_ms columns
|
|
-- - Change changes JSONB -> old_values, new_values, changed_fields
|
|
-- ============================================
|
|
|
|
-- UP Migration
|
|
BEGIN;
|
|
|
|
-- ============================================
|
|
-- 1. Ensure severity ENUM exists
|
|
-- ============================================
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_type
|
|
WHERE typname = 'severity'
|
|
AND typnamespace = (SELECT oid FROM pg_namespace WHERE nspname = 'audit')
|
|
) THEN
|
|
CREATE TYPE audit.severity AS ENUM ('info', 'warning', 'error', 'critical');
|
|
RAISE NOTICE 'Created audit.severity enum';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ============================================
|
|
-- 2. Rename entity_type -> resource_type (if legacy)
|
|
-- ============================================
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'audit' AND table_name = 'audit_logs' AND column_name = 'entity_type'
|
|
) THEN
|
|
-- Drop old index if exists
|
|
DROP INDEX IF EXISTS audit.idx_audit_logs_entity;
|
|
|
|
-- Rename column
|
|
ALTER TABLE audit.audit_logs RENAME COLUMN entity_type TO resource_type;
|
|
|
|
-- Update column type if needed
|
|
ALTER TABLE audit.audit_logs ALTER COLUMN resource_type TYPE VARCHAR(100);
|
|
|
|
RAISE NOTICE 'Renamed entity_type to resource_type';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ============================================
|
|
-- 3. Rename entity_id -> resource_id (if legacy)
|
|
-- ============================================
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'audit' AND table_name = 'audit_logs' AND column_name = 'entity_id'
|
|
) THEN
|
|
ALTER TABLE audit.audit_logs RENAME COLUMN entity_id TO resource_id;
|
|
ALTER TABLE audit.audit_logs ALTER COLUMN resource_id TYPE VARCHAR(255);
|
|
RAISE NOTICE 'Renamed entity_id to resource_id';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ============================================
|
|
-- 4. Add actor columns
|
|
-- ============================================
|
|
|
|
-- user_email
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'audit' AND table_name = 'audit_logs' AND column_name = 'user_email'
|
|
) THEN
|
|
ALTER TABLE audit.audit_logs ADD COLUMN user_email VARCHAR(255);
|
|
|
|
-- Backfill user_email from users table
|
|
UPDATE audit.audit_logs al
|
|
SET user_email = u.email
|
|
FROM users.users u
|
|
WHERE al.user_id = u.id
|
|
AND al.user_email IS NULL;
|
|
|
|
RAISE NOTICE 'Added user_email column and backfilled from users table';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- actor_type
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'audit' AND table_name = 'audit_logs' AND column_name = 'actor_type'
|
|
) THEN
|
|
ALTER TABLE audit.audit_logs ADD COLUMN actor_type VARCHAR(50) DEFAULT 'user';
|
|
RAISE NOTICE 'Added actor_type column';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ============================================
|
|
-- 5. Add resource_name column
|
|
-- ============================================
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'audit' AND table_name = 'audit_logs' AND column_name = 'resource_name'
|
|
) THEN
|
|
ALTER TABLE audit.audit_logs ADD COLUMN resource_name VARCHAR(255);
|
|
RAISE NOTICE 'Added resource_name column';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ============================================
|
|
-- 6. Add severity column
|
|
-- ============================================
|
|
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'audit' AND table_name = 'audit_logs' AND column_name = 'severity'
|
|
) THEN
|
|
ALTER TABLE audit.audit_logs ADD COLUMN severity audit.severity DEFAULT 'info';
|
|
|
|
-- Create index for severity filtering
|
|
CREATE INDEX idx_audit_logs_severity ON audit.audit_logs(tenant_id, severity)
|
|
WHERE severity IN ('warning', 'error', 'critical');
|
|
|
|
RAISE NOTICE 'Added severity column';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ============================================
|
|
-- 7. Add context columns
|
|
-- ============================================
|
|
|
|
-- request_id
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'audit' AND table_name = 'audit_logs' AND column_name = 'request_id'
|
|
) THEN
|
|
ALTER TABLE audit.audit_logs ADD COLUMN request_id VARCHAR(100);
|
|
RAISE NOTICE 'Added request_id column';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- session_id
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'audit' AND table_name = 'audit_logs' AND column_name = 'session_id'
|
|
) THEN
|
|
ALTER TABLE audit.audit_logs ADD COLUMN session_id UUID;
|
|
RAISE NOTICE 'Added session_id column';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ============================================
|
|
-- 8. Add HTTP context columns
|
|
-- ============================================
|
|
|
|
-- endpoint
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'audit' AND table_name = 'audit_logs' AND column_name = 'endpoint'
|
|
) THEN
|
|
ALTER TABLE audit.audit_logs ADD COLUMN endpoint VARCHAR(255);
|
|
RAISE NOTICE 'Added endpoint column';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- http_method
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'audit' AND table_name = 'audit_logs' AND column_name = 'http_method'
|
|
) THEN
|
|
ALTER TABLE audit.audit_logs ADD COLUMN http_method VARCHAR(10);
|
|
RAISE NOTICE 'Added http_method column';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- response_status
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'audit' AND table_name = 'audit_logs' AND column_name = 'response_status'
|
|
) THEN
|
|
ALTER TABLE audit.audit_logs ADD COLUMN response_status SMALLINT;
|
|
RAISE NOTICE 'Added response_status column';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- duration_ms
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'audit' AND table_name = 'audit_logs' AND column_name = 'duration_ms'
|
|
) THEN
|
|
ALTER TABLE audit.audit_logs ADD COLUMN duration_ms INTEGER;
|
|
RAISE NOTICE 'Added duration_ms column';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ============================================
|
|
-- 9. Migrate changes JSONB -> old_values, new_values, changed_fields
|
|
-- ============================================
|
|
|
|
-- old_values
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'audit' AND table_name = 'audit_logs' AND column_name = 'old_values'
|
|
) THEN
|
|
ALTER TABLE audit.audit_logs ADD COLUMN old_values JSONB;
|
|
RAISE NOTICE 'Added old_values column';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- new_values
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'audit' AND table_name = 'audit_logs' AND column_name = 'new_values'
|
|
) THEN
|
|
ALTER TABLE audit.audit_logs ADD COLUMN new_values JSONB;
|
|
RAISE NOTICE 'Added new_values column';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- changed_fields
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'audit' AND table_name = 'audit_logs' AND column_name = 'changed_fields'
|
|
) THEN
|
|
ALTER TABLE audit.audit_logs ADD COLUMN changed_fields JSONB;
|
|
RAISE NOTICE 'Added changed_fields column';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- Migrate data from 'changes' column if it exists
|
|
DO $$
|
|
BEGIN
|
|
IF EXISTS (
|
|
SELECT 1 FROM information_schema.columns
|
|
WHERE table_schema = 'audit' AND table_name = 'audit_logs' AND column_name = 'changes'
|
|
) THEN
|
|
-- Extract old_values from changes
|
|
UPDATE audit.audit_logs
|
|
SET old_values = changes->'old'
|
|
WHERE changes IS NOT NULL
|
|
AND changes ? 'old'
|
|
AND old_values IS NULL;
|
|
|
|
-- Extract new_values from changes
|
|
UPDATE audit.audit_logs
|
|
SET new_values = changes->'new'
|
|
WHERE changes IS NOT NULL
|
|
AND changes ? 'new'
|
|
AND new_values IS NULL;
|
|
|
|
-- Extract changed_fields from changes
|
|
UPDATE audit.audit_logs
|
|
SET changed_fields = changes->'fields'
|
|
WHERE changes IS NOT NULL
|
|
AND changes ? 'fields'
|
|
AND changed_fields IS NULL;
|
|
|
|
-- Alternative: if changes is flat diff, create changed_fields from keys
|
|
UPDATE audit.audit_logs
|
|
SET changed_fields = (
|
|
SELECT jsonb_agg(key)
|
|
FROM jsonb_object_keys(changes) AS key
|
|
WHERE key NOT IN ('old', 'new', 'fields')
|
|
)
|
|
WHERE changes IS NOT NULL
|
|
AND NOT (changes ? 'old')
|
|
AND NOT (changes ? 'new')
|
|
AND changed_fields IS NULL;
|
|
|
|
-- Drop old column after migration
|
|
ALTER TABLE audit.audit_logs DROP COLUMN changes;
|
|
|
|
RAISE NOTICE 'Migrated changes column to old_values, new_values, changed_fields';
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ============================================
|
|
-- 10. Update/create indexes for new structure
|
|
-- ============================================
|
|
|
|
-- Index for resource queries
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_indexes
|
|
WHERE schemaname = 'audit' AND indexname = 'idx_audit_logs_resource'
|
|
) THEN
|
|
CREATE INDEX idx_audit_logs_resource ON audit.audit_logs(tenant_id, resource_type, resource_id);
|
|
END IF;
|
|
END $$;
|
|
|
|
-- ============================================
|
|
-- 11. Update audit.log_event function
|
|
-- ============================================
|
|
|
|
CREATE OR REPLACE FUNCTION audit.log_event(
|
|
p_tenant_id UUID,
|
|
p_user_id UUID,
|
|
p_action audit.action_type,
|
|
p_resource_type VARCHAR,
|
|
p_resource_id VARCHAR DEFAULT NULL,
|
|
p_resource_name VARCHAR DEFAULT NULL,
|
|
p_description TEXT DEFAULT NULL,
|
|
p_old_values JSONB DEFAULT NULL,
|
|
p_new_values JSONB DEFAULT NULL,
|
|
p_changed_fields JSONB DEFAULT NULL,
|
|
p_metadata JSONB DEFAULT '{}'::jsonb,
|
|
p_severity audit.severity DEFAULT 'info'
|
|
)
|
|
RETURNS UUID AS $$
|
|
DECLARE
|
|
v_id UUID;
|
|
v_user_email VARCHAR(255);
|
|
BEGIN
|
|
-- Get user email for denormalization
|
|
SELECT email INTO v_user_email
|
|
FROM users.users
|
|
WHERE id = p_user_id;
|
|
|
|
INSERT INTO audit.audit_logs (
|
|
tenant_id, user_id, user_email, action, resource_type, resource_id,
|
|
resource_name, description, old_values, new_values, changed_fields,
|
|
metadata, severity
|
|
) VALUES (
|
|
p_tenant_id, p_user_id, v_user_email, p_action, p_resource_type, p_resource_id,
|
|
p_resource_name, p_description, p_old_values, p_new_values, p_changed_fields,
|
|
p_metadata, p_severity
|
|
) RETURNING id INTO v_id;
|
|
|
|
RETURN v_id;
|
|
END;
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
-- ============================================
|
|
-- 12. Update comments
|
|
-- ============================================
|
|
|
|
COMMENT ON COLUMN audit.audit_logs.user_email IS 'Denormalized user email for fast queries';
|
|
COMMENT ON COLUMN audit.audit_logs.actor_type IS 'Type of actor: user, system, api_key, webhook';
|
|
COMMENT ON COLUMN audit.audit_logs.resource_type IS 'Type of resource: user, product, invoice, etc.';
|
|
COMMENT ON COLUMN audit.audit_logs.resource_id IS 'ID of the affected resource';
|
|
COMMENT ON COLUMN audit.audit_logs.resource_name IS 'Human-readable name of the resource';
|
|
COMMENT ON COLUMN audit.audit_logs.severity IS 'Log severity level: info, warning, error, critical';
|
|
COMMENT ON COLUMN audit.audit_logs.request_id IS 'Correlation ID for request tracing';
|
|
COMMENT ON COLUMN audit.audit_logs.session_id IS 'Session ID for session correlation';
|
|
COMMENT ON COLUMN audit.audit_logs.endpoint IS 'API endpoint path';
|
|
COMMENT ON COLUMN audit.audit_logs.http_method IS 'HTTP method: GET, POST, PUT, DELETE, etc.';
|
|
COMMENT ON COLUMN audit.audit_logs.response_status IS 'HTTP response status code';
|
|
COMMENT ON COLUMN audit.audit_logs.duration_ms IS 'Request duration in milliseconds';
|
|
COMMENT ON COLUMN audit.audit_logs.old_values IS 'Previous values before change';
|
|
COMMENT ON COLUMN audit.audit_logs.new_values IS 'New values after change';
|
|
COMMENT ON COLUMN audit.audit_logs.changed_fields IS 'List of fields that were changed';
|
|
|
|
COMMIT;
|