♻️ refactor: remove client db and refactor test (#11123)

* ♻️ refactor: refactor to remove client db

* remove tableViewer

*  tests: remove tests
This commit is contained in:
Arvin Xu 2026-01-03 13:59:45 +08:00 committed by GitHub
parent bc44cba10a
commit bb2799dc75
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
71 changed files with 116 additions and 1785 deletions

View file

@ -42,8 +42,7 @@
"build:electron": "cross-env NODE_OPTIONS=--max-old-space-size=8192 NEXT_PUBLIC_IS_DESKTOP_APP=1 tsx scripts/electronWorkflow/buildNextApp.mts",
"build:vercel": "npm run prebuild && cross-env NODE_OPTIONS=--max-old-space-size=6144 next build --webpack",
"clean:node_modules": "bash -lc 'set -e; echo \"Removing all node_modules...\"; rm -rf node_modules; pnpm -r exec rm -rf node_modules; rm -rf apps/desktop/node_modules; echo \"All node_modules removed.\"'",
"db:generate": "drizzle-kit generate && npm run db:generate-client && npm run workflow:dbml",
"db:generate-client": "tsx ./scripts/migrateClientDB/compile-migrations.ts",
"db:generate": "drizzle-kit generate && npm run workflow:dbml",
"db:migrate": "MIGRATION_DB=1 tsx ./scripts/migrateServerDB/index.ts",
"db:studio": "drizzle-kit studio",
"db:visualize": "dbdocs build docs/development/database-schema.dbml --project lobe-chat",

View file

@ -1,43 +0,0 @@
import { Pool as NeonPool, neonConfig } from '@neondatabase/serverless';
import { drizzle as neonDrizzle } from 'drizzle-orm/neon-serverless';
import * as migrator from 'drizzle-orm/neon-serverless/migrator';
import { drizzle as nodeDrizzle } from 'drizzle-orm/node-postgres';
import * as nodeMigrator from 'drizzle-orm/node-postgres/migrator';
import { join } from 'node:path';
import { Pool as NodePool } from 'pg';
import ws from 'ws';
import { serverDBEnv } from '@/config/db';
import * as schema from '../schemas';
const migrationsFolder = join(__dirname, '../../migrations');
export const getTestDBInstance = async () => {
let connectionString = serverDBEnv.DATABASE_TEST_URL;
if (!connectionString) {
throw new Error(`You are try to use database, but "DATABASE_TEST_URL" is not set correctly`);
}
if (serverDBEnv.DATABASE_DRIVER === 'node') {
const client = new NodePool({ connectionString });
const db = nodeDrizzle(client, { schema });
await nodeMigrator.migrate(db, { migrationsFolder });
return db;
}
// https://github.com/neondatabase/serverless/blob/main/CONFIG.md#websocketconstructor-typeof-websocket--undefined
neonConfig.webSocketConstructor = ws;
const client = new NeonPool({ connectionString });
const db = neonDrizzle(client, { schema });
await migrator.migrate(db, { migrationsFolder });
return db;
};

View file

@ -0,0 +1,50 @@
import { PGlite } from '@electric-sql/pglite';
import { vector } from '@electric-sql/pglite/vector';
import { drizzle as pgliteDrizzle } from 'drizzle-orm/pglite';
import { migrate as pgliteMigrate } from 'drizzle-orm/pglite/migrator';
import { drizzle as nodeDrizzle } from 'drizzle-orm/node-postgres';
import { migrate as nodeMigrate } from 'drizzle-orm/node-postgres/migrator';
import { join } from 'node:path';
import { Pool as NodePool } from 'pg';
import { serverDBEnv } from '@/config/db';
import * as schema from '../schemas';
import { LobeChatDatabase } from '../type';
const migrationsFolder = join(__dirname, '../../migrations');
const isServerDBMode = process.env.TEST_SERVER_DB === '1';
let testClientDB: ReturnType<typeof pgliteDrizzle<typeof schema>> | null = null;
let testServerDB: ReturnType<typeof nodeDrizzle<typeof schema>> | null = null;
export const getTestDB = async (): Promise<LobeChatDatabase> => {
// Server DB mode (node-postgres)
if (isServerDBMode) {
if (testServerDB) return testServerDB as unknown as LobeChatDatabase;
const connectionString = serverDBEnv.DATABASE_TEST_URL;
if (!connectionString) {
throw new Error('DATABASE_TEST_URL is not set');
}
const client = new NodePool({ connectionString });
testServerDB = nodeDrizzle(client, { schema });
await nodeMigrate(testServerDB, { migrationsFolder });
return testServerDB as unknown as LobeChatDatabase;
}
// Client DB mode (PGlite)
if (testClientDB) return testClientDB as unknown as LobeChatDatabase;
const pglite = new PGlite({ extensions: { vector } });
testClientDB = pgliteDrizzle({ client: pglite, schema });
await pgliteMigrate(testClientDB, { migrationsFolder });
return testClientDB as unknown as LobeChatDatabase;
};

File diff suppressed because it is too large Load diff

View file

@ -2,10 +2,10 @@
import { eq } from 'drizzle-orm';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { getTestDB } from '../../core/getTestDB';
import { sessionGroups, users } from '../../schemas';
import { LobeChatDatabase } from '../../type';
import { SessionGroupModel } from '../sessionGroup';
import { getTestDB } from './_util';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -1,30 +0,0 @@
import { PGlite } from '@electric-sql/pglite';
import { vector } from '@electric-sql/pglite/vector';
import { drizzle } from 'drizzle-orm/pglite';
import migrations from '../../core/migrations.json';
import * as schema from '../../schemas';
import { LobeChatDatabase } from '../../type';
const isServerDBMode = process.env.TEST_SERVER_DB === '1';
let testClientDB: ReturnType<typeof drizzle<typeof schema>> | null = null;
export const getTestDB = async () => {
if (isServerDBMode) {
const { getTestDBInstance } = await import('../../core/dbForTest');
return await getTestDBInstance();
}
if (testClientDB) return testClientDB as unknown as LobeChatDatabase;
// 直接使用 pglite 内置资源,不需要从 CDN 下载
const pglite = new PGlite({ extensions: { vector } });
testClientDB = drizzle({ client: pglite, schema });
// @ts-expect-error - migrate internal API
await testClientDB.dialect.migrate(migrations, testClientDB.session, {});
return testClientDB as unknown as LobeChatDatabase;
};

View file

@ -16,7 +16,7 @@ import {
} from '../../schemas';
import { LobeChatDatabase } from '../../type';
import { AgentModel } from '../agent';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { AiModelSelectItem, NewAiModelItem, aiModels, users } from '../../schemas';
import { LobeChatDatabase } from '../../type';
import { AiModelModel } from '../aiModel';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -8,7 +8,7 @@ import { sleep } from '@/utils/sleep';
import { aiProviders, users } from '../../schemas';
import { LobeChatDatabase } from '../../type';
import { AiProviderModel } from '../aiProvider';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { apiKeys, users } from '../../schemas';
import { LobeChatDatabase } from '../../type';
import { ApiKeyModel } from '../apiKey';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -7,7 +7,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { asyncTasks, users } from '../../schemas';
import { LobeChatDatabase } from '../../type';
import { AsyncTaskModel } from '../asyncTask';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -12,7 +12,7 @@ import {
users,
} from '../../schemas';
import { ChatGroupModel } from '../chatGroup';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const userId = 'test-user';
const otherUserId = 'other-user';

View file

@ -6,7 +6,7 @@ import { LobeChatDatabase } from '../../type';import { uuid } from '@/utils/uuid
import { chunks, embeddings, fileChunks, files, unstructuredChunks, users } from '../../schemas';
import { ChunkModel } from '../chunk';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
import { codeEmbedding, designThinkingQuery, designThinkingQuery2 } from './fixtures/embedding';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -5,7 +5,7 @@ import { documents, files, users } from '../../schemas';
import { LobeChatDatabase } from '../../type';
import { DocumentModel } from '../document';
import { FileModel } from '../file';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it } from 'vitest';
import { LobeChatDatabase } from '../../type';
import { DrizzleMigrationModel } from '../drizzleMigration';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { chunks, embeddings, users } from '../../schemas';
import { LobeChatDatabase } from '../../type';
import { EmbeddingModel } from '../embedding';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
import { designThinkingQuery } from './fixtures/embedding';
const userId = 'embedding-user-test';

