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>
196 lines
5.0 KiB
TypeScript
196 lines
5.0 KiB
TypeScript
import { IssueTitle } from '../value-objects/issue-title.vo.js';
|
|
import { IssueStatus } from '../value-objects/issue-status.vo.js';
|
|
import { IssuePriority } from '../value-objects/issue-priority.vo.js';
|
|
import { IssueCreatedEvent } from '../events/issue-created.event.js';
|
|
import { IssueUpdatedEvent } from '../events/issue-updated.event.js';
|
|
|
|
interface IssueProps {
|
|
id: number | null;
|
|
key: string;
|
|
title: IssueTitle;
|
|
content: string | null;
|
|
status: IssueStatus;
|
|
priority: IssuePriority;
|
|
assignee: string | null;
|
|
/** null — автор задачи удалил аккаунт */
|
|
reporter: string | null;
|
|
projectId: string;
|
|
dueDate: Date | null;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
}
|
|
|
|
/**
|
|
* Доменная сущность. Никогда не импортирует ничего из infrastructure/
|
|
* presentation — только чистая бизнес-логика и инварианты.
|
|
*
|
|
* `id` — Int, генерируется базой данных (SERIAL), поэтому у новой задачи
|
|
* он равен null до сохранения в репозитории. `key` (например, "ISSUE-1")
|
|
* присваивается на уровне приложения при создании и не зависит от id.
|
|
*/
|
|
export class Issue {
|
|
private domainEvents: unknown[] = [];
|
|
|
|
private constructor(private readonly props: IssueProps) {}
|
|
|
|
/** Фабричный метод для создания НОВОЙ задачи (порождает доменное событие) */
|
|
static create(params: {
|
|
key: string;
|
|
title: string;
|
|
content?: string | null;
|
|
priority?: number;
|
|
assignee?: string | null;
|
|
reporter: string;
|
|
projectId: string;
|
|
dueDate?: Date | null;
|
|
}): Issue {
|
|
const issue = new Issue({
|
|
id: null,
|
|
key: params.key,
|
|
title: IssueTitle.create(params.title),
|
|
content: params.content ?? null,
|
|
status: IssueStatus.open(),
|
|
priority: params.priority !== undefined ? IssuePriority.create(params.priority) : IssuePriority.default(),
|
|
assignee: params.assignee ?? null,
|
|
reporter: params.reporter,
|
|
projectId: params.projectId,
|
|
dueDate: params.dueDate ?? null,
|
|
createdAt: new Date(),
|
|
updatedAt: new Date(),
|
|
});
|
|
|
|
issue.addDomainEvent(new IssueCreatedEvent(issue.key));
|
|
|
|
return issue;
|
|
}
|
|
|
|
/** Восстановление сущности из персистентного слоя (без событий) */
|
|
static reconstitute(props: {
|
|
id: number;
|
|
key: string;
|
|
title: string;
|
|
content: string | null;
|
|
status: string;
|
|
priority: number;
|
|
assignee: string | null;
|
|
reporter: string | null;
|
|
projectId: string;
|
|
dueDate: Date | null;
|
|
createdAt: Date;
|
|
updatedAt: Date;
|
|
}): Issue {
|
|
return new Issue({
|
|
id: props.id,
|
|
key: props.key,
|
|
title: IssueTitle.create(props.title),
|
|
content: props.content,
|
|
status: IssueStatus.create(props.status),
|
|
priority: IssuePriority.create(props.priority),
|
|
assignee: props.assignee,
|
|
reporter: props.reporter,
|
|
projectId: props.projectId,
|
|
dueDate: props.dueDate,
|
|
createdAt: props.createdAt,
|
|
updatedAt: props.updatedAt,
|
|
});
|
|
}
|
|
|
|
get id(): number | null {
|
|
return this.props.id;
|
|
}
|
|
|
|
get key(): string {
|
|
return this.props.key;
|
|
}
|
|
|
|
get title(): string {
|
|
return this.props.title.value;
|
|
}
|
|
|
|
get content(): string | null {
|
|
return this.props.content;
|
|
}
|
|
|
|
get status(): string {
|
|
return this.props.status.value;
|
|
}
|
|
|
|
get priority(): number {
|
|
return this.props.priority.value;
|
|
}
|
|
|
|
get assignee(): string | null {
|
|
return this.props.assignee;
|
|
}
|
|
|
|
get reporter(): string | null {
|
|
return this.props.reporter;
|
|
}
|
|
|
|
get projectId(): string {
|
|
return this.props.projectId;
|
|
}
|
|
|
|
get dueDate(): Date | null {
|
|
return this.props.dueDate;
|
|
}
|
|
|
|
get createdAt(): Date {
|
|
return this.props.createdAt;
|
|
}
|
|
|
|
get updatedAt(): Date {
|
|
return this.props.updatedAt;
|
|
}
|
|
|
|
updateTitle(title: string): void {
|
|
this.props.title = IssueTitle.create(title);
|
|
this.touch();
|
|
}
|
|
|
|
updateContent(content: string | null): void {
|
|
this.props.content = content;
|
|
this.touch();
|
|
}
|
|
|
|
changeStatus(status: string): void {
|
|
this.props.status = IssueStatus.create(status);
|
|
this.touch();
|
|
}
|
|
|
|
changePriority(priority: number): void {
|
|
this.props.priority = IssuePriority.create(priority);
|
|
this.touch();
|
|
}
|
|
|
|
assignTo(userId: string): void {
|
|
this.props.assignee = userId;
|
|
this.touch();
|
|
}
|
|
|
|
unassign(): void {
|
|
this.props.assignee = null;
|
|
this.touch();
|
|
}
|
|
|
|
updateDueDate(dueDate: Date | null): void {
|
|
this.props.dueDate = dueDate;
|
|
this.touch();
|
|
}
|
|
|
|
private touch(): void {
|
|
this.props.updatedAt = new Date();
|
|
this.addDomainEvent(new IssueUpdatedEvent(this.key));
|
|
}
|
|
|
|
private addDomainEvent(event: unknown): void {
|
|
this.domainEvents.push(event);
|
|
}
|
|
|
|
pullDomainEvents(): unknown[] {
|
|
const events = [...this.domainEvents];
|
|
this.domainEvents = [];
|
|
return events;
|
|
}
|
|
}
|