From cf841ffc5bb94bb046ac5066657c83c160cf1f5d 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 ++++++++++++++ src/database/server/models/user.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 | 129 +++--- src/services/plugin/client.ts | 30 +- src/services/plugin/index.ts | 4 +- src/services/session/client.test.ts | 376 +++++++----------- src/services/session/client.ts | 95 +++-- src/services/session/index.ts | 4 +- src/services/session/type.ts | 15 +- src/services/topic/client.test.ts | 191 ++++----- src/services/topic/client.ts | 52 +-- src/services/topic/index.ts | 5 +- src/services/user/client.test.ts | 64 +-- src/services/user/client.ts | 38 +- src/services/user/index.ts | 4 +- .../slices/sessionGroup/reducer.test.ts | 12 +- src/store/user/slices/common/action.ts | 3 +- src/types/meta.ts | 9 - src/types/session/sessionGroup.ts | 6 +- 30 files changed, 1071 insertions(+), 808 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 33e41ab2bc19..5adedc02fc5b 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/server/models/user.ts b/src/database/server/models/user.ts index 74b4130805d5..11b2d4715da5 100644 --- a/src/database/server/models/user.ts +++ b/src/database/server/models/user.ts @@ -94,7 +94,7 @@ export class UserModel { return this.db.delete(userSettings).where(eq(userSettings.id, this.userId)); } - async updateSetting(value: Partial) { + async updateSetting(value: DeepPartial) { const { keyVaults, ...res } = value; // Encrypt keyVaults 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..dbddc8df3710 100644 --- a/src/services/plugin/client.test.ts +++ b/src/services/plugin/client.test.ts @@ -1,30 +1,35 @@ 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 { installedPlugins, users } from '@/database/schemas'; 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 +37,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 +57,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 +72,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 +89,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 +165,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/session/client.test.ts b/src/services/session/client.test.ts index 4a7274ce547a..95f83a167963 100644 --- a/src/services/session/client.test.ts +++ b/src/services/session/client.test.ts @@ -1,193 +1,130 @@ +import { eq, not } from 'drizzle-orm/expressions'; import { Mock, beforeEach, describe, expect, it, vi } from 'vitest'; -import { SessionModel } from '@/database/_deprecated/models/session'; -import { SessionGroupModel } from '@/database/_deprecated/models/sessionGroup'; +import { clientDB } from '@/database/client/db'; +import { migrate } from '@/database/client/migrate'; +import { + NewSession, + SessionItem, + agents, + agentsToSessions, + sessionGroups, + sessions, + users, +} from '@/database/schemas'; import { LobeAgentConfig } from '@/types/agent'; import { LobeAgentSession, LobeSessionType, SessionGroups } from '@/types/session'; import { ClientService } from './client'; -const sessionService = new ClientService(); - -// Mock the SessionModel -vi.mock('@/database/_deprecated/models/session', () => { - return { - SessionModel: { - create: vi.fn(), - query: vi.fn(), - delete: vi.fn(), - clearTable: vi.fn(), - update: vi.fn(), - count: vi.fn(), - batchCreate: vi.fn(), - findById: vi.fn(), - isEmpty: vi.fn(), - queryByKeyword: vi.fn(), - updateConfig: vi.fn(), - queryByGroupIds: vi.fn(), - updatePinned: vi.fn(), - duplicate: vi.fn(), - queryWithGroups: vi.fn(), - }, - }; +const userId = 'message-db'; +const sessionService = new ClientService(userId); + +const mockSessionId = 'mock-session-id'; + +// Mock data +beforeEach(async () => { + await migrate(); + + // 在每个测试用例之前,清空表 + await clientDB.transaction(async (trx) => { + await trx.insert(users).values([{ id: userId }, { id: '456' }]); + await trx.insert(sessions).values([{ id: mockSessionId, userId: userId }]); + await trx.insert(sessionGroups).values([ + { id: 'group-1', name: 'group-A', sort: 2, userId }, + { id: 'group-2', name: 'group-B', sort: 1, userId }, + { id: 'group-4', name: 'group-C', sort: 1, userId: '456' }, + ]); + }); }); -// Mock the SessionGroupModel -vi.mock('@/database/_deprecated/models/sessionGroup', () => { - return { - SessionGroupModel: { - create: vi.fn(), - query: vi.fn(), - delete: vi.fn(), - clear: vi.fn(), - update: vi.fn(), - batchCreate: vi.fn(), - isEmpty: vi.fn(), - updateOrder: vi.fn(), - queryByKeyword: vi.fn(), - updateConfig: vi.fn(), - queryByGroupIds: vi.fn(), - }, - }; +afterEach(async () => { + // 在每个测试用例之后,清空表 + await clientDB.delete(users); }); describe('SessionService', () => { - const mockSessionId = 'mock-session-id'; const mockSession = { id: mockSessionId, type: 'agent', meta: { title: 'Mock Session' }, } as LobeAgentSession; - const mockSessions = [mockSession]; - - beforeEach(() => { - // Reset all mocks before running each test case - vi.resetAllMocks(); - }); describe('createSession', () => { it('should create a new session and return its id', async () => { // Setup const sessionType = LobeSessionType.Agent; const defaultValue = { meta: { title: 'New Session' } } as Partial; - (SessionModel.create as Mock).mockResolvedValue(mockSession); // Execute const sessionId = await sessionService.createSession(sessionType, defaultValue); // Assert - expect(SessionModel.create).toHaveBeenCalledWith(sessionType, defaultValue); - expect(sessionId).toBe(mockSessionId); - }); - - it('should throw an error if session creation fails', async () => { - // Setup - const sessionType = LobeSessionType.Agent; - const defaultValue = { meta: { title: 'New Session' } } as Partial; - (SessionModel.create as Mock).mockResolvedValue(null); - - // Execute & Assert - await expect(sessionService.createSession(sessionType, defaultValue)).rejects.toThrow( - 'session create Error', - ); - }); - }); - - describe('batchCreateSessions', () => { - it('should batch create sessions', async () => { - // Setup - (SessionModel.batchCreate as Mock).mockResolvedValue(mockSessions); - - // Execute - const result = await sessionService.batchCreateSessions(mockSessions); - - // Assert - expect(SessionModel.batchCreate).toHaveBeenCalledWith(mockSessions); - expect(result).toBe(mockSessions); - }); - }); - - describe('getSessionsByType', () => { - it('should retrieve sessions with their group ids', async () => { - // Setup - (SessionModel.query as Mock).mockResolvedValue(mockSessions); - - // Execute - const sessions = await sessionService.getSessionsByType(); - - // Assert - expect(SessionModel.query).toHaveBeenCalled(); - expect(sessions).toBe(mockSessions); - }); - - it('should retrieve all agent sessions', async () => { - // Setup - // Assuming that SessionModel.query has been modified to accept filters - const agentSessions = mockSessions.filter((session) => session.type === 'agent'); - (SessionModel.query as Mock).mockResolvedValue(agentSessions); - - // Execute - const result = await sessionService.getSessionsByType('agent'); - - // Assert - // Assuming that SessionModel.query would be called with a filter for agents - expect(SessionModel.query).toHaveBeenCalled(); // Add filter argument if applicable - expect(result).toBe(agentSessions); + expect(sessionId).toMatch(/^ssn_/); }); }); describe('removeSession', () => { it('should remove a session by its id', async () => { - // Setup - (SessionModel.delete as Mock).mockResolvedValue(true); - // Execute - const result = await sessionService.removeSession(mockSessionId); + await sessionService.removeSession(mockSessionId); + + // Assert + const result = await clientDB.query.sessions.findFirst({ + where: eq(sessions.id, mockSessionId), + }); // Assert - expect(SessionModel.delete).toHaveBeenCalledWith(mockSessionId); - expect(result).toBe(true); + expect(result).toBeUndefined(); }); }); describe('removeAllSessions', () => { it('should clear all sessions from the table', async () => { // Setup - (SessionModel.clearTable as Mock).mockResolvedValue(true); + await clientDB + .insert(sessions) + .values([{ userId: userId }, { userId: userId }, { userId: userId }]); // Execute - const result = await sessionService.removeAllSessions(); + await sessionService.removeAllSessions(); // Assert - expect(SessionModel.clearTable).toHaveBeenCalled(); - expect(result).toBe(true); + const result = await clientDB.query.sessions.findMany({ + where: eq(sessionGroups.userId, userId), + }); + + expect(result.length).toBe(0); }); }); describe('updateSession', () => { - it('should update the group of a session', async () => { + it.skip('should update the group of a session', async () => { // Setup const groupId = 'new-group'; - (SessionModel.update as Mock).mockResolvedValue({ ...mockSession, group: groupId }); // Execute - const result = await sessionService.updateSession(mockSessionId, { group: groupId }); + await sessionService.updateSession(mockSessionId, { group: groupId }); // Assert - expect(SessionModel.update).toHaveBeenCalledWith(mockSessionId, { group: groupId }); - expect(result).toEqual({ ...mockSession, group: groupId }); + const result = await clientDB.query.sessions.findFirst({ + where: eq(sessions.id, mockSessionId), + }); + expect(result).toMatchObject({ group: groupId }); }); - it('should update the meta of a session', async () => { + it.skip('should update the meta of a session', async () => { // Setup const newMeta = { description: 'Updated description' }; - (SessionModel.update as Mock).mockResolvedValue({ ...mockSession, meta: newMeta }); // Execute - const result = await sessionService.updateSession(mockSessionId, { meta: newMeta }); + await sessionService.updateSession(mockSessionId, { meta: newMeta }); // Assert - expect(SessionModel.update).toHaveBeenCalledWith(mockSessionId, { meta: newMeta }); + const result = await clientDB.query.sessions.findFirst({ + where: eq(sessions.id, mockSessionId), + }); + expect(result).toEqual({ ...mockSession, meta: newMeta }); }); @@ -199,121 +136,109 @@ describe('SessionService', () => { await sessionService.updateSession(mockSessionId, { pinned }); // Assert - expect(SessionModel.update).toHaveBeenCalledWith(mockSessionId, { pinned: 1 }); + const result = await clientDB.query.sessions.findFirst({ + where: eq(sessions.id, mockSessionId), + }); + + expect(result!.pinned).toBeTruthy(); }); }); - describe('updateSessionConfig', () => { + describe.skip('updateSessionConfig', () => { it('should update the config of a session', async () => { // Setup const newConfig = { model: 'abc' } as LobeAgentConfig; - (SessionModel.updateConfig as Mock).mockResolvedValue({ ...mockSession, config: newConfig }); // Execute - const result = await sessionService.updateSessionConfig(mockSessionId, newConfig); + await sessionService.updateSessionConfig(mockSessionId, newConfig); // Assert - expect(SessionModel.updateConfig).toHaveBeenCalledWith(mockSessionId, newConfig); + const result = await sessionService.getSessionConfig(mockSessionId); expect(result).toEqual({ ...mockSession, config: newConfig }); }); }); describe('countSessions', () => { it('should return false if no sessions exist', async () => { - // Setup - (SessionModel.count as Mock).mockResolvedValue(0); + await clientDB.delete(sessions); // Execute const result = await sessionService.countSessions(); // Assert - expect(SessionModel.count).toHaveBeenCalled(); expect(result).toBe(0); }); it('should return true if sessions exist', async () => { // Setup - (SessionModel.count as Mock).mockResolvedValue(1); + await clientDB.delete(sessions); + await clientDB.insert(sessions).values([{ userId }]); // Execute const result = await sessionService.countSessions(); // Assert - expect(SessionModel.count).toHaveBeenCalled(); expect(result).toBe(1); }); }); - describe('hasSessions', () => { - it('should return false if no sessions exist', async () => { - // Setup - (SessionModel.count as Mock).mockResolvedValue(0); - - // Execute - const result = await sessionService.hasSessions(); - - // Assert - expect(SessionModel.count).toHaveBeenCalled(); - expect(result).toBe(false); - }); - - it('should return true if sessions exist', async () => { - // Setup - (SessionModel.count as Mock).mockResolvedValue(1); - - // Execute - const result = await sessionService.hasSessions(); - - // Assert - expect(SessionModel.count).toHaveBeenCalled(); - expect(result).toBe(true); - }); - }); - describe('searchSessions', () => { it('should return sessions that match the keyword', async () => { // Setup - const keyword = 'search'; - (SessionModel.queryByKeyword as Mock).mockResolvedValue(mockSessions); + await clientDB.insert(agents).values({ userId, id: 'agent-1', title: 'Session Name' }); + await clientDB + .insert(agentsToSessions) + .values({ agentId: 'agent-1', sessionId: mockSessionId }); // Execute + const keyword = 'Name'; const result = await sessionService.searchSessions(keyword); // Assert - expect(SessionModel.queryByKeyword).toHaveBeenCalledWith(keyword); - expect(result).toBe(mockSessions); + // TODO: 后续需要把这个搜索的标题和描述都加上,现在这个 client 搜索会有问题 + expect(result).toMatchObject([{ id: mockSessionId }]); }); }); - describe('cloneSession', () => { + describe.skip('cloneSession', () => { it('should duplicate a session and return its id', async () => { // Setup const newTitle = 'Duplicated Session'; - (SessionModel.duplicate as Mock).mockResolvedValue({ - ...mockSession, + const session: NewSession = { id: 'duplicated-session-id', - }); + title: '123', + userId, + }; + await clientDB.insert(sessions).values([session]); + await clientDB.insert(agents).values({ userId, id: 'agent-1' }); + await clientDB + .insert(agentsToSessions) + .values({ agentId: 'agent-1', sessionId: 'duplicated-session-id' }); // Execute const duplicatedSessionId = await sessionService.cloneSession(mockSessionId, newTitle); // Assert - expect(SessionModel.duplicate).toHaveBeenCalledWith(mockSessionId, newTitle); - expect(duplicatedSessionId).toBe('duplicated-session-id'); + + const result = await clientDB.query.sessions.findFirst({ + where: eq(sessions.id, duplicatedSessionId!), + }); + expect(result).toEqual({}); }); }); describe('getGroupedSessions', () => { it('should retrieve sessions with their group', async () => { - // Setup - (SessionModel.queryWithGroups as Mock).mockResolvedValue(mockSessions); - // Execute const sessionsWithGroup = await sessionService.getGroupedSessions(); - // Assert - expect(SessionModel.queryWithGroups).toHaveBeenCalled(); - expect(sessionsWithGroup).toBe(mockSessions); + expect(sessionsWithGroup).toMatchObject({ + sessionGroups: [ + { id: 'group-2', name: 'group-B', sort: 1 }, + { id: 'group-1', name: 'group-A', sort: 2 }, + ], + sessions: [{ id: 'mock-session-id', type: 'agent' }], + }); }); }); @@ -323,84 +248,66 @@ describe('SessionService', () => { // Setup const groupName = 'New Group'; const sort = 1; - (SessionGroupModel.create as Mock).mockResolvedValue({ - id: 'new-group-id', - name: groupName, - sort, - }); // Execute const groupId = await sessionService.createSessionGroup(groupName, sort); // Assert - expect(SessionGroupModel.create).toHaveBeenCalledWith(groupName, sort); - expect(groupId).toBe('new-group-id'); - }); - }); - - describe('batchCreateSessionGroups', () => { - it('should batch create session groups', async () => { - // Setup - const groups = [ - { id: 'group-1', name: 'Group 1', sort: 1 }, - { id: 'group-2', name: 'Group 2', sort: 2 }, - ] as SessionGroups; + expect(groupId).toMatch(/^sg_/); - (SessionGroupModel.batchCreate as Mock).mockResolvedValue(groups); - - // Execute - const result = await sessionService.batchCreateSessionGroups(groups); + const result = await clientDB.query.sessionGroups.findFirst({ + where: eq(sessionGroups.id, groupId), + }); - // Assert - expect(SessionGroupModel.batchCreate).toHaveBeenCalledWith(groups); - expect(result).toBe(groups); + expect(result).toMatchObject({ id: groupId, name: groupName, sort }); }); }); describe('removeSessionGroup', () => { it('should remove a session group by its id', async () => { - // Setup - const removeChildren = true; - (SessionGroupModel.delete as Mock).mockResolvedValue(true); - + const groupId = 'group-1'; // Execute - const result = await sessionService.removeSessionGroup('group-id', removeChildren); + await sessionService.removeSessionGroup(groupId); + const result = await clientDB.query.sessionGroups.findFirst({ + where: eq(sessionGroups.id, groupId), + }); // Assert - expect(SessionGroupModel.delete).toHaveBeenCalledWith('group-id', removeChildren); - expect(result).toBe(true); + expect(result).toBeUndefined(); }); }); describe('clearSessionGroups', () => { it('should clear all session groups', async () => { - // Setup - (SessionGroupModel.clear as Mock).mockResolvedValue(true); - // Execute - const result = await sessionService.removeSessionGroups(); + await sessionService.removeSessionGroups(); // Assert - expect(SessionGroupModel.clear).toHaveBeenCalled(); - expect(result).toBe(true); + const result = await clientDB.query.sessionGroups.findMany({ + where: eq(sessionGroups.userId, userId), + }); + + expect(result.length).toBe(0); + + const result2 = await clientDB.query.sessionGroups.findMany({ + where: not(eq(sessionGroups.userId, userId)), + }); + + expect(result2.length).toBeGreaterThan(0); }); }); describe('getSessionGroups', () => { it('should retrieve all session groups', async () => { - // Setup - const groups = [ - { id: 'group-1', name: 'Group 1', sort: 1 }, - { id: 'group-2', name: 'Group 2', sort: 2 }, - ]; - (SessionGroupModel.query as Mock).mockResolvedValue(groups); - // Execute const result = await sessionService.getSessionGroups(); // Assert - expect(SessionGroupModel.query).toHaveBeenCalled(); - expect(result).toBe(groups); + const groups = [ + { id: 'group-2', name: 'group-B', sort: 1 }, + { id: 'group-1', name: 'group-A', sort: 2 }, + ]; + expect(result).toMatchObject(groups); }); }); @@ -409,14 +316,15 @@ describe('SessionService', () => { // Setup const groupId = 'group-1'; const data = { name: 'Updated Group', sort: 2 }; - (SessionGroupModel.update as Mock).mockResolvedValue({ id: groupId, ...data }); // Execute - const result = await sessionService.updateSessionGroup(groupId, data); + await sessionService.updateSessionGroup(groupId, data); // Assert - expect(SessionGroupModel.update).toHaveBeenCalledWith(groupId, data); - expect(result).toEqual({ id: groupId, ...data }); + const result = await clientDB.query.sessionGroups.findFirst({ + where: eq(sessionGroups.id, groupId), + }); + expect(result).toMatchObject({ id: groupId, ...data }); }); }); @@ -427,14 +335,18 @@ describe('SessionService', () => { { id: 'group-1', sort: 2 }, { id: 'group-2', sort: 1 }, ]; - (SessionGroupModel.updateOrder as Mock).mockResolvedValue(true); // Execute - const result = await sessionService.updateSessionGroupOrder(sortMap); + await sessionService.updateSessionGroupOrder(sortMap); // Assert - expect(SessionGroupModel.updateOrder).toHaveBeenCalledWith(sortMap); - expect(result).toBe(true); + const data = await clientDB.query.sessionGroups.findMany({ + where: eq(sessionGroups.userId, userId), + }); + expect(data).toMatchObject([ + { id: 'group-1', sort: 2 }, + { id: 'group-2', sort: 1 }, + ]); }); }); }); diff --git a/src/services/session/client.ts b/src/services/session/client.ts index 2d205cca8c8c..3592bc1f4691 100644 --- a/src/services/session/client.ts +++ b/src/services/session/client.ts @@ -1,9 +1,11 @@ import { DeepPartial } from 'utility-types'; import { INBOX_SESSION_ID } from '@/const/session'; -import { SessionModel } from '@/database/_deprecated/models/session'; -import { SessionGroupModel } from '@/database/_deprecated/models/sessionGroup'; import { UserModel } from '@/database/_deprecated/models/user'; +import { clientDB } from '@/database/client/db'; +import { AgentItem } from '@/database/schemas'; +import { SessionModel } from '@/database/server/models/session'; +import { SessionGroupModel } from '@/database/server/models/sessionGroup'; import { useUserStore } from '@/store/user'; import { LobeAgentChatConfig, LobeAgentConfig } from '@/types/agent'; import { MetaData } from '@/types/meta'; @@ -20,11 +22,22 @@ import { merge } from '@/utils/merge'; import { ISessionService } from './type'; export class ClientService implements ISessionService { - async createSession( - type: LobeSessionType, - defaultValue: Partial, - ): Promise { - const item = await SessionModel.create(type, defaultValue); + private sessionModel: SessionModel; + private sessionGroupModel: SessionGroupModel; + + constructor(userId: string) { + this.sessionGroupModel = new SessionGroupModel(clientDB as any, userId); + this.sessionModel = new SessionModel(clientDB as any, userId); + } + + async createSession(type: LobeSessionType, data: Partial): Promise { + const { config, group, meta, ...session } = data; + + const item = await this.sessionModel.create({ + config: { ...config, ...meta } as any, + session: { ...session, groupId: group }, + type, + }); if (!item) { throw new Error('session create Error'); } @@ -32,17 +45,18 @@ export class ClientService implements ISessionService { } async batchCreateSessions(importSessions: LobeSessions) { - return SessionModel.batchCreate(importSessions); + // @ts-ignore + return this.sessionModel.batchCreate(importSessions); } async cloneSession(id: string, newTitle: string): Promise { - const res = await SessionModel.duplicate(id, newTitle); + const res = await this.sessionModel.duplicate(id, newTitle); if (res) return res?.id; } async getGroupedSessions(): Promise { - return SessionModel.queryWithGroups(); + return this.sessionModel.queryWithGroups(); } async getSessionConfig(id: string): Promise { @@ -50,54 +64,50 @@ export class ClientService implements ISessionService { return UserModel.getAgentConfig(); } - const res = await SessionModel.findById(id); + const res = await this.sessionModel.findByIdOrSlug(id); if (!res) throw new Error('Session not found'); - return res.config as LobeAgentConfig; + return res.agent as LobeAgentConfig; } + /** + * 这个方法要对应移除的 + */ async getSessionsByType(type: 'agent' | 'group' | 'all' = 'all'): Promise { switch (type) { // TODO: add a filter to get only agents or agents case 'group': { - return SessionModel.query(); + // @ts-ignore + return this.sessionModel.query(); } case 'agent': { - return SessionModel.query(); + // @ts-ignore + return this.sessionModel.query(); } case 'all': { - return SessionModel.query(); + // @ts-ignore + return this.sessionModel.query(); } } } - async getAllAgents(): Promise { - // TODO: add a filter to get only agents - return await SessionModel.query(); - } - async countSessions() { - return SessionModel.count(); - } - - async hasSessions() { - return (await this.countSessions()) !== 0; + return this.sessionModel.count(); } async searchSessions(keyword: string) { - return SessionModel.queryByKeyword(keyword); + return this.sessionModel.queryByKeyword(keyword); } async updateSession( id: string, data: Partial>, ) { - const pinned = typeof data.pinned === 'boolean' ? (data.pinned ? 1 : 0) : undefined; - const prev = await SessionModel.findById(id); + const prev = await this.sessionModel.findByIdOrSlug(id); - return SessionModel.update(id, merge(prev, { ...data, pinned })); + return this.sessionModel.update(id, merge(prev, data)); } async updateSessionConfig( @@ -112,7 +122,7 @@ export class ClientService implements ISessionService { return useUserStore.getState().updateDefaultAgent({ config }); } - return SessionModel.updateConfig(activeId, config); + return this.sessionModel.updateConfig(activeId, config as AgentItem); } async updateSessionMeta( @@ -124,7 +134,7 @@ export class ClientService implements ISessionService { // inbox 不允许修改 meta if (activeId === INBOX_SESSION_ID) return; - return SessionModel.update(activeId, { meta }); + return this.sessionModel.update(activeId, meta); } async updateSessionChatConfig( @@ -137,11 +147,11 @@ export class ClientService implements ISessionService { } async removeSession(id: string) { - return SessionModel.delete(id); + return this.sessionModel.delete(id); } async removeAllSessions() { - return SessionModel.clearTable(); + return this.sessionModel.deleteAll(); } // ************************************** // @@ -149,7 +159,7 @@ export class ClientService implements ISessionService { // ************************************** // async createSessionGroup(name: string, sort?: number) { - const item = await SessionGroupModel.create(name, sort); + const item = await this.sessionGroupModel.create({ name, sort }); if (!item) { throw new Error('session group create Error'); } @@ -157,27 +167,28 @@ export class ClientService implements ISessionService { return item.id; } - async batchCreateSessionGroups(groups: SessionGroups) { - return SessionGroupModel.batchCreate(groups); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + async batchCreateSessionGroups(_groups: SessionGroups) { + return { added: 0, ids: [], skips: [], success: true }; } - async removeSessionGroup(id: string, removeChildren?: boolean) { - return await SessionGroupModel.delete(id, removeChildren); + async removeSessionGroup(id: string) { + return await this.sessionGroupModel.delete(id); } async updateSessionGroup(id: string, data: Partial) { - return SessionGroupModel.update(id, data); + return this.sessionGroupModel.update(id, data); } async updateSessionGroupOrder(sortMap: { id: string; sort: number }[]) { - return SessionGroupModel.updateOrder(sortMap); + return this.sessionGroupModel.updateOrder(sortMap); } async getSessionGroups(): Promise { - return SessionGroupModel.query(); + return this.sessionGroupModel.query(); } async removeSessionGroups() { - return SessionGroupModel.clear(); + return this.sessionGroupModel.deleteAll(); } } diff --git a/src/services/session/index.ts b/src/services/session/index.ts index 0d55e3cc45e5..34f8d766a500 100644 --- a/src/services/session/index.ts +++ b/src/services/session/index.ts @@ -2,4 +2,6 @@ import { ClientService } from './client'; import { ServerService } from './server'; export const sessionService = - 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/session/type.ts b/src/services/session/type.ts index f51f2f39780d..59528fe138ca 100644 --- a/src/services/session/type.ts +++ b/src/services/session/type.ts @@ -16,13 +16,21 @@ import { export interface ISessionService { createSession(type: LobeSessionType, defaultValue: Partial): Promise; + + /** + * 需要废弃 + * @deprecated + */ batchCreateSessions(importSessions: LobeSessions): Promise; cloneSession(id: string, newTitle: string): Promise; getGroupedSessions(): Promise; + + /** + * @deprecated + */ getSessionsByType(type: 'agent' | 'group' | 'all'): Promise; countSessions(): Promise; - hasSessions(): Promise; searchSessions(keyword: string): Promise; updateSession( @@ -53,6 +61,11 @@ export interface ISessionService { // ************************************** // createSessionGroup(name: string, sort?: number): Promise; + + /** + * 需要废弃 + * @deprecated + */ batchCreateSessionGroups(groups: SessionGroups): Promise; getSessionGroups(): Promise; 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/services/user/client.test.ts b/src/services/user/client.test.ts index f79f8294a2f5..3887518ed28a 100644 --- a/src/services/user/client.test.ts +++ b/src/services/user/client.test.ts @@ -1,22 +1,15 @@ +import { eq } from 'drizzle-orm'; import { DeepPartial } from 'utility-types'; -import { Mock, beforeEach, describe, expect, it, vi } from 'vitest'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { UserModel } from '@/database/_deprecated/models/user'; +import { clientDB } from '@/database/client/db'; +import { migrate } from '@/database/client/migrate'; +import { userSettings, users } from '@/database/schemas'; import { UserPreference } from '@/types/user'; import { UserSettings } from '@/types/user/settings'; -import { AsyncLocalStorage } from '@/utils/localStorage'; import { ClientService } from './client'; -vi.mock('@/database/_deprecated/models/user', () => ({ - UserModel: { - getUser: vi.fn(), - updateSettings: vi.fn(), - resetSettings: vi.fn(), - updateAvatar: vi.fn(), - }, -})); - const mockUser = { avatar: 'avatar.png', settings: { themeMode: 'light' } as unknown as UserSettings, @@ -26,63 +19,70 @@ const mockUser = { const mockPreference = { useCmdEnterToSend: true, } as UserPreference; +const clientService = new ClientService(mockUser.uuid); -describe('ClientService', () => { - let clientService: ClientService; +beforeEach(async () => { + vi.clearAllMocks(); - beforeEach(() => { - vi.clearAllMocks(); - clientService = new ClientService(); - }); + await migrate(); + + await clientDB.insert(users).values({ id: mockUser.uuid, avatar: 'avatar.png' }); + await clientDB + .insert(userSettings) + .values({ id: mockUser.uuid, general: { themeMode: 'light' } }); +}); + +afterEach(async () => { + await clientDB.delete(users); +}); +describe('ClientService', () => { it('should get user state correctly', async () => { - (UserModel.getUser as Mock).mockResolvedValue(mockUser); const spyOn = vi .spyOn(clientService['preferenceStorage'], 'getFromLocalStorage') .mockResolvedValue(mockPreference); const userState = await clientService.getUserState(); - expect(userState).toEqual({ + expect(userState).toMatchObject({ avatar: mockUser.avatar, isOnboard: true, canEnablePWAGuide: false, hasConversation: false, canEnableTrace: false, preference: mockPreference, - settings: mockUser.settings, + settings: { general: { themeMode: 'light' } }, userId: mockUser.uuid, }); - expect(UserModel.getUser).toHaveBeenCalledTimes(1); expect(spyOn).toHaveBeenCalledTimes(1); }); it('should update user settings correctly', async () => { const settingsPatch: DeepPartial = { general: { themeMode: 'dark' } }; - (UserModel.updateSettings as Mock).mockResolvedValue(undefined); await clientService.updateUserSettings(settingsPatch); - expect(UserModel.updateSettings).toHaveBeenCalledWith(settingsPatch); - expect(UserModel.updateSettings).toHaveBeenCalledTimes(1); + const result = await clientDB.query.userSettings.findFirst({ + where: eq(userSettings.id, mockUser.uuid), + }); + + expect(result).toMatchObject(settingsPatch); }); it('should reset user settings correctly', async () => { - (UserModel.resetSettings as Mock).mockResolvedValue(undefined); - await clientService.resetUserSettings(); - expect(UserModel.resetSettings).toHaveBeenCalledTimes(1); + const result = await clientDB.query.userSettings.findFirst({ + where: eq(userSettings.id, mockUser.uuid), + }); + + expect(result).toBeUndefined(); }); it('should update user avatar correctly', async () => { const newAvatar = 'new-avatar.png'; - (UserModel.updateAvatar as Mock).mockResolvedValue(undefined); await clientService.updateAvatar(newAvatar); - - expect(UserModel.updateAvatar).toHaveBeenCalledWith(newAvatar); - expect(UserModel.updateAvatar).toHaveBeenCalledTimes(1); }); it('should update user preference correctly', async () => { diff --git a/src/services/user/client.ts b/src/services/user/client.ts index 500d856a11b5..a38c3399173e 100644 --- a/src/services/user/client.ts +++ b/src/services/user/client.ts @@ -1,8 +1,9 @@ import { DeepPartial } from 'utility-types'; -import { MessageModel } from '@/database/_deprecated/models/message'; -import { SessionModel } from '@/database/_deprecated/models/session'; -import { UserModel } from '@/database/_deprecated/models/user'; +import { clientDB } from '@/database/client/db'; +import { MessageModel } from '@/database/server/models/message'; +import { SessionModel } from '@/database/server/models/session'; +import { UserModel } from '@/database/server/models/user'; import { UserGuide, UserInitializationState, UserPreference } from '@/types/user'; import { UserSettings } from '@/types/user/settings'; import { AsyncLocalStorage } from '@/utils/localStorage'; @@ -11,39 +12,46 @@ import { IUserService } from './type'; export class ClientService implements IUserService { private preferenceStorage: AsyncLocalStorage; + private userModel: UserModel; + private messageModel: MessageModel; + private sessionModel: SessionModel; + private userId: string; - constructor() { + constructor(userId: string) { this.preferenceStorage = new AsyncLocalStorage('LOBE_PREFERENCE'); + this.userModel = new UserModel(clientDB as any, userId); + this.userId = userId; + this.messageModel = new MessageModel(clientDB as any, userId); + this.sessionModel = new SessionModel(clientDB as any, userId); } async getUserState(): Promise { - const user = await UserModel.getUser(); - const messageCount = await MessageModel.count(); - const sessionCount = await SessionModel.count(); + const state = await this.userModel.getUserState(); + const user = await UserModel.findById(clientDB as any, this.userId); + const messageCount = await this.messageModel.count(); + const sessionCount = await this.sessionModel.count(); return { - avatar: user.avatar, + ...state, + avatar: user?.avatar as string, canEnablePWAGuide: messageCount >= 4, canEnableTrace: messageCount >= 4, hasConversation: messageCount > 0 || sessionCount > 0, isOnboard: true, preference: await this.preferenceStorage.getFromLocalStorage(), - settings: user.settings as UserSettings, - userId: user.uuid, }; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - updateUserSettings = async (patch: DeepPartial, _?: any) => { - return UserModel.updateSettings(patch); + updateUserSettings = async (patch: DeepPartial) => { + return this.userModel.updateSetting(patch); }; resetUserSettings = async () => { - return UserModel.resetSettings(); + return this.userModel.deleteSetting(); }; updateAvatar(avatar: string) { - return UserModel.updateAvatar(avatar); + return this.userModel.updateUser({ avatar }); } async updatePreference(preference: Partial) { diff --git a/src/services/user/index.ts b/src/services/user/index.ts index e472bdaef2c0..80272a68900a 100644 --- a/src/services/user/index.ts +++ b/src/services/user/index.ts @@ -2,4 +2,6 @@ import { ClientService } from './client'; import { ServerService } from './server'; export const userService = - 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/store/session/slices/sessionGroup/reducer.test.ts b/src/store/session/slices/sessionGroup/reducer.test.ts index 17a947ca217a..52fe51b9e329 100644 --- a/src/store/session/slices/sessionGroup/reducer.test.ts +++ b/src/store/session/slices/sessionGroup/reducer.test.ts @@ -10,14 +10,14 @@ describe('sessionGroupsReducer', () => { { id: nanoid(), name: 'Group 1', - createdAt: Date.now(), - updatedAt: Date.now(), + createdAt: new Date(), + updatedAt: new Date(), }, { id: nanoid(), name: 'Group 2', - createdAt: Date.now(), - updatedAt: Date.now(), + createdAt: new Date(), + updatedAt: new Date(), sort: 1, }, ]; @@ -26,8 +26,8 @@ describe('sessionGroupsReducer', () => { const newItem: SessionGroupItem = { id: nanoid(), name: 'New Group', - createdAt: Date.now(), - updatedAt: Date.now(), + createdAt: new Date(), + updatedAt: new Date(), }; const result = sessionGroupsReducer(initialState, { diff --git a/src/store/user/slices/common/action.ts b/src/store/user/slices/common/action.ts index 5f523d8d7dde..1952e7c6e5e3 100644 --- a/src/store/user/slices/common/action.ts +++ b/src/store/user/slices/common/action.ts @@ -48,7 +48,8 @@ export const createCommonSlice: StateCreator< updateAvatar: async (avatar) => { const { ClientService } = await import('@/services/user/client'); - const clientService = new ClientService(); + // TODO: add userId + const clientService = new ClientService(''); await clientService.updateAvatar(avatar); await get().refreshUserState(); 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; } diff --git a/src/types/session/sessionGroup.ts b/src/types/session/sessionGroup.ts index 85fb3675021b..1c8dbcda048a 100644 --- a/src/types/session/sessionGroup.ts +++ b/src/types/session/sessionGroup.ts @@ -8,11 +8,11 @@ export enum SessionDefaultGroup { export type SessionGroupId = SessionDefaultGroup | string; export interface SessionGroupItem { - createdAt: number; + createdAt: Date; id: string; name: string; - sort?: number; - updatedAt: number; + sort?: number | null; + updatedAt: Date; } export type SessionGroups = SessionGroupItem[];