metis-desk/modules/companies/src/api/domain/software-installation.aggregate.ts
David 3ceac47cd6 feat: implement software installation management
- Add SoftwareInstallationValues value object for encapsulating software installation data.
- Create HTTP routes for managing software installations, including GET, POST, and DELETE operations.
- Implement Sequelize repository for software installations, handling database interactions.
- Add migration script for creating necessary database tables for software installations.
- Create persistence mapper for transforming between domain models and database rows.
- Define snapshot schema for software installations to ensure data consistency.
- Introduce common schemas for software installations to validate API requests and responses.
- Develop UI components for displaying and managing software installations, including history dialogs and forms.
- Implement API client for interacting with software installation endpoints.
- Write tests for software installations panel and form components to ensure functionality.
2026-10-12 20:31:21 +02:00

103 lines
2.8 KiB
TypeScript

import type { UniqueID } from "@repo/rdx-ddd";
import { AggregateRoot } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { z } from "zod/v4";
import { SoftwareInstallationRevision } from "./software-installation-revision.entity";
import {
SOFTWARE_STATE_TRANSITIONS,
SoftwareInstallationError,
} from "./software-installation-values";
import type { SoftwareInstallationValues } from "./value-objects/software-installation-values.vo";
type Props = Readonly<{
companyId: UniqueID;
values: SoftwareInstallationValues;
history: readonly SoftwareInstallationRevision[];
}>;
export class SoftwareInstallation extends AggregateRoot<Props> {
private constructor(props: Props, id?: UniqueID) {
super(Object.freeze({ ...props, history: Object.freeze([...props.history]) }), id);
}
static create(
companyId: UniqueID,
values: SoftwareInstallationValues,
actorId: UniqueID,
recordedAt: string
): Result<SoftwareInstallation, SoftwareInstallationError> {
if (
!(
z.iso.datetime().safeParse(recordedAt).success &&
["pending", "configured"].includes(values.state)
)
) {
return Result.fail(new SoftwareInstallationError("INVALID_SOFTWARE_TRANSITION"));
}
return Result.ok(
new SoftwareInstallation({
companyId,
values,
history: [SoftwareInstallationRevision.create({ values, actorId, recordedAt })],
})
);
}
static rehydrate(props: Props, id: UniqueID) {
return new SoftwareInstallation(props, id);
}
get companyId() {
return this.props.companyId;
}
get values() {
return this.props.values;
}
get history() {
return this.props.history;
}
update(
values: SoftwareInstallationValues,
actorId: UniqueID,
recordedAt: string
): Result<SoftwareInstallation, SoftwareInstallationError> {
if (this.values.equals(values)) {
return Result.ok(this);
}
if (!this.values.sameTarget(values)) {
return Result.fail(new SoftwareInstallationError("SOFTWARE_TARGET_IMMUTABLE"));
}
if (
!SOFTWARE_STATE_TRANSITIONS[this.values.state].some((state) => state === values.state) ||
(this.values.closed && !this.values.equals(values))
) {
return Result.fail(new SoftwareInstallationError("INVALID_SOFTWARE_TRANSITION"));
}
if (
!z.iso.datetime().safeParse(recordedAt).success ||
recordedAt < (this.history.at(-1)?.recordedAt ?? "")
) {
return Result.fail(new SoftwareInstallationError("INVALID_REQUEST"));
}
return Result.ok(
new SoftwareInstallation(
{
...this.props,
values,
history: [
...this.history,
SoftwareInstallationRevision.create({ values, actorId, recordedAt }),
],
},
this.id
)
);
}
}