first commit

This commit is contained in:
2026-10-03 13:40:26 +02:00
parent 732a5b5a16
commit 566f415fa7
555 changed files with 62704 additions and 0 deletions

View 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;
}