Keep issues when assignee or reporter is deleted

Switch issue.assigneeId/reporterId FKs from CASCADE to SET NULL and make
reporter nullable through the entity, DTOs and GraphQL types.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
vadimandClaude Opus 5.5 committed 2026-10-01 11:23:51 +03:00
1 parent 4b8c855855
commit 2696f33a7e
8 files changed
+31 -21

No files matched your search

@@ -0,0 +1,14 @@
-- DropForeignKey
ALTER TABLE "issue" DROP CONSTRAINT "issue_assigneeId_fkey";
-- DropForeignKey
ALTER TABLE "issue" DROP CONSTRAINT "issue_reporterId_fkey";
-- AlterTable
ALTER TABLE "issue" ALTER COLUMN "reporterId" DROP NOT NULL;
-- AddForeignKey
ALTER TABLE "issue" ADD CONSTRAINT "issue_assigneeId_fkey" FOREIGN KEY ("assigneeId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "issue" ADD CONSTRAINT "issue_reporterId_fkey" FOREIGN KEY ("reporterId") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+3 -3
View File
@@ -86,9 +86,9 @@ model Issue {
status String status String
priority Int @default(0) priority Int @default(0)
assigneeId String? assigneeId String?
assignee User? @relation(name: "assigneeIssues", fields: [assigneeId], references: [id], onDelete: Cascade) assignee User? @relation(name: "assigneeIssues", fields: [assigneeId], references: [id], onDelete: SetNull)
reporterId String reporterId String?
reporter User @relation(name: "reporterIssues", fields: [reporterId], references: [id], onDelete: Cascade) reporter User? @relation(name: "reporterIssues", fields: [reporterId], references: [id], onDelete: SetNull)
projectId String projectId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) project Project @relation(fields: [projectId], references: [id], onDelete: Cascade)
dueDate DateTime? @db.Timestamptz(3) dueDate DateTime? @db.Timestamptz(3)
@@ -8,9 +8,9 @@ export class IssueWithUsersDto {
status: string; status: string;
priority: number; priority: number;
assigneeId: string | null; assigneeId: string | null;
reporterId: string; reporterId: string | null;
assignee: UserDto | null; assignee: UserDto | null;
reporter: UserDto; reporter: UserDto | null;
projectId: string; projectId: string;
dueDate: Date | null; dueDate: Date | null;
createdAt: Date; createdAt: Date;
@@ -6,7 +6,7 @@ export class IssueDto {
status: string; status: string;
priority: number; priority: number;
assigneeId: string | null; assigneeId: string | null;
reporterId: string; reporterId: string | null;
projectId: string; projectId: string;
dueDate: Date | null; dueDate: Date | null;
createdAt: Date; createdAt: Date;
@@ -23,11 +23,6 @@ export class IssueApplicationMapper {
/** Раскрывает assignee/reporter в полноценных пользователей (см. GetIssueUseCase) */ /** Раскрывает assignee/reporter в полноценных пользователей (см. GetIssueUseCase) */
static toDtoWithUsers(issue: Issue, usersById: ReadonlyMap<string, UserDto>): IssueWithUsersDto { static toDtoWithUsers(issue: Issue, usersById: ReadonlyMap<string, UserDto>): IssueWithUsersDto {
const reporter = usersById.get(issue.reporter);
if (!reporter) {
throw new Error(`Reporter user ${issue.reporter} not found for issue ${issue.key}`);
}
return { return {
id: issue.id as number, id: issue.id as number,
key: issue.key, key: issue.key,
@@ -38,7 +33,7 @@ export class IssueApplicationMapper {
assigneeId: issue.assignee, assigneeId: issue.assignee,
reporterId: issue.reporter, reporterId: issue.reporter,
assignee: issue.assignee ? (usersById.get(issue.assignee) ?? null) : null, assignee: issue.assignee ? (usersById.get(issue.assignee) ?? null) : null,
reporter, reporter: issue.reporter ? (usersById.get(issue.reporter) ?? null) : null,
projectId: issue.projectId, projectId: issue.projectId,
dueDate: issue.dueDate, dueDate: issue.dueDate,
createdAt: issue.createdAt, createdAt: issue.createdAt,
@@ -12,7 +12,8 @@ interface IssueProps {
status: IssueStatus; status: IssueStatus;
priority: IssuePriority; priority: IssuePriority;
assignee: string | null; assignee: string | null;
reporter: string; /** null — автор задачи удалил аккаунт */
reporter: string | null;
projectId: string; projectId: string;
dueDate: Date | null; dueDate: Date | null;
createdAt: Date; createdAt: Date;
@@ -72,7 +73,7 @@ export class Issue {
status: string; status: string;
priority: number; priority: number;
assignee: string | null; assignee: string | null;
reporter: string; reporter: string | null;
projectId: string; projectId: string;
dueDate: Date | null; dueDate: Date | null;
createdAt: Date; createdAt: Date;
@@ -122,7 +123,7 @@ export class Issue {
return this.props.assignee; return this.props.assignee;
} }
get reporter(): string { get reporter(): string | null {
return this.props.reporter; return this.props.reporter;
} }
@@ -24,14 +24,14 @@ export class IssueDetailType {
@Field(() => String, { nullable: true }) @Field(() => String, { nullable: true })
assigneeId: string | null; assigneeId: string | null;
@Field() @Field(() => String, { nullable: true })
reporterId: string; reporterId: string | null;
@Field(() => UserType, { nullable: true }) @Field(() => UserType, { nullable: true })
assignee: UserType | null; assignee: UserType | null;
@Field(() => UserType) @Field(() => UserType, { nullable: true })
reporter: UserType; reporter: UserType | null;
@Field() @Field()
projectId: string; projectId: string;
@@ -23,8 +23,8 @@ export class IssueType {
@Field(() => String, { nullable: true }) @Field(() => String, { nullable: true })
assigneeId: string | null; assigneeId: string | null;
@Field() @Field(() => String, { nullable: true })
reporterId: string; reporterId: string | null;
@Field() @Field()
projectId: string; projectId: string;