From 2696f33a7e8f8a439b16a178aef358da6e3736d4 Mon Sep 17 00:00:00 2001 From: Vadim Date: Thu, 1 Oct 2026 11:23:51 +0300 Subject: [PATCH] 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 --- .../migration.sql | 14 ++++++++++++++ prisma/schema.prisma | 6 +++--- .../issue/application/dto/issue-with-users.dto.ts | 4 ++-- src/modules/issue/application/dto/issue.dto.ts | 2 +- .../mappers/issue-application.mapper.ts | 7 +------ src/modules/issue/domain/entities/issue.entity.ts | 7 ++++--- .../presentation/graphql/issue-detail.type.ts | 8 ++++---- .../issue/presentation/graphql/issue.type.ts | 4 ++-- 8 files changed, 31 insertions(+), 21 deletions(-) create mode 100644 prisma/migrations/20261001081818_issue_users_set_null/migration.sql diff --git a/prisma/migrations/20261001081818_issue_users_set_null/migration.sql b/prisma/migrations/20261001081818_issue_users_set_null/migration.sql new file mode 100644 index 0000000..b657666 --- /dev/null +++ b/prisma/migrations/20261001081818_issue_users_set_null/migration.sql @@ -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; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index b958e05..3b77c1f 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -86,9 +86,9 @@ model Issue { status String priority Int @default(0) assigneeId String? - assignee User? @relation(name: "assigneeIssues", fields: [assigneeId], references: [id], onDelete: Cascade) - reporterId String - reporter User @relation(name: "reporterIssues", fields: [reporterId], references: [id], onDelete: Cascade) + 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) diff --git a/src/modules/issue/application/dto/issue-with-users.dto.ts b/src/modules/issue/application/dto/issue-with-users.dto.ts index 25d2ec8..e670b69 100644 --- a/src/modules/issue/application/dto/issue-with-users.dto.ts +++ b/src/modules/issue/application/dto/issue-with-users.dto.ts @@ -8,9 +8,9 @@ export class IssueWithUsersDto { status: string; priority: number; assigneeId: string | null; - reporterId: string; + reporterId: string | null; assignee: UserDto | null; - reporter: UserDto; + reporter: UserDto | null; projectId: string; dueDate: Date | null; createdAt: Date; diff --git a/src/modules/issue/application/dto/issue.dto.ts b/src/modules/issue/application/dto/issue.dto.ts index b032088..1319394 100644 --- a/src/modules/issue/application/dto/issue.dto.ts +++ b/src/modules/issue/application/dto/issue.dto.ts @@ -6,7 +6,7 @@ export class IssueDto { status: string; priority: number; assigneeId: string | null; - reporterId: string; + reporterId: string | null; projectId: string; dueDate: Date | null; createdAt: Date; diff --git a/src/modules/issue/application/mappers/issue-application.mapper.ts b/src/modules/issue/application/mappers/issue-application.mapper.ts index 7167756..95e951c 100644 --- a/src/modules/issue/application/mappers/issue-application.mapper.ts +++ b/src/modules/issue/application/mappers/issue-application.mapper.ts @@ -23,11 +23,6 @@ export class IssueApplicationMapper { /** Раскрывает assignee/reporter в полноценных пользователей (см. GetIssueUseCase) */ static toDtoWithUsers(issue: Issue, usersById: ReadonlyMap): IssueWithUsersDto { - const reporter = usersById.get(issue.reporter); - if (!reporter) { - throw new Error(`Reporter user ${issue.reporter} not found for issue ${issue.key}`); - } - return { id: issue.id as number, key: issue.key, @@ -38,7 +33,7 @@ export class IssueApplicationMapper { assigneeId: issue.assignee, reporterId: issue.reporter, assignee: issue.assignee ? (usersById.get(issue.assignee) ?? null) : null, - reporter, + reporter: issue.reporter ? (usersById.get(issue.reporter) ?? null) : null, projectId: issue.projectId, dueDate: issue.dueDate, createdAt: issue.createdAt, diff --git a/src/modules/issue/domain/entities/issue.entity.ts b/src/modules/issue/domain/entities/issue.entity.ts index 2fd5b82..eb7539f 100644 --- a/src/modules/issue/domain/entities/issue.entity.ts +++ b/src/modules/issue/domain/entities/issue.entity.ts @@ -12,7 +12,8 @@ interface IssueProps { status: IssueStatus; priority: IssuePriority; assignee: string | null; - reporter: string; + /** null — автор задачи удалил аккаунт */ + reporter: string | null; projectId: string; dueDate: Date | null; createdAt: Date; @@ -72,7 +73,7 @@ export class Issue { status: string; priority: number; assignee: string | null; - reporter: string; + reporter: string | null; projectId: string; dueDate: Date | null; createdAt: Date; @@ -122,7 +123,7 @@ export class Issue { return this.props.assignee; } - get reporter(): string { + get reporter(): string | null { return this.props.reporter; } diff --git a/src/modules/issue/presentation/graphql/issue-detail.type.ts b/src/modules/issue/presentation/graphql/issue-detail.type.ts index acf09e1..aaea814 100644 --- a/src/modules/issue/presentation/graphql/issue-detail.type.ts +++ b/src/modules/issue/presentation/graphql/issue-detail.type.ts @@ -24,14 +24,14 @@ export class IssueDetailType { @Field(() => String, { nullable: true }) assigneeId: string | null; - @Field() - reporterId: string; + @Field(() => String, { nullable: true }) + reporterId: string | null; @Field(() => UserType, { nullable: true }) assignee: UserType | null; - @Field(() => UserType) - reporter: UserType; + @Field(() => UserType, { nullable: true }) + reporter: UserType | null; @Field() projectId: string; diff --git a/src/modules/issue/presentation/graphql/issue.type.ts b/src/modules/issue/presentation/graphql/issue.type.ts index a2f71a1..a9fd6e2 100644 --- a/src/modules/issue/presentation/graphql/issue.type.ts +++ b/src/modules/issue/presentation/graphql/issue.type.ts @@ -23,8 +23,8 @@ export class IssueType { @Field(() => String, { nullable: true }) assigneeId: string | null; - @Field() - reporterId: string; + @Field(() => String, { nullable: true }) + reporterId: string | null; @Field() projectId: string;