46 lines
3.7 KiB
TypeScript
46 lines
3.7 KiB
TypeScript
import { desktopAssert, desktopId } from './contract.mjs'
|
|
import { assertDetailProjectIdle, detailTransactionLock } from '@/lib/detail-generator/repository'
|
|
|
|
type ProjectRef = { id: string; source: 'web' | 'desktop' }
|
|
type Database = { $transaction: (action: (tx: any) => Promise<any>) => Promise<any> }
|
|
const tableFor = (source: string) => source === 'web' ? 'detail_generator_projects' : 'desktop_workspace_projects'
|
|
|
|
/** A web project and its desktop continuation are one deletion scope. Files stay private. */
|
|
export async function deleteLinkedProject(db: Database, ownerId: string, source: string, id: string, input: unknown) {
|
|
desktopId(id)
|
|
const revision = (input as { revision?: number })?.revision
|
|
desktopAssert(['desktop', 'web'].includes(source) && Number.isSafeInteger(revision) && Number(revision) > 0, '项目来源或版本无效')
|
|
return db.$transaction(async tx => {
|
|
await detailTransactionLock(tx, 'desktop-tasks:' + ownerId)
|
|
const targetTable = tableFor(source)
|
|
const target = (await tx.$queryRawUnsafe(`SELECT id,revision,deleted_at AS deleted${source === 'desktop' ? ',source_web_id AS "sourceWebId"' : ''} FROM ${targetTable} WHERE id=$1 AND owner_id=$2`, id, ownerId))[0]
|
|
desktopAssert(target, '项目不存在或无权访问', 404, 'not_found')
|
|
const webId = source === 'web' ? id : target.sourceWebId
|
|
const refs: ProjectRef[] = [{ id, source: source as ProjectRef['source'] }]
|
|
if (webId) {
|
|
refs.push({ id: webId, source: 'web' })
|
|
const siblings = await tx.$queryRawUnsafe('SELECT id FROM desktop_workspace_projects WHERE owner_id=$1 AND source_web_id=$2', ownerId, webId)
|
|
for (const row of siblings) refs.push({ id: row.id, source: 'desktop' })
|
|
}
|
|
const unique = Array.from(new Map(refs.map(ref => [ref.source + ':' + ref.id, ref])).values()).sort((a, b) => (a.source + a.id).localeCompare(b.source + b.id))
|
|
for (const ref of unique) {
|
|
await detailTransactionLock(tx, ref.source === 'web' ? 'detail-project:' + ref.id : 'desktop:' + ref.id)
|
|
const rows = await tx.$queryRawUnsafe(`SELECT revision,deleted_at AS deleted${ref.source === 'desktop' ? ',conversation' : ''} FROM ${tableFor(ref.source)} WHERE id=$1 AND owner_id=$2 FOR UPDATE`, ref.id, ownerId)
|
|
const row = rows[0]
|
|
desktopAssert(row, '关联项目不存在或无权访问', 404, 'not_found')
|
|
if (ref.id === id && ref.source === source && !row.deleted) desktopAssert(row.revision === revision, '项目已有新版本,未删除;请刷新列表后重试', 409, 'revision_conflict')
|
|
if (row.deleted) continue
|
|
if (ref.source === 'web') {
|
|
await assertDetailProjectIdle(ref.id, ownerId, tx)
|
|
const unknown = await tx.$queryRawUnsafe("SELECT id FROM detail_generator_jobs WHERE project_id=$1 AND owner_id=$2 AND status='unknown' LIMIT 1", ref.id, ownerId)
|
|
desktopAssert(!unknown.length, '此项目有运行中或结果待核查的任务,请先处理原任务再删除', 409, 'task_busy')
|
|
} else {
|
|
const tasks = await tx.$queryRawUnsafe("SELECT id FROM desktop_creation_tasks WHERE project_id=$1 AND owner_id=$2 AND (status IN ('queued','running','received','unknown') OR lease_until>NOW()) LIMIT 1", ref.id, ownerId)
|
|
desktopAssert(!tasks.length && !row.conversation?.some((item: any) => item.pending || item.unknown), '此项目有运行中或结果待核查的任务,请先处理原任务再删除', 409, 'task_busy')
|
|
}
|
|
}
|
|
for (const ref of unique) await tx.$executeRawUnsafe(`UPDATE ${tableFor(ref.source)} SET deleted_at=NOW(),updated_at=NOW(),revision=revision+1 WHERE id=$1 AND owner_id=$2 AND deleted_at IS NULL`, ref.id, ownerId)
|
|
return { deleted: true, deletedProjects: unique }
|
|
})
|
|
}
|