From 2a4139bbe3872664f5af38b8b464afd22d3eee1f Mon Sep 17 00:00:00 2001 From: arvinxx Date: Tue, 3 Dec 2024 02:57:19 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=A7=20wip:=20pglite=20instance?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 4 +- scripts/migrateClientDB/compile-migrations.ts | 14 + src/database/client/db.ts | 13 + src/database/client/migrate.ts | 24 ++ src/database/client/migrations.json | 289 ++++++++++++++ .../migrations/0006_add_knowledge_base.sql | 2 +- .../migrations/0007_fix_embedding_table.sql | 4 + src/database/server/models/topic.ts | 2 +- .../GlobalProvider/StoreInitialization.tsx | 6 + src/services/message/client.test.ts | 351 ++++++++++-------- src/services/message/client.ts | 73 ++-- src/services/message/index.test.ts | 48 --- src/services/message/index.ts | 4 +- src/services/message/server.ts | 8 +- src/services/message/type.ts | 6 +- src/services/plugin/client.test.ts | 128 ++++--- src/services/plugin/client.ts | 30 +- src/services/plugin/index.ts | 4 +- src/services/topic/client.test.ts | 191 ++++------ src/services/topic/client.ts | 52 +-- src/services/topic/index.ts | 5 +- src/types/meta.ts | 9 - 22 files changed, 794 insertions(+), 473 deletions(-) create mode 100644 scripts/migrateClientDB/compile-migrations.ts create mode 100644 src/database/client/db.ts create mode 100644 src/database/client/migrate.ts create mode 100644 src/database/client/migrations.json delete mode 100644 src/services/message/index.test.ts diff --git a/package.json b/package.json index 862de202b46f..f97e4c7587e1 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,8 @@ "build-sitemap": "tsx ./scripts/buildSitemapIndex/index.ts", "build:analyze": "ANALYZE=true next build", "build:docker": "DOCKER=true next build && npm run build-sitemap", - "db:generate": "drizzle-kit generate", + "db:generate": "drizzle-kit generate && npm run db:generate-client", + "db:generate-client": "tsx ./scripts/migrateClientDB/compile-migrations.ts", "db:migrate": "MIGRATION_DB=1 tsx ./scripts/migrateServerDB/index.ts", "db:push": "drizzle-kit push", "db:push-test": "NODE_ENV=test drizzle-kit push", @@ -117,6 +118,7 @@ "@clerk/themes": "^2.1.37", "@codesandbox/sandpack-react": "^2.19.9", "@cyntler/react-doc-viewer": "^1.17.0", + "@electric-sql/pglite": "^0.2.14", "@google/generative-ai": "^0.21.0", "@huggingface/inference": "^2.8.1", "@icons-pack/react-simple-icons": "9.6.0", diff --git a/scripts/migrateClientDB/compile-migrations.ts b/scripts/migrateClientDB/compile-migrations.ts new file mode 100644 index 000000000000..c33e9dff5fb1 --- /dev/null +++ b/scripts/migrateClientDB/compile-migrations.ts @@ -0,0 +1,14 @@ +import { readMigrationFiles } from 'drizzle-orm/migrator'; +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; + +const dbBase = join(__dirname, '../../src/database'); +const migrationsFolder = join(dbBase, './migrations'); +const migrations = readMigrationFiles({ migrationsFolder: migrationsFolder }); + +writeFileSync( + join(dbBase, './client/migrations.json'), + JSON.stringify(migrations, null, 2), // null, 2 adds indentation for better readability +); + +console.log('🏁 client migrations.json compiled!'); diff --git a/src/database/client/db.ts b/src/database/client/db.ts new file mode 100644 index 000000000000..5de7ff4f6530 --- /dev/null +++ b/src/database/client/db.ts @@ -0,0 +1,13 @@ +import { IdbFs, PGlite } from '@electric-sql/pglite'; +import { vector } from '@electric-sql/pglite/vector'; +import { drizzle } from 'drizzle-orm/pglite'; + +import * as schema from '../schemas'; + +const client = new PGlite({ + extensions: { vector }, + fs: new IdbFs('lobechat'), + relaxedDurability: true, +}); + +export const clientDB = drizzle({ client, schema }); diff --git a/src/database/client/migrate.ts b/src/database/client/migrate.ts new file mode 100644 index 000000000000..1c3f2054071f --- /dev/null +++ b/src/database/client/migrate.ts @@ -0,0 +1,24 @@ +import { clientDB } from './db'; +import migrations from './migrations.json'; + +export const migrate = async () => { + //prevent multiple schema migrations to be run + let isLocalDBSchemaSynced = false; + + if (!isLocalDBSchemaSynced) { + const start = Date.now(); + try { + // refs: https://github.com/drizzle-team/drizzle-orm/discussions/2532 + // @ts-ignore + await clientDB.dialect.migrate(migrations, clientDB.session, {}); + isLocalDBSchemaSynced = true; + + console.info(`✅ Local database ready in ${Date.now() - start}ms`); + } catch (cause) { + console.error('❌ Local database schema migration failed', cause); + throw cause; + } + } + + return clientDB; +}; diff --git a/src/database/client/migrations.json b/src/database/client/migrations.json new file mode 100644 index 000000000000..f6600bba1d51 --- /dev/null +++ b/src/database/client/migrations.json @@ -0,0 +1,289 @@ +[ + { + "sql": [ + "CREATE TABLE IF NOT EXISTS \"agents\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"slug\" varchar(100),\n\t\"title\" text,\n\t\"description\" text,\n\t\"tags\" jsonb DEFAULT '[]'::jsonb,\n\t\"avatar\" text,\n\t\"background_color\" text,\n\t\"plugins\" jsonb DEFAULT '[]'::jsonb,\n\t\"user_id\" text NOT NULL,\n\t\"chat_config\" jsonb,\n\t\"few_shots\" jsonb,\n\t\"model\" text,\n\t\"params\" jsonb DEFAULT '{}'::jsonb,\n\t\"provider\" text,\n\t\"system_role\" text,\n\t\"tts\" jsonb,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"agents_slug_unique\" UNIQUE(\"slug\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"agents_tags\" (\n\t\"agent_id\" text NOT NULL,\n\t\"tag_id\" integer NOT NULL,\n\tCONSTRAINT \"agents_tags_agent_id_tag_id_pk\" PRIMARY KEY(\"agent_id\",\"tag_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"agents_to_sessions\" (\n\t\"agent_id\" text NOT NULL,\n\t\"session_id\" text NOT NULL,\n\tCONSTRAINT \"agents_to_sessions_agent_id_session_id_pk\" PRIMARY KEY(\"agent_id\",\"session_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"files\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"user_id\" text NOT NULL,\n\t\"file_type\" varchar(255) NOT NULL,\n\t\"name\" text NOT NULL,\n\t\"size\" integer NOT NULL,\n\t\"url\" text NOT NULL,\n\t\"metadata\" jsonb,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"files_to_agents\" (\n\t\"file_id\" text NOT NULL,\n\t\"agent_id\" text NOT NULL,\n\tCONSTRAINT \"files_to_agents_file_id_agent_id_pk\" PRIMARY KEY(\"file_id\",\"agent_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"files_to_messages\" (\n\t\"file_id\" text NOT NULL,\n\t\"message_id\" text NOT NULL,\n\tCONSTRAINT \"files_to_messages_file_id_message_id_pk\" PRIMARY KEY(\"file_id\",\"message_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"files_to_sessions\" (\n\t\"file_id\" text NOT NULL,\n\t\"session_id\" text NOT NULL,\n\tCONSTRAINT \"files_to_sessions_file_id_session_id_pk\" PRIMARY KEY(\"file_id\",\"session_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"user_installed_plugins\" (\n\t\"user_id\" text NOT NULL,\n\t\"identifier\" text NOT NULL,\n\t\"type\" text NOT NULL,\n\t\"manifest\" jsonb,\n\t\"settings\" jsonb,\n\t\"custom_params\" jsonb,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"user_installed_plugins_user_id_identifier_pk\" PRIMARY KEY(\"user_id\",\"identifier\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"market\" (\n\t\"id\" serial PRIMARY KEY NOT NULL,\n\t\"agent_id\" text,\n\t\"plugin_id\" integer,\n\t\"type\" text NOT NULL,\n\t\"view\" integer DEFAULT 0,\n\t\"like\" integer DEFAULT 0,\n\t\"used\" integer DEFAULT 0,\n\t\"user_id\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"message_plugins\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"tool_call_id\" text,\n\t\"type\" text DEFAULT 'default',\n\t\"api_name\" text,\n\t\"arguments\" text,\n\t\"identifier\" text,\n\t\"state\" jsonb,\n\t\"error\" jsonb\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"message_tts\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"content_md5\" text,\n\t\"file_id\" text,\n\t\"voice\" text\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"message_translates\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"content\" text,\n\t\"from\" text,\n\t\"to\" text\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"messages\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"role\" text NOT NULL,\n\t\"content\" text,\n\t\"model\" text,\n\t\"provider\" text,\n\t\"favorite\" boolean DEFAULT false,\n\t\"error\" jsonb,\n\t\"tools\" jsonb,\n\t\"trace_id\" text,\n\t\"observation_id\" text,\n\t\"user_id\" text NOT NULL,\n\t\"session_id\" text,\n\t\"topic_id\" text,\n\t\"parent_id\" text,\n\t\"quota_id\" text,\n\t\"agent_id\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"plugins\" (\n\t\"id\" serial PRIMARY KEY NOT NULL,\n\t\"identifier\" text NOT NULL,\n\t\"title\" text NOT NULL,\n\t\"description\" text,\n\t\"avatar\" text,\n\t\"author\" text,\n\t\"manifest\" text NOT NULL,\n\t\"locale\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"plugins_identifier_unique\" UNIQUE(\"identifier\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"plugins_tags\" (\n\t\"plugin_id\" integer NOT NULL,\n\t\"tag_id\" integer NOT NULL,\n\tCONSTRAINT \"plugins_tags_plugin_id_tag_id_pk\" PRIMARY KEY(\"plugin_id\",\"tag_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"session_groups\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"name\" text NOT NULL,\n\t\"sort\" integer,\n\t\"user_id\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"sessions\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"slug\" varchar(100) NOT NULL,\n\t\"title\" text,\n\t\"description\" text,\n\t\"avatar\" text,\n\t\"background_color\" text,\n\t\"type\" text DEFAULT 'agent',\n\t\"user_id\" text NOT NULL,\n\t\"group_id\" text,\n\t\"pinned\" boolean DEFAULT false,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"tags\" (\n\t\"id\" serial PRIMARY KEY NOT NULL,\n\t\"slug\" text NOT NULL,\n\t\"name\" text,\n\t\"user_id\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"tags_slug_unique\" UNIQUE(\"slug\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"topics\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"session_id\" text,\n\t\"user_id\" text NOT NULL,\n\t\"favorite\" boolean DEFAULT false,\n\t\"title\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"user_settings\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"tts\" jsonb,\n\t\"key_vaults\" text,\n\t\"general\" jsonb,\n\t\"language_model\" jsonb,\n\t\"system_agent\" jsonb,\n\t\"default_agent\" jsonb,\n\t\"tool\" jsonb\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"users\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"username\" text,\n\t\"email\" text,\n\t\"avatar\" text,\n\t\"phone\" text,\n\t\"first_name\" text,\n\t\"last_name\" text,\n\t\"is_onboarded\" boolean DEFAULT false,\n\t\"clerk_created_at\" timestamp with time zone,\n\t\"preference\" jsonb DEFAULT '{\"guide\":{\"moveSettingsToAvatar\":true,\"topic\":true},\"telemetry\":null,\"useCmdEnterToSend\":false}'::jsonb,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"key\" text,\n\tCONSTRAINT \"users_username_unique\" UNIQUE(\"username\")\n);\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents\" ADD CONSTRAINT \"agents_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_tags\" ADD CONSTRAINT \"agents_tags_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_tags\" ADD CONSTRAINT \"agents_tags_tag_id_tags_id_fk\" FOREIGN KEY (\"tag_id\") REFERENCES \"public\".\"tags\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_to_sessions\" ADD CONSTRAINT \"agents_to_sessions_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_to_sessions\" ADD CONSTRAINT \"agents_to_sessions_session_id_sessions_id_fk\" FOREIGN KEY (\"session_id\") REFERENCES \"public\".\"sessions\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files\" ADD CONSTRAINT \"files_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files_to_agents\" ADD CONSTRAINT \"files_to_agents_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files_to_agents\" ADD CONSTRAINT \"files_to_agents_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files_to_messages\" ADD CONSTRAINT \"files_to_messages_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files_to_messages\" ADD CONSTRAINT \"files_to_messages_message_id_messages_id_fk\" FOREIGN KEY (\"message_id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files_to_sessions\" ADD CONSTRAINT \"files_to_sessions_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files_to_sessions\" ADD CONSTRAINT \"files_to_sessions_session_id_sessions_id_fk\" FOREIGN KEY (\"session_id\") REFERENCES \"public\".\"sessions\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"user_installed_plugins\" ADD CONSTRAINT \"user_installed_plugins_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"market\" ADD CONSTRAINT \"market_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"market\" ADD CONSTRAINT \"market_plugin_id_plugins_id_fk\" FOREIGN KEY (\"plugin_id\") REFERENCES \"public\".\"plugins\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"market\" ADD CONSTRAINT \"market_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_plugins\" ADD CONSTRAINT \"message_plugins_id_messages_id_fk\" FOREIGN KEY (\"id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_tts\" ADD CONSTRAINT \"message_tts_id_messages_id_fk\" FOREIGN KEY (\"id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_tts\" ADD CONSTRAINT \"message_tts_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_translates\" ADD CONSTRAINT \"message_translates_id_messages_id_fk\" FOREIGN KEY (\"id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_session_id_sessions_id_fk\" FOREIGN KEY (\"session_id\") REFERENCES \"public\".\"sessions\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_topic_id_topics_id_fk\" FOREIGN KEY (\"topic_id\") REFERENCES \"public\".\"topics\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_parent_id_messages_id_fk\" FOREIGN KEY (\"parent_id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_quota_id_messages_id_fk\" FOREIGN KEY (\"quota_id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"plugins_tags\" ADD CONSTRAINT \"plugins_tags_plugin_id_plugins_id_fk\" FOREIGN KEY (\"plugin_id\") REFERENCES \"public\".\"plugins\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"plugins_tags\" ADD CONSTRAINT \"plugins_tags_tag_id_tags_id_fk\" FOREIGN KEY (\"tag_id\") REFERENCES \"public\".\"tags\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"session_groups\" ADD CONSTRAINT \"session_groups_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"sessions\" ADD CONSTRAINT \"sessions_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"sessions\" ADD CONSTRAINT \"sessions_group_id_session_groups_id_fk\" FOREIGN KEY (\"group_id\") REFERENCES \"public\".\"session_groups\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"tags\" ADD CONSTRAINT \"tags_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"topics\" ADD CONSTRAINT \"topics_session_id_sessions_id_fk\" FOREIGN KEY (\"session_id\") REFERENCES \"public\".\"sessions\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"topics\" ADD CONSTRAINT \"topics_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"user_settings\" ADD CONSTRAINT \"user_settings_id_users_id_fk\" FOREIGN KEY (\"id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nCREATE INDEX IF NOT EXISTS \"messages_created_at_idx\" ON \"messages\" (\"created_at\");", + "\nCREATE UNIQUE INDEX IF NOT EXISTS \"slug_user_id_unique\" ON \"sessions\" (\"slug\",\"user_id\");\n" + ], + "bps": true, + "folderMillis": 1716982944425, + "hash": "1513c1da50dc083fc0bd9783fe88c60e4fa80b60db645aa87bfda54332252c65" + }, + { + "sql": [ + "ALTER TABLE \"messages\" ADD COLUMN \"client_id\" text;", + "\nALTER TABLE \"session_groups\" ADD COLUMN \"client_id\" text;", + "\nALTER TABLE \"sessions\" ADD COLUMN \"client_id\" text;", + "\nALTER TABLE \"topics\" ADD COLUMN \"client_id\" text;", + "\nCREATE INDEX IF NOT EXISTS \"messages_client_id_idx\" ON \"messages\" (\"client_id\");", + "\nALTER TABLE \"messages\" ADD CONSTRAINT \"messages_client_id_unique\" UNIQUE(\"client_id\");", + "\nALTER TABLE \"session_groups\" ADD CONSTRAINT \"session_groups_client_id_unique\" UNIQUE(\"client_id\");", + "\nALTER TABLE \"sessions\" ADD CONSTRAINT \"sessions_client_id_unique\" UNIQUE(\"client_id\");", + "\nALTER TABLE \"topics\" ADD CONSTRAINT \"topics_client_id_unique\" UNIQUE(\"client_id\");\n" + ], + "bps": true, + "folderMillis": 1717153686544, + "hash": "ddb29ee7e7a675c12b44996e4be061b1736e8f785052242801f4cdfb2a94f258" + }, + { + "sql": [ + "ALTER TABLE \"messages\" DROP CONSTRAINT \"messages_client_id_unique\";", + "\nALTER TABLE \"session_groups\" DROP CONSTRAINT \"session_groups_client_id_unique\";", + "\nALTER TABLE \"sessions\" DROP CONSTRAINT \"sessions_client_id_unique\";", + "\nALTER TABLE \"topics\" DROP CONSTRAINT \"topics_client_id_unique\";", + "\nDROP INDEX IF EXISTS \"messages_client_id_idx\";", + "\nCREATE UNIQUE INDEX IF NOT EXISTS \"message_client_id_user_unique\" ON \"messages\" (\"client_id\",\"user_id\");", + "\nALTER TABLE \"session_groups\" ADD CONSTRAINT \"session_group_client_id_user_unique\" UNIQUE(\"client_id\",\"user_id\");", + "\nALTER TABLE \"sessions\" ADD CONSTRAINT \"sessions_client_id_user_id_unique\" UNIQUE(\"client_id\",\"user_id\");", + "\nALTER TABLE \"topics\" ADD CONSTRAINT \"topic_client_id_user_id_unique\" UNIQUE(\"client_id\",\"user_id\");" + ], + "bps": true, + "folderMillis": 1717587734458, + "hash": "90b61fc3e744d8e2609418d9e25274ff07af4caf87370bb614db511d67900d73" + }, + { + "sql": [ + "CREATE TABLE IF NOT EXISTS \"user_budgets\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"free_budget_id\" text,\n\t\"free_budget_key\" text,\n\t\"subscription_budget_id\" text,\n\t\"subscription_budget_key\" text,\n\t\"package_budget_id\" text,\n\t\"package_budget_key\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"user_subscriptions\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"user_id\" text NOT NULL,\n\t\"stripe_id\" text,\n\t\"currency\" text,\n\t\"pricing\" integer,\n\t\"billing_paid_at\" integer,\n\t\"billing_cycle_start\" integer,\n\t\"billing_cycle_end\" integer,\n\t\"cancel_at_period_end\" boolean,\n\t\"cancel_at\" integer,\n\t\"next_billing\" jsonb,\n\t\"plan\" text,\n\t\"recurring\" text,\n\t\"storage_limit\" integer,\n\t\"status\" integer,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nALTER TABLE \"users\" ALTER COLUMN \"preference\" DROP DEFAULT;", + "\nDO $$ BEGIN\n ALTER TABLE \"user_budgets\" ADD CONSTRAINT \"user_budgets_id_users_id_fk\" FOREIGN KEY (\"id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"user_subscriptions\" ADD CONSTRAINT \"user_subscriptions_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nALTER TABLE \"users\" DROP COLUMN IF EXISTS \"key\";\n" + ], + "bps": true, + "folderMillis": 1718460779230, + "hash": "535a9aba48be3d75762f29bbb195736f17abfe51f41a548debe925949dd0caf2" + }, + { + "sql": [ + "CREATE TABLE IF NOT EXISTS \"nextauth_accounts\" (\n\t\"access_token\" text,\n\t\"expires_at\" integer,\n\t\"id_token\" text,\n\t\"provider\" text NOT NULL,\n\t\"providerAccountId\" text NOT NULL,\n\t\"refresh_token\" text,\n\t\"scope\" text,\n\t\"session_state\" text,\n\t\"token_type\" text,\n\t\"type\" text NOT NULL,\n\t\"userId\" text NOT NULL,\n\tCONSTRAINT \"nextauth_accounts_provider_providerAccountId_pk\" PRIMARY KEY(\"provider\",\"providerAccountId\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"nextauth_authenticators\" (\n\t\"counter\" integer NOT NULL,\n\t\"credentialBackedUp\" boolean NOT NULL,\n\t\"credentialDeviceType\" text NOT NULL,\n\t\"credentialID\" text NOT NULL,\n\t\"credentialPublicKey\" text NOT NULL,\n\t\"providerAccountId\" text NOT NULL,\n\t\"transports\" text,\n\t\"userId\" text NOT NULL,\n\tCONSTRAINT \"nextauth_authenticators_userId_credentialID_pk\" PRIMARY KEY(\"userId\",\"credentialID\"),\n\tCONSTRAINT \"nextauth_authenticators_credentialID_unique\" UNIQUE(\"credentialID\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"nextauth_sessions\" (\n\t\"expires\" timestamp NOT NULL,\n\t\"sessionToken\" text PRIMARY KEY NOT NULL,\n\t\"userId\" text NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"nextauth_verificationtokens\" (\n\t\"expires\" timestamp NOT NULL,\n\t\"identifier\" text NOT NULL,\n\t\"token\" text NOT NULL,\n\tCONSTRAINT \"nextauth_verificationtokens_identifier_token_pk\" PRIMARY KEY(\"identifier\",\"token\")\n);\n", + "\nALTER TABLE \"users\" ADD COLUMN \"full_name\" text;", + "\nALTER TABLE \"users\" ADD COLUMN \"email_verified_at\" timestamp with time zone;", + "\nDO $$ BEGIN\n ALTER TABLE \"nextauth_accounts\" ADD CONSTRAINT \"nextauth_accounts_userId_users_id_fk\" FOREIGN KEY (\"userId\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"nextauth_authenticators\" ADD CONSTRAINT \"nextauth_authenticators_userId_users_id_fk\" FOREIGN KEY (\"userId\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"nextauth_sessions\" ADD CONSTRAINT \"nextauth_sessions_userId_users_id_fk\" FOREIGN KEY (\"userId\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n" + ], + "bps": true, + "folderMillis": 1721724512422, + "hash": "c63c5819d73414632ea32c543cfb997be31a2be3fad635c148c97e726c57fd16" + }, + { + "sql": [ + "-- Custom SQL migration file, put you code below! --\nCREATE EXTENSION IF NOT EXISTS vector;\n" + ], + "bps": true, + "folderMillis": 1722944166657, + "hash": "c112a4eb471fa4efe791b250057a1e33040515a0c60361c7d7a59044ec9e1667" + }, + { + "sql": [ + "CREATE TABLE IF NOT EXISTS \"agents_files\" (\n\t\"file_id\" text NOT NULL,\n\t\"agent_id\" text NOT NULL,\n\t\"enabled\" boolean DEFAULT true,\n\t\"user_id\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"agents_files_file_id_agent_id_user_id_pk\" PRIMARY KEY(\"file_id\",\"agent_id\",\"user_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"agents_knowledge_bases\" (\n\t\"agent_id\" text NOT NULL,\n\t\"knowledge_base_id\" text NOT NULL,\n\t\"user_id\" text NOT NULL,\n\t\"enabled\" boolean DEFAULT true,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"agents_knowledge_bases_agent_id_knowledge_base_id_pk\" PRIMARY KEY(\"agent_id\",\"knowledge_base_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"async_tasks\" (\n\t\"id\" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,\n\t\"type\" text,\n\t\"status\" text,\n\t\"error\" jsonb,\n\t\"user_id\" text NOT NULL,\n\t\"duration\" integer,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"file_chunks\" (\n\t\"file_id\" varchar,\n\t\"chunk_id\" uuid,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"file_chunks_file_id_chunk_id_pk\" PRIMARY KEY(\"file_id\",\"chunk_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"global_files\" (\n\t\"hash_id\" varchar(64) PRIMARY KEY NOT NULL,\n\t\"file_type\" varchar(255) NOT NULL,\n\t\"size\" integer NOT NULL,\n\t\"url\" text NOT NULL,\n\t\"metadata\" jsonb,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"knowledge_base_files\" (\n\t\"knowledge_base_id\" text NOT NULL,\n\t\"file_id\" text NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\tCONSTRAINT \"knowledge_base_files_knowledge_base_id_file_id_pk\" PRIMARY KEY(\"knowledge_base_id\",\"file_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"knowledge_bases\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"name\" text NOT NULL,\n\t\"description\" text,\n\t\"avatar\" text,\n\t\"type\" text,\n\t\"user_id\" text NOT NULL,\n\t\"is_public\" boolean DEFAULT false,\n\t\"settings\" jsonb,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"message_chunks\" (\n\t\"message_id\" text,\n\t\"chunk_id\" uuid,\n\tCONSTRAINT \"message_chunks_chunk_id_message_id_pk\" PRIMARY KEY(\"chunk_id\",\"message_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"message_queries\" (\n\t\"id\" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,\n\t\"message_id\" text NOT NULL,\n\t\"rewrite_query\" text,\n\t\"user_query\" text,\n\t\"embeddings_id\" uuid\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"message_query_chunks\" (\n\t\"id\" text,\n\t\"query_id\" uuid,\n\t\"chunk_id\" uuid,\n\t\"similarity\" numeric(6, 5),\n\tCONSTRAINT \"message_query_chunks_chunk_id_id_query_id_pk\" PRIMARY KEY(\"chunk_id\",\"id\",\"query_id\")\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"chunks\" (\n\t\"id\" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,\n\t\"text\" text,\n\t\"abstract\" text,\n\t\"metadata\" jsonb,\n\t\"index\" integer,\n\t\"type\" varchar,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"user_id\" text\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"embeddings\" (\n\t\"id\" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,\n\t\"chunk_id\" uuid,\n\t\"embeddings\" vector(1024),\n\t\"model\" text,\n\t\"user_id\" text\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"unstructured_chunks\" (\n\t\"id\" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,\n\t\"text\" text,\n\t\"metadata\" jsonb,\n\t\"index\" integer,\n\t\"type\" varchar,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"parent_id\" varchar,\n\t\"composite_id\" uuid,\n\t\"user_id\" text,\n\t\"file_id\" varchar\n);\n", + "\nALTER TABLE \"files_to_messages\" RENAME TO \"messages_files\";", + "\nDROP TABLE \"files_to_agents\";", + "\nALTER TABLE \"files\" ADD COLUMN \"file_hash\" varchar(64);", + "\nALTER TABLE \"files\" ADD COLUMN \"chunk_task_id\" uuid;", + "\nALTER TABLE \"files\" ADD COLUMN \"embedding_task_id\" uuid;", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_files\" ADD CONSTRAINT \"agents_files_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_files\" ADD CONSTRAINT \"agents_files_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_files\" ADD CONSTRAINT \"agents_files_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_knowledge_bases\" ADD CONSTRAINT \"agents_knowledge_bases_agent_id_agents_id_fk\" FOREIGN KEY (\"agent_id\") REFERENCES \"public\".\"agents\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_knowledge_bases\" ADD CONSTRAINT \"agents_knowledge_bases_knowledge_base_id_knowledge_bases_id_fk\" FOREIGN KEY (\"knowledge_base_id\") REFERENCES \"public\".\"knowledge_bases\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"agents_knowledge_bases\" ADD CONSTRAINT \"agents_knowledge_bases_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"async_tasks\" ADD CONSTRAINT \"async_tasks_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"file_chunks\" ADD CONSTRAINT \"file_chunks_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"file_chunks\" ADD CONSTRAINT \"file_chunks_chunk_id_chunks_id_fk\" FOREIGN KEY (\"chunk_id\") REFERENCES \"public\".\"chunks\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"knowledge_base_files\" ADD CONSTRAINT \"knowledge_base_files_knowledge_base_id_knowledge_bases_id_fk\" FOREIGN KEY (\"knowledge_base_id\") REFERENCES \"public\".\"knowledge_bases\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"knowledge_base_files\" ADD CONSTRAINT \"knowledge_base_files_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"knowledge_bases\" ADD CONSTRAINT \"knowledge_bases_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_chunks\" ADD CONSTRAINT \"message_chunks_message_id_messages_id_fk\" FOREIGN KEY (\"message_id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_chunks\" ADD CONSTRAINT \"message_chunks_chunk_id_chunks_id_fk\" FOREIGN KEY (\"chunk_id\") REFERENCES \"public\".\"chunks\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_queries\" ADD CONSTRAINT \"message_queries_message_id_messages_id_fk\" FOREIGN KEY (\"message_id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_queries\" ADD CONSTRAINT \"message_queries_embeddings_id_embeddings_id_fk\" FOREIGN KEY (\"embeddings_id\") REFERENCES \"public\".\"embeddings\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_query_chunks\" ADD CONSTRAINT \"message_query_chunks_id_messages_id_fk\" FOREIGN KEY (\"id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_query_chunks\" ADD CONSTRAINT \"message_query_chunks_query_id_message_queries_id_fk\" FOREIGN KEY (\"query_id\") REFERENCES \"public\".\"message_queries\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"message_query_chunks\" ADD CONSTRAINT \"message_query_chunks_chunk_id_chunks_id_fk\" FOREIGN KEY (\"chunk_id\") REFERENCES \"public\".\"chunks\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages_files\" ADD CONSTRAINT \"messages_files_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages_files\" ADD CONSTRAINT \"messages_files_message_id_messages_id_fk\" FOREIGN KEY (\"message_id\") REFERENCES \"public\".\"messages\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"chunks\" ADD CONSTRAINT \"chunks_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"embeddings\" ADD CONSTRAINT \"embeddings_chunk_id_chunks_id_fk\" FOREIGN KEY (\"chunk_id\") REFERENCES \"public\".\"chunks\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"embeddings\" ADD CONSTRAINT \"embeddings_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"unstructured_chunks\" ADD CONSTRAINT \"unstructured_chunks_composite_id_chunks_id_fk\" FOREIGN KEY (\"composite_id\") REFERENCES \"public\".\"chunks\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"unstructured_chunks\" ADD CONSTRAINT \"unstructured_chunks_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"unstructured_chunks\" ADD CONSTRAINT \"unstructured_chunks_file_id_files_id_fk\" FOREIGN KEY (\"file_id\") REFERENCES \"public\".\"files\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files\" ADD CONSTRAINT \"files_file_hash_global_files_hash_id_fk\" FOREIGN KEY (\"file_hash\") REFERENCES \"public\".\"global_files\"(\"hash_id\") ON DELETE no action ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files\" ADD CONSTRAINT \"files_chunk_task_id_async_tasks_id_fk\" FOREIGN KEY (\"chunk_task_id\") REFERENCES \"public\".\"async_tasks\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"files\" ADD CONSTRAINT \"files_embedding_task_id_async_tasks_id_fk\" FOREIGN KEY (\"embedding_task_id\") REFERENCES \"public\".\"async_tasks\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n" + ], + "bps": true, + "folderMillis": 1724089032064, + "hash": "bc4e36664868d14888b9e9aef180b3e02c563fa3c253111787e68b8ea4cd995f" + }, + { + "sql": [ + "-- step 1: create a temporary table to store the rows we want to keep\nCREATE TEMP TABLE embeddings_temp AS\nSELECT DISTINCT ON (chunk_id) *\nFROM embeddings\nORDER BY chunk_id, random();\n", + "\n\n-- step 2: delete all rows from the original table\nDELETE FROM embeddings;\n", + "\n\n-- step 3: insert the rows we want to keep back into the original table\nINSERT INTO embeddings\nSELECT * FROM embeddings_temp;\n", + "\n\n-- step 4: drop the temporary table\nDROP TABLE embeddings_temp;\n", + "\n\n-- step 5: now it's safe to add the unique constraint\nALTER TABLE \"embeddings\" ADD CONSTRAINT \"embeddings_chunk_id_unique\" UNIQUE(\"chunk_id\");\n" + ], + "bps": true, + "folderMillis": 1724254147447, + "hash": "e99840848ffbb33ca4d7ead6158f02b8d12cb4ff5706d4529d7fa586afa4c2a9" + }, + { + "sql": [ + "CREATE TABLE IF NOT EXISTS \"rag_eval_dataset_records\" (\n\t\"id\" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name \"rag_eval_dataset_records_id_seq\" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),\n\t\"dataset_id\" integer NOT NULL,\n\t\"ideal\" text,\n\t\"question\" text,\n\t\"reference_files\" text[],\n\t\"metadata\" jsonb,\n\t\"user_id\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"rag_eval_datasets\" (\n\t\"id\" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name \"rag_eval_datasets_id_seq\" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 30000 CACHE 1),\n\t\"description\" text,\n\t\"name\" text NOT NULL,\n\t\"knowledge_base_id\" text,\n\t\"user_id\" text,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"rag_eval_evaluations\" (\n\t\"id\" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name \"rag_eval_evaluations_id_seq\" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),\n\t\"name\" text NOT NULL,\n\t\"description\" text,\n\t\"eval_records_url\" text,\n\t\"status\" text,\n\t\"error\" jsonb,\n\t\"dataset_id\" integer NOT NULL,\n\t\"knowledge_base_id\" text,\n\t\"language_model\" text,\n\t\"embedding_model\" text,\n\t\"user_id\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nCREATE TABLE IF NOT EXISTS \"rag_eval_evaluation_records\" (\n\t\"id\" integer PRIMARY KEY GENERATED ALWAYS AS IDENTITY (sequence name \"rag_eval_evaluation_records_id_seq\" INCREMENT BY 1 MINVALUE 1 MAXVALUE 2147483647 START WITH 1 CACHE 1),\n\t\"question\" text NOT NULL,\n\t\"answer\" text,\n\t\"context\" text[],\n\t\"ideal\" text,\n\t\"status\" text,\n\t\"error\" jsonb,\n\t\"language_model\" text,\n\t\"embedding_model\" text,\n\t\"question_embedding_id\" uuid,\n\t\"duration\" integer,\n\t\"dataset_record_id\" integer NOT NULL,\n\t\"evaluation_id\" integer NOT NULL,\n\t\"user_id\" text,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_dataset_records\" ADD CONSTRAINT \"rag_eval_dataset_records_dataset_id_rag_eval_datasets_id_fk\" FOREIGN KEY (\"dataset_id\") REFERENCES \"public\".\"rag_eval_datasets\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_dataset_records\" ADD CONSTRAINT \"rag_eval_dataset_records_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_datasets\" ADD CONSTRAINT \"rag_eval_datasets_knowledge_base_id_knowledge_bases_id_fk\" FOREIGN KEY (\"knowledge_base_id\") REFERENCES \"public\".\"knowledge_bases\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_datasets\" ADD CONSTRAINT \"rag_eval_datasets_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluations\" ADD CONSTRAINT \"rag_eval_evaluations_dataset_id_rag_eval_datasets_id_fk\" FOREIGN KEY (\"dataset_id\") REFERENCES \"public\".\"rag_eval_datasets\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluations\" ADD CONSTRAINT \"rag_eval_evaluations_knowledge_base_id_knowledge_bases_id_fk\" FOREIGN KEY (\"knowledge_base_id\") REFERENCES \"public\".\"knowledge_bases\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluations\" ADD CONSTRAINT \"rag_eval_evaluations_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluation_records\" ADD CONSTRAINT \"rag_eval_evaluation_records_question_embedding_id_embeddings_id_fk\" FOREIGN KEY (\"question_embedding_id\") REFERENCES \"public\".\"embeddings\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluation_records\" ADD CONSTRAINT \"rag_eval_evaluation_records_dataset_record_id_rag_eval_dataset_records_id_fk\" FOREIGN KEY (\"dataset_record_id\") REFERENCES \"public\".\"rag_eval_dataset_records\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluation_records\" ADD CONSTRAINT \"rag_eval_evaluation_records_evaluation_id_rag_eval_evaluations_id_fk\" FOREIGN KEY (\"evaluation_id\") REFERENCES \"public\".\"rag_eval_evaluations\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"rag_eval_evaluation_records\" ADD CONSTRAINT \"rag_eval_evaluation_records_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n" + ], + "bps": true, + "folderMillis": 1725366565650, + "hash": "9646161fa041354714f823d726af27247bcd6e60fa3be5698c0d69f337a5700b" + }, + { + "sql": [ + "DROP TABLE \"user_budgets\";", + "\nDROP TABLE \"user_subscriptions\";" + ], + "bps": true, + "folderMillis": 1729699958471, + "hash": "7dad43a2a25d1aec82124a4e53f8d82f8505c3073f23606c1dc5d2a4598eacf9" + }, + { + "sql": [ + "DROP TABLE \"agents_tags\" CASCADE;", + "\nDROP TABLE \"market\" CASCADE;", + "\nDROP TABLE \"plugins\" CASCADE;", + "\nDROP TABLE \"plugins_tags\" CASCADE;", + "\nDROP TABLE \"tags\" CASCADE;", + "\nALTER TABLE \"agents\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"agents_files\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"agents_knowledge_bases\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"async_tasks\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"files\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"global_files\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"knowledge_bases\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"messages\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"chunks\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"unstructured_chunks\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"rag_eval_dataset_records\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"rag_eval_dataset_records\" ADD COLUMN \"updated_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"rag_eval_datasets\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"rag_eval_evaluations\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"rag_eval_evaluation_records\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"rag_eval_evaluation_records\" ADD COLUMN \"updated_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"session_groups\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"sessions\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"topics\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"user_installed_plugins\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;", + "\nALTER TABLE \"users\" ADD COLUMN \"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL;" + ], + "bps": true, + "folderMillis": 1730900133049, + "hash": "a7d801b679e25ef3ffda343366992b2835c089363e9d7c09074336d40e438004" + }, + { + "sql": [ + "ALTER TABLE \"topics\" ADD COLUMN \"history_summary\" text;", + "\nALTER TABLE \"topics\" ADD COLUMN \"metadata\" jsonb;\n" + ], + "bps": true, + "folderMillis": 1731138670427, + "hash": "80c2eae0600190b354e4fd6b619687a66186b992ec687495bb55c6c163a98fa6" + }, + { + "sql": [ + "CREATE TABLE IF NOT EXISTS \"threads\" (\n\t\"id\" text PRIMARY KEY NOT NULL,\n\t\"title\" text,\n\t\"type\" text NOT NULL,\n\t\"status\" text DEFAULT 'active',\n\t\"topic_id\" text NOT NULL,\n\t\"source_message_id\" text NOT NULL,\n\t\"parent_thread_id\" text,\n\t\"user_id\" text NOT NULL,\n\t\"last_active_at\" timestamp with time zone DEFAULT now(),\n\t\"accessed_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"created_at\" timestamp with time zone DEFAULT now() NOT NULL,\n\t\"updated_at\" timestamp with time zone DEFAULT now() NOT NULL\n);\n", + "\nALTER TABLE \"messages\" ADD COLUMN \"thread_id\" text;", + "\nDO $$ BEGIN\n ALTER TABLE \"threads\" ADD CONSTRAINT \"threads_topic_id_topics_id_fk\" FOREIGN KEY (\"topic_id\") REFERENCES \"public\".\"topics\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"threads\" ADD CONSTRAINT \"threads_parent_thread_id_threads_id_fk\" FOREIGN KEY (\"parent_thread_id\") REFERENCES \"public\".\"threads\"(\"id\") ON DELETE set null ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"threads\" ADD CONSTRAINT \"threads_user_id_users_id_fk\" FOREIGN KEY (\"user_id\") REFERENCES \"public\".\"users\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n", + "\nDO $$ BEGIN\n ALTER TABLE \"messages\" ADD CONSTRAINT \"messages_thread_id_threads_id_fk\" FOREIGN KEY (\"thread_id\") REFERENCES \"public\".\"threads\"(\"id\") ON DELETE cascade ON UPDATE no action;\nEXCEPTION\n WHEN duplicate_object THEN null;\nEND $$;\n" + ], + "bps": true, + "folderMillis": 1731858381716, + "hash": "d8263bfefe296ed366379c7b7fc65195d12e6a1c0a9f1c96097ea28f2123fe50" + } +] \ No newline at end of file diff --git a/src/database/migrations/0006_add_knowledge_base.sql b/src/database/migrations/0006_add_knowledge_base.sql index fe0599676869..421b6b36d3f7 100644 --- a/src/database/migrations/0006_add_knowledge_base.sql +++ b/src/database/migrations/0006_add_knowledge_base.sql @@ -121,7 +121,7 @@ CREATE TABLE IF NOT EXISTS "unstructured_chunks" ( "file_id" varchar ); --> statement-breakpoint -ALTER TABLE "files_to_messages" RENAME TO "messages_files"; +ALTER TABLE "files_to_messages" RENAME TO "messages_files";--> statement-breakpoint DROP TABLE "files_to_agents";--> statement-breakpoint ALTER TABLE "files" ADD COLUMN "file_hash" varchar(64);--> statement-breakpoint ALTER TABLE "files" ADD COLUMN "chunk_task_id" uuid;--> statement-breakpoint diff --git a/src/database/migrations/0007_fix_embedding_table.sql b/src/database/migrations/0007_fix_embedding_table.sql index bd54e795b2b3..7922a02cc682 100644 --- a/src/database/migrations/0007_fix_embedding_table.sql +++ b/src/database/migrations/0007_fix_embedding_table.sql @@ -3,16 +3,20 @@ CREATE TEMP TABLE embeddings_temp AS SELECT DISTINCT ON (chunk_id) * FROM embeddings ORDER BY chunk_id, random(); +--> statement-breakpoint -- step 2: delete all rows from the original table DELETE FROM embeddings; +--> statement-breakpoint -- step 3: insert the rows we want to keep back into the original table INSERT INTO embeddings SELECT * FROM embeddings_temp; +--> statement-breakpoint -- step 4: drop the temporary table DROP TABLE embeddings_temp; +--> statement-breakpoint -- step 5: now it's safe to add the unique constraint ALTER TABLE "embeddings" ADD CONSTRAINT "embeddings_chunk_id_unique" UNIQUE("chunk_id"); diff --git a/src/database/server/models/topic.ts b/src/database/server/models/topic.ts index ad914d614fe6..9e11ed61fdf7 100644 --- a/src/database/server/models/topic.ts +++ b/src/database/server/models/topic.ts @@ -2,9 +2,9 @@ import { Column, count, inArray, sql } from 'drizzle-orm'; import { and, desc, eq, exists, isNull, like, or } from 'drizzle-orm/expressions'; import { LobeChatDatabase } from '@/database/type'; +import { idGenerator } from '@/database/utils/idGenerator'; import { NewMessage, TopicItem, messages, topics } from '../../schemas'; -import { idGenerator } from '@/database/utils/idGenerator'; export interface CreateTopicParams { favorite?: boolean; diff --git a/src/layout/GlobalProvider/StoreInitialization.tsx b/src/layout/GlobalProvider/StoreInitialization.tsx index 24345ef8ec8b..c704cc928a7b 100644 --- a/src/layout/GlobalProvider/StoreInitialization.tsx +++ b/src/layout/GlobalProvider/StoreInitialization.tsx @@ -6,6 +6,7 @@ import { useTranslation } from 'react-i18next'; import { createStoreUpdater } from 'zustand-utils'; import { LOBE_URL_IMPORT_NAME } from '@/const/url'; +import { migrate } from '@/database/client/migrate'; import { useIsMobile } from '@/hooks/useIsMobile'; import { useEnabledDataSync } from '@/hooks/useSyncData'; import { useAgentStore } from '@/store/agent'; @@ -90,6 +91,11 @@ const StoreInitialization = memo(() => { } }, [router, mobile]); + useEffect(() => { + migrate().then(() => { + console.log('migrate success!'); + }); + }, []); return null; }); diff --git a/src/services/message/client.test.ts b/src/services/message/client.test.ts index 867fda6c5b72..eced3f893d2c 100644 --- a/src/services/message/client.test.ts +++ b/src/services/message/client.test.ts @@ -1,133 +1,156 @@ import dayjs from 'dayjs'; -import { Mock, describe, expect, it, vi } from 'vitest'; +import { and, eq } from 'drizzle-orm'; +import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { CreateMessageParams, MessageModel } from '@/database/_deprecated/models/message'; +import { MessageModel } from '@/database/_deprecated/models/message'; +import { clientDB } from '@/database/client/db'; +import { migrate } from '@/database/client/migrate'; +import { + MessageItem, + files, + messagePlugins, + messageTTS, + messageTranslates, + messages, + sessions, + topics, + users, +} from '@/database/schemas'; import { ChatMessage, ChatMessageError, - ChatPluginPayload, ChatTTS, ChatTranslate, + CreateMessageParams, } from '@/types/message'; import { ClientService } from './client'; -const messageService = new ClientService(); - -// Mock the MessageModel -vi.mock('@/database/_deprecated/models/message', () => { - return { - MessageModel: { - create: vi.fn(), - batchCreate: vi.fn(), - count: vi.fn(), - query: vi.fn(), - delete: vi.fn(), - bulkDelete: vi.fn(), - queryBySessionId: vi.fn(), - update: vi.fn(), - updatePlugin: vi.fn(), - batchDelete: vi.fn(), - clearTable: vi.fn(), - batchUpdate: vi.fn(), - queryAll: vi.fn(), - updatePluginState: vi.fn(), - }, - }; +const userId = 'message-db'; +const sessionId = '1'; +const topicId = 'topic-id'; + +// Mock data +const mockMessageId = 'mock-message-id'; +const mockMessage = { + id: mockMessageId, + content: 'Mock message content', + sessionId, + role: 'user', +} as ChatMessage; + +const mockMessages = [mockMessage]; + +beforeEach(async () => { + await migrate(); + + // 在每个测试用例之前,清空表 + await clientDB.transaction(async (trx) => { + await trx.delete(users); + await trx.insert(users).values([{ id: userId }, { id: '456' }]); + + await trx.insert(sessions).values([{ id: sessionId, userId }]); + await trx.insert(topics).values([{ id: topicId, sessionId, userId }]); + await trx.insert(files).values({ + id: 'f1', + userId: userId, + url: 'abc', + name: 'file-1', + fileType: 'image/png', + size: 1000, + }); + }); }); -describe('MessageClientService', () => { - // Mock data - const mockMessageId = 'mock-message-id'; - const mockMessage = { - id: mockMessageId, - content: 'Mock message content', - sessionId: 'mock-session-id', - createdAt: 100, - updatedAt: 100, - role: 'user', - // ... other properties - } as ChatMessage; - const mockMessages = [mockMessage]; - - beforeEach(() => { - // Reset all mocks before running each test case - vi.resetAllMocks(); - }); +afterEach(async () => { + // 在每个测试用例之后,清空表 + await clientDB.delete(users); +}); + +const messageService = new ClientService(userId); +describe('MessageClientService', () => { describe('create', () => { it('should create a message and return its id', async () => { // Setup - const createParams = { + const createParams: CreateMessageParams = { content: 'New message content', - sessionId: '1', - // ... other properties - } as CreateMessageParams; - (MessageModel.create as Mock).mockResolvedValue({ id: mockMessageId }); + sessionId, + role: 'user', + }; // Execute const messageId = await messageService.createMessage(createParams); // Assert - expect(MessageModel.create).toHaveBeenCalledWith(createParams); - expect(messageId).toBe(mockMessageId); + expect(messageId).toMatch(/^msg_/); }); }); describe('batchCreate', () => { it('should batch create messages', async () => { - // Setup - (MessageModel.batchCreate as Mock).mockResolvedValue(mockMessages); - // Execute - const result = await messageService.batchCreateMessages(mockMessages); + await messageService.batchCreateMessages([ + { + content: 'Mock message content', + sessionId, + role: 'user', + }, + { + content: 'Mock message content', + sessionId, + role: 'user', + }, + ] as MessageItem[]); + const count = await clientDB.$count(messages); // Assert - expect(MessageModel.batchCreate).toHaveBeenCalledWith(mockMessages); - expect(result).toBe(mockMessages); + expect(count).toBe(2); }); }); describe('removeMessage', () => { it('should remove a message by id', async () => { - // Setup - (MessageModel.delete as Mock).mockResolvedValue(true); - // Execute - const result = await messageService.removeMessage(mockMessageId); + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); + await messageService.removeMessage(mockMessageId); // Assert - expect(MessageModel.delete).toHaveBeenCalledWith(mockMessageId); - expect(result).toBe(true); + const count = await clientDB.$count(messages); + + expect(count).toBe(0); }); }); describe('removeMessages', () => { it('should remove a message by id', async () => { // Setup - (MessageModel.bulkDelete as Mock).mockResolvedValue(true); + await clientDB.insert(messages).values([ + { id: mockMessageId, role: 'user', userId }, + { role: 'assistant', userId }, + ]); // Execute - const result = await messageService.removeMessages([mockMessageId]); + await messageService.removeMessages([mockMessageId]); // Assert - expect(MessageModel.bulkDelete).toHaveBeenCalledWith([mockMessageId]); - expect(result).toBe(true); + const count = await clientDB.$count(messages); + + expect(count).toBe(1); }); }); describe('getMessages', () => { it('should retrieve messages by sessionId and topicId', async () => { // Setup - const sessionId = 'session-id'; - const topicId = 'topic-id'; - (MessageModel.query as Mock).mockResolvedValue(mockMessages); + await clientDB + .insert(messages) + .values({ id: mockMessageId, sessionId, topicId, role: 'user', userId }); // Execute - const messages = await messageService.getMessages(sessionId, topicId); + const data = await messageService.getMessages(sessionId, topicId); // Assert - expect(MessageModel.query).toHaveBeenCalledWith({ sessionId, topicId }); - expect(messages).toEqual(mockMessages.map((i) => ({ ...i, imageList: [] }))); + expect(data[0]).toMatchObject({ id: mockMessageId, role: 'user' }); }); }); @@ -135,14 +158,21 @@ describe('MessageClientService', () => { it('should retrieve all messages in a session', async () => { // Setup const sessionId = 'session-id'; - (MessageModel.queryBySessionId as Mock).mockResolvedValue(mockMessages); + await clientDB.insert(sessions).values([ + { id: 'bbb', userId }, + { id: sessionId, userId }, + ]); + await clientDB.insert(messages).values([ + { sessionId, topicId, role: 'user', userId }, + { sessionId, topicId, role: 'assistant', userId }, + { sessionId: 'bbb', topicId, role: 'assistant', userId }, + ]); // Execute - const messages = await messageService.getAllMessagesInSession(sessionId); + const data = await messageService.getAllMessagesInSession(sessionId); // Assert - expect(MessageModel.queryBySessionId).toHaveBeenCalledWith(sessionId); - expect(messages).toBe(mockMessages); + expect(data.length).toBe(2); }); }); @@ -150,77 +180,85 @@ describe('MessageClientService', () => { it('should batch remove messages by assistantId and topicId', async () => { // Setup const assistantId = 'assistant-id'; - const topicId = 'topic-id'; - (MessageModel.batchDelete as Mock).mockResolvedValue(true); + const sessionId = 'session-id'; + await clientDB.insert(sessions).values([ + { id: 'bbb', userId }, + { id: sessionId, userId }, + ]); + await clientDB.insert(messages).values([ + { sessionId, topicId, role: 'user', userId }, + { sessionId, topicId, role: 'assistant', userId }, + { sessionId: 'bbb', topicId, role: 'assistant', userId }, + ]); // Execute - const result = await messageService.removeMessagesByAssistant(assistantId, topicId); + await messageService.removeMessagesByAssistant(sessionId, topicId); // Assert - expect(MessageModel.batchDelete).toHaveBeenCalledWith(assistantId, topicId); - expect(result).toBe(true); + const result = await clientDB.query.messages.findMany({ + where: and(eq(messages.sessionId, sessionId), eq(messages.topicId, topicId)), + }); + + expect(result.length).toBe(0); }); }); describe('clearAllMessage', () => { it('should clear all messages from the table', async () => { // Setup - (MessageModel.clearTable as Mock).mockResolvedValue(true); + await clientDB.insert(users).values({ id: 'another' }); + await clientDB.insert(messages).values([ + { id: mockMessageId, role: 'user', userId }, + { role: 'user', userId: 'another' }, + ]); // Execute - const result = await messageService.removeAllMessages(); + await messageService.removeAllMessages(); // Assert - expect(MessageModel.clearTable).toHaveBeenCalled(); - expect(result).toBe(true); - }); - }); - - describe('bindMessagesToTopic', () => { - it('should batch update messages to bind them to a topic', async () => { - // Setup - const topicId = 'topic-id'; - const messageIds = [mockMessageId]; - (MessageModel.batchUpdate as Mock).mockResolvedValue(mockMessages); - - // Execute - const result = await messageService.bindMessagesToTopic(topicId, messageIds); - - // Assert - expect(MessageModel.batchUpdate).toHaveBeenCalledWith(messageIds, { topicId }); - expect(result).toBe(mockMessages); + const result = await clientDB.query.messages.findMany({ + where: eq(messages.userId, userId), + }); + expect(result.length).toBe(0); }); }); describe('getAllMessages', () => { it('should retrieve all messages', async () => { - // Setup - (MessageModel.queryAll as Mock).mockResolvedValue(mockMessages); + await clientDB.insert(messages).values([ + { sessionId, topicId, content: '1', role: 'user', userId }, + { sessionId, topicId, content: '2', role: 'assistant', userId }, + ]); // Execute - const messages = await messageService.getAllMessages(); + const data = await messageService.getAllMessages(); // Assert - expect(MessageModel.queryAll).toHaveBeenCalled(); - expect(messages).toBe(mockMessages); + expect(data).toMatchObject([ + { sessionId, topicId, content: '1', role: 'user', userId }, + { sessionId, topicId, content: '2', role: 'assistant', userId }, + ]); }); }); describe('updateMessageError', () => { it('should update the error field of a message', async () => { // Setup + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); const newError = { type: 'InvalidProviderAPIKey', message: 'Error occurred', } as ChatMessageError; - (MessageModel.update as Mock).mockResolvedValue({ ...mockMessage, error: newError }); // Execute - const result = await messageService.updateMessageError(mockMessageId, newError); + await messageService.updateMessageError(mockMessageId, newError); // Assert - expect(MessageModel.update).toHaveBeenCalledWith(mockMessageId, { error: newError }); - expect(result).toEqual({ ...mockMessage, error: newError }); + const result = await clientDB.query.messages.findFirst({ + where: eq(messages.id, mockMessageId), + }); + + expect(result!.error).toEqual(newError); }); }); @@ -248,88 +286,85 @@ describe('MessageClientService', () => { describe('updateMessagePluginState', () => { it('should update the plugin state of a message', async () => { // Setup + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); + await clientDB.insert(messagePlugins).values({ id: mockMessageId }); const key = 'stateKey'; const value = 'stateValue'; const newPluginState = { [key]: value }; - (MessageModel.updatePluginState as Mock).mockResolvedValue({ - ...mockMessage, - pluginState: newPluginState, - }); // Execute - const result = await messageService.updateMessagePluginState(mockMessageId, { key: value }); + await messageService.updateMessagePluginState(mockMessageId, { stateKey: value }); // Assert - expect(MessageModel.updatePluginState).toHaveBeenCalledWith(mockMessageId, { key: value }); - expect(result).toEqual({ ...mockMessage, pluginState: newPluginState }); + const result = await clientDB.query.messagePlugins.findFirst({ + where: eq(messagePlugins.id, mockMessageId), + }); + expect(result!.state).toEqual(newPluginState); }); }); describe('updateMessagePluginArguments', () => { it('should update the plugin arguments object of a message', async () => { // Setup - const key = 'stateKey'; + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); + await clientDB.insert(messagePlugins).values({ id: mockMessageId }); const value = 'stateValue'; - (MessageModel.updatePlugin as Mock).mockResolvedValue({}); // Execute await messageService.updateMessagePluginArguments(mockMessageId, { key: value }); // Assert - expect(MessageModel.updatePlugin).toHaveBeenCalledWith(mockMessageId, { - arguments: '{"key":"stateValue"}', + const result = await clientDB.query.messagePlugins.findFirst({ + where: eq(messageTTS.id, mockMessageId), }); + expect(result).toMatchObject({ arguments: '{"key":"stateValue"}' }); }); it('should update the plugin arguments string of a message', async () => { // Setup - const key = 'stateKey'; + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); + await clientDB.insert(messagePlugins).values({ id: mockMessageId }); const value = 'stateValue'; - (MessageModel.updatePlugin as Mock).mockResolvedValue({}); - // Execute await messageService.updateMessagePluginArguments( mockMessageId, - JSON.stringify({ key: value }), + JSON.stringify({ abc: value }), ); // Assert - expect(MessageModel.updatePlugin).toHaveBeenCalledWith(mockMessageId, { - arguments: '{"key":"stateValue"}', + const result = await clientDB.query.messagePlugins.findFirst({ + where: eq(messageTTS.id, mockMessageId), }); + expect(result).toMatchObject({ arguments: '{"abc":"stateValue"}' }); }); }); describe('countMessages', () => { it('should count the total number of messages', async () => { // Setup - const mockCount = 10; - (MessageModel.count as Mock).mockResolvedValue(mockCount); + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); // Execute const count = await messageService.countMessages(); // Assert - expect(MessageModel.count).toHaveBeenCalled(); - expect(count).toBe(mockCount); + expect(count).toBe(1); }); }); describe('countTodayMessages', () => { it('should count the number of messages created today', async () => { // Setup - const today = dayjs().format('YYYY-MM-DD'); const mockMessages = [ - { ...mockMessage, createdAt: today }, - { ...mockMessage, createdAt: today }, - { ...mockMessage, createdAt: '2023-01-01' }, + { ...mockMessage, id: undefined, createdAt: new Date(), userId }, + { ...mockMessage, id: undefined, createdAt: new Date(), userId }, + { ...mockMessage, id: undefined, createdAt: new Date('2023-01-01'), userId }, ]; - (MessageModel.queryAll as Mock).mockResolvedValue(mockMessages); + await clientDB.insert(messages).values(mockMessages); // Execute const count = await messageService.countTodayMessages(); // Assert - expect(MessageModel.queryAll).toHaveBeenCalled(); expect(count).toBe(2); }); }); @@ -337,45 +372,46 @@ describe('MessageClientService', () => { describe('updateMessageTTS', () => { it('should update the TTS field of a message', async () => { // Setup - const newTTS: ChatTTS = { - contentMd5: 'abc', - file: 'file-abc', - }; - - (MessageModel.update as Mock).mockResolvedValue({ ...mockMessage, tts: newTTS }); + await clientDB + .insert(files) + .values({ id: 'file-abc', fileType: 'text', name: 'abc', url: 'abc', size: 100, userId }); + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); + const newTTS: ChatTTS = { contentMd5: 'abc', file: 'file-abc' }; // Execute - const result = await messageService.updateMessageTTS(mockMessageId, newTTS); + await messageService.updateMessageTTS(mockMessageId, newTTS); // Assert - expect(MessageModel.update).toHaveBeenCalledWith(mockMessageId, { tts: newTTS }); - expect(result).toEqual({ ...mockMessage, tts: newTTS }); + const result = await clientDB.query.messageTTS.findFirst({ + where: eq(messageTTS.id, mockMessageId), + }); + + expect(result).toMatchObject({ contentMd5: 'abc', fileId: 'file-abc', id: mockMessageId }); }); }); describe('updateMessageTranslate', () => { it('should update the translate field of a message', async () => { // Setup - const newTranslate: ChatTranslate = { - content: 'Translated text', - to: 'es', - }; - - (MessageModel.update as Mock).mockResolvedValue({ ...mockMessage, translate: newTranslate }); + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); + const newTranslate: ChatTranslate = { content: 'Translated text', to: 'es' }; // Execute - const result = await messageService.updateMessageTranslate(mockMessageId, newTranslate); + await messageService.updateMessageTranslate(mockMessageId, newTranslate); // Assert - expect(MessageModel.update).toHaveBeenCalledWith(mockMessageId, { translate: newTranslate }); - expect(result).toEqual({ ...mockMessage, translate: newTranslate }); + const result = await clientDB.query.messageTranslates.findFirst({ + where: eq(messageTranslates.id, mockMessageId), + }); + + expect(result).toMatchObject(newTranslate); }); }); describe('hasMessages', () => { it('should return true if there are messages', async () => { // Setup - (MessageModel.count as Mock).mockResolvedValue(1); + await clientDB.insert(messages).values({ id: mockMessageId, role: 'user', userId }); // Execute const result = await messageService.hasMessages(); @@ -385,9 +421,6 @@ describe('MessageClientService', () => { }); it('should return false if there are no messages', async () => { - // Setup - (MessageModel.count as Mock).mockResolvedValue(0); - // Execute const result = await messageService.hasMessages(); diff --git a/src/services/message/client.ts b/src/services/message/client.ts index d51d6d72a1a4..56d3e59fd57b 100644 --- a/src/services/message/client.ts +++ b/src/services/message/client.ts @@ -1,10 +1,9 @@ import dayjs from 'dayjs'; -import { FileModel } from '@/database/_deprecated/models/file'; -import { MessageModel } from '@/database/_deprecated/models/message'; -import { DB_Message } from '@/database/_deprecated/schemas/message'; +import { clientDB } from '@/database/client/db'; +import { MessageItem } from '@/database/schemas'; +import { MessageModel } from '@/database/server/models/message'; import { - ChatFileItem, ChatMessage, ChatMessageError, ChatTTS, @@ -15,101 +14,85 @@ import { import { IMessageService } from './type'; export class ClientService implements IMessageService { + private messageModel: MessageModel; + + constructor(userId: string) { + this.messageModel = new MessageModel(clientDB as any, userId); + } + async createMessage(data: CreateMessageParams) { - const { id } = await MessageModel.create(data); + const { id } = await this.messageModel.create(data); return id; } - async batchCreateMessages(messages: ChatMessage[]) { - return MessageModel.batchCreate(messages); + async batchCreateMessages(messages: MessageItem[]) { + return this.messageModel.batchCreate(messages); } async getMessages(sessionId: string, topicId?: string): Promise { - const messages = await MessageModel.query({ sessionId, topicId }); - - const fileList = (await Promise.all( - messages - .flatMap((item) => item.files) - .filter(Boolean) - .map(async (id) => FileModel.findById(id!)), - )) as ChatFileItem[]; - - return messages.map((item) => ({ - ...item, - imageList: fileList - .filter((file) => item.files?.includes(file.id) && file.fileType.startsWith('image')) - .map((file) => ({ - alt: file.name, - id: file.id, - url: file.url, - })), - })); + return this.messageModel.query({ sessionId, topicId }); } async getAllMessages() { - return MessageModel.queryAll(); + return this.messageModel.queryAll(); } async countMessages() { - return MessageModel.count(); + return this.messageModel.count(); } async countTodayMessages() { - const topics = await MessageModel.queryAll(); + const topics = await this.messageModel.queryAll(); return topics.filter( (item) => dayjs(item.createdAt).format('YYYY-MM-DD') === dayjs().format('YYYY-MM-DD'), ).length; } async getAllMessagesInSession(sessionId: string) { - return MessageModel.queryBySessionId(sessionId); + return this.messageModel.queryBySessionId(sessionId); } async updateMessageError(id: string, error: ChatMessageError) { - return MessageModel.update(id, { error }); + return this.messageModel.update(id, { error }); } - async updateMessage(id: string, message: Partial) { - return MessageModel.update(id, message); + async updateMessage(id: string, message: Partial) { + return this.messageModel.update(id, message); } async updateMessageTTS(id: string, tts: Partial | false) { - return MessageModel.update(id, { tts }); + return this.messageModel.updateTTS(id, tts as any); } async updateMessageTranslate(id: string, translate: Partial | false) { - return MessageModel.update(id, { translate }); + return this.messageModel.updateTranslate(id, translate as any); } async updateMessagePluginState(id: string, value: Record) { - return MessageModel.updatePluginState(id, value); + return this.messageModel.updatePluginState(id, value); } async updateMessagePluginArguments(id: string, value: string | Record) { const args = typeof value === 'string' ? value : JSON.stringify(value); - return MessageModel.updatePlugin(id, { arguments: args }); - } - - async bindMessagesToTopic(topicId: string, messageIds: string[]) { - return MessageModel.batchUpdate(messageIds, { topicId }); + return this.messageModel.updateMessagePlugin(id, { arguments: args }); } async removeMessage(id: string) { - return MessageModel.delete(id); + return this.messageModel.deleteMessage(id); } async removeMessages(ids: string[]) { - return MessageModel.bulkDelete(ids); + return this.messageModel.deleteMessages(ids); } async removeMessagesByAssistant(assistantId: string, topicId?: string) { - return MessageModel.batchDelete(assistantId, topicId); + return this.messageModel.deleteMessagesBySession(assistantId, topicId); } async removeAllMessages() { - return MessageModel.clearTable(); + return this.messageModel.deleteAllMessages(); } async hasMessages() { diff --git a/src/services/message/index.test.ts b/src/services/message/index.test.ts deleted file mode 100644 index 625261f53c90..000000000000 --- a/src/services/message/index.test.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { Mock, describe, expect, it, vi } from 'vitest'; - -import { CreateMessageParams, MessageModel } from '@/database/_deprecated/models/message'; -import { ChatMessage, ChatMessageError, ChatPluginPayload } from '@/types/message'; - -import { messageService } from './index'; - -// Mock the MessageModel -vi.mock('@/database/_deprecated/models/message', () => { - return { - MessageModel: { - count: vi.fn(), - }, - }; -}); - -describe('MessageService', () => { - beforeEach(() => { - // Reset all mocks before running each test case - vi.resetAllMocks(); - }); - - describe('hasMessages', () => { - it('should return true if there are messages', async () => { - // Setup - (MessageModel.count as Mock).mockResolvedValue(1); - - // Execute - const hasMessages = await messageService.hasMessages(); - - // Assert - expect(MessageModel.count).toHaveBeenCalled(); - expect(hasMessages).toBe(true); - }); - - it('should return false if there are no messages', async () => { - // Setup - (MessageModel.count as Mock).mockResolvedValue(0); - - // Execute - const hasMessages = await messageService.hasMessages(); - - // Assert - expect(MessageModel.count).toHaveBeenCalled(); - expect(hasMessages).toBe(false); - }); - }); -}); diff --git a/src/services/message/index.ts b/src/services/message/index.ts index 930eaf6034ed..c1c1fbdb542b 100644 --- a/src/services/message/index.ts +++ b/src/services/message/index.ts @@ -2,4 +2,6 @@ import { ClientService } from './client'; import { ServerService } from './server'; export const messageService = - process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService(); + process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' + ? new ServerService() + : new ClientService('123'); diff --git a/src/services/message/server.ts b/src/services/message/server.ts index 6562b4ec3e25..6860ede39df9 100644 --- a/src/services/message/server.ts +++ b/src/services/message/server.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-unused-vars */ import { INBOX_SESSION_ID } from '@/const/session'; +import { MessageItem } from '@/database/schemas'; import { lambdaClient } from '@/libs/trpc/client'; import { ChatMessage, @@ -19,7 +20,7 @@ export class ServerService implements IMessageService { }); } - batchCreateMessages(messages: ChatMessage[]): Promise { + batchCreateMessages(messages: MessageItem[]): Promise { return lambdaClient.message.batchCreateMessages.mutate(messages); } @@ -33,6 +34,7 @@ export class ServerService implements IMessageService { getAllMessages(): Promise { return lambdaClient.message.getAllMessages.query(); } + getAllMessagesInSession(sessionId: string): Promise { return lambdaClient.message.getAllMessagesInSession.query({ sessionId: this.toDbSessionId(sessionId), @@ -79,10 +81,6 @@ export class ServerService implements IMessageService { return lambdaClient.message.updatePluginState.mutate({ id, value }); } - bindMessagesToTopic(_topicId: string, _messageIds: string[]): Promise { - throw new Error('Method not implemented.'); - } - removeMessage(id: string): Promise { return lambdaClient.message.removeMessage.mutate({ id }); } diff --git a/src/services/message/type.ts b/src/services/message/type.ts index 2929620ad128..4e197ddd0c6e 100644 --- a/src/services/message/type.ts +++ b/src/services/message/type.ts @@ -1,4 +1,5 @@ import { DB_Message } from '@/database/_deprecated/schemas/message'; +import { MessageItem } from '@/database/schemas'; import { ChatMessage, ChatMessageError, @@ -11,7 +12,7 @@ import { export interface IMessageService { createMessage(data: CreateMessageParams): Promise; - batchCreateMessages(messages: ChatMessage[]): Promise; + batchCreateMessages(messages: MessageItem[]): Promise; getMessages(sessionId: string, topicId?: string): Promise; getAllMessages(): Promise; @@ -20,11 +21,10 @@ export interface IMessageService { countTodayMessages(): Promise; updateMessageError(id: string, error: ChatMessageError): Promise; - updateMessage(id: string, message: Partial): Promise; + updateMessage(id: string, message: Partial): Promise; updateMessageTTS(id: string, tts: Partial | false): Promise; updateMessageTranslate(id: string, translate: Partial | false): Promise; updateMessagePluginState(id: string, value: Record): Promise; - bindMessagesToTopic(topicId: string, messageIds: string[]): Promise; removeMessage(id: string): Promise; removeMessages(ids: string[]): Promise; diff --git a/src/services/plugin/client.test.ts b/src/services/plugin/client.test.ts index e2b6ccc66822..94db48ffb2fb 100644 --- a/src/services/plugin/client.test.ts +++ b/src/services/plugin/client.test.ts @@ -1,30 +1,38 @@ import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { eq } from 'drizzle-orm'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { PluginModel } from '@/database/_deprecated/models/plugin'; import { DB_Plugin } from '@/database/_deprecated/schemas/plugin'; +import { clientDB } from '@/database/client/db'; +import { migrate } from '@/database/client/migrate'; +import { files, installedPlugins, sessions, topics, users } from '@/database/schemas'; +import { ChatMessage } from '@/types/message'; import { LobeTool } from '@/types/tool'; import { LobeToolCustomPlugin } from '@/types/tool/plugin'; import { ClientService } from './client'; import { InstallPluginParams } from './type'; -const pluginService = new ClientService(); - // Mocking modules and functions -vi.mock('@/database/_deprecated/models/plugin', () => ({ - PluginModel: { - getList: vi.fn(), - create: vi.fn(), - delete: vi.fn(), - update: vi.fn(), - clear: vi.fn(), - }, -})); - -beforeEach(() => { - vi.resetAllMocks(); +const userId = 'message-db'; +const pluginService = new ClientService(userId); + +// Mock data +beforeEach(async () => { + await migrate(); + + // 在每个测试用例之前,清空表 + await clientDB.transaction(async (trx) => { + await trx.delete(users); + await trx.insert(users).values([{ id: userId }, { id: '456' }]); + }); +}); + +afterEach(async () => { + // 在每个测试用例之后,清空表 + await clientDB.delete(users); }); describe('PluginService', () => { @@ -32,18 +40,19 @@ describe('PluginService', () => { it('should install a plugin', async () => { // Arrange const fakePlugin = { - identifier: 'test-plugin', + identifier: 'test-plugin-d', manifest: { name: 'TestPlugin', version: '1.0.0' } as unknown as LobeChatPluginManifest, type: 'plugin', } as InstallPluginParams; - vi.mocked(PluginModel.create).mockResolvedValue(fakePlugin); // Act - const installedPlugin = await pluginService.installPlugin(fakePlugin); + await pluginService.installPlugin(fakePlugin); // Assert - expect(PluginModel.create).toHaveBeenCalledWith(fakePlugin); - expect(installedPlugin).toEqual(fakePlugin); + const result = await clientDB.query.installedPlugins.findFirst({ + where: eq(installedPlugins.identifier, fakePlugin.identifier), + }); + expect(result).toMatchObject(fakePlugin); }); }); @@ -51,14 +60,14 @@ describe('PluginService', () => { it('should return a list of installed plugins', async () => { // Arrange const fakePlugins = [{ identifier: 'test-plugin', type: 'plugin' }] as LobeTool[]; - vi.mocked(PluginModel.getList).mockResolvedValue(fakePlugins as DB_Plugin[]); - + await clientDB + .insert(installedPlugins) + .values([{ identifier: 'test-plugin', type: 'plugin', userId }]); // Act - const installedPlugins = await pluginService.getInstalledPlugins(); + const data = await pluginService.getInstalledPlugins(); // Assert - expect(PluginModel.getList).toHaveBeenCalled(); - expect(installedPlugins).toEqual(fakePlugins); + expect(data).toMatchObject(fakePlugins); }); }); @@ -66,13 +75,15 @@ describe('PluginService', () => { it('should uninstall a plugin', async () => { // Arrange const identifier = 'test-plugin'; - vi.mocked(PluginModel.delete).mockResolvedValue(); + await clientDB.insert(installedPlugins).values([{ identifier, type: 'plugin', userId }]); // Act - const result = await pluginService.uninstallPlugin(identifier); + await pluginService.uninstallPlugin(identifier); // Assert - expect(PluginModel.delete).toHaveBeenCalledWith(identifier); + const result = await clientDB.query.installedPlugins.findFirst({ + where: eq(installedPlugins.identifier, identifier), + }); expect(result).toBe(undefined); }); }); @@ -81,67 +92,74 @@ describe('PluginService', () => { it('should create a custom plugin', async () => { // Arrange const customPlugin = { - identifier: 'custom-plugin', + identifier: 'custom-plugin-x', manifest: {}, type: 'customPlugin', } as LobeToolCustomPlugin; - vi.mocked(PluginModel.create).mockResolvedValue(customPlugin); // Act - const result = await pluginService.createCustomPlugin(customPlugin); + await pluginService.createCustomPlugin(customPlugin); // Assert - expect(PluginModel.create).toHaveBeenCalledWith({ - ...customPlugin, - type: 'customPlugin', + const result = await clientDB.query.installedPlugins.findFirst({ + where: eq(installedPlugins.identifier, customPlugin.identifier), }); - expect(result).toEqual(customPlugin); + expect(result).toMatchObject(customPlugin); }); }); describe('updatePlugin', () => { it('should update a plugin', async () => { // Arrange - const id = 'plugin-id'; - const value = { settings: { ab: '1' } } as unknown as LobeToolCustomPlugin; - vi.mocked(PluginModel.update).mockResolvedValue(1); + const identifier = 'plugin-id'; + const value = { customParams: { ab: '1' } } as unknown as LobeToolCustomPlugin; + await clientDB.insert(installedPlugins).values([{ identifier, type: 'plugin', userId }]); // Act - const result = await pluginService.updatePlugin(id, value); + await pluginService.updatePlugin(identifier, value); // Assert - expect(PluginModel.update).toHaveBeenCalledWith(id, value); - expect(result).toEqual(undefined); + const result = await clientDB.query.installedPlugins.findFirst({ + where: eq(installedPlugins.identifier, identifier), + }); + expect(result).toMatchObject(value); }); }); describe('updatePluginManifest', () => { it('should update a plugin manifest', async () => { // Arrange - const id = 'plugin-id'; + const identifier = 'plugin-id'; const manifest = { name: 'NewPluginManifest' } as unknown as LobeChatPluginManifest; - vi.mocked(PluginModel.update).mockResolvedValue(1); + await clientDB.insert(installedPlugins).values([{ identifier, type: 'plugin', userId }]); // Act - const result = await pluginService.updatePluginManifest(id, manifest); + await pluginService.updatePluginManifest(identifier, manifest); // Assert - expect(PluginModel.update).toHaveBeenCalledWith(id, { manifest }); - expect(result).toEqual(undefined); + const result = await clientDB.query.installedPlugins.findFirst({ + where: eq(installedPlugins.identifier, identifier), + }); + expect(result).toMatchObject({ manifest }); }); }); describe('removeAllPlugins', () => { it('should remove all plugins', async () => { // Arrange - vi.mocked(PluginModel.clear).mockResolvedValue(undefined); + await clientDB.insert(installedPlugins).values([ + { identifier: '123', type: 'plugin', userId }, + { identifier: '234', type: 'plugin', userId }, + ]); // Act - const result = await pluginService.removeAllPlugins(); + await pluginService.removeAllPlugins(); // Assert - expect(PluginModel.clear).toHaveBeenCalled(); - expect(result).toBe(undefined); + const result = await clientDB.query.installedPlugins.findMany({ + where: eq(installedPlugins.userId, userId), + }); + expect(result.length).toEqual(0); }); }); @@ -150,13 +168,17 @@ describe('PluginService', () => { // Arrange const id = 'plugin-id'; const settings = { color: 'blue' }; + await clientDB.insert(installedPlugins).values([{ identifier: id, type: 'plugin', userId }]); // Act - const result = await pluginService.updatePluginSettings(id, settings); + await pluginService.updatePluginSettings(id, settings); // Assert - expect(PluginModel.update).toHaveBeenCalledWith(id, { settings }); - expect(result).toEqual(undefined); + const result = await clientDB.query.installedPlugins.findFirst({ + where: eq(installedPlugins.identifier, id), + }); + + expect(result).toMatchObject({ settings }); }); }); }); diff --git a/src/services/plugin/client.ts b/src/services/plugin/client.ts index c56f73119c14..1e534c5500e8 100644 --- a/src/services/plugin/client.ts +++ b/src/services/plugin/client.ts @@ -1,42 +1,52 @@ import { LobeChatPluginManifest } from '@lobehub/chat-plugin-sdk'; -import { PluginModel } from '@/database/_deprecated/models/plugin'; +import { clientDB } from '@/database/client/db'; +import { PluginModel } from '@/database/server/models/plugin'; import { LobeTool } from '@/types/tool'; import { LobeToolCustomPlugin } from '@/types/tool/plugin'; import { IPluginService, InstallPluginParams } from './type'; export class ClientService implements IPluginService { + private pluginModel: PluginModel; + + constructor(userId: string) { + this.pluginModel = new PluginModel(clientDB as any, userId); + } + installPlugin = async (plugin: InstallPluginParams) => { - return PluginModel.create(plugin); + await this.pluginModel.create(plugin); + return; }; getInstalledPlugins = () => { - return PluginModel.getList() as Promise; + return this.pluginModel.query() as Promise; }; - uninstallPlugin(identifier: string) { - return PluginModel.delete(identifier); + async uninstallPlugin(identifier: string) { + await this.pluginModel.delete(identifier); + return; } async createCustomPlugin(customPlugin: LobeToolCustomPlugin) { - return PluginModel.create({ ...customPlugin, type: 'customPlugin' }); + await this.pluginModel.create({ ...customPlugin, type: 'customPlugin' }); + return; } async updatePlugin(id: string, value: LobeToolCustomPlugin) { - await PluginModel.update(id, value); + await this.pluginModel.update(id, value); return; } async updatePluginManifest(id: string, manifest: LobeChatPluginManifest) { - await PluginModel.update(id, { manifest }); + await this.pluginModel.update(id, { manifest }); } async removeAllPlugins() { - return PluginModel.clear(); + await this.pluginModel.deleteAll(); } // eslint-disable-next-line @typescript-eslint/no-unused-vars async updatePluginSettings(id: string, settings: any, _?: AbortSignal) { - await PluginModel.update(id, { settings }); + await this.pluginModel.update(id, { settings }); } } diff --git a/src/services/plugin/index.ts b/src/services/plugin/index.ts index 77b3ab38869b..58265d316138 100644 --- a/src/services/plugin/index.ts +++ b/src/services/plugin/index.ts @@ -2,4 +2,6 @@ import { ClientService } from './client'; import { ServerService } from './server'; export const pluginService = - process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService(); + process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' + ? new ServerService() + : new ClientService('123'); diff --git a/src/services/topic/client.test.ts b/src/services/topic/client.test.ts index 211abefa7d36..ae943c2b6d7b 100644 --- a/src/services/topic/client.test.ts +++ b/src/services/topic/client.test.ts @@ -1,75 +1,66 @@ -import { Mock, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; +import { eq } from 'drizzle-orm'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { SessionModel } from '@/database/_deprecated/models/session'; -import { CreateTopicParams, TopicModel } from '@/database/_deprecated/models/topic'; +import { clientDB } from '@/database/client/db'; +import { migrate } from '@/database/client/migrate'; +import { sessions, topics, users } from '@/database/schemas'; import { ChatTopic } from '@/types/topic'; import { ClientService } from './client'; -const topicService = new ClientService(); -// Mock the TopicModel -vi.mock('@/database/_deprecated/models/topic', () => { - return { - TopicModel: { - create: vi.fn(), - query: vi.fn(), - delete: vi.fn(), - count: vi.fn(), - batchDeleteBySessionId: vi.fn(), - batchDelete: vi.fn(), - clearTable: vi.fn(), - toggleFavorite: vi.fn(), - batchCreate: vi.fn(), - update: vi.fn(), - queryAll: vi.fn(), - queryByKeyword: vi.fn(), - }, - }; -}); +// Mock data +const userId = 'topic-user-test'; +const sessionId = 'topic-session'; +const mockTopicId = 'mock-topic-id'; -describe('TopicService', () => { - // Mock data - const mockTopicId = 'mock-topic-id'; - const mockTopic: ChatTopic = { - createdAt: 100, - updatedAt: 100, - id: mockTopicId, - title: 'Mock Topic', - }; - const mockTopics = [mockTopic]; - - beforeEach(() => { - // Reset all mocks before running each test case - vi.resetAllMocks(); +const mockTopic = { + id: mockTopicId, + title: 'Mock Topic', +}; + +const topicService = new ClientService(userId); + +beforeEach(async () => { + // Reset all mocks before running each test case + vi.resetAllMocks(); + + await migrate(); + + await clientDB.delete(users); + + // 创建测试数据 + await clientDB.transaction(async (tx) => { + await tx.insert(users).values({ id: userId }); + await tx.insert(sessions).values({ id: sessionId, userId }); + await tx.insert(topics).values({ ...mockTopic, sessionId, userId }); }); +}); +describe('TopicService', () => { describe('createTopic', () => { it('should create a topic and return its id', async () => { // Setup - const createParams: CreateTopicParams = { + const createParams = { title: 'New Topic', - sessionId: '1', + sessionId: sessionId, }; - (TopicModel.create as Mock).mockResolvedValue(mockTopic); // Execute const topicId = await topicService.createTopic(createParams); // Assert - expect(TopicModel.create).toHaveBeenCalledWith(createParams); - expect(topicId).toBe(mockTopicId); + expect(topicId).toBeDefined(); }); + it('should throw an error if topic creation fails', async () => { // Setup - const createParams: CreateTopicParams = { + const createParams = { title: 'New Topic', - sessionId: '1', + sessionId: 123 as any, // sessionId should be string }; - (TopicModel.create as Mock).mockResolvedValue(null); - // Execute & Assert - await expect(topicService.createTopic(createParams)).rejects.toThrow('topic create Error'); + await expect(topicService.createTopic(createParams)).rejects.toThrowError(); }); }); @@ -77,56 +68,46 @@ describe('TopicService', () => { // Example for getTopics it('should query topics with given parameters', async () => { // Setup - const queryParams = { sessionId: 'session-id' }; - (TopicModel.query as Mock).mockResolvedValue(mockTopics); + const queryParams = { sessionId }; // Execute - const topics = await topicService.getTopics(queryParams); + const data = await topicService.getTopics(queryParams); // Assert - expect(TopicModel.query).toHaveBeenCalledWith(queryParams); - expect(topics).toBe(mockTopics); + expect(data[0]).toMatchObject(mockTopic); }); }); describe('updateTopic', () => { // Example for updateFavorite it('should toggle favorite status of a topic', async () => { - // Setup - const newState = true; - // Execute - await topicService.updateTopic(mockTopicId, { favorite: newState }); + const result = await topicService.updateTopic(mockTopicId, { favorite: true }); // Assert - expect(TopicModel.update).toHaveBeenCalledWith(mockTopicId, { favorite: 1 }); + expect(result[0].favorite).toBeTruthy(); }); it('should update the title of a topic', async () => { // Setup const newTitle = 'Updated Topic Title'; - (TopicModel.update as Mock).mockResolvedValue({ ...mockTopic, title: newTitle }); // Execute const result = await topicService.updateTopic(mockTopicId, { title: newTitle }); // Assert - expect(TopicModel.update).toHaveBeenCalledWith(mockTopicId, { title: newTitle }); - expect(result).toEqual({ ...mockTopic, title: newTitle }); + expect(result[0].title).toEqual(newTitle); }); }); describe('removeTopic', () => { it('should remove a topic by id', async () => { - // Setup - (TopicModel.delete as Mock).mockResolvedValue(true); - // Execute - const result = await topicService.removeTopic(mockTopicId); + await topicService.removeTopic(mockTopicId); + const result = await clientDB.query.topics.findFirst({ where: eq(topics.id, mockTopicId) }); // Assert - expect(TopicModel.delete).toHaveBeenCalledWith(mockTopicId); - expect(result).toBe(true); + expect(result).toBeUndefined(); }); }); @@ -134,111 +115,101 @@ describe('TopicService', () => { it('should remove all topics with a given session id', async () => { // Setup const sessionId = 'session-id'; - (TopicModel.batchDeleteBySessionId as Mock).mockResolvedValue(true); // Execute - const result = await topicService.removeTopics(sessionId); + await topicService.removeTopics(sessionId); + const result = await clientDB.query.topics.findMany({ + where: eq(topics.sessionId, sessionId), + }); - // Assert - expect(TopicModel.batchDeleteBySessionId).toHaveBeenCalledWith(sessionId); - expect(result).toBe(true); + expect(result.length).toEqual(0); }); }); describe('batchRemoveTopics', () => { it('should batch remove topics', async () => { + await clientDB.insert(topics).values([{ id: 'topic-id-1', title: 'topic-title', userId }]); // Setup const topicIds = [mockTopicId, 'another-topic-id']; - (TopicModel.batchDelete as Mock).mockResolvedValue(true); // Execute - const result = await topicService.batchRemoveTopics(topicIds); + await topicService.batchRemoveTopics(topicIds); + + const count = await clientDB.$count(topics); // Assert - expect(TopicModel.batchDelete).toHaveBeenCalledWith(topicIds); - expect(result).toBe(true); + expect(count).toBe(1); }); }); describe('removeAllTopic', () => { it('should clear all topics from the table', async () => { - // Setup - (TopicModel.clearTable as Mock).mockResolvedValue(true); - // Execute - const result = await topicService.removeAllTopic(); + await topicService.removeAllTopic(); + const count = await clientDB.$count(topics); // Assert - expect(TopicModel.clearTable).toHaveBeenCalled(); - expect(result).toBe(true); + expect(count).toBe(0); }); }); describe('batchCreateTopics', () => { it('should batch create topics', async () => { - // Setup - (TopicModel.batchCreate as Mock).mockResolvedValue(mockTopics); - // Execute - const result = await topicService.batchCreateTopics(mockTopics); + const result = await topicService.batchCreateTopics([ + { id: 'topic-id-1', title: 'topic-title' }, + { id: 'topic-id-2', title: 'topic-title' }, + ] as ChatTopic[]); // Assert - expect(TopicModel.batchCreate).toHaveBeenCalledWith(mockTopics); - expect(result).toBe(mockTopics); + expect(result.success).toBeTruthy(); + expect(result.added).toBe(2); }); }); describe('getAllTopics', () => { it('should retrieve all topics', async () => { - // Setup - (TopicModel.queryAll as Mock).mockResolvedValue(mockTopics); - + await clientDB.insert(topics).values([ + { id: 'topic-id-1', title: 'topic-title', userId }, + { id: 'topic-id-2', title: 'topic-title', userId }, + ]); // Execute const result = await topicService.getAllTopics(); // Assert - expect(TopicModel.queryAll).toHaveBeenCalled(); - expect(result).toBe(mockTopics); + expect(result.length).toEqual(3); }); }); describe('searchTopics', () => { it('should return all topics that match the keyword', async () => { // Setup - const keyword = 'search'; - (TopicModel.queryByKeyword as Mock).mockResolvedValue(mockTopics); + const keyword = 'Topic'; // Execute - const result = await topicService.searchTopics(keyword, undefined); + const result = await topicService.searchTopics(keyword, sessionId); // Assert - expect(TopicModel.queryByKeyword).toHaveBeenCalledWith(keyword, undefined); - expect(result).toBe(mockTopics); + expect(result.length).toEqual(1); }); - }); - - describe('countTopics', () => { - it('should return false if no topics exist', async () => { + it('should return empty topic if not match the keyword', async () => { // Setup - (TopicModel.count as Mock).mockResolvedValue(0); + const keyword = 'search'; // Execute - const result = await topicService.countTopics(); + const result = await topicService.searchTopics(keyword, sessionId); // Assert - expect(TopicModel.count).toHaveBeenCalled(); - expect(result).toBe(0); + expect(result.length).toEqual(0); }); + }); - it('should return true if topics exist', async () => { - // Setup - (TopicModel.count as Mock).mockResolvedValue(1); - + describe('countTopics', () => { + it('should return topic counts', async () => { // Execute const result = await topicService.countTopics(); // Assert - expect(TopicModel.count).toHaveBeenCalled(); expect(result).toBe(1); }); }); diff --git a/src/services/topic/client.ts b/src/services/topic/client.ts index eeb2ffa2e395..337b8bcb2bba 100644 --- a/src/services/topic/client.ts +++ b/src/services/topic/client.ts @@ -1,11 +1,17 @@ -import { TopicModel } from '@/database/_deprecated/models/topic'; +import { clientDB } from '@/database/client/db'; +import { TopicModel } from '@/database/server/models/topic'; import { ChatTopic } from '@/types/topic'; import { CreateTopicParams, ITopicService, QueryTopicParams } from './type'; export class ClientService implements ITopicService { + private topicModel: TopicModel; + constructor(userId: string) { + this.topicModel = new TopicModel(clientDB as any, userId); + } + async createTopic(params: CreateTopicParams): Promise { - const item = await TopicModel.create(params as any); + const item = await this.topicModel.create(params as any); if (!item) { throw new Error('topic create Error'); @@ -15,56 +21,54 @@ export class ClientService implements ITopicService { } async batchCreateTopics(importTopics: ChatTopic[]) { - return TopicModel.batchCreate(importTopics as any); + const data = await this.topicModel.batchCreate(importTopics as any); + + return { added: data.length, ids: [], skips: [], success: true }; } async cloneTopic(id: string, newTitle?: string) { - return TopicModel.duplicateTopic(id, newTitle); + const data = await this.topicModel.duplicate(id, newTitle); + return data.topic.id; } - async getTopics(params: QueryTopicParams): Promise { - return TopicModel.query(params); + async getTopics(params: QueryTopicParams) { + const data = await this.topicModel.query(params); + return data as unknown as Promise; } async searchTopics(keyword: string, sessionId?: string) { - return TopicModel.queryByKeyword(keyword, sessionId); - } + const data = await this.topicModel.queryByKeyword(keyword, sessionId); - async getAllTopics() { - return TopicModel.queryAll(); + return data as unknown as Promise; } - async countTopics() { - return TopicModel.count(); - } + async getAllTopics() { + const data = await this.topicModel.queryAll(); - async updateTopicFavorite(id: string, favorite?: boolean) { - return this.updateTopic(id, { favorite }); + return data as unknown as Promise; } - async updateTopicTitle(id: string, text: string) { - return this.updateTopic(id, { title: text }); + async countTopics() { + return this.topicModel.count(); } async updateTopic(id: string, data: Partial) { - const favorite = typeof data.favorite !== 'undefined' ? (data.favorite ? 1 : 0) : undefined; - - return TopicModel.update(id, { ...data, favorite }); + return this.topicModel.update(id, data as any); } async removeTopic(id: string) { - return TopicModel.delete(id); + return this.topicModel.delete(id); } async removeTopics(sessionId: string) { - return TopicModel.batchDeleteBySessionId(sessionId); + return this.topicModel.batchDeleteBySessionId(sessionId); } async batchRemoveTopics(topics: string[]) { - return TopicModel.batchDelete(topics); + return this.topicModel.batchDelete(topics); } async removeAllTopic() { - return TopicModel.clearTable(); + return this.topicModel.deleteAll(); } } diff --git a/src/services/topic/index.ts b/src/services/topic/index.ts index 360656149ea5..1c9330e9a499 100644 --- a/src/services/topic/index.ts +++ b/src/services/topic/index.ts @@ -1,6 +1,7 @@ - import { ClientService } from './client'; import { ServerService } from './server'; export const topicService = - process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' ? new ServerService() : new ClientService(); + process.env.NEXT_PUBLIC_SERVICE_MODE === 'server' + ? new ServerService() + : new ClientService('123'); diff --git a/src/types/meta.ts b/src/types/meta.ts index 459aece85cdb..23ce2b941c2f 100644 --- a/src/types/meta.ts +++ b/src/types/meta.ts @@ -21,19 +21,10 @@ export const LobeMetaDataSchema = z.object({ export type MetaData = z.infer; export interface BaseDataModel { - /** - * @deprecated - */ - createAt?: number; - createdAt: number; id: string; meta: MetaData; - /** - * @deprecated - */ - updateAt?: number; updatedAt: number; }