first commit
This commit is contained in:
133
libs/api/tracker/domain/src/lib/release/release.ts
Normal file
133
libs/api/tracker/domain/src/lib/release/release.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import {
|
||||
BusinessRuleError,
|
||||
VersionedAggregateRoot,
|
||||
assertThat,
|
||||
} from '@task-stream/api/shared/domain';
|
||||
import { ReleaseArchivedEvent } from '../events/release-archived.event';
|
||||
import { ReleaseCreatedEvent } from '../events/release-created.event';
|
||||
import { ReleaseShippedEvent } from '../events/release-shipped.event';
|
||||
import { ReleaseUpdatedEvent } from '../events/release-updated.event';
|
||||
import { ReleaseStatus } from './release-status';
|
||||
|
||||
/** Data of a release (what ReleaseCreatedEvent carries). */
|
||||
export interface ReleaseProps {
|
||||
id: string;
|
||||
projectId: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
targetDate: string | null;
|
||||
status: ReleaseStatus;
|
||||
releasedAt: string | null;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
/** A version of the product: controls which tickets ship together. */
|
||||
export class Release extends VersionedAggregateRoot implements ReleaseProps {
|
||||
public projectId!: string;
|
||||
public name!: string;
|
||||
public description: string | null = null;
|
||||
public targetDate: string | null = null;
|
||||
public status: ReleaseStatus = ReleaseStatus.PLANNED;
|
||||
public releasedAt: string | null = null;
|
||||
public createdAt!: string;
|
||||
|
||||
constructor(id: string) {
|
||||
super();
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
/** Only planned releases can receive or lose tickets. */
|
||||
assertAcceptsIssues(): void {
|
||||
if (this.status !== ReleaseStatus.PLANNED) {
|
||||
throw new BusinessRuleError(
|
||||
`Release ${this.name} is ${this.status} and is locked`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
update(
|
||||
actorId: string,
|
||||
changes: {
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
targetDate?: string | null;
|
||||
},
|
||||
): void {
|
||||
this.assertAcceptsIssues();
|
||||
const patch: {
|
||||
name?: string;
|
||||
description?: string | null;
|
||||
targetDate?: string | null;
|
||||
} = {};
|
||||
if (changes.name !== undefined) {
|
||||
const name = changes.name.trim();
|
||||
assertThat(name.length > 0, 'Release name is required');
|
||||
if (name !== this.name) patch.name = name;
|
||||
}
|
||||
if (changes.description !== undefined) {
|
||||
const description = changes.description?.trim() || null;
|
||||
if (description !== this.description) patch.description = description;
|
||||
}
|
||||
if (
|
||||
changes.targetDate !== undefined &&
|
||||
changes.targetDate !== this.targetDate
|
||||
) {
|
||||
if (changes.targetDate) {
|
||||
assertThat(
|
||||
!Number.isNaN(Date.parse(changes.targetDate)),
|
||||
'targetDate must be an ISO date',
|
||||
);
|
||||
}
|
||||
patch.targetDate = changes.targetDate;
|
||||
}
|
||||
if (Object.keys(patch).length === 0) return;
|
||||
this.apply(
|
||||
new ReleaseUpdatedEvent(actorId, this.id, this.projectId, patch),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ships the release. Unfinished tickets block it unless forced, so nothing
|
||||
* goes out unnoticed. (Not called `publish`: that name belongs to AggregateRoot.)
|
||||
*/
|
||||
ship(actorId: string, unfinishedIssues: number, force = false): void {
|
||||
this.assertAcceptsIssues();
|
||||
if (unfinishedIssues > 0 && !force) {
|
||||
throw new BusinessRuleError(
|
||||
`Release ${this.name} still has ${unfinishedIssues} unfinished ticket(s); move them out or publish with force`,
|
||||
);
|
||||
}
|
||||
this.apply(
|
||||
new ReleaseShippedEvent(
|
||||
actorId,
|
||||
this.id,
|
||||
this.projectId,
|
||||
unfinishedIssues,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
archive(actorId: string): void {
|
||||
if (this.status === ReleaseStatus.ARCHIVED) return;
|
||||
this.apply(new ReleaseArchivedEvent(actorId, this.id, this.projectId));
|
||||
}
|
||||
|
||||
/* ---- State transitions (also replayed by loadFromHistory) ---- */
|
||||
|
||||
onReleaseCreatedEvent({ release }: ReleaseCreatedEvent): void {
|
||||
Object.assign(this, release);
|
||||
}
|
||||
|
||||
onReleaseUpdatedEvent(event: ReleaseUpdatedEvent): void {
|
||||
Object.assign(this, event.changes);
|
||||
}
|
||||
|
||||
onReleaseShippedEvent(event: ReleaseShippedEvent): void {
|
||||
this.status = ReleaseStatus.RELEASED;
|
||||
this.releasedAt = event.occurredAt;
|
||||
}
|
||||
|
||||
onReleaseArchivedEvent(): void {
|
||||
this.status = ReleaseStatus.ARCHIVED;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user