-- Migration: Supporting Systems -- Description: Creates keys, artifacts, queue_stats, pack_environment, pack_testing, -- and webhook function tables. -- Consolidates former migrations: 000009 (keys_artifacts), 000010 (webhook_system), -- 000011 (pack_environments), and 000012 (pack_testing). -- Version: 20250101000007 -- ============================================================================ -- KEY TABLE -- ============================================================================ CREATE TABLE key ( id BIGSERIAL PRIMARY KEY, ref TEXT NOT NULL UNIQUE, owner_type owner_type_enum NOT NULL, owner TEXT, owner_identity BIGINT REFERENCES identity(id), owner_pack BIGINT REFERENCES pack(id), owner_pack_ref TEXT, owner_action BIGINT, -- Forward reference to action table owner_action_ref TEXT, owner_sensor BIGINT, -- Forward reference to sensor table owner_sensor_ref TEXT, name TEXT NOT NULL, encrypted BOOLEAN NOT NULL, encryption_key_hash TEXT, value TEXT NOT NULL, created TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated TIMESTAMPTZ NOT NULL DEFAULT NOW(), -- Constraints CONSTRAINT key_ref_lowercase CHECK (ref = LOWER(ref)), CONSTRAINT key_ref_format CHECK (ref ~ '^[^.]+(\.[^.]+)*$') ); -- Unique index on owner_type, owner, name CREATE UNIQUE INDEX idx_key_unique ON key(owner_type, owner, name); -- Indexes CREATE INDEX idx_key_ref ON key(ref); CREATE INDEX idx_key_owner_type ON key(owner_type); CREATE INDEX idx_key_owner_identity ON key(owner_identity); CREATE INDEX idx_key_owner_pack ON key(owner_pack); CREATE INDEX idx_key_owner_action ON key(owner_action); CREATE INDEX idx_key_owner_sensor ON key(owner_sensor); CREATE INDEX idx_key_created ON key(created DESC); CREATE INDEX idx_key_owner_type_owner ON key(owner_type, owner); CREATE INDEX idx_key_owner_identity_name ON key(owner_identity, name); CREATE INDEX idx_key_owner_pack_name ON key(owner_pack, name); -- Function to validate and set owner fields CREATE OR REPLACE FUNCTION validate_key_owner() RETURNS TRIGGER AS $$ DECLARE owner_count INTEGER := 0; BEGIN -- Count how many owner fields are set IF NEW.owner_identity IS NOT NULL THEN owner_count := owner_count + 1; END IF; IF NEW.owner_pack IS NOT NULL THEN owner_count := owner_count + 1; END IF; IF NEW.owner_action IS NOT NULL THEN owner_count := owner_count + 1; END IF; IF NEW.owner_sensor IS NOT NULL THEN owner_count := owner_count + 1; END IF; -- System owner should have no owner fields set IF NEW.owner_type = 'system' THEN IF owner_count > 0 THEN RAISE EXCEPTION 'System owner cannot have specific owner fields set'; END IF; NEW.owner := 'system'; -- All other types must have exactly one owner field set ELSIF owner_count != 1 THEN RAISE EXCEPTION 'Exactly one owner field must be set for owner_type %', NEW.owner_type; -- Validate owner_type matches the populated field and set owner ELSIF NEW.owner_type = 'identity' THEN IF NEW.owner_identity IS NULL THEN RAISE EXCEPTION 'owner_identity must be set for owner_type identity'; END IF; NEW.owner := NEW.owner_identity::TEXT; ELSIF NEW.owner_type = 'pack' THEN IF NEW.owner_pack IS NULL THEN RAISE EXCEPTION 'owner_pack must be set for owner_type pack'; END IF; NEW.owner := NEW.owner_pack::TEXT; ELSIF NEW.owner_type = 'action' THEN IF NEW.owner_action IS NULL THEN RAISE EXCEPTION 'owner_action must be set for owner_type action'; END IF; NEW.owner := NEW.owner_action::TEXT; ELSIF NEW.owner_type = 'sensor' THEN IF NEW.owner_sensor IS NULL THEN RAISE EXCEPTION 'owner_sensor must be set for owner_type sensor'; END IF; NEW.owner := NEW.owner_sensor::TEXT; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; -- Trigger to validate owner fields CREATE TRIGGER validate_key_owner_trigger BEFORE INSERT OR UPDATE ON key FOR EACH ROW EXECUTE FUNCTION validate_key_owner(); -- Trigger for updated timestamp CREATE TRIGGER update_key_updated BEFORE UPDATE ON key FOR EACH ROW EXECUTE FUNCTION update_updated_column(); -- Comments COMMENT ON TABLE key IS 'Keys store configuration values and secrets with ownership scoping'; COMMENT ON COLUMN key.ref IS 'Unique key reference (format: [owner.]name)'; COMMENT ON COLUMN key.owner_type IS 'Type of owner (system, identity, pack, action, sensor)'; COMMENT ON COLUMN key.owner IS 'Owner identifier (auto-populated by trigger)'; COMMENT ON COLUMN key.owner_identity IS 'Identity owner (if owner_type=identity)'; COMMENT ON COLUMN key.owner_pack IS 'Pack owner (if owner_type=pack)'; COMMENT ON COLUMN key.owner_pack_ref IS 'Pack reference for owner_pack'; COMMENT ON COLUMN key.owner_action IS 'Action owner (if owner_type=action)'; COMMENT ON COLUMN key.owner_sensor IS 'Sensor owner (if owner_type=sensor)'; COMMENT ON COLUMN key.name IS 'Key name within owner scope'; COMMENT ON COLUMN key.encrypted IS 'Whether the value is encrypted'; COMMENT ON COLUMN key.encryption_key_hash IS 'Hash of encryption key used'; COMMENT ON COLUMN key.value IS 'The actual value (encrypted if encrypted=true)'; -- Add foreign key constraints for action and sensor references ALTER TABLE key ADD CONSTRAINT key_owner_action_fkey FOREIGN KEY (owner_action) REFERENCES action(id) ON DELETE CASCADE; ALTER TABLE key ADD CONSTRAINT key_owner_sensor_fkey FOREIGN KEY (owner_sensor) REFERENCES sensor(id) ON DELETE CASCADE; -- ============================================================================ -- ARTIFACT TABLE -- ============================================================================ CREATE TABLE artifact ( id BIGSERIAL PRIMARY KEY, ref TEXT NOT NULL, scope owner_type_enum NOT NULL DEFAULT 'system', owner TEXT NOT NULL DEFAULT '', type artifact_type_enum NOT NULL, retention_policy artifact_retention_enum NOT NULL DEFAULT 'versions', retention_limit INTEGER NOT NULL DEFAULT 1, created TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Indexes CREATE INDEX idx_artifact_ref ON artifact(ref); CREATE INDEX idx_artifact_scope ON artifact(scope); CREATE INDEX idx_artifact_owner ON artifact(owner); CREATE INDEX idx_artifact_type ON artifact(type); CREATE INDEX idx_artifact_created ON artifact(created DESC); CREATE INDEX idx_artifact_scope_owner ON artifact(scope, owner); CREATE INDEX idx_artifact_type_created ON artifact(type, created DESC); -- Trigger CREATE TRIGGER update_artifact_updated BEFORE UPDATE ON artifact FOR EACH ROW EXECUTE FUNCTION update_updated_column(); -- Comments COMMENT ON TABLE artifact IS 'Artifacts track files, logs, and outputs from executions'; COMMENT ON COLUMN artifact.ref IS 'Artifact reference/path'; COMMENT ON COLUMN artifact.scope IS 'Owner type (system, identity, pack, action, sensor)'; COMMENT ON COLUMN artifact.owner IS 'Owner identifier'; COMMENT ON COLUMN artifact.type IS 'Artifact type (file, url, progress, etc.)'; COMMENT ON COLUMN artifact.retention_policy IS 'How to retain artifacts (versions, days, hours, minutes)'; COMMENT ON COLUMN artifact.retention_limit IS 'Numeric limit for retention policy'; -- ============================================================================ -- QUEUE_STATS TABLE -- ============================================================================ CREATE TABLE queue_stats ( action_id BIGINT PRIMARY KEY REFERENCES action(id) ON DELETE CASCADE, queue_length INTEGER NOT NULL DEFAULT 0, active_count INTEGER NOT NULL DEFAULT 0, max_concurrent INTEGER NOT NULL DEFAULT 1, oldest_enqueued_at TIMESTAMPTZ, total_enqueued BIGINT NOT NULL DEFAULT 0, total_completed BIGINT NOT NULL DEFAULT 0, last_updated TIMESTAMPTZ NOT NULL DEFAULT NOW() ); -- Indexes CREATE INDEX idx_queue_stats_last_updated ON queue_stats(last_updated); -- Comments COMMENT ON TABLE queue_stats IS 'Real-time queue statistics for action execution ordering'; COMMENT ON COLUMN queue_stats.action_id IS 'Foreign key to action table'; COMMENT ON COLUMN queue_stats.queue_length IS 'Number of executions waiting in queue'; COMMENT ON COLUMN queue_stats.active_count IS 'Number of currently running executions'; COMMENT ON COLUMN queue_stats.max_concurrent IS 'Maximum concurrent executions allowed'; COMMENT ON COLUMN queue_stats.oldest_enqueued_at IS 'Timestamp of oldest queued execution (NULL if queue empty)'; COMMENT ON COLUMN queue_stats.total_enqueued IS 'Total executions enqueued since queue creation'; COMMENT ON COLUMN queue_stats.total_completed IS 'Total executions completed since queue creation'; COMMENT ON COLUMN queue_stats.last_updated IS 'Timestamp of last statistics update'; -- ============================================================================ -- PACK ENVIRONMENT TABLE -- ============================================================================ CREATE TABLE IF NOT EXISTS pack_environment ( id BIGSERIAL PRIMARY KEY, pack BIGINT NOT NULL REFERENCES pack(id) ON DELETE CASCADE, pack_ref TEXT NOT NULL, runtime BIGINT NOT NULL REFERENCES runtime(id) ON DELETE CASCADE, runtime_ref TEXT NOT NULL, env_path TEXT NOT NULL, status pack_environment_status_enum NOT NULL DEFAULT 'pending', installed_at TIMESTAMPTZ, last_verified TIMESTAMPTZ, install_log TEXT, install_error TEXT, metadata JSONB DEFAULT '{}'::jsonb, created TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated TIMESTAMPTZ NOT NULL DEFAULT NOW(), UNIQUE(pack, runtime) ); -- Indexes CREATE INDEX IF NOT EXISTS idx_pack_environment_pack ON pack_environment(pack); CREATE INDEX IF NOT EXISTS idx_pack_environment_runtime ON pack_environment(runtime); CREATE INDEX IF NOT EXISTS idx_pack_environment_status ON pack_environment(status); CREATE INDEX IF NOT EXISTS idx_pack_environment_pack_ref ON pack_environment(pack_ref); CREATE INDEX IF NOT EXISTS idx_pack_environment_runtime_ref ON pack_environment(runtime_ref); CREATE INDEX IF NOT EXISTS idx_pack_environment_pack_runtime ON pack_environment(pack, runtime); -- Trigger for updated timestamp CREATE TRIGGER update_pack_environment_updated BEFORE UPDATE ON pack_environment FOR EACH ROW EXECUTE FUNCTION update_updated_column(); -- Comments COMMENT ON TABLE pack_environment IS 'Tracks pack-specific runtime environments for dependency isolation'; COMMENT ON COLUMN pack_environment.pack IS 'Pack that owns this environment'; COMMENT ON COLUMN pack_environment.pack_ref IS 'Pack reference for quick lookup'; COMMENT ON COLUMN pack_environment.runtime IS 'Runtime used for this environment'; COMMENT ON COLUMN pack_environment.runtime_ref IS 'Runtime reference for quick lookup'; COMMENT ON COLUMN pack_environment.env_path IS 'Filesystem path to the environment directory (e.g., /opt/attune/packenvs/mypack/python)'; COMMENT ON COLUMN pack_environment.status IS 'Current installation status'; COMMENT ON COLUMN pack_environment.installed_at IS 'When the environment was successfully installed'; COMMENT ON COLUMN pack_environment.last_verified IS 'Last time the environment was verified as working'; COMMENT ON COLUMN pack_environment.install_log IS 'Installation output logs'; COMMENT ON COLUMN pack_environment.install_error IS 'Error message if installation failed'; COMMENT ON COLUMN pack_environment.metadata IS 'Additional metadata (installed packages, versions, etc.)'; -- ============================================================================ -- PACK ENVIRONMENT: Update existing runtimes with installer metadata -- ============================================================================ -- Python runtime installers UPDATE runtime SET installers = jsonb_build_object( 'base_path_template', '/opt/attune/packenvs/{pack_ref}/{runtime_name_lower}', 'installers', jsonb_build_array( jsonb_build_object( 'name', 'create_venv', 'description', 'Create Python virtual environment', 'command', 'python3', 'args', jsonb_build_array('-m', 'venv', '{env_path}'), 'cwd', '{pack_path}', 'env', jsonb_build_object(), 'order', 1, 'optional', false ), jsonb_build_object( 'name', 'upgrade_pip', 'description', 'Upgrade pip to latest version', 'command', '{env_path}/bin/pip', 'args', jsonb_build_array('install', '--upgrade', 'pip'), 'cwd', '{pack_path}', 'env', jsonb_build_object(), 'order', 2, 'optional', true ), jsonb_build_object( 'name', 'install_requirements', 'description', 'Install pack Python dependencies', 'command', '{env_path}/bin/pip', 'args', jsonb_build_array('install', '-r', '{pack_path}/requirements.txt'), 'cwd', '{pack_path}', 'env', jsonb_build_object(), 'order', 3, 'optional', false, 'condition', jsonb_build_object( 'file_exists', '{pack_path}/requirements.txt' ) ) ), 'executable_templates', jsonb_build_object( 'python', '{env_path}/bin/python', 'pip', '{env_path}/bin/pip' ) ) WHERE ref = 'core.python'; -- Node.js runtime installers UPDATE runtime SET installers = jsonb_build_object( 'base_path_template', '/opt/attune/packenvs/{pack_ref}/{runtime_name_lower}', 'installers', jsonb_build_array( jsonb_build_object( 'name', 'npm_install', 'description', 'Install Node.js dependencies', 'command', 'npm', 'args', jsonb_build_array('install', '--prefix', '{env_path}'), 'cwd', '{pack_path}', 'env', jsonb_build_object( 'NODE_PATH', '{env_path}/node_modules' ), 'order', 1, 'optional', false, 'condition', jsonb_build_object( 'file_exists', '{pack_path}/package.json' ) ) ), 'executable_templates', jsonb_build_object( 'node', 'node', 'npm', 'npm' ), 'env_vars', jsonb_build_object( 'NODE_PATH', '{env_path}/node_modules' ) ) WHERE ref = 'core.nodejs'; -- Shell runtime (no environment needed, uses system shell) UPDATE runtime SET installers = jsonb_build_object( 'base_path_template', '/opt/attune/packenvs/{pack_ref}/{runtime_name_lower}', 'installers', jsonb_build_array(), 'executable_templates', jsonb_build_object( 'sh', 'sh', 'bash', 'bash' ), 'requires_environment', false ) WHERE ref = 'core.shell'; -- Native runtime (no environment needed, binaries are standalone) UPDATE runtime SET installers = jsonb_build_object( 'base_path_template', '/opt/attune/packenvs/{pack_ref}/{runtime_name_lower}', 'installers', jsonb_build_array(), 'executable_templates', jsonb_build_object(), 'requires_environment', false ) WHERE ref = 'core.native'; -- Built-in sensor runtime (internal, no environment) UPDATE runtime SET installers = jsonb_build_object( 'installers', jsonb_build_array(), 'requires_environment', false ) WHERE ref = 'core.sensor.builtin'; -- ============================================================================ -- PACK ENVIRONMENT: Helper functions -- ============================================================================ -- Function to get environment path for a pack/runtime combination CREATE OR REPLACE FUNCTION get_pack_environment_path(p_pack_ref TEXT, p_runtime_ref TEXT) RETURNS TEXT AS $$ DECLARE v_runtime_name TEXT; v_base_template TEXT; v_result TEXT; BEGIN -- Get runtime name and base path template SELECT LOWER(name), installers->>'base_path_template' INTO v_runtime_name, v_base_template FROM runtime WHERE ref = p_runtime_ref; IF v_base_template IS NULL THEN v_base_template := '/opt/attune/packenvs/{pack_ref}/{runtime_name_lower}'; END IF; -- Replace template variables v_result := v_base_template; v_result := REPLACE(v_result, '{pack_ref}', p_pack_ref); v_result := REPLACE(v_result, '{runtime_ref}', p_runtime_ref); v_result := REPLACE(v_result, '{runtime_name_lower}', v_runtime_name); RETURN v_result; END; $$ LANGUAGE plpgsql IMMUTABLE; COMMENT ON FUNCTION get_pack_environment_path IS 'Calculate the filesystem path for a pack runtime environment'; -- Function to check if a runtime requires an environment CREATE OR REPLACE FUNCTION runtime_requires_environment(p_runtime_ref TEXT) RETURNS BOOLEAN AS $$ DECLARE v_requires BOOLEAN; BEGIN SELECT COALESCE((installers->>'requires_environment')::boolean, true) INTO v_requires FROM runtime WHERE ref = p_runtime_ref; RETURN COALESCE(v_requires, false); END; $$ LANGUAGE plpgsql STABLE; COMMENT ON FUNCTION runtime_requires_environment IS 'Check if a runtime needs a pack-specific environment'; -- ============================================================================ -- PACK ENVIRONMENT: Status view -- ============================================================================ CREATE OR REPLACE VIEW v_pack_environment_status AS SELECT pe.id, pe.pack, p.ref AS pack_ref, p.label AS pack_name, pe.runtime, r.ref AS runtime_ref, r.name AS runtime_name, pe.env_path, pe.status, pe.installed_at, pe.last_verified, CASE WHEN pe.status = 'ready' AND pe.last_verified < NOW() - INTERVAL '7 days' THEN true ELSE false END AS needs_verification, CASE WHEN pe.status = 'ready' THEN 'healthy' WHEN pe.status = 'failed' THEN 'unhealthy' WHEN pe.status IN ('pending', 'installing') THEN 'provisioning' WHEN pe.status = 'outdated' THEN 'needs_update' ELSE 'unknown' END AS health_status, pe.install_error, pe.created, pe.updated FROM pack_environment pe JOIN pack p ON pe.pack = p.id JOIN runtime r ON pe.runtime = r.id; COMMENT ON VIEW v_pack_environment_status IS 'Consolidated view of pack environment status with health indicators'; -- ============================================================================ -- PACK TEST EXECUTION TABLE -- ============================================================================ CREATE TABLE IF NOT EXISTS pack_test_execution ( id BIGSERIAL PRIMARY KEY, pack_id BIGINT NOT NULL REFERENCES pack(id) ON DELETE CASCADE, pack_version VARCHAR(50) NOT NULL, execution_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), trigger_reason VARCHAR(50) NOT NULL, -- 'install', 'update', 'manual', 'validation' total_tests INT NOT NULL, passed INT NOT NULL, failed INT NOT NULL, skipped INT NOT NULL, pass_rate DECIMAL(5,4) NOT NULL, -- 0.0000 to 1.0000 duration_ms BIGINT NOT NULL, result JSONB NOT NULL, -- Full test result structure created TIMESTAMPTZ NOT NULL DEFAULT NOW(), CONSTRAINT valid_test_counts CHECK (total_tests >= 0 AND passed >= 0 AND failed >= 0 AND skipped >= 0), CONSTRAINT valid_pass_rate CHECK (pass_rate >= 0.0 AND pass_rate <= 1.0), CONSTRAINT valid_trigger_reason CHECK (trigger_reason IN ('install', 'update', 'manual', 'validation')) ); -- Indexes for efficient queries CREATE INDEX idx_pack_test_execution_pack_id ON pack_test_execution(pack_id); CREATE INDEX idx_pack_test_execution_time ON pack_test_execution(execution_time DESC); CREATE INDEX idx_pack_test_execution_pass_rate ON pack_test_execution(pass_rate); CREATE INDEX idx_pack_test_execution_trigger ON pack_test_execution(trigger_reason); -- Comments for documentation COMMENT ON TABLE pack_test_execution IS 'Tracks pack test execution results for validation and auditing'; COMMENT ON COLUMN pack_test_execution.pack_id IS 'Reference to the pack being tested'; COMMENT ON COLUMN pack_test_execution.pack_version IS 'Version of the pack at test time'; COMMENT ON COLUMN pack_test_execution.trigger_reason IS 'What triggered the test: install, update, manual, validation'; COMMENT ON COLUMN pack_test_execution.pass_rate IS 'Percentage of tests passed (0.0 to 1.0)'; COMMENT ON COLUMN pack_test_execution.result IS 'Full JSON structure with detailed test results'; -- Pack test result summary view (all test executions with pack info) CREATE OR REPLACE VIEW pack_test_summary AS SELECT p.id AS pack_id, p.ref AS pack_ref, p.label AS pack_label, pte.id AS test_execution_id, pte.pack_version, pte.execution_time AS test_time, pte.trigger_reason, pte.total_tests, pte.passed, pte.failed, pte.skipped, pte.pass_rate, pte.duration_ms, ROW_NUMBER() OVER (PARTITION BY p.id ORDER BY pte.execution_time DESC) AS rn FROM pack p LEFT JOIN pack_test_execution pte ON p.id = pte.pack_id WHERE pte.id IS NOT NULL; COMMENT ON VIEW pack_test_summary IS 'Summary of all pack test executions with pack details'; -- Latest test results per pack view CREATE OR REPLACE VIEW pack_latest_test AS SELECT pack_id, pack_ref, pack_label, test_execution_id, pack_version, test_time, trigger_reason, total_tests, passed, failed, skipped, pass_rate, duration_ms FROM pack_test_summary WHERE rn = 1; COMMENT ON VIEW pack_latest_test IS 'Latest test results for each pack'; -- Function to get pack test statistics CREATE OR REPLACE FUNCTION get_pack_test_stats(p_pack_id BIGINT) RETURNS TABLE ( total_executions BIGINT, successful_executions BIGINT, failed_executions BIGINT, avg_pass_rate DECIMAL, avg_duration_ms BIGINT, last_test_time TIMESTAMPTZ, last_test_passed BOOLEAN ) AS $$ BEGIN RETURN QUERY SELECT COUNT(*)::BIGINT AS total_executions, COUNT(*) FILTER (WHERE passed = total_tests)::BIGINT AS successful_executions, COUNT(*) FILTER (WHERE failed > 0)::BIGINT AS failed_executions, AVG(pass_rate) AS avg_pass_rate, AVG(duration_ms)::BIGINT AS avg_duration_ms, MAX(execution_time) AS last_test_time, (SELECT failed = 0 FROM pack_test_execution WHERE pack_id = p_pack_id ORDER BY execution_time DESC LIMIT 1) AS last_test_passed FROM pack_test_execution WHERE pack_id = p_pack_id; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION get_pack_test_stats IS 'Get statistical summary of test executions for a pack'; -- Function to check if pack has recent passing tests CREATE OR REPLACE FUNCTION pack_has_passing_tests( p_pack_id BIGINT, p_hours_ago INT DEFAULT 24 ) RETURNS BOOLEAN AS $$ DECLARE v_has_passing_tests BOOLEAN; BEGIN SELECT EXISTS( SELECT 1 FROM pack_test_execution WHERE pack_id = p_pack_id AND execution_time > NOW() - (p_hours_ago || ' hours')::INTERVAL AND failed = 0 AND total_tests > 0 ) INTO v_has_passing_tests; RETURN v_has_passing_tests; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION pack_has_passing_tests IS 'Check if pack has recent passing test executions'; -- Add trigger to update pack metadata on test execution CREATE OR REPLACE FUNCTION update_pack_test_metadata() RETURNS TRIGGER AS $$ BEGIN -- Could update pack table with last_tested timestamp if we add that column -- For now, just a placeholder for future functionality RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER trigger_update_pack_test_metadata AFTER INSERT ON pack_test_execution FOR EACH ROW EXECUTE FUNCTION update_pack_test_metadata(); COMMENT ON TRIGGER trigger_update_pack_test_metadata ON pack_test_execution IS 'Updates pack metadata when tests are executed'; -- ============================================================================ -- WEBHOOK FUNCTIONS -- ============================================================================ -- Drop existing functions to avoid signature conflicts DROP FUNCTION IF EXISTS enable_trigger_webhook(BIGINT, JSONB); DROP FUNCTION IF EXISTS enable_trigger_webhook(BIGINT); DROP FUNCTION IF EXISTS disable_trigger_webhook(BIGINT); DROP FUNCTION IF EXISTS regenerate_trigger_webhook_key(BIGINT); -- Function to enable webhooks for a trigger CREATE OR REPLACE FUNCTION enable_trigger_webhook( p_trigger_id BIGINT, p_config JSONB DEFAULT '{}'::jsonb ) RETURNS TABLE( webhook_enabled BOOLEAN, webhook_key VARCHAR(255), webhook_url TEXT ) AS $$ DECLARE v_webhook_key VARCHAR(255); v_api_base_url TEXT := 'http://localhost:8080'; -- Default, should be configured BEGIN -- Check if trigger exists IF NOT EXISTS (SELECT 1 FROM trigger WHERE id = p_trigger_id) THEN RAISE EXCEPTION 'Trigger with id % does not exist', p_trigger_id; END IF; -- Generate webhook key if one doesn't exist SELECT t.webhook_key INTO v_webhook_key FROM trigger t WHERE t.id = p_trigger_id; IF v_webhook_key IS NULL THEN v_webhook_key := generate_webhook_key(); END IF; -- Update trigger to enable webhooks UPDATE trigger SET webhook_enabled = TRUE, webhook_key = v_webhook_key, webhook_config = p_config, updated = NOW() WHERE id = p_trigger_id; -- Return webhook details RETURN QUERY SELECT TRUE, v_webhook_key, v_api_base_url || '/api/v1/webhooks/' || v_webhook_key; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION enable_trigger_webhook(BIGINT, JSONB) IS 'Enables webhooks for a trigger with optional configuration. Generates a new webhook key if one does not exist. Returns webhook details.'; -- Function to disable webhooks for a trigger CREATE OR REPLACE FUNCTION disable_trigger_webhook( p_trigger_id BIGINT ) RETURNS BOOLEAN AS $$ BEGIN -- Check if trigger exists IF NOT EXISTS (SELECT 1 FROM trigger WHERE id = p_trigger_id) THEN RAISE EXCEPTION 'Trigger with id % does not exist', p_trigger_id; END IF; -- Update trigger to disable webhooks -- Set webhook_key to NULL when disabling to remove it from API responses UPDATE trigger SET webhook_enabled = FALSE, webhook_key = NULL, updated = NOW() WHERE id = p_trigger_id; RETURN TRUE; END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION disable_trigger_webhook(BIGINT) IS 'Disables webhooks for a trigger. Webhook key is removed when disabled.'; -- Function to regenerate webhook key for a trigger CREATE OR REPLACE FUNCTION regenerate_trigger_webhook_key( p_trigger_id BIGINT ) RETURNS TABLE( webhook_key VARCHAR(255), previous_key_revoked BOOLEAN ) AS $$ DECLARE v_new_key VARCHAR(255); v_old_key VARCHAR(255); v_webhook_enabled BOOLEAN; BEGIN -- Check if trigger exists IF NOT EXISTS (SELECT 1 FROM trigger WHERE id = p_trigger_id) THEN RAISE EXCEPTION 'Trigger with id % does not exist', p_trigger_id; END IF; -- Get current webhook state SELECT t.webhook_key, t.webhook_enabled INTO v_old_key, v_webhook_enabled FROM trigger t WHERE t.id = p_trigger_id; -- Check if webhooks are enabled IF NOT v_webhook_enabled THEN RAISE EXCEPTION 'Webhooks are not enabled for trigger %', p_trigger_id; END IF; -- Generate new key v_new_key := generate_webhook_key(); -- Update trigger with new key UPDATE trigger SET webhook_key = v_new_key, updated = NOW() WHERE id = p_trigger_id; -- Return new key and whether old key was present RETURN QUERY SELECT v_new_key, (v_old_key IS NOT NULL); END; $$ LANGUAGE plpgsql; COMMENT ON FUNCTION regenerate_trigger_webhook_key(BIGINT) IS 'Regenerates webhook key for a trigger. Returns new key and whether a previous key was revoked.'; -- Verify all webhook functions exist DO $$ BEGIN IF NOT EXISTS ( SELECT 1 FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = current_schema() AND p.proname = 'enable_trigger_webhook' ) THEN RAISE EXCEPTION 'enable_trigger_webhook function not found after migration'; END IF; IF NOT EXISTS ( SELECT 1 FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = current_schema() AND p.proname = 'disable_trigger_webhook' ) THEN RAISE EXCEPTION 'disable_trigger_webhook function not found after migration'; END IF; IF NOT EXISTS ( SELECT 1 FROM pg_proc p JOIN pg_namespace n ON p.pronamespace = n.oid WHERE n.nspname = current_schema() AND p.proname = 'regenerate_trigger_webhook_key' ) THEN RAISE EXCEPTION 'regenerate_trigger_webhook_key function not found after migration'; END IF; RAISE NOTICE 'All webhook functions successfully created'; END $$;