View file

@ -15,7 +15,7 @@ import {
} from '../../schemas';
import { LobeChatDatabase } from '../../type';
import { FileModel } from '../file';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -15,7 +15,7 @@ import {
} from '../../schemas';
import { LobeChatDatabase } from '../../type';
import { GenerationModel } from '../generation';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -13,7 +13,7 @@ import {
} from '../../schemas';
import { LobeChatDatabase } from '../../type';
import { GenerationBatchModel } from '../generationBatch';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { generationBatches, generationTopics, generations, users } from '../../schemas';
import { LobeChatDatabase } from '../../type';
import { GenerationTopicModel } from '../generationTopic';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
// Mock FileService
const mockGetFullFileUrl = vi.fn();

View file

@ -14,7 +14,7 @@ import {
} from '../../schemas';
import { LobeChatDatabase } from '../../type';
import { KnowledgeBaseModel } from '../knowledgeBase';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -19,7 +19,7 @@ import {
} from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { MessageModel } from '../../message';
import { getTestDB } from '../_util';
import { getTestDB } from '../../../core/getTestDB';
import { codeEmbedding } from '../fixtures/embedding';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -18,7 +18,7 @@ import {
} from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { MessageModel } from '../../message';
import { getTestDB } from '../_util';
import { getTestDB } from '../../../core/getTestDB';
import { codeEmbedding } from '../fixtures/embedding';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -28,7 +28,7 @@ import {
} from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { MessageModel } from '../../message';
import { getTestDB } from '../_util';
import { getTestDB } from '../../../core/getTestDB';
import { codeEmbedding } from '../fixtures/embedding';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -7,7 +7,7 @@ import { uuid } from '@/utils/uuid';
import { embeddings, files, messageQueries, messages, sessions, users } from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { MessageModel } from '../../message';
import { getTestDB } from '../_util';
import { getTestDB } from '../../../core/getTestDB';
import { codeEmbedding } from '../fixtures/embedding';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { agents, messages, sessions, threads, topics, users } from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { MessageModel } from '../../message';
import { getTestDB } from '../_util';
import { getTestDB } from '../../../core/getTestDB';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -23,7 +23,7 @@ import {
} from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { MessageModel } from '../../message';
import { getTestDB } from '../_util';
import { getTestDB } from '../../../core/getTestDB';
import { codeEmbedding } from '../fixtures/embedding';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -5,7 +5,7 @@ import { ThreadStatus, ThreadType } from '@/types/index';
import { messages, sessions, threads, topics, users } from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { MessageModel } from '../../message';
import { getTestDB } from '../_util';
import { getTestDB } from '../../../core/getTestDB';
const userId = 'message-task-user-test';
const sessionId = 'message-task-session';

