Add issue attachments

- IssueAttachment link table (issue <-> file) + migration
- GET/POST /issues/:issueId/attachments, DELETE .../attachments/:fileId
- Only own uploaded files can be attached; attach is idempotent
- Registers issues as a file owner: attached files stay visible to issue
  participants and are not collected by GC
- IssueDetail.attachments GraphQL field via @ResolveField, issue module untouched

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vadimandClaude Opus 5.5 committed 2026-10-02 09:18:02 +03:00
1 parent f32c6e348d
commit 423afa658d
28 files changed
+828 -16

No files matched your search

@@ -0,0 +1,18 @@
-- CreateTable
CREATE TABLE "issue_attachment" (
"issueId" INTEGER NOT NULL,
"fileId" TEXT NOT NULL,
"createdAt" TIMESTAMPTZ(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "issue_attachment_pkey" PRIMARY KEY ("issueId","fileId")
);
-- CreateIndex
CREATE INDEX "issue_attachment_fileId_idx" ON "issue_attachment"("fileId");
-- AddForeignKey
ALTER TABLE "issue_attachment" ADD CONSTRAINT "issue_attachment_issueId_fkey" FOREIGN KEY ("issueId") REFERENCES "issue"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "issue_attachment" ADD CONSTRAINT "issue_attachment_fileId_fkey" FOREIGN KEY ("fileId") REFERENCES "file"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+33 -16
View File
@@ -81,22 +81,23 @@ model Verification {
}
model Issue {
id Int @id @default(autoincrement())
key String
title String
content String?
status String
priority Int @default(0)
assigneeId String?
assignee User? @relation(name: "assigneeIssues", fields: [assigneeId], references: [id], onDelete: SetNull)
reporterId String?
reporter User? @relation(name: "reporterIssues", fields: [reporterId], references: [id], onDelete: SetNull)
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
dueDate DateTime? @db.Timestamptz(3)
comments Comment[]
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
id Int @id @default(autoincrement())
key String
title String
content String?
status String
priority Int @default(0)
assigneeId String?
assignee User? @relation(name: "assigneeIssues", fields: [assigneeId], references: [id], onDelete: SetNull)
reporterId String?
reporter User? @relation(name: "reporterIssues", fields: [reporterId], references: [id], onDelete: SetNull)
projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
dueDate DateTime? @db.Timestamptz(3)
comments Comment[]
attachments IssueAttachment[]
createdAt DateTime @default(now()) @db.Timestamptz(3)
updatedAt DateTime @updatedAt @db.Timestamptz(3)
@@index([status])
@@index([assigneeId])
@@ -225,8 +226,24 @@ model File {
uploader User? @relation(fields: [uploaderId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now()) @db.Timestamptz(3)
issueAttachments IssueAttachment[]
@@unique([storageKey])
@@index([uploaderId])
@@index([createdAt])
@@map("file")
}
/// Вложение задачи — связь задачи с файлом. Удаление задачи или файла
/// удаляет связь каскадом; сам файл, оставшийся без связей, удалит GC.
model IssueAttachment {
issueId Int
issue Issue @relation(fields: [issueId], references: [id], onDelete: Cascade)
fileId String
file File @relation(fields: [fileId], references: [id], onDelete: Cascade)
createdAt DateTime @default(now()) @db.Timestamptz(3)
@@id([issueId, fileId])
@@index([fileId])
@@map("issue_attachment")
}