mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-08 20:57:08 +08:00
fix(server): session unique index conflict (#12865)
This commit is contained in:
+9
-7
@@ -5,14 +5,16 @@ ALTER TABLE "ai_sessions_metadata" ALTER COLUMN "doc_id" DROP NOT NULL;
|
|||||||
ALTER TABLE "ai_sessions_metadata" ADD COLUMN "pinned" BOOLEAN NOT NULL DEFAULT false;
|
ALTER TABLE "ai_sessions_metadata" ADD COLUMN "pinned" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
-- AlterTable
|
-- AlterTable
|
||||||
CREATE UNIQUE INDEX idx_ai_session_unique_pinned
|
ALTER TABLE "ai_sessions_metadata" ADD COLUMN "prompt_action" VARCHAR(32) DEFAULT '';
|
||||||
ON ai_sessions_metadata (user_id, workspace_id)
|
|
||||||
WHERE pinned = true AND deleted_at IS NULL;
|
|
||||||
|
|
||||||
-- AlterTable
|
-- CreateIndex
|
||||||
CREATE UNIQUE INDEX idx_ai_session_unique_doc_root
|
CREATE UNIQUE INDEX "ai_session_unique_pinned_idx" ON "ai_sessions_metadata" (user_id, workspace_id) WHERE pinned = true AND deleted_at IS NULL;
|
||||||
ON ai_sessions_metadata (user_id, workspace_id, doc_id)
|
|
||||||
WHERE parent_session_id IS NULL AND doc_id IS NOT NULL AND deleted_at IS NULL;
|
-- CreateIndex
|
||||||
|
CREATE UNIQUE INDEX "ai_session_unique_doc_session_idx" ON "ai_sessions_metadata" (user_id, workspace_id, doc_id) WHERE prompt_action IS NULL AND parent_session_id IS NULL AND doc_id IS NOT NULL AND deleted_at IS NULL;
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "ai_sessions_metadata_prompt_name_idx" ON "ai_sessions_metadata"("prompt_name");
|
||||||
|
|
||||||
-- DropIndex
|
-- DropIndex
|
||||||
DROP INDEX "ai_sessions_metadata_user_id_workspace_id_idx";
|
DROP INDEX "ai_sessions_metadata_user_id_workspace_id_idx";
|
||||||
|
|||||||
@@ -436,6 +436,7 @@ model AiSession {
|
|||||||
workspaceId String @map("workspace_id") @db.VarChar
|
workspaceId String @map("workspace_id") @db.VarChar
|
||||||
docId String? @map("doc_id") @db.VarChar
|
docId String? @map("doc_id") @db.VarChar
|
||||||
promptName String @map("prompt_name") @db.VarChar(32)
|
promptName String @map("prompt_name") @db.VarChar(32)
|
||||||
|
promptAction String? @default("") @map("prompt_action") @db.VarChar(32)
|
||||||
pinned Boolean @default(false)
|
pinned Boolean @default(false)
|
||||||
// the session id of the parent session if this session is a forked session
|
// the session id of the parent session if this session is a forked session
|
||||||
parentSessionId String? @map("parent_session_id") @db.VarChar
|
parentSessionId String? @map("parent_session_id") @db.VarChar
|
||||||
@@ -449,6 +450,12 @@ model AiSession {
|
|||||||
messages AiSessionMessage[]
|
messages AiSessionMessage[]
|
||||||
context AiContext[]
|
context AiContext[]
|
||||||
|
|
||||||
|
//NOTE:
|
||||||
|
// unrecorded index:
|
||||||
|
// @@index([userId, workspaceId]) where pinned = true and deleted_at is null
|
||||||
|
// @@index([userId, workspaceId, docId]) where prompt_action is null and parent_session_id is null and doc_id is not null and deleted_at is null
|
||||||
|
// since prisma does not support partial indexes, those indexes are only exists in migration files.
|
||||||
|
@@index([promptName])
|
||||||
@@index([userId])
|
@@index([userId])
|
||||||
@@index([userId, workspaceId, docId])
|
@@index([userId, workspaceId, docId])
|
||||||
@@map("ai_sessions_metadata")
|
@@map("ai_sessions_metadata")
|
||||||
|
|||||||
@@ -25,6 +25,12 @@ Generated by [AVA](https://avajs.dev).
|
|||||||
{
|
{
|
||||||
docId: 'doc-id-1',
|
docId: 'doc-id-1',
|
||||||
pinned: false,
|
pinned: false,
|
||||||
|
promptName: 'action-prompt',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
docId: 'doc-id-1',
|
||||||
|
pinned: false,
|
||||||
|
promptName: 'test-prompt',
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
BIN
Binary file not shown.
@@ -59,6 +59,7 @@ test.beforeEach(async t => {
|
|||||||
docId,
|
docId,
|
||||||
userId: user.id,
|
userId: user.id,
|
||||||
promptName: 'prompt-name',
|
promptName: 'prompt-name',
|
||||||
|
promptAction: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
WorkspaceModel,
|
WorkspaceModel,
|
||||||
} from '../../models';
|
} from '../../models';
|
||||||
import { createTestingModule, type TestingModule } from '../utils';
|
import { createTestingModule, type TestingModule } from '../utils';
|
||||||
|
import { cleanObject } from '../utils/copilot';
|
||||||
|
|
||||||
interface Context {
|
interface Context {
|
||||||
module: TestingModule;
|
module: TestingModule;
|
||||||
@@ -65,6 +66,7 @@ const createTestSession = async (
|
|||||||
docId: string | null;
|
docId: string | null;
|
||||||
pinned: boolean;
|
pinned: boolean;
|
||||||
promptName: string;
|
promptName: string;
|
||||||
|
promptAction: string | null;
|
||||||
}> = {}
|
}> = {}
|
||||||
) => {
|
) => {
|
||||||
const sessionData = {
|
const sessionData = {
|
||||||
@@ -74,6 +76,7 @@ const createTestSession = async (
|
|||||||
docId: null,
|
docId: null,
|
||||||
pinned: false,
|
pinned: false,
|
||||||
promptName: 'test-prompt',
|
promptName: 'test-prompt',
|
||||||
|
promptAction: null,
|
||||||
...overrides,
|
...overrides,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -98,6 +101,12 @@ test('should list and filter session type', async t => {
|
|||||||
await createTestSession(t, { sessionId: randomUUID() });
|
await createTestSession(t, { sessionId: randomUUID() });
|
||||||
await createTestSession(t, { sessionId: randomUUID(), pinned: true });
|
await createTestSession(t, { sessionId: randomUUID(), pinned: true });
|
||||||
await createTestSession(t, { sessionId: randomUUID(), docId });
|
await createTestSession(t, { sessionId: randomUUID(), docId });
|
||||||
|
await createTestSession(t, {
|
||||||
|
sessionId: randomUUID(),
|
||||||
|
docId,
|
||||||
|
promptName: 'action-prompt',
|
||||||
|
promptAction: 'action',
|
||||||
|
});
|
||||||
|
|
||||||
// should list sessions
|
// should list sessions
|
||||||
{
|
{
|
||||||
@@ -113,7 +122,13 @@ test('should list and filter session type', async t => {
|
|||||||
const docSessions = await copilotSession.list(user.id, workspace.id, docId);
|
const docSessions = await copilotSession.list(user.id, workspace.id, docId);
|
||||||
|
|
||||||
t.snapshot(
|
t.snapshot(
|
||||||
docSessions.map(s => ({ docId: s.docId, pinned: s.pinned })),
|
cleanObject(docSessions, [
|
||||||
|
'id',
|
||||||
|
'userId',
|
||||||
|
'createdAt',
|
||||||
|
'messages',
|
||||||
|
'tokenCost',
|
||||||
|
]),
|
||||||
'doc sessions should only include sessions with matching docId'
|
'doc sessions should only include sessions with matching docId'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -206,6 +221,7 @@ test('should pin and unpin sessions', async t => {
|
|||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
docId: null,
|
docId: null,
|
||||||
promptName: 'test-prompt',
|
promptName: 'test-prompt',
|
||||||
|
promptAction: null,
|
||||||
pinned: true,
|
pinned: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -220,6 +236,7 @@ test('should pin and unpin sessions', async t => {
|
|||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
docId: null,
|
docId: null,
|
||||||
promptName: 'test-prompt',
|
promptName: 'test-prompt',
|
||||||
|
promptAction: null,
|
||||||
pinned: true,
|
pinned: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -336,6 +353,15 @@ test('session updates and type conversions', async t => {
|
|||||||
await convertSession('workspace_to_pinned', { pinned: true }); // Workspace → Pinned session
|
await convertSession('workspace_to_pinned', { pinned: true }); // Workspace → Pinned session
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// not allow convert to action prompt
|
||||||
|
{
|
||||||
|
await t.throwsAsync(
|
||||||
|
copilotSession.update(user.id, sessionId, {
|
||||||
|
promptName: 'action-prompt',
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
t.snapshot(conversionSteps, 'session type conversion steps');
|
t.snapshot(conversionSteps, 'session type conversion steps');
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ type ChatSession = {
|
|||||||
// connect ids
|
// connect ids
|
||||||
userId: string;
|
userId: string;
|
||||||
promptName: string;
|
promptName: string;
|
||||||
|
promptAction: string | null;
|
||||||
parentSessionId?: string | null;
|
parentSessionId?: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -95,9 +96,9 @@ export class CopilotSessionModel extends BaseModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NOTE: just for test, remove it after copilot prompt model is ready
|
// NOTE: just for test, remove it after copilot prompt model is ready
|
||||||
async createPrompt(name: string, model: string) {
|
async createPrompt(name: string, model: string, action?: string) {
|
||||||
await this.db.aiPrompt.create({
|
await this.db.aiPrompt.create({
|
||||||
data: { name, model },
|
data: { name, model, action: action ?? null },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,6 +117,7 @@ export class CopilotSessionModel extends BaseModel {
|
|||||||
// connect
|
// connect
|
||||||
userId: state.userId,
|
userId: state.userId,
|
||||||
promptName: state.promptName,
|
promptName: state.promptName,
|
||||||
|
promptAction: state.promptAction,
|
||||||
parentSessionId: state.parentSessionId,
|
parentSessionId: state.parentSessionId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -134,7 +136,9 @@ export class CopilotSessionModel extends BaseModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional()
|
@Transactional()
|
||||||
async getChatSessionId(state: Omit<ChatSession, 'promptName'>) {
|
async getChatSessionId(
|
||||||
|
state: Omit<ChatSession, 'promptName' | 'promptAction'>
|
||||||
|
) {
|
||||||
const extraCondition: Record<string, any> = {};
|
const extraCondition: Record<string, any> = {};
|
||||||
if (state.parentSessionId) {
|
if (state.parentSessionId) {
|
||||||
// also check session id if provided session is forked session
|
// also check session id if provided session is forked session
|
||||||
@@ -284,10 +288,21 @@ export class CopilotSessionModel extends BaseModel {
|
|||||||
if (!session) {
|
if (!session) {
|
||||||
throw new CopilotSessionNotFound();
|
throw new CopilotSessionNotFound();
|
||||||
}
|
}
|
||||||
if (data.promptName && session.prompt.action) {
|
if (data.promptName) {
|
||||||
throw new CopilotSessionInvalidInput(
|
if (session.prompt.action) {
|
||||||
`Cannot update prompt for action: ${session.id}`
|
throw new CopilotSessionInvalidInput(
|
||||||
);
|
`Cannot update prompt for action: ${session.id}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const prompt = await this.db.aiPrompt.findFirst({
|
||||||
|
where: { name: data.promptName },
|
||||||
|
});
|
||||||
|
// always not allow to update to action prompt
|
||||||
|
if (!prompt || prompt.action) {
|
||||||
|
throw new CopilotSessionInvalidInput(
|
||||||
|
`Prompt ${data.promptName} not found or not available for session ${sessionId}`
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (data.pinned && data.pinned !== session.pinned) {
|
if (data.pinned && data.pinned !== session.pinned) {
|
||||||
// if pin the session, unpin exists session in the workspace
|
// if pin the session, unpin exists session in the workspace
|
||||||
|
|||||||
@@ -1880,5 +1880,14 @@ export async function refreshPrompts(db: PrismaClient) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await db.aiSession.updateMany({
|
||||||
|
where: {
|
||||||
|
promptName: prompt.name,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
promptAction: prompt.action ?? null,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ export class ChatSessionService {
|
|||||||
...state,
|
...state,
|
||||||
sessionId,
|
sessionId,
|
||||||
promptName: state.prompt.name,
|
promptName: state.prompt.name,
|
||||||
|
promptAction: state.prompt.action ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -615,7 +616,7 @@ export class ChatSessionService {
|
|||||||
|
|
||||||
await tx.aiSession.updateMany({
|
await tx.aiSession.updateMany({
|
||||||
where: { id: { in: actionIds } },
|
where: { id: { in: actionIds } },
|
||||||
data: { deletedAt: new Date() },
|
data: { pinned: false, deletedAt: new Date() },
|
||||||
});
|
});
|
||||||
|
|
||||||
return [...sessionIds, ...actionIds];
|
return [...sessionIds, ...actionIds];
|
||||||
|
|||||||
Reference in New Issue
Block a user