diff --git a/apps/dbagent/migrations/0002_military_violations.sql b/apps/dbagent/migrations/0002_military_violations.sql new file mode 100644 index 00000000..03cb5b3a --- /dev/null +++ b/apps/dbagent/migrations/0002_military_violations.sql @@ -0,0 +1,55 @@ +CREATE TABLE "slack_conversations" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "slack_channel_id" text NOT NULL, + "slack_team_id" text NOT NULL, + "project_id" uuid NOT NULL, + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "uq_slack_conversations_ids" UNIQUE("slack_channel_id","slack_team_id") +); +--> statement-breakpoint +CREATE TABLE "slack_memory" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "slack_user_id" uuid NOT NULL, + "conversation_id" uuid NOT NULL, + "key" text NOT NULL, + "value" jsonb NOT NULL, + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "uq_slack_memory" UNIQUE("slack_user_id","conversation_id","key") +); +--> statement-breakpoint +CREATE TABLE "slack_user_links" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "slack_user_id" uuid NOT NULL, + "platform_user_id" text NOT NULL, + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "uq_slack_user_links" UNIQUE("slack_user_id","platform_user_id") +); +--> statement-breakpoint +CREATE TABLE "slack_user_projects" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "slack_user_id" uuid NOT NULL, + "project_id" uuid NOT NULL, + "created_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "uq_slack_user_projects" UNIQUE("slack_user_id","project_id") +); +--> statement-breakpoint +CREATE TABLE "slack_users" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "slack_user_id" text NOT NULL, + "slack_team_id" text NOT NULL, + "email" text, + "display_name" text, + "created_at" timestamp with time zone DEFAULT now(), + "updated_at" timestamp with time zone DEFAULT now(), + CONSTRAINT "uq_slack_users_ids" UNIQUE("slack_user_id","slack_team_id") +); +--> statement-breakpoint +ALTER TABLE "slack_conversations" ADD CONSTRAINT "fk_slack_conversations_project" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "slack_memory" ADD CONSTRAINT "fk_slack_memory_user" FOREIGN KEY ("slack_user_id") REFERENCES "public"."slack_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "slack_memory" ADD CONSTRAINT "fk_slack_memory_conversation" FOREIGN KEY ("conversation_id") REFERENCES "public"."slack_conversations"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "slack_user_links" ADD CONSTRAINT "fk_slack_user_links_slack_user" FOREIGN KEY ("slack_user_id") REFERENCES "public"."slack_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "slack_user_projects" ADD CONSTRAINT "fk_slack_user_projects_user" FOREIGN KEY ("slack_user_id") REFERENCES "public"."slack_users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "slack_user_projects" ADD CONSTRAINT "fk_slack_user_projects_project" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE no action ON UPDATE no action; \ No newline at end of file diff --git a/apps/dbagent/migrations/meta/0002_snapshot.json b/apps/dbagent/migrations/meta/0002_snapshot.json new file mode 100644 index 00000000..5bb1d700 --- /dev/null +++ b/apps/dbagent/migrations/meta/0002_snapshot.json @@ -0,0 +1,925 @@ +{ + "id": "8c2d1b99-2735-4e04-af0f-92ae5293960c", + "prevId": "a626c3ba-915d-43f6-a5e8-388c9b37266f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.aws_cluster_connections": { + "name": "aws_cluster_connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "cluster_id": { + "name": "cluster_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "fk_aws_cluster_connections_cluster": { + "name": "fk_aws_cluster_connections_cluster", + "tableFrom": "aws_cluster_connections", + "tableTo": "aws_clusters", + "columnsFrom": ["cluster_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fk_aws_cluster_connections_connection": { + "name": "fk_aws_cluster_connections_connection", + "tableFrom": "aws_cluster_connections", + "tableTo": "connections", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.aws_clusters": { + "name": "aws_clusters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "cluster_identifier": { + "name": "cluster_identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'us-east-1'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_aws_clusters_integration_identifier": { + "name": "uq_aws_clusters_integration_identifier", + "nullsNotDistinct": false, + "columns": ["cluster_identifier"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connection_info": { + "name": "connection_info", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "fk_connections_info_connection": { + "name": "fk_connections_info_connection", + "tableFrom": "connection_info", + "tableTo": "connections", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_connections_info": { + "name": "uq_connections_info", + "nullsNotDistinct": false, + "columns": ["connection_id", "type"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.connections": { + "name": "connections", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_default": { + "name": "is_default", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "connection_string": { + "name": "connection_string", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "fk_connections_project": { + "name": "fk_connections_project", + "tableFrom": "connections", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_connections_name": { + "name": "uq_connections_name", + "nullsNotDistinct": false, + "columns": ["project_id", "name"] + }, + "uq_connections_connection_string": { + "name": "uq_connections_connection_string", + "nullsNotDistinct": false, + "columns": ["project_id", "connection_string"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.integrations": { + "name": "integrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "fk_integrations_project": { + "name": "fk_integrations_project", + "tableFrom": "integrations", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_integrations_name": { + "name": "uq_integrations_name", + "nullsNotDistinct": false, + "columns": ["project_id", "name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_projects_name": { + "name": "uq_projects_name", + "nullsNotDistinct": false, + "columns": ["owner_id", "name"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedule_runs": { + "name": "schedule_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "schedule_id": { + "name": "schedule_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "result": { + "name": "result", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "notification_level": { + "name": "notification_level", + "type": "notification_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "idx_schedule_runs_created_at": { + "name": "idx_schedule_runs_created_at", + "columns": [ + { + "expression": "schedule_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "fk_schedule_runs_schedule": { + "name": "fk_schedule_runs_schedule", + "tableFrom": "schedule_runs", + "tableTo": "schedules", + "columnsFrom": ["schedule_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.schedules": { + "name": "schedules", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "connection_id": { + "name": "connection_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "playbook": { + "name": "playbook", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "schedule_type": { + "name": "schedule_type", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "varchar(255)", + "primaryKey": false, + "notNull": false + }, + "additional_instructions": { + "name": "additional_instructions", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "min_interval": { + "name": "min_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "max_interval": { + "name": "max_interval", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_run": { + "name": "last_run", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_run": { + "name": "next_run", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "schedule_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'disabled'" + }, + "failures": { + "name": "failures", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "keep_history": { + "name": "keep_history", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 300 + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'openai-gpt-4o'" + }, + "max_steps": { + "name": "max_steps", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "notify_level": { + "name": "notify_level", + "type": "notification_level", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'alert'" + }, + "extra_notification_text": { + "name": "extra_notification_text", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "fk_schedules_project": { + "name": "fk_schedules_project", + "tableFrom": "schedules", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fk_schedules_connection": { + "name": "fk_schedules_connection", + "tableFrom": "schedules", + "tableTo": "connections", + "columnsFrom": ["connection_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_conversations": { + "name": "slack_conversations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slack_channel_id": { + "name": "slack_channel_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_team_id": { + "name": "slack_team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "fk_slack_conversations_project": { + "name": "fk_slack_conversations_project", + "tableFrom": "slack_conversations", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_slack_conversations_ids": { + "name": "uq_slack_conversations_ids", + "nullsNotDistinct": false, + "columns": ["slack_channel_id", "slack_team_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_memory": { + "name": "slack_memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slack_user_id": { + "name": "slack_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "conversation_id": { + "name": "conversation_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "fk_slack_memory_user": { + "name": "fk_slack_memory_user", + "tableFrom": "slack_memory", + "tableTo": "slack_users", + "columnsFrom": ["slack_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fk_slack_memory_conversation": { + "name": "fk_slack_memory_conversation", + "tableFrom": "slack_memory", + "tableTo": "slack_conversations", + "columnsFrom": ["conversation_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_slack_memory": { + "name": "uq_slack_memory", + "nullsNotDistinct": false, + "columns": ["slack_user_id", "conversation_id", "key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_user_links": { + "name": "slack_user_links", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slack_user_id": { + "name": "slack_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "platform_user_id": { + "name": "platform_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "fk_slack_user_links_slack_user": { + "name": "fk_slack_user_links_slack_user", + "tableFrom": "slack_user_links", + "tableTo": "slack_users", + "columnsFrom": ["slack_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_slack_user_links": { + "name": "uq_slack_user_links", + "nullsNotDistinct": false, + "columns": ["slack_user_id", "platform_user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_user_projects": { + "name": "slack_user_projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slack_user_id": { + "name": "slack_user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "fk_slack_user_projects_user": { + "name": "fk_slack_user_projects_user", + "tableFrom": "slack_user_projects", + "tableTo": "slack_users", + "columnsFrom": ["slack_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "fk_slack_user_projects_project": { + "name": "fk_slack_user_projects_project", + "tableFrom": "slack_user_projects", + "tableTo": "projects", + "columnsFrom": ["project_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_slack_user_projects": { + "name": "uq_slack_user_projects", + "nullsNotDistinct": false, + "columns": ["slack_user_id", "project_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.slack_users": { + "name": "slack_users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "slack_user_id": { + "name": "slack_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slack_team_id": { + "name": "slack_team_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "uq_slack_users_ids": { + "name": "uq_slack_users_ids", + "nullsNotDistinct": false, + "columns": ["slack_user_id", "slack_team_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.notification_level": { + "name": "notification_level", + "schema": "public", + "values": ["info", "warning", "alert"] + }, + "public.schedule_status": { + "name": "schedule_status", + "schema": "public", + "values": ["disabled", "scheduled", "running"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/apps/dbagent/migrations/meta/_journal.json b/apps/dbagent/migrations/meta/_journal.json index 8fc71670..06b1f742 100644 --- a/apps/dbagent/migrations/meta/_journal.json +++ b/apps/dbagent/migrations/meta/_journal.json @@ -15,6 +15,13 @@ "when": 1741117716570, "tag": "0001_panoramic_silhouette", "breakpoints": true + }, + { + "idx": 2, + "version": "7", + "when": 1741250031700, + "tag": "0002_military_violations", + "breakpoints": true } ] } diff --git a/apps/dbagent/src/auth.ts b/apps/dbagent/src/auth.ts index 016f5096..82c521d5 100644 --- a/apps/dbagent/src/auth.ts +++ b/apps/dbagent/src/auth.ts @@ -1,32 +1,53 @@ import NextAuth from 'next-auth'; import Credentials from 'next-auth/providers/credentials'; +import SlackProvider from 'next-auth/providers/slack'; +import { getOrCreateSlackUser, linkUserToPlatform } from './lib/db/slack'; import { env } from './lib/env/server'; function getProviders() { + const providers = []; + + // Add OpenID provider if configured if (env.AUTH_OPENID_ID && env.AUTH_OPENID_SECRET && env.AUTH_OPENID_ISSUER) { - return [ - { + providers.push({ + id: 'default', + name: 'OpenID', + type: 'oidc', + options: { + clientId: env.AUTH_OPENID_ID, + clientSecret: env.AUTH_OPENID_SECRET, + issuer: env.AUTH_OPENID_ISSUER + } + } as const); + } else { + providers.push( + Credentials({ id: 'default', - name: 'OpenID', - type: 'oidc', - options: { - clientId: env.AUTH_OPENID_ID, - clientSecret: env.AUTH_OPENID_SECRET, - issuer: env.AUTH_OPENID_ISSUER + name: 'Local auth', + async authorize() { + return { id: 'local', name: 'User', email: 'user@localhost' }; } - } as const - ]; + }) + ); } - return [ - Credentials({ - id: 'default', - name: 'Local auth', - async authorize() { - return { id: 'local', name: 'User', email: 'user@localhost' }; - } - }) - ]; + // Add Slack provider + if (env.SLACK_CLIENT_ID && env.SLACK_CLIENT_SECRET) { + providers.push( + SlackProvider({ + clientId: env.SLACK_CLIENT_ID, + clientSecret: env.SLACK_CLIENT_SECRET, + // Request additional scopes for bot functionality + authorization: { + params: { + scope: 'openid email profile identify chat:write channels:read im:history' + } + } + }) + ); + } + + return providers; } const { handlers, signIn, signOut, auth } = NextAuth({ @@ -39,9 +60,47 @@ const { handlers, signIn, signOut, auth } = NextAuth({ strategy: 'jwt' }, callbacks: { - async jwt({ token, account, user }) { + async signIn({ account, profile, user }) { + if (account?.provider === 'slack' && profile) { + try { + // Create or update the Slack user record + const slackUser = await getOrCreateSlackUser( + profile.sub as string, + profile.team_id as string, + profile.email as string, + profile.name as string + ); + + // Link the Slack user to the platform user + if (user.id) { + await linkUserToPlatform(slackUser!.id, user.id); + } + + return true; + } catch (error) { + console.error('Error linking Slack user:', error); + return false; + } + } + return true; + }, + async jwt({ token, account, user, profile }) { // Initial sign-in if (account && user) { + // Store Slack-specific data in the token if it's a Slack sign-in + if (account.provider === 'slack' && profile) { + return { + accessToken: account.access_token, + refreshToken: account.refresh_token, + expiresAt: Date.now() + (account.expires_in ?? 100) * 1000, + user: { + ...user, + slackTeamId: profile.team_id, + slackUserId: profile.sub + } + }; + } + // Default token for other providers return { accessToken: account.access_token, refreshToken: account.refresh_token, @@ -49,7 +108,6 @@ const { handlers, signIn, signOut, auth } = NextAuth({ user }; } - return token; }, async session({ session, token }) { @@ -63,11 +121,12 @@ const { handlers, signIn, signOut, auth } = NextAuth({ }; // @ts-expect-error Types don't match session.error = token.error; + // @ts-expect-error Types don't match + session.slackTeamId = token.slackTeamId; return session; }, authorized: async ({ auth }) => { - // Logged in users are authenticated, otherwise redirect to login page return !!auth; } } diff --git a/apps/dbagent/src/lib/db/schema.ts b/apps/dbagent/src/lib/db/schema.ts index afbbe549..b9f93ea7 100644 --- a/apps/dbagent/src/lib/db/schema.ts +++ b/apps/dbagent/src/lib/db/schema.ts @@ -174,3 +174,105 @@ export const projects = pgTable( }, (table) => [unique('uq_projects_name').on(table.ownerId, table.name)] ); + +export const slackUsers = pgTable( + 'slack_users', + { + id: uuid('id').primaryKey().defaultRandom().notNull(), + slackUserId: text('slack_user_id').notNull(), + slackTeamId: text('slack_team_id').notNull(), + email: text('email'), + displayName: text('display_name'), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow() + }, + (table) => [unique('uq_slack_users_ids').on(table.slackUserId, table.slackTeamId)] +); + +export const slackConversations = pgTable( + 'slack_conversations', + { + id: uuid('id').primaryKey().defaultRandom().notNull(), + slackChannelId: text('slack_channel_id').notNull(), + slackTeamId: text('slack_team_id').notNull(), + projectId: uuid('project_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow() + }, + (table) => [ + foreignKey({ + columns: [table.projectId], + foreignColumns: [projects.id], + name: 'fk_slack_conversations_project' + }), + unique('uq_slack_conversations_ids').on(table.slackChannelId, table.slackTeamId) + ] +); + +export const slackUserProjects = pgTable( + 'slack_user_projects', + { + id: uuid('id').primaryKey().defaultRandom().notNull(), + slackUserId: uuid('slack_user_id').notNull(), + projectId: uuid('project_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow() + }, + (table) => [ + foreignKey({ + columns: [table.slackUserId], + foreignColumns: [slackUsers.id], + name: 'fk_slack_user_projects_user' + }), + foreignKey({ + columns: [table.projectId], + foreignColumns: [projects.id], + name: 'fk_slack_user_projects_project' + }), + unique('uq_slack_user_projects').on(table.slackUserId, table.projectId) + ] +); + +export const slackMemory = pgTable( + 'slack_memory', + { + id: uuid('id').primaryKey().defaultRandom().notNull(), + slackUserId: uuid('slack_user_id').notNull(), + conversationId: uuid('conversation_id').notNull(), + key: text('key').notNull(), + value: jsonb('value').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow() + }, + (table) => [ + foreignKey({ + columns: [table.slackUserId], + foreignColumns: [slackUsers.id], + name: 'fk_slack_memory_user' + }), + foreignKey({ + columns: [table.conversationId], + foreignColumns: [slackConversations.id], + name: 'fk_slack_memory_conversation' + }), + unique('uq_slack_memory').on(table.slackUserId, table.conversationId, table.key) + ] +); + +export const slackUserLinks = pgTable( + 'slack_user_links', + { + id: uuid('id').primaryKey().defaultRandom().notNull(), + slackUserId: uuid('slack_user_id').notNull(), + platformUserId: text('platform_user_id').notNull(), + createdAt: timestamp('created_at', { withTimezone: true }).defaultNow(), + updatedAt: timestamp('updated_at', { withTimezone: true }).defaultNow() + }, + (table) => [ + foreignKey({ + columns: [table.slackUserId], + foreignColumns: [slackUsers.id], + name: 'fk_slack_user_links_slack_user' + }), + unique('uq_slack_user_links').on(table.slackUserId, table.platformUserId) + ] +); diff --git a/apps/dbagent/src/lib/db/slack.ts b/apps/dbagent/src/lib/db/slack.ts new file mode 100644 index 00000000..6f0ba997 --- /dev/null +++ b/apps/dbagent/src/lib/db/slack.ts @@ -0,0 +1,137 @@ +import { and, eq } from 'drizzle-orm'; +import { db } from './db'; +import { slackConversations, slackMemory, slackUserLinks, slackUserProjects, slackUsers } from './schema'; + +export async function getOrCreateSlackUser( + slackUserId: string, + slackTeamId: string, + email?: string, + displayName?: string +) { + const existingUser = await db + .select() + .from(slackUsers) + .where(and(eq(slackUsers.slackUserId, slackUserId), eq(slackUsers.slackTeamId, slackTeamId))) + .limit(1); + + if (existingUser.length > 0) { + return existingUser[0]; + } + + const [newUser] = await db + .insert(slackUsers) + .values({ + slackUserId, + slackTeamId, + email, + displayName + }) + .returning(); + + return newUser; +} + +export async function linkUserToPlatform(slackUserId: string, platformUserId: string) { + const [link] = await db + .insert(slackUserLinks) + .values({ + slackUserId, + platformUserId + }) + .onConflictDoNothing() + .returning(); + + return link; +} + +export async function getPlatformUserId(slackUserId: string) { + const result = await db.select().from(slackUserLinks).where(eq(slackUserLinks.slackUserId, slackUserId)).limit(1); + + return result[0]?.platformUserId; +} + +export async function getOrCreateSlackConversation(slackChannelId: string, slackTeamId: string, projectId: string) { + const existingConversation = await db + .select() + .from(slackConversations) + .where(and(eq(slackConversations.slackChannelId, slackChannelId), eq(slackConversations.slackTeamId, slackTeamId))) + .limit(1); + + if (existingConversation.length > 0) { + return existingConversation[0]; + } + + const [newConversation] = await db + .insert(slackConversations) + .values({ + slackChannelId, + slackTeamId, + projectId + }) + .returning(); + + return newConversation; +} + +export async function linkUserToProject(slackUserId: string, projectId: string) { + const [link] = await db + .insert(slackUserProjects) + .values({ + slackUserId, + projectId + }) + .onConflictDoNothing() + .returning(); + + return link; +} + +export async function getUserProjects(slackUserId: string) { + return db.select().from(slackUserProjects).where(eq(slackUserProjects.slackUserId, slackUserId)); +} + +export async function setMemoryValue(slackUserId: string, conversationId: string, key: string, value: any) { + await db + .insert(slackMemory) + .values({ + slackUserId, + conversationId, + key, + value + }) + .onConflictDoUpdate({ + target: [slackMemory.slackUserId, slackMemory.conversationId, slackMemory.key], + set: { + value, + updatedAt: new Date() + } + }); +} + +export async function getMemoryValue(slackUserId: string, conversationId: string, key: string) { + const result = await db + .select() + .from(slackMemory) + .where( + and( + eq(slackMemory.slackUserId, slackUserId), + eq(slackMemory.conversationId, conversationId), + eq(slackMemory.key, key) + ) + ) + .limit(1); + + return result[0]?.value as T; +} + +export async function deleteMemoryValue(slackUserId: string, conversationId: string, key: string) { + await db + .delete(slackMemory) + .where( + and( + eq(slackMemory.slackUserId, slackUserId), + eq(slackMemory.conversationId, conversationId), + eq(slackMemory.key, key) + ) + ); +} diff --git a/apps/dbagent/src/lib/slack/generate-response.ts b/apps/dbagent/src/lib/slack/generate-response.ts index 252826d9..700208a9 100644 --- a/apps/dbagent/src/lib/slack/generate-response.ts +++ b/apps/dbagent/src/lib/slack/generate-response.ts @@ -1,25 +1,89 @@ -import { CoreMessage, generateText } from "ai"; -import { chatSystemPrompt, getModelInstance, getTools } from "../ai/aidba"; -import { getConnection } from "../db/connections"; -import { getTargetDbConnection } from "../targetdb/db"; - -export const generateResponse = async ( - messages: CoreMessage[], -) => { - const connection = await getConnection(connectionId); +import { CoreMessage, generateText } from 'ai'; +import { chatSystemPrompt, getModelInstance, getTools } from '../ai/aidba'; +import { getConnection } from '../db/connections'; +import { getProjectById } from '../db/projects'; +import { getMemoryValue, getUserProjects, setMemoryValue } from '../db/slack'; +import { getTargetDbConnection } from '../targetdb/db'; + +// Function to handle project-related commands +async function handleProjectCommand(text: string, userId: string, conversationId: string): Promise { + const useProjectMatch = text.match(/^use project (.+)$/i); + if (useProjectMatch) { + const projectName = useProjectMatch[1]; + const userProjects = await getUserProjects(userId); + for (const userProject of userProjects) { + const { project } = await getProjectById(userProject.projectId); + if (project && project.name.toLowerCase() === projectName?.toLowerCase()) { + await setMemoryValue(userId, conversationId, 'currentProjectId', project.id); + return `Now working with project "${project.name}". You can start asking questions about your database.`; + } + } + return `Project "${projectName}" not found or you don't have access to it. Try 'list projects' to see available projects.`; + } + + if (text.toLowerCase() === 'list projects') { + const userProjects = await getUserProjects(userId); + if (userProjects.length === 0) { + return "You don't have access to any projects yet. Please ask your administrator to grant you access."; + } + const projects = await Promise.all(userProjects.map((up) => getProjectById(up.projectId))); + const projectList = projects + .filter((p) => p !== null) + .map((p) => `- ${p.project?.name}`) + .join('\n'); + return `Your projects:\n${projectList}\n\nUse 'use project ' to select a project.`; + } + + return ''; +} + +export const generateResponse = async (messages: CoreMessage[], conversationId: string) => { + // Get the conversation context + const lastMessage = messages[messages.length - 1]; + if (!lastMessage || lastMessage.role !== 'user') { + return "I couldn't understand your message."; + } + + // Extract user context from the conversation ID + const userContext = await getMemoryValue('system', conversationId, 'userContext'); + if (!userContext) { + return 'Error: User context not found.'; + } + + // Handle project-related commands + const projectCommandResponse = await handleProjectCommand( + lastMessage.content as string, + userContext.userId, + conversationId + ); + if (projectCommandResponse) { + return projectCommandResponse; + } + + // If no project is selected, prompt the user to select one + const projectId = await getMemoryValue(userContext.userId, conversationId, 'currentProjectId'); + if (!projectId) { + return "Please select a project first by using 'use project ' or 'list projects' to see available projects."; + } + + // Get the database connection for the current project + const connection = await getConnection(projectId); if (!connection) { - throw new Error("Connection not found"); + return 'No database connection found for this project. Please set up a connection first.'; } + + // Get the database client const targetClient = await getTargetDbConnection(connection.connectionString); + // Generate the response using the AI model const { text } = await generateText({ - model: getModelInstance("openai-gpt-4o"), + model: getModelInstance('openai-gpt-4o'), messages, system: chatSystemPrompt, tools: await getTools(connection, targetClient), - maxSteps: 20, + maxSteps: 20 }); // Convert markdown to Slack mrkdwn format - return text.replace(/\[(.*?)\]\((.*?)\)/g, "<$2|$1>").replace(/\*\*/g, "*"); + return text.replace(/\[(.*?)\]\((.*?)\)/g, '<$2|$1>').replace(/\*\*/g, '*'); }; diff --git a/apps/dbagent/src/lib/slack/handle-app-mentions.ts b/apps/dbagent/src/lib/slack/handle-app-mentions.ts index 4bac7640..758e09aa 100644 --- a/apps/dbagent/src/lib/slack/handle-app-mentions.ts +++ b/apps/dbagent/src/lib/slack/handle-app-mentions.ts @@ -1,49 +1,74 @@ -import { AppMentionEvent } from "@slack/web-api"; -import { client, getThread } from "./utils"; -import { generateResponse } from "./generate-response"; - -const updateStatusUtil = async ( - initialStatus: string, - event: AppMentionEvent, -) => { +import { AppMentionEvent } from '@slack/web-api'; +import { getMemoryValue, getOrCreateSlackConversation, getOrCreateSlackUser } from '../db/slack'; +import { generateResponse } from './generate-response'; +import { client, getThread } from './utils'; + +const updateStatusUtil = async (initialStatus: string, event: AppMentionEvent) => { const initialMessage = await client.chat.postMessage({ channel: event.channel, thread_ts: event.thread_ts ?? event.ts, - text: initialStatus, + text: initialStatus }); - if (!initialMessage || !initialMessage.ts) - throw new Error("Failed to post initial message"); + if (!initialMessage || !initialMessage.ts) throw new Error('Failed to post initial message'); const updateMessage = async (status: string) => { await client.chat.update({ channel: event.channel, ts: initialMessage.ts as string, - text: status, + text: status }); }; return updateMessage; }; -export async function handleNewAppMention( - event: AppMentionEvent, - botUserId: string, -) { - console.log("Handling app mention"); +export async function handleNewAppMention(event: AppMentionEvent, botUserId: string) { + console.log('Handling app mention'); if (event.bot_id || event.bot_id === botUserId || event.bot_profile) { - console.log("Skipping app mention"); + console.log('Skipping app mention'); return; } - const { thread_ts, channel } = event; - const updateMessage = await updateStatusUtil("is thinking...", event); + const { thread_ts, channel, user, team } = event; + + // Get or create user record + const slackUser = await getOrCreateSlackUser(user!, team ?? ''); + if (!slackUser) { + return await client.chat.postMessage({ + channel: channel, + thread_ts: thread_ts ?? event.ts, + text: 'Error: User not found' + }); + } + + // Get conversation context + const projectId = await getMemoryValue(slackUser.id, channel, 'currentProjectId'); + if (!projectId) { + return await client.chat.postMessage({ + channel: event.channel, + thread_ts: thread_ts ?? event.ts, + text: "Please set up a project first by saying 'use project '" + }); + } + + // Get or create conversation record + const conversation = await getOrCreateSlackConversation(channel, team ?? '', projectId); + if (!conversation) { + return await client.chat.postMessage({ + channel: channel, + thread_ts: thread_ts ?? event.ts, + text: 'Error: Conversation not found' + }); + } + + const updateMessage = await updateStatusUtil('is thinking...', event); if (thread_ts) { const messages = await getThread(channel, thread_ts, botUserId); - const result = await generateResponse(messages); + const result = await generateResponse(messages, conversation.id); updateMessage(result); } else { - const result = await generateResponse([{ role: "user", content: event.text }]); + const result = await generateResponse([{ role: 'user', content: event.text }], conversation.id); updateMessage(result); } } diff --git a/apps/dbagent/src/lib/slack/handle-messages.ts b/apps/dbagent/src/lib/slack/handle-messages.ts index a77a559e..7d8bb783 100644 --- a/apps/dbagent/src/lib/slack/handle-messages.ts +++ b/apps/dbagent/src/lib/slack/handle-messages.ts @@ -1,20 +1,16 @@ -import type { - AssistantThreadStartedEvent, - GenericMessageEvent, -} from "@slack/web-api"; -import { client, getThread, updateStatusUtil } from "./utils"; -import { generateResponse } from "./generate-response"; +import type { AssistantThreadStartedEvent, GenericMessageEvent } from '@slack/web-api'; +import { getMemoryValue, getOrCreateSlackConversation, getOrCreateSlackUser } from '../db/slack'; +import { generateResponse } from './generate-response'; +import { client, getThread, updateStatusUtil } from './utils'; -export async function assistantThreadMessage( - event: AssistantThreadStartedEvent, -) { +export async function assistantThreadMessage(event: AssistantThreadStartedEvent) { const { channel_id, thread_ts } = event.assistant_thread; console.log(`Thread started: ${channel_id} ${thread_ts}`); await client.chat.postMessage({ channel: channel_id, thread_ts: thread_ts, - text: "Hello! I'm your AI database expert. I can help you manage and optimize your PostgreSQL database. Which project would you like to work with?", + text: "Hello! I'm your AI database expert. I can help you manage and optimize your PostgreSQL database. Which project would you like to work with?" }); await client.assistant.threads.setSuggestedPrompts({ @@ -22,39 +18,60 @@ export async function assistantThreadMessage( thread_ts: thread_ts, prompts: [ { - title: "List my projects", - message: "Show me my available projects", + title: 'List my projects', + message: 'Show me my available projects' }, { - title: "Check database health", - message: "Check the health of my database", + title: 'Check database health', + message: 'Check the health of my database' }, { - title: "Optimize queries", - message: "Help me optimize my slow queries", - }, - ], + title: 'Optimize queries', + message: 'Help me optimize my slow queries' + } + ] }); } -export async function handleNewAssistantMessage( - event: GenericMessageEvent, - botUserId: string, -) { - if ( - event.bot_id || - event.bot_id === botUserId || - event.bot_profile || - !event.thread_ts - ) - return; +export async function handleNewAssistantMessage(event: GenericMessageEvent, botUserId: string) { + if (event.bot_id || event.bot_id === botUserId || event.bot_profile || !event.thread_ts) return; - const { thread_ts, channel } = event; + const { thread_ts, channel, user, team } = event; const updateStatus = updateStatusUtil(channel, thread_ts); - updateStatus("is thinking..."); + updateStatus('is thinking...'); + + // Get or create user record + const slackUser = await getOrCreateSlackUser(user!, team ?? ''); + if (!slackUser) { + return await client.chat.postMessage({ + channel: channel, + thread_ts: thread_ts, + text: 'Error: User not found' + }); + } + + // Get conversation context + const projectId = await getMemoryValue(slackUser.id, channel, 'currentProjectId'); + if (!projectId) { + return await client.chat.postMessage({ + channel: channel, + thread_ts: thread_ts, + text: "Please set up a project first by saying 'use project '" + }); + } + + // Get or create conversation record + const conversation = await getOrCreateSlackConversation(channel, team ?? '', projectId); + if (!conversation) { + return await client.chat.postMessage({ + channel: channel, + thread_ts: thread_ts, + text: 'Error: Conversation not found' + }); + } const messages = await getThread(channel, thread_ts, botUserId); - const result = await generateResponse(messages); + const result = await generateResponse(messages, conversation.id); await client.chat.postMessage({ channel: channel, @@ -63,14 +80,14 @@ export async function handleNewAssistantMessage( unfurl_links: false, blocks: [ { - type: "section", + type: 'section', text: { - type: "mrkdwn", - text: result, - }, - }, - ], + type: 'mrkdwn', + text: result + } + } + ] }); - updateStatus(""); + updateStatus(''); }