View file

@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { messageGroups, messages, topics, users } from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { MessageModel } from '../../message';
import { getTestDB } from '../_util';
import { getTestDB } from '../../../core/getTestDB';
const userId = 'message-query-perf-test-user';
const topicId = 'perf-test-topic-1';

View file

@ -6,7 +6,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { messageGroups, messages, topics, users } from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { MessageModel } from '../../message';
import { getTestDB } from '../_util';
import { getTestDB } from '../../../core/getTestDB';
const userId = 'message-query-test-user';
const topicId = 'test-topic-1';

View file

@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { oauthHandoffs } from '../../schemas';
import { LobeChatDatabase } from '../../type';
import { OAuthHandoffModel } from '../oauthHandoff';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const serverDB: LobeChatDatabase = await getTestDB();
const oauthHandoffModel = new OAuthHandoffModel(serverDB);

View file

@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { LobeChatDatabase } from '../../type';
import { NewInstalledPlugin, userInstalledPlugins, users } from '../../schemas';
import { PluginModel } from '../plugin';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -17,7 +17,7 @@ import {
import { LobeChatDatabase } from '../../type';
import { idGenerator } from '../../utils/idGenerator';
import { SessionModel } from '../session';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { sessionGroups, users } from '../../schemas';
import { LobeChatDatabase } from '../../type';
import { SessionGroupModel } from '../sessionGroup';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { sessions, threads, topics, users } from '../../schemas';
import { LobeChatDatabase } from '../../type';
import { ThreadModel } from '../thread';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const userId = 'thread-user-test';
const otherUserId = 'other-user-test';

View file

@ -5,7 +5,7 @@ import { documents, sessions, topicDocuments, topics, users } from '../../schema
import { LobeChatDatabase } from '../../type';
import { DocumentModel } from '../document';
import { TopicDocumentModel } from '../topicDocument';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -4,7 +4,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { agents, messages, sessions, topics, users } from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { CreateTopicParams, TopicModel } from '../../topic';
import { getTestDB } from '../_util';
import { getTestDB } from '../../../core/getTestDB';
const userId = 'topic-create-user';
const userId2 = 'topic-create-user-2';

View file

@ -12,7 +12,7 @@ import {
} from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { TopicModel } from '../../topic';
import { getTestDB } from '../_util';
import { getTestDB } from '../../../core/getTestDB';
const userId = 'topic-delete-user';
const userId2 = 'topic-delete-user-2';

View file

@ -11,7 +11,7 @@ import {
} from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { TopicModel } from '../../topic';
import { getTestDB } from '../_util';
import { getTestDB } from '../../../core/getTestDB';
const userId = 'topic-query-user';
const userId2 = 'topic-query-user-2';

View file

@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { agents, agentsToSessions, messages, sessions, topics, users } from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { TopicModel } from '../../topic';
import { getTestDB } from '../_util';
import { getTestDB } from '../../../core/getTestDB';
const userId = 'topic-stats-user';
const userId2 = 'topic-stats-user-2';

View file

@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { sessions, topics, users } from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { TopicModel } from '../../topic';
import { getTestDB } from '../_util';
import { getTestDB } from '../../../core/getTestDB';
const userId = 'topic-update-user';
const sessionId = 'topic-update-session';

View file

@ -5,7 +5,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { nextauthAccounts, userSettings, users } from '../../schemas';
import { LobeChatDatabase } from '../../type';
import { ListUsersForMemoryExtractorCursor, UserModel, UserNotFoundError } from '../user';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const userId = 'user-model-test';
const otherUserId = 'other-user-test';

View file

@ -24,7 +24,7 @@ import {
CreateUserMemoryPreferenceParams,
UserMemoryModel,
} from '../userMemory';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
const serverDB: LobeChatDatabase = await getTestDB();

View file

@ -9,7 +9,7 @@ import { idGenerator } from '@/database/utils/idGenerator';
import { userMemoriesIdentities, users } from '../../schemas';
import { LobeChatDatabase } from '../../type';
import { UserMemoryIdentityModel } from '../userMemory/identity';
import { getTestDB } from './_util';
import { getTestDB } from '../../core/getTestDB';
// Helper to generate unique identity IDs
const genIdentityId = () => `mem_${nanoid(12)}`;

View file

@ -3,7 +3,7 @@ import { beforeEach, describe, expect, it } from 'vitest';
import { NewUserMemoryContext, userMemories, userMemoriesContexts, users } from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { getTestDB } from '../../__tests__/_util';
import { getTestDB } from '../../../core/getTestDB';
import { UserMemoryContextModel } from '../context';
const userId = 'context-test-user';

View file

@ -8,7 +8,7 @@ import {
users,
} from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { getTestDB } from '../../__tests__/_util';
import { getTestDB } from '../../../core/getTestDB';
import { UserMemoryExperienceModel } from '../experience';
const userId = 'experience-test-user';

View file

@ -9,7 +9,7 @@ import {
users,
} from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { getTestDB } from '../../__tests__/_util';
import { getTestDB } from '../../../core/getTestDB';
import { UserMemoryIdentityModel } from '../identity';
const userId = 'identity-test-user';

View file

@ -8,7 +8,7 @@ import {
users,
} from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { getTestDB } from '../../__tests__/_util';
import { getTestDB } from '../../../core/getTestDB';
import { UserMemoryPreferenceModel } from '../preference';
const userId = 'preference-test-user';

View file

@ -2,7 +2,7 @@
import { BUILTIN_AGENT_SLUGS } from '@lobechat/builtin-agents';
import { beforeEach, describe, expect, it } from 'vitest';
import { getTestDB } from '../../models/__tests__/_util';
import { getTestDB } from '../../core/getTestDB';
import { agents } from '../../schemas/agent';
import { chatGroups, chatGroupsAgents } from '../../schemas/chatGroup';
import { users } from '../../schemas/user';

View file

@ -1,7 +1,7 @@
import { eq, inArray } from 'drizzle-orm';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { getTestDB } from '../../../models/__tests__/_util';
import { getTestDB } from '../../../core/getTestDB';
import { agents, messages, sessions, topics, users } from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { AgentMigrationRepo } from '../index';

View file

@ -8,7 +8,7 @@ import { AiProviderModelListItem, EnabledAiModel } from 'model-bank';
import { DEFAULT_MODEL_PROVIDER_LIST } from 'model-bank/modelProviders';
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { getTestDB } from '../../models/__tests__/_util';
import { getTestDB } from '../../core/getTestDB';
import { LobeChatDatabase } from '../../type';
import { AiInfraRepos } from './index';

View file

@ -2,7 +2,7 @@
import { MessageGroupType } from '@lobechat/types';
import { beforeEach, describe, expect, it } from 'vitest';
import { getTestDB } from '../../models/__tests__/_util';
import { getTestDB } from '../../core/getTestDB';
import { messageGroups, messages } from '../../schemas/message';
import { topics } from '../../schemas/topic';
import { users } from '../../schemas/user';

View file

@ -1,6 +1,6 @@
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { getTestDB } from '../../models/__tests__/_util';
import { getTestDB } from '../../core/getTestDB';
import {
agents,
agentsKnowledgeBases,

View file

@ -2,7 +2,7 @@ import type { ImportPgDataStructure } from '@lobechat/types';
import { eq, inArray } from 'drizzle-orm';
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { getTestDB } from '../../../models/__tests__/_util';
import { getTestDB } from '../../../core/getTestDB';
import * as Schema from '../../../schemas';
import { DataImporterRepos } from '../index';
import agentsData from './fixtures/agents.json';

View file

@ -3,7 +3,7 @@ import type { ImporterEntryData } from '@lobechat/types';
import { eq, inArray } from 'drizzle-orm';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { getTestDBInstance } from '@/database/core/dbForTest';
import { getTestDB } from '../../../../core/getTestDB';
import {
agents,
agentsToSessions,
@ -19,7 +19,7 @@ import mockImportData from './fixtures/messages.json';
const CURRENT_CONFIG_VERSION = 7;
const serverDB = await getTestDBInstance();
const serverDB = await getTestDB();
const userId = 'test-user-id';
let importer: DataImporterRepos;

View file

@ -1,7 +1,7 @@
import { eq } from 'drizzle-orm';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { getTestDB } from '../../../models/__tests__/_util';
import { getTestDB } from '../../../core/getTestDB';
import * as Schema from '../../../schemas';
import { HomeRepository } from '../index';

View file

@ -1,7 +1,7 @@
// @vitest-environment node
import { beforeEach, describe, expect, it } from 'vitest';
import { getTestDB } from '../../models/__tests__/_util';
import { getTestDB } from '../../core/getTestDB';
import { NewAgent, agents } from '../../schemas/agent';
import { NewChatGroup, chatGroups } from '../../schemas/chatGroup';
import { agentsToSessions } from '../../schemas/relations';

View file

@ -2,7 +2,7 @@
import { FilesTabs } from '@lobechat/types';
import { beforeEach, describe, expect, it } from 'vitest';
import { getTestDB } from '../../models/__tests__/_util';
import { getTestDB } from '../../core/getTestDB';
import { NewDocument, documents } from '../../schemas/file';
import { NewFile, files } from '../../schemas/file';
import { users } from '../../schemas/user';

View file

@ -1,7 +1,7 @@
// @vitest-environment node
import { beforeEach, describe, expect, it } from 'vitest';
import { getTestDB } from '../../models/__tests__/_util';
import { getTestDB } from '../../core/getTestDB';
import { NewAgent, agents } from '../../schemas/agent';
import { NewFile, files } from '../../schemas/file';
import { messages } from '../../schemas/message';

View file

@ -1,255 +0,0 @@
import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest';
import { getTestDB } from '../../models/__tests__/_util';
import { LobeChatDatabase } from '../../type';
import { TableViewerRepo } from './index';
const userId = 'user-table-viewer';
// Mock database execution
const mockExecute = vi.fn();
const mockDB = {
execute: mockExecute,
};
let serverDB: LobeChatDatabase;
let repo: TableViewerRepo;
beforeAll(async () => {
serverDB = await getTestDB();
repo = new TableViewerRepo(serverDB, userId);
}, 30000);
beforeEach(() => {
vi.clearAllMocks();
});
describe('TableViewerRepo', () => {
describe('getAllTables', () => {
it('should handle custom schema', async () => {
const result = await repo.getAllTables('custom_schema');
expect(result).toBeDefined();
});
});
describe('getTableDetails', () => {
it('should return table column details', async () => {
const tableName = 'test_table';
const mockColumns = {
rows: [
{
column_name: 'id',
data_type: 'uuid',
is_nullable: 'NO',
column_default: null,
is_primary_key: true,
foreign_key: null,
},
],
};
const testRepo = new TableViewerRepo(mockDB as any, userId);
mockExecute.mockResolvedValueOnce(mockColumns);
const result = await testRepo.getTableDetails(tableName);
expect(result).toEqual([
{
name: 'id',
type: 'uuid',
nullable: false,
defaultValue: null,
isPrimaryKey: true,
foreignKey: null,
},
]);
});
});
describe('getTableData', () => {
it('should return paginated data with filters', async () => {
const tableName = 'test_table';
const pagination = {
page: 1,
pageSize: 10,
sortBy: 'id',
sortOrder: 'desc' as const,
};
const filters = [
{
column: 'name',
operator: 'contains' as const,
value: 'test',
},
];
const mockData = {
rows: [{ id: 1, name: 'test' }],
};
const mockCount = {
rows: [{ total: 1 }],
};
const testRepo = new TableViewerRepo(mockDB as any, userId);
mockExecute.mockResolvedValueOnce(mockData).mockResolvedValueOnce(mockCount);
const result = await testRepo.getTableData(tableName, pagination, filters);
expect(result).toEqual({
data: mockData.rows,
pagination: {
page: 1,
pageSize: 10,
total: 1,
},
});
});
});
describe('updateRow', () => {
it('should update and return row data', async () => {
const tableName = 'test_table';
const id = '123';
const primaryKeyColumn = 'id';
const data = { name: 'updated' };
const mockResult = {
rows: [{ id: '123', name: 'updated' }],
};
const testRepo = new TableViewerRepo(mockDB as any, userId);
mockExecute.mockResolvedValueOnce(mockResult);
const result = await testRepo.updateRow(tableName, id, primaryKeyColumn, data);
expect(result).toEqual(mockResult.rows[0]);
expect(mockExecute).toHaveBeenCalledTimes(1);
});
});
describe('deleteRow', () => {
it('should delete a row', async () => {
const tableName = 'test_table';
const id = '123';
const primaryKeyColumn = 'id';
const testRepo = new TableViewerRepo(mockDB as any, userId);
mockExecute.mockResolvedValueOnce({ rows: [] });
await testRepo.deleteRow(tableName, id, primaryKeyColumn);
expect(mockExecute).toHaveBeenCalledTimes(1);
});
});
describe('insertRow', () => {
it('should insert and return new row data', async () => {
const tableName = 'test_table';
const data = { name: 'new row' };
const mockResult = {
rows: [{ id: '123', name: 'new row' }],
};
const testRepo = new TableViewerRepo(mockDB as any, userId);
mockExecute.mockResolvedValueOnce(mockResult);
const result = await testRepo.insertRow(tableName, data);
expect(result).toEqual(mockResult.rows[0]);
expect(mockExecute).toHaveBeenCalledTimes(1);
});
});
describe('getTableCount', () => {
it('should return table count', async () => {
const tableName = 'test_table';
const mockResult = {
rows: [{ total: 42 }],
};
const testRepo = new TableViewerRepo(mockDB as any, userId);
mockExecute.mockResolvedValueOnce(mockResult);
const result = await testRepo.getTableCount(tableName);
expect(result).toBe(42);
expect(mockExecute).toHaveBeenCalledTimes(1);
});
});
describe('batchDelete', () => {
it('should delete multiple rows', async () => {
const tableName = 'test_table';
const ids = ['1', '2', '3'];
const primaryKeyColumn = 'id';
const testRepo = new TableViewerRepo(mockDB as any, userId);
mockExecute.mockResolvedValueOnce({ rows: [] });
await testRepo.batchDelete(tableName, ids, primaryKeyColumn);
expect(mockExecute).toHaveBeenCalledTimes(1);
});
});
describe('exportTableData', () => {
it('should export table data with default pagination', async () => {
const tableName = 'test_table';
const mockData = {
rows: [{ id: 1, name: 'test' }],
};
const mockCount = {
rows: [{ total: 1 }],
};
const testRepo = new TableViewerRepo(mockDB as any, userId);
mockExecute.mockResolvedValueOnce(mockData).mockResolvedValueOnce(mockCount);
const result = await testRepo.exportTableData(tableName);
expect(result).toEqual({
data: mockData.rows,
pagination: {
page: 1,
pageSize: 1000,
total: 1,
},
});
});
it('should export table data with custom pagination and filters', async () => {
const tableName = 'test_table';
const pagination = { page: 2, pageSize: 50 };
const filters = [
{
column: 'status',
operator: 'equals' as const,
value: 'active',
},
];
const mockData = {
rows: [{ id: 1, status: 'active' }],
};
const mockCount = {
rows: [{ total: 1 }],
};
mockExecute.mockResolvedValueOnce(mockData).mockResolvedValueOnce(mockCount);
const testRepo = new TableViewerRepo(mockDB as any, userId);
const result = await testRepo.exportTableData(tableName, pagination, filters);
expect(result).toEqual({
data: mockData.rows,
pagination: {
page: 2,
pageSize: 50,
total: 1,
},
});
});
});
});

View file

@ -1,251 +0,0 @@
import type {
FilterCondition,
PaginationParams,
TableBasicInfo,
TableColumnInfo,
} from '@lobechat/types';
import { sql } from 'drizzle-orm';
import pMap from 'p-map';
import { LobeChatDatabase } from '../../type';
export class TableViewerRepo {
private userId: string;
private db: LobeChatDatabase;
constructor(db: LobeChatDatabase, userId: string) {
this.userId = userId;
this.db = db;
}
/**
* Get all tables in the database
*/
async getAllTables(schema = 'public'): Promise<TableBasicInfo[]> {
const query = sql`
SELECT
table_name as name,
table_type as type
FROM information_schema.tables
WHERE table_schema = ${schema}
ORDER BY table_name;
`;
const tables = await this.db.execute(query);
const tableNames = tables.rows.map((row) => row.name) as string[];
const counts = await pMap(tableNames, async (name) => this.getTableCount(name), {
concurrency: 10,
});
return tables.rows.map((row, index) => ({
count: counts[index],
name: row.name,
type: row.type,
})) as TableBasicInfo[];
}
/**
* Get detailed structure information for a specified table
*/
async getTableDetails(tableName: string): Promise<TableColumnInfo[]> {
const query = sql`
SELECT
c.column_name,
c.data_type,
c.is_nullable,
c.column_default,
-- Primary key information
(
SELECT true
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
WHERE tc.table_name = c.table_name
AND kcu.column_name = c.column_name
AND tc.constraint_type = 'PRIMARY KEY'
) is_primary_key,
-- Foreign key information
(
SELECT json_build_object(
'table', ccu.table_name,
'column', ccu.column_name
)
FROM information_schema.table_constraints tc
JOIN information_schema.key_column_usage kcu
ON tc.constraint_name = kcu.constraint_name
JOIN information_schema.constraint_column_usage ccu
ON ccu.constraint_name = tc.constraint_name
WHERE tc.table_name = c.table_name
AND kcu.column_name = c.column_name
AND tc.constraint_type = 'FOREIGN KEY'
) foreign_key
FROM information_schema.columns c
WHERE c.table_name = ${tableName}
AND c.table_schema = 'public'
ORDER BY c.ordinal_position;
`;
const columns = await this.db.execute(query);
return columns.rows.map((col: any) => ({
defaultValue: col.column_default,
foreignKey: col.foreign_key,
isPrimaryKey: !!col.is_primary_key,
name: col.column_name,
nullable: col.is_nullable === 'YES',
type: col.data_type,
}));
}
/**
* Get table data with support for pagination, sorting, and filtering
*/
async getTableData(tableName: string, pagination: PaginationParams, filters?: FilterCondition[]) {
const offset = (pagination.page - 1) * pagination.pageSize;
// Build base query
let baseQuery = sql`SELECT * FROM ${sql.identifier(tableName)}`;
// Add filter conditions
if (filters && filters.length > 0) {
const whereConditions = filters.map((filter) => {
const column = sql.identifier(filter.column);
switch (filter.operator) {
case 'equals': {
return sql`${column} = ${filter.value}`;
}
case 'contains': {
return sql`${column} ILIKE ${`%${filter.value}%`}`;
}
case 'startsWith': {
return sql`${column} ILIKE ${`${filter.value}%`}`;
}
case 'endsWith': {
return sql`${column} ILIKE ${`%${filter.value}`}`;
}
default: {
return sql`1=1`;
}
}
});
baseQuery = sql`${baseQuery} WHERE ${sql.join(whereConditions, sql` AND `)}`;
}
// Add sorting
if (pagination.sortBy) {
const direction = pagination.sortOrder === 'desc' ? sql`DESC` : sql`ASC`;
baseQuery = sql`${baseQuery} ORDER BY ${sql.identifier(pagination.sortBy)} ${direction}`;
}
// Add pagination
const query = sql`${baseQuery} LIMIT ${pagination.pageSize} OFFSET ${offset}`;
// Get total count
const countQuery = sql`SELECT COUNT(*) as total FROM ${sql.identifier(tableName)}`;
// Execute queries in parallel
const [data, count] = await Promise.all([this.db.execute(query), this.db.execute(countQuery)]);
return {
data: data.rows,
pagination: {
page: pagination.page,
pageSize: pagination.pageSize,
total: Number(count.rows[0].total),
},
};
}
/**
* Update a row in the table
*/
async updateRow(
tableName: string,
id: string,
primaryKeyColumn: string,
data: Record<string, any>,
) {
const setColumns = Object.entries(data).map(([key, value]) => {
return sql`${sql.identifier(key)} = ${value}`;
});
const query = sql`
UPDATE ${sql.identifier(tableName)}
SET ${sql.join(setColumns, sql`, `)}
WHERE ${sql.identifier(primaryKeyColumn)} = ${id}
RETURNING *
`;
const result = await this.db.execute(query);
return result.rows[0];
}
/**
* Delete a row from the table
*/
async deleteRow(tableName: string, id: string, primaryKeyColumn: string) {
const query = sql`
DELETE FROM ${sql.identifier(tableName)}
WHERE ${sql.identifier(primaryKeyColumn)} = ${id}
`;
await this.db.execute(query);
}
/**
* Insert new row data
*/
async insertRow(tableName: string, data: Record<string, any>) {
const columns = Object.keys(data).map((key) => sql.identifier(key));
const values = Object.values(data);
const query = sql`
INSERT INTO ${sql.identifier(tableName)}
(${sql.join(columns, sql`, `)})
VALUES (${sql.join(
values.map((v) => sql`${v}`),
sql`, `,
)})
RETURNING *
`;
const result = await this.db.execute(query);
return result.rows[0];
}
/**
* Get total record count of a table
*/
async getTableCount(tableName: string): Promise<number> {
const query = sql`SELECT COUNT(*) as total FROM ${sql.identifier(tableName)}`;
const result = await this.db.execute(query);
return Number(result.rows[0].total);
}
/**
* Batch delete data
*/
async batchDelete(tableName: string, ids: string[], primaryKeyColumn: string) {
const query = sql`
DELETE FROM ${sql.identifier(tableName)}
WHERE ${sql.identifier(primaryKeyColumn)} = ANY(${ids})
`;
await this.db.execute(query);
}
/**
* Export table data (supports paginated export)
*/
async exportTableData(
tableName: string,
pagination?: PaginationParams,
filters?: FilterCondition[],
) {
return this.getTableData(tableName, pagination || { page: 1, pageSize: 1000 }, filters);
}
}

View file

@ -4,7 +4,7 @@ import { readFileSync } from 'node:fs';
import path from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { getTestDB } from '../../../models/__tests__/_util';
import { getTestDB } from '../../../core/getTestDB';
import { agents, messagePlugins, messages, topics, users } from '../../../schemas';
import { LobeChatDatabase } from '../../../type';
import { TopicImporterRepo } from '../index';

View file

@ -1,7 +1,7 @@
// @vitest-environment node
import { beforeEach, describe, expect, it } from 'vitest';
import { getTestDB } from '../../../models/__tests__/_util';
import { getTestDB } from '../../../core/getTestDB';
import { messages } from '../../../schemas/message';
import { topics } from '../../../schemas/topic';
import { users } from '../../../schemas/user';

View file

@ -3,7 +3,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { DrizzleAdapter } from '@/libs/oidc-provider/adapter';
import { getTestDBInstance } from '../../../core/dbForTest';
import { getTestDB } from '../../../core/getTestDB';
import { users } from '../../../schemas';
import {
oidcAccessTokens,
@ -14,7 +14,7 @@ import {
oidcSessions,
} from '../../../schemas/oidc';
let serverDB = await getTestDBInstance();
let serverDB = await getTestDB();
// Test data
const testModelName = 'Session';

View file

@ -7,12 +7,12 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { KeyVaultsGateKeeper } from '@/server/modules/KeyVaultsEncrypt';
import { getTestDBInstance } from '../../../core/dbForTest';
import { getTestDB } from '../../../core/getTestDB';
import { SessionModel } from '../../../models/session';
import { UserModel, UserNotFoundError } from '../../../models/user';
import { UserSettingsItem, nextauthAccounts, userSettings, users } from '../../../schemas';
let serverDB = await getTestDBInstance();
let serverDB = await getTestDB();
const userId = 'user-db';
const userEmail = 'user@example.com';

View file

@ -1 +1 @@
export * from '../src/models/__tests__/_util';
export * from '../src/core/getTestDB';

View file

@ -1,6 +1,6 @@
export interface ExportDatabaseData {
data: Record<string, object[]>;
schemaHash?: string;
schemaHash: string;
url?: string;
}

View file

@ -29,7 +29,6 @@ export * from './serverConfig';
export * from './service';
export * from './session';
export * from './stepContext';
export * from './tableViewer';
export * from './tool';
export * from './topic';
export * from './user';

View file

@ -1,30 +0,0 @@
export interface TableBasicInfo {
count: number;
name: string;
type: 'BASE TABLE' | 'VIEW';
}
export interface TableColumnInfo {
defaultValue?: string;
foreignKey?: {
column: string;
table: string;
};
isPrimaryKey: boolean;
name: string;
nullable: boolean;
type: string;
}
export interface PaginationParams {
page: number;
pageSize: number;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
}
export interface FilterCondition {
column: string;
operator: 'equals' | 'contains' | 'startsWith' | 'endsWith';
value: any;
}

View file

@ -1,14 +0,0 @@
import { readMigrationFiles } from 'drizzle-orm/migrator';
import { writeFileSync } from 'node:fs';
import { join } from 'node:path';
const dbBase = join(__dirname, '../../packages/database');
const migrationsFolder = join(dbBase, './migrations');
const migrations = readMigrationFiles({ migrationsFolder: migrationsFolder });
writeFileSync(
join(dbBase, './src/core/migrations.json'),
JSON.stringify(migrations, null, 2), // null, 2 adds indentation for better readability
);
console.log('🏁 client migrations.json compiled!');

View file

@ -8,7 +8,7 @@ import { exportService } from './export';
class ConfigService {
exportAll = async () => {
const { data, url } = await exportService.exportData();
const { data, url, schemaHash } = await exportService.exportData();
const filename = `${dayjs().format('YYYY-MM-DD-hh-mm')}_${BRANDING_NAME}-data.json`;
// if url exists, means export data from server and upload the data to S3
@ -18,24 +18,10 @@ class ConfigService {
return;
}
// or export to file with the data
const result = await this.createDataStructure(data, 'postgres');
const result: ImportPgDataStructure = { data, mode: 'postgres', schemaHash };
exportJSONFile(result, filename);
};
private createDataStructure = async (
data: any,
mode: 'pglite' | 'postgres',
): Promise<ImportPgDataStructure> => {
const { default: json } = await import('@/database/core/migrations.json');
const latestHash = json.at(-1)?.hash;
if (!latestHash) {
throw new Error('Not find database sql hash');
}
return { data, mode, schemaHash: latestHash };
};
}
export const configService = new ConfigService();