Build reusable bot framework
This commit is contained in:
71
config/migrations/0001_core_tables.sql
Normal file
71
config/migrations/0001_core_tables.sql
Normal file
@@ -0,0 +1,71 @@
|
||||
-- Baseline legacy tables and normalize their timestamps as UTC.
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id UUID PRIMARY KEY,
|
||||
username VARCHAR(255) UNIQUE NOT NULL,
|
||||
password_hashed BYTEA NOT NULL,
|
||||
timezone VARCHAR(64) NOT NULL DEFAULT 'UTC',
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
ALTER TABLE users
|
||||
ADD COLUMN IF NOT EXISTS timezone VARCHAR(64) NOT NULL DEFAULT 'UTC';
|
||||
|
||||
CREATE TABLE IF NOT EXISTS notifications (
|
||||
id UUID PRIMARY KEY,
|
||||
user_uuid UUID REFERENCES users(id) ON DELETE CASCADE UNIQUE,
|
||||
discord_webhook VARCHAR(500),
|
||||
discord_enabled BOOLEAN DEFAULT FALSE,
|
||||
ntfy_topic VARCHAR(255),
|
||||
ntfy_enabled BOOLEAN DEFAULT FALSE,
|
||||
last_message_sent TIMESTAMPTZ,
|
||||
current_notification_status VARCHAR(50) DEFAULT 'inactive',
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'users'
|
||||
AND column_name = 'created_at'
|
||||
AND data_type = 'timestamp without time zone'
|
||||
) THEN
|
||||
ALTER TABLE users ALTER COLUMN created_at TYPE TIMESTAMPTZ
|
||||
USING created_at AT TIME ZONE 'UTC';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'notifications'
|
||||
AND column_name = 'last_message_sent'
|
||||
AND data_type = 'timestamp without time zone'
|
||||
) THEN
|
||||
ALTER TABLE notifications ALTER COLUMN last_message_sent TYPE TIMESTAMPTZ
|
||||
USING last_message_sent AT TIME ZONE 'UTC';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'notifications'
|
||||
AND column_name = 'created_at'
|
||||
AND data_type = 'timestamp without time zone'
|
||||
) THEN
|
||||
ALTER TABLE notifications ALTER COLUMN created_at TYPE TIMESTAMPTZ
|
||||
USING created_at AT TIME ZONE 'UTC';
|
||||
END IF;
|
||||
|
||||
IF EXISTS (
|
||||
SELECT 1 FROM information_schema.columns
|
||||
WHERE table_schema = 'public'
|
||||
AND table_name = 'notifications'
|
||||
AND column_name = 'updated_at'
|
||||
AND data_type = 'timestamp without time zone'
|
||||
) THEN
|
||||
ALTER TABLE notifications ALTER COLUMN updated_at TYPE TIMESTAMPTZ
|
||||
USING updated_at AT TIME ZONE 'UTC';
|
||||
END IF;
|
||||
END $$;
|
||||
35
config/migrations/0002_scheduled_jobs.sql
Normal file
35
config/migrations/0002_scheduled_jobs.sql
Normal file
@@ -0,0 +1,35 @@
|
||||
CREATE TABLE IF NOT EXISTS scheduled_jobs (
|
||||
id UUID PRIMARY KEY,
|
||||
user_uuid UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
job_type VARCHAR(100) NOT NULL,
|
||||
payload JSONB NOT NULL DEFAULT '{}'::JSONB,
|
||||
run_at TIMESTAMPTZ NOT NULL,
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
max_attempts INTEGER NOT NULL DEFAULT 3,
|
||||
leased_by VARCHAR(255),
|
||||
lease_until TIMESTAMPTZ,
|
||||
last_error TEXT,
|
||||
idempotency_key VARCHAR(255),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
completed_at TIMESTAMPTZ,
|
||||
cancelled_at TIMESTAMPTZ,
|
||||
CONSTRAINT scheduled_jobs_status_check
|
||||
CHECK (status IN ('pending', 'running', 'completed', 'failed', 'cancelled')),
|
||||
CONSTRAINT scheduled_jobs_attempts_check
|
||||
CHECK (attempts >= 0 AND max_attempts > 0),
|
||||
CONSTRAINT scheduled_jobs_idempotency_unique
|
||||
UNIQUE (job_type, idempotency_key)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS scheduled_jobs_claim_idx
|
||||
ON scheduled_jobs (run_at, created_at)
|
||||
WHERE status = 'pending';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS scheduled_jobs_expired_lease_idx
|
||||
ON scheduled_jobs (lease_until)
|
||||
WHERE status = 'running';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS scheduled_jobs_user_idx
|
||||
ON scheduled_jobs (user_uuid, created_at DESC);
|
||||
33
config/migrations/0003_outbound_messages.sql
Normal file
33
config/migrations/0003_outbound_messages.sql
Normal file
@@ -0,0 +1,33 @@
|
||||
CREATE TABLE IF NOT EXISTS outbound_messages (
|
||||
id UUID PRIMARY KEY,
|
||||
user_uuid UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
channel VARCHAR(50) NOT NULL,
|
||||
payload JSONB NOT NULL,
|
||||
available_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending',
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
max_attempts INTEGER NOT NULL DEFAULT 3,
|
||||
leased_by VARCHAR(255),
|
||||
lease_until TIMESTAMPTZ,
|
||||
last_error TEXT,
|
||||
idempotency_key VARCHAR(255) NOT NULL UNIQUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
delivered_at TIMESTAMPTZ,
|
||||
cancelled_at TIMESTAMPTZ,
|
||||
CONSTRAINT outbound_messages_status_check
|
||||
CHECK (status IN ('pending', 'delivering', 'delivered', 'failed', 'cancelled')),
|
||||
CONSTRAINT outbound_messages_attempts_check
|
||||
CHECK (attempts >= 0 AND max_attempts > 0)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS outbound_messages_claim_idx
|
||||
ON outbound_messages (channel, available_at, created_at)
|
||||
WHERE status = 'pending';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS outbound_messages_expired_lease_idx
|
||||
ON outbound_messages (lease_until)
|
||||
WHERE status = 'delivering';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS outbound_messages_user_idx
|
||||
ON outbound_messages (user_uuid, created_at DESC);
|
||||
71
config/migrations/0004_auth_identity.sql
Normal file
71
config/migrations/0004_auth_identity.sql
Normal file
@@ -0,0 +1,71 @@
|
||||
-- Provider identities allow passwordless platform users while legacy credentials remain valid.
|
||||
ALTER TABLE users
|
||||
ALTER COLUMN username DROP NOT NULL,
|
||||
ALTER COLUMN password_hashed DROP NOT NULL;
|
||||
|
||||
DO $$
|
||||
BEGIN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM pg_constraint WHERE conname = 'users_credentials_pair_check'
|
||||
) THEN
|
||||
ALTER TABLE users ADD CONSTRAINT users_credentials_pair_check
|
||||
CHECK (
|
||||
(username IS NULL AND password_hashed IS NULL)
|
||||
OR (username IS NOT NULL AND password_hashed IS NOT NULL)
|
||||
);
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS provider_identities (
|
||||
id UUID PRIMARY KEY,
|
||||
user_uuid UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
provider VARCHAR(50) NOT NULL,
|
||||
provider_user_id VARCHAR(255) NOT NULL,
|
||||
display_name VARCHAR(255),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
CONSTRAINT provider_identities_provider_user_unique
|
||||
UNIQUE (provider, provider_user_id),
|
||||
CONSTRAINT provider_identities_user_provider_unique
|
||||
UNIQUE (user_uuid, provider)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS provider_identities_user_idx
|
||||
ON provider_identities (user_uuid);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id UUID PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
key_type VARCHAR(20) NOT NULL,
|
||||
user_uuid UUID REFERENCES users(id) ON DELETE CASCADE,
|
||||
service_name VARCHAR(255),
|
||||
key_prefix VARCHAR(20) NOT NULL,
|
||||
key_hash CHAR(64) NOT NULL UNIQUE,
|
||||
scopes JSONB NOT NULL DEFAULT '[]'::JSONB,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ,
|
||||
revoked_at TIMESTAMPTZ,
|
||||
last_used_at TIMESTAMPTZ,
|
||||
CONSTRAINT api_keys_type_check
|
||||
CHECK (key_type IN ('user', 'service')),
|
||||
CONSTRAINT api_keys_scopes_check
|
||||
CHECK (jsonb_typeof(scopes) = 'array'),
|
||||
CONSTRAINT api_keys_owner_check
|
||||
CHECK (
|
||||
(key_type = 'user' AND user_uuid IS NOT NULL
|
||||
AND service_name IS NULL AND scopes = '[]'::JSONB)
|
||||
OR
|
||||
(key_type = 'service' AND user_uuid IS NULL
|
||||
AND service_name IS NOT NULL)
|
||||
)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS api_keys_prefix_idx
|
||||
ON api_keys (key_prefix);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS api_keys_user_idx
|
||||
ON api_keys (user_uuid, created_at DESC)
|
||||
WHERE key_type = 'user';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS api_keys_service_idx
|
||||
ON api_keys (service_name, created_at DESC)
|
||||
WHERE key_type = 'service';
|
||||
2
config/migrations/0005_outbox_delivery_metadata.sql
Normal file
2
config/migrations/0005_outbox_delivery_metadata.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE outbound_messages
|
||||
ADD COLUMN IF NOT EXISTS external_message_id VARCHAR(255);
|
||||
2
config/migrations/0006_api_key_prefix.sql
Normal file
2
config/migrations/0006_api_key_prefix.sql
Normal file
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE api_keys
|
||||
ALTER COLUMN key_prefix TYPE VARCHAR(32);
|
||||
Reference in New Issue
Block a user