first commit
This commit is contained in:
99
libs/api/tracker/domain/src/lib/comment/comment.ts
Normal file
99
libs/api/tracker/domain/src/lib/comment/comment.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
import {
|
||||
BusinessRuleError,
|
||||
ForbiddenError,
|
||||
VersionedAggregateRoot,
|
||||
assertThat,
|
||||
} from '@task-stream/api/shared/domain';
|
||||
import { CommentAddedEvent } from '../events/comment-added.event';
|
||||
import { CommentDeletedEvent } from '../events/comment-deleted.event';
|
||||
import { CommentEditedEvent } from '../events/comment-edited.event';
|
||||
|
||||
/** Data of a comment (what CommentAddedEvent carries). */
|
||||
export interface CommentProps {
|
||||
id: string;
|
||||
issueId: string;
|
||||
projectId: string;
|
||||
authorId: string;
|
||||
body: string;
|
||||
deleted: boolean;
|
||||
createdAt: string;
|
||||
editedAt: string | null;
|
||||
}
|
||||
|
||||
const MAX_BODY = 20_000;
|
||||
|
||||
/** Decisions and explanations inside a ticket. */
|
||||
export class Comment extends VersionedAggregateRoot implements CommentProps {
|
||||
public issueId!: string;
|
||||
public projectId!: string;
|
||||
public authorId!: string;
|
||||
public body!: string;
|
||||
public deleted = false;
|
||||
public createdAt!: string;
|
||||
public editedAt: string | null = null;
|
||||
|
||||
constructor(id: string) {
|
||||
super();
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
edit(actorId: string, body: string): void {
|
||||
this.assertAlive();
|
||||
if (actorId !== this.authorId) {
|
||||
throw new ForbiddenError('Only the author can edit a comment');
|
||||
}
|
||||
const next = validCommentBody(body);
|
||||
if (next === this.body) return;
|
||||
this.apply(
|
||||
new CommentEditedEvent(
|
||||
actorId,
|
||||
this.id,
|
||||
this.projectId,
|
||||
this.issueId,
|
||||
next,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/** @param canModerate whether the actor manages the project (may delete others' comments). */
|
||||
delete(actorId: string, canModerate: boolean): void {
|
||||
this.assertAlive();
|
||||
if (actorId !== this.authorId && !canModerate) {
|
||||
throw new ForbiddenError(
|
||||
'Only the author or a project admin can delete a comment',
|
||||
);
|
||||
}
|
||||
this.apply(
|
||||
new CommentDeletedEvent(actorId, this.id, this.projectId, this.issueId),
|
||||
);
|
||||
}
|
||||
|
||||
/* ---- State transitions (also replayed by loadFromHistory) ---- */
|
||||
|
||||
onCommentAddedEvent({ comment }: CommentAddedEvent): void {
|
||||
Object.assign(this, comment);
|
||||
}
|
||||
|
||||
onCommentEditedEvent(event: CommentEditedEvent): void {
|
||||
this.body = event.body;
|
||||
this.editedAt = event.occurredAt;
|
||||
}
|
||||
|
||||
onCommentDeletedEvent(): void {
|
||||
this.deleted = true;
|
||||
}
|
||||
|
||||
private assertAlive(): void {
|
||||
if (this.deleted) throw new BusinessRuleError('Comment was deleted');
|
||||
}
|
||||
}
|
||||
|
||||
export function validCommentBody(body: string): string {
|
||||
const trimmed = body.trim();
|
||||
assertThat(trimmed.length > 0, 'Comment cannot be empty');
|
||||
assertThat(
|
||||
trimmed.length <= MAX_BODY,
|
||||
`Comment cannot exceed ${MAX_BODY} characters`,
|
||||
);
|
||||
return trimmed;
|
||||
}
|
||||
Reference in New Issue
Block a user