mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 04:48:53 +00:00
fix(server): session unique index conflict (#12865)
This commit is contained in:
@@ -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;
|
||||
|
||||
-- AlterTable
|
||||
CREATE UNIQUE INDEX idx_ai_session_unique_pinned
|
||||
ON ai_sessions_metadata (user_id, workspace_id)
|
||||
WHERE pinned = true AND deleted_at IS NULL;
|
||||
ALTER TABLE "ai_sessions_metadata" ADD COLUMN "prompt_action" VARCHAR(32) DEFAULT '';
|
||||
|
||||
-- AlterTable
|
||||
CREATE UNIQUE INDEX idx_ai_session_unique_doc_root
|
||||
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_pinned_idx" ON "ai_sessions_metadata" (user_id, workspace_id) WHERE pinned = true 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
|
||||
DROP INDEX "ai_sessions_metadata_user_id_workspace_id_idx";
|
||||
|
||||
@@ -436,6 +436,7 @@ model AiSession {
|
||||
workspaceId String @map("workspace_id") @db.VarChar
|
||||
docId String? @map("doc_id") @db.VarChar
|
||||
promptName String @map("prompt_name") @db.VarChar(32)
|
||||
promptAction String? @default("") @map("prompt_action") @db.VarChar(32)
|
||||
pinned Boolean @default(false)
|
||||
// the session id of the parent session if this session is a forked session
|
||||
parentSessionId String? @map("parent_session_id") @db.VarChar
|
||||
@@ -449,6 +450,12 @@ model AiSession {
|
||||
messages AiSessionMessage[]
|
||||
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, workspaceId, docId])
|
||||
@@map("ai_sessions_metadata")
|
||||
|
||||
@@ -25,6 +25,12 @@ Generated by [AVA](https://avajs.dev).
|
||||
{
|
||||
docId: 'doc-id-1',
|
||||
pinned: false,
|
||||
promptName: 'action-prompt',
|
||||
},
|
||||
{
|
||||
docId: 'doc-id-1',
|
||||
pinned: false,
|
||||
promptName: 'test-prompt',
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
Binary file not shown.
@@ -59,6 +59,7 @@ test.beforeEach(async t => {
|
||||
docId,
|
||||
userId: user.id,
|
||||
promptName: 'prompt-name',
|
||||
promptAction: null,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
WorkspaceModel,
|
||||
} from '../../models';
|
||||
import { createTestingModule, type TestingModule } from '../utils';
|
||||
import { cleanObject } from '../utils/copilot';
|
||||
|
||||
interface Context {
|
||||
module: TestingModule;
|
||||
@@ -65,6 +66,7 @@ const createTestSession = async (
|
||||
docId: string | null;
|
||||
pinned: boolean;
|
||||
promptName: string;
|
||||
promptAction: string | null;
|
||||
}> = {}
|
||||
) => {
|
||||
const sessionData = {
|
||||
@@ -74,6 +76,7 @@ const createTestSession = async (
|
||||
docId: null,
|
||||
pinned: false,
|
||||
promptName: 'test-prompt',
|
||||
promptAction: null,
|
||||
...overrides,
|
||||
};
|
||||
|
||||
@@ -98,6 +101,12 @@ test('should list and filter session type', async t => {
|
||||
await createTestSession(t, { sessionId: randomUUID() });
|
||||
await createTestSession(t, { sessionId: randomUUID(), pinned: true });
|
||||
await createTestSession(t, { sessionId: randomUUID(), docId });
|
||||
await createTestSession(t, {
|
||||
sessionId: randomUUID(),
|
||||
docId,
|
||||
promptName: 'action-prompt',
|
||||
promptAction: 'action',
|
||||
});
|
||||
|
||||
// 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);
|
||||
|
||||
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'
|
||||
);
|
||||
}
|
||||
@@ -206,6 +221,7 @@ test('should pin and unpin sessions', async t => {
|
||||
workspaceId: workspace.id,
|
||||
docId: null,
|
||||
promptName: 'test-prompt',
|
||||
promptAction: null,
|
||||
pinned: true,
|
||||
});
|
||||
|
||||
@@ -220,6 +236,7 @@ test('should pin and unpin sessions', async t => {
|
||||
workspaceId: workspace.id,
|
||||
docId: null,
|
||||
promptName: 'test-prompt',
|
||||
promptAction: null,
|
||||
pinned: true,
|
||||
});
|
||||
|
||||
@@ -336,6 +353,15 @@ test('session updates and type conversions', async t => {
|
||||
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');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -47,6 +47,7 @@ type ChatSession = {
|
||||
// connect ids
|
||||
userId: string;
|
||||
promptName: string;
|
||||
promptAction: 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
|
||||
async createPrompt(name: string, model: string) {
|
||||
async createPrompt(name: string, model: string, action?: string) {
|
||||
await this.db.aiPrompt.create({
|
||||
data: { name, model },
|
||||
data: { name, model, action: action ?? null },
|
||||
});
|
||||
}
|
||||
|
||||
@@ -116,6 +117,7 @@ export class CopilotSessionModel extends BaseModel {
|
||||
// connect
|
||||
userId: state.userId,
|
||||
promptName: state.promptName,
|
||||
promptAction: state.promptAction,
|
||||
parentSessionId: state.parentSessionId,
|
||||
},
|
||||
});
|
||||
@@ -134,7 +136,9 @@ export class CopilotSessionModel extends BaseModel {
|
||||
}
|
||||
|
||||
@Transactional()
|
||||
async getChatSessionId(state: Omit<ChatSession, 'promptName'>) {
|
||||
async getChatSessionId(
|
||||
state: Omit<ChatSession, 'promptName' | 'promptAction'>
|
||||
) {
|
||||
const extraCondition: Record<string, any> = {};
|
||||
if (state.parentSessionId) {
|
||||
// also check session id if provided session is forked session
|
||||
@@ -284,10 +288,21 @@ export class CopilotSessionModel extends BaseModel {
|
||||
if (!session) {
|
||||
throw new CopilotSessionNotFound();
|
||||
}
|
||||
if (data.promptName && session.prompt.action) {
|
||||
throw new CopilotSessionInvalidInput(
|
||||
`Cannot update prompt for action: ${session.id}`
|
||||
);
|
||||
if (data.promptName) {
|
||||
if (session.prompt.action) {
|
||||
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 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,
|
||||
sessionId,
|
||||
promptName: state.prompt.name,
|
||||
promptAction: state.prompt.action ?? null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -615,7 +616,7 @@ export class ChatSessionService {
|
||||
|
||||
await tx.aiSession.updateMany({
|
||||
where: { id: { in: actionIds } },
|
||||
data: { deletedAt: new Date() },
|
||||
data: { pinned: false, deletedAt: new Date() },
|
||||
});
|
||||
|
||||
return [...sessionIds, ...actionIds];
|
||||
|
||||
Reference in New Issue
Block a user