mirror of
https://github.com/lobehub/lobehub
synced 2026-04-21 09:37:28 +00:00
* 🐛 feat(db): add findExclusiveFileIds, deleteWithFiles, deleteAllWithFiles to KnowledgeBaseModel Add methods to safely clean up vector storage when deleting knowledge bases: - findExclusiveFileIds: identifies files belonging only to a specific KB - deleteWithFiles: deletes KB and its exclusive files with chunks/embeddings - deleteAllWithFiles: bulk version for deleting all user KBs * 🐛 fix(kb): wire vector cleanup in TRPC router, OpenAPI service, and client - TRPC removeKnowledgeBase: use deleteWithFiles when removeFiles=true + S3 cleanup - TRPC removeAllKnowledgeBases: use deleteAllWithFiles + S3 cleanup - OpenAPI deleteKnowledgeBase: use deleteWithFiles + S3 cleanup - Client service: default removeFiles=true when deleting knowledge base * 🐛 fix(knowledgeBase): change default behavior of deleteKnowledgeBase to not remove files and update related tests Signed-off-by: Innei <tukon479@gmail.com> * ✨ feat(knowledgeBase): add optional query parameter to deleteKnowledgeBase for file removal - Introduced `removeFiles` query parameter to control the deletion of exclusive files and derived data when deleting a knowledge base. - Updated `KnowledgeBaseController`, `KnowledgeBaseService`, and related schemas to support this new functionality. This change enhances the flexibility of the delete operation, allowing users to choose whether to remove associated files. Signed-off-by: Innei <tukon479@gmail.com> * 🐛 fix: cascade knowledge base deletion and add orphan cleanup runbook * ✨ feat(knowledgeRepo): implement cascading deletion for file-backed documents - Enhanced the `KnowledgeRepo` to ensure that when a document with an associated file is deleted, all related data (files, chunks, embeddings) are also removed. - Introduced a new method `deleteDocumentWithRelations` to handle the cascading deletion logic. - Updated tests to verify that all related entities are deleted when a file-backed document is removed. This change improves data integrity by ensuring that no orphaned records remain after deletions. Signed-off-by: Innei <tukon479@gmail.com> * Defer DocumentService file initialization * Fix flaky database tests and knowledge repo fixtures * Add deletion regression tests for folders and external files * ⏪ chore: remove kb orphan cleanup files from pr --------- Signed-off-by: Innei <tukon479@gmail.com>
95 lines
2.7 KiB
TypeScript
95 lines
2.7 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
|
|
import type { LobeChatDatabase } from '@/database/type';
|
|
import { FileService } from '@/server/services/file';
|
|
|
|
import { KnowledgeBaseService } from '../../packages/openapi/src/services/knowledge-base.service';
|
|
|
|
vi.mock('@/server/services/file');
|
|
|
|
describe('KnowledgeBaseService.deleteKnowledgeBase', () => {
|
|
let db: LobeChatDatabase;
|
|
let deleteFilesSpy: ReturnType<typeof vi.fn>;
|
|
|
|
beforeEach(() => {
|
|
db = {
|
|
query: {
|
|
knowledgeBases: {
|
|
findFirst: vi.fn().mockResolvedValue({ id: 'kb-1', userId: 'user-1' }),
|
|
},
|
|
},
|
|
} as unknown as LobeChatDatabase;
|
|
|
|
deleteFilesSpy = vi.fn().mockResolvedValue(undefined);
|
|
vi.mocked(FileService).mockImplementation(() => ({ deleteFiles: deleteFilesSpy }) as any);
|
|
});
|
|
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
const createService = () => {
|
|
const service = new KnowledgeBaseService(db, 'user-1');
|
|
|
|
vi.spyOn(service as any, 'log').mockImplementation(() => {});
|
|
vi.spyOn(service as any, 'resolveOperationPermission').mockResolvedValue({
|
|
isPermitted: true,
|
|
message: '',
|
|
});
|
|
|
|
return service;
|
|
};
|
|
|
|
it('should always delete exclusive files together with the knowledge base', async () => {
|
|
const service = createService();
|
|
const deleteWithFilesSpy = vi.fn().mockResolvedValue({
|
|
deletedFiles: [],
|
|
});
|
|
|
|
Reflect.set(service, 'knowledgeBaseModel', {
|
|
deleteWithFiles: deleteWithFilesSpy,
|
|
});
|
|
|
|
await expect(service.deleteKnowledgeBase('kb-1')).resolves.toEqual({
|
|
message: 'Knowledge base deleted successfully',
|
|
success: true,
|
|
});
|
|
|
|
expect(deleteWithFilesSpy).toHaveBeenCalledWith('kb-1');
|
|
});
|
|
|
|
it('should delete external files when deleted knowledge-base files have URLs', async () => {
|
|
const service = createService();
|
|
|
|
Reflect.set(service, 'knowledgeBaseModel', {
|
|
deleteWithFiles: vi.fn().mockResolvedValue({
|
|
deletedFiles: [
|
|
{ id: 'file-1', url: 'https://example.com/a.pdf' },
|
|
{ id: 'file-2', url: null },
|
|
{ id: 'file-3', url: 'https://example.com/b.pdf' },
|
|
],
|
|
}),
|
|
});
|
|
|
|
await service.deleteKnowledgeBase('kb-1');
|
|
|
|
expect(deleteFilesSpy).toHaveBeenCalledWith([
|
|
'https://example.com/a.pdf',
|
|
'https://example.com/b.pdf',
|
|
]);
|
|
});
|
|
|
|
it('should skip external file deletion when deleted files have no URLs', async () => {
|
|
const service = createService();
|
|
|
|
Reflect.set(service, 'knowledgeBaseModel', {
|
|
deleteWithFiles: vi.fn().mockResolvedValue({
|
|
deletedFiles: [{ id: 'file-1', url: null }],
|
|
}),
|
|
});
|
|
|
|
await service.deleteKnowledgeBase('kb-1');
|
|
|
|
expect(deleteFilesSpy).not.toHaveBeenCalled();
|
|
});
|
|
});
|