metis-desk/modules/resources/src/api/domain/resource.aggregate.ts
2026-10-25 01:19:17 +02:00

263 lines
7.2 KiB
TypeScript

import { AggregateRoot, UniqueID } from "@repo/rdx-ddd";
import { Result } from "@repo/rdx-utils";
import { z } from "zod/v4";
import { ResourceCatalogError } from "./resource-definition";
import { ResourceRevision } from "./resource-revision.entity";
import { ResourceOwnershipSchema } from "./resource-scope";
import type { ResourceSpecification } from "./value-objects/resource-values.vo";
import { ResourceValues } from "./value-objects/resource-values.vo";
type ResourceProps = Readonly<
({ scope: "company"; companyId: UniqueID } | { scope: "global"; companyId: null }) & {
specification: ResourceSpecification;
values: ResourceValues;
history: readonly ResourceRevision[];
}
>;
export class Resource extends AggregateRoot<ResourceProps> {
private constructor(props: ResourceProps, id?: UniqueID) {
super({ ...props, history: Object.freeze([...props.history]) }, id);
const ownership = ResourceOwnershipSchema.safeParse({
scope: props.scope,
companyId:
props.companyId instanceof UniqueID
? props.companyId.toPrimitive()
: props.companyId === null
? null
: undefined,
});
if (!ownership.success) {
throw new ResourceCatalogError("INVALID_RESOURCE_SCOPE");
}
}
static create(
companyId: UniqueID,
specification: ResourceSpecification,
values: ResourceValues,
actorId: UniqueID,
recordedAt: string
): Result<Resource, ResourceCatalogError> {
if (!(companyId instanceof UniqueID)) {
return Result.fail(new ResourceCatalogError("INVALID_RESOURCE_SCOPE"));
}
return Result.ok<Resource>(
new Resource({
scope: "company",
companyId,
specification,
values,
history: [ResourceRevision.create({ values, specification, actorId, recordedAt })],
})
);
}
static rehydrate(props: ResourceProps, id: UniqueID) {
return new Resource(props, id);
}
static createGlobal(
id: UniqueID,
specification: ResourceSpecification,
values: ResourceValues,
actorId: UniqueID,
recordedAt: string
): Result<Resource, ResourceCatalogError> {
if (
!z.uuidv7().safeParse(id.toPrimitive()).success ||
values.toPrimitive().integration ||
values.toPrimitive().templateId !== specification.toPrimitive().templateId
) {
return Result.fail(new ResourceCatalogError("RESOURCE_GLOBAL_TEMPLATE_NOT_ALLOWED"));
}
return Result.ok(
new Resource(
{
scope: "global",
companyId: null,
specification,
values,
history: [ResourceRevision.create({ values, specification, actorId, recordedAt })],
},
id
)
);
}
get scope() {
return this.props.scope;
}
belongsToCompany(companyId: UniqueID) {
return this.props.scope === "company" && this.props.companyId.equals(companyId);
}
requireCompanyId(): UniqueID {
if (this.props.scope !== "company") {
throw new ResourceCatalogError("RESOURCE_SCOPE_UNSUPPORTED");
}
return this.props.companyId;
}
get companyId() {
return this.props.companyId;
}
get specification() {
return this.props.specification;
}
get values() {
return this.props.values;
}
get history() {
return this.props.history;
}
synchronizeSpecification(
specification: ResourceSpecification,
actorId: UniqueID,
recordedAt: string
) {
if (this.scope !== "company") {
return Result.fail(new ResourceCatalogError("RESOURCE_SCOPE_UNSUPPORTED"));
}
return this.synchronizeValues(specification, actorId, recordedAt);
}
synchronizeGlobalSpecification(
specification: ResourceSpecification,
actorId: UniqueID,
recordedAt: string
) {
if (
this.scope !== "global" ||
this.values.toPrimitive().integration ||
specification.templateId.toPrimitive() !== this.values.toPrimitive().templateId
) {
return Result.fail(new ResourceCatalogError("RESOURCE_SCOPE_UNSUPPORTED"));
}
return this.synchronizeValues(specification, actorId, recordedAt);
}
private synchronizeValues(
specification: ResourceSpecification,
actorId: UniqueID,
recordedAt: string
) {
const allowed = new Set(specification.toPrimitive().fields.map((field) => field.fieldId));
const values = ResourceValues.reconstruct(
{
...this.values.toPrimitive(),
characteristics: Object.fromEntries(
Object.entries(this.values.toPrimitive().characteristics).filter(([id]) =>
allowed.has(id)
)
),
},
specification
);
if (!values.isSuccess) {
return values;
}
if (this.specification.equals(specification) && this.values.equals(values.data)) {
return Result.ok(this);
}
return Result.ok(
new Resource(
{
...this.props,
specification,
values: values.data,
history: [
...this.history,
ResourceRevision.create({ specification, values: values.data, actorId, recordedAt }),
],
},
this.id
)
);
}
update(
values: ResourceValues,
actorId: UniqueID,
recordedAt: string
): Result<Resource, ResourceCatalogError> {
if (this.scope !== "company") {
return Result.fail(new ResourceCatalogError("RESOURCE_SCOPE_UNSUPPORTED"));
}
return this.updateValues(values, actorId, recordedAt);
}
updateGlobal(values: ResourceValues, actorId: UniqueID, recordedAt: string) {
if (this.scope !== "global" || values.toPrimitive().integration) {
return Result.fail(new ResourceCatalogError("RESOURCE_SCOPE_UNSUPPORTED"));
}
return this.updateValues(values, actorId, recordedAt);
}
private updateValues(
values: ResourceValues,
actorId: UniqueID,
recordedAt: string
): Result<Resource, ResourceCatalogError> {
if (values.toPrimitive().templateId !== this.values.toPrimitive().templateId) {
return Result.fail(new ResourceCatalogError("TEMPLATE_FIXED"));
}
const previous = this.values.toPrimitive().integration;
const next = values.toPrimitive().integration;
if (
previous?.kind !== next?.kind ||
(previous?.kind === "mailbox" &&
next?.kind === "mailbox" &&
(previous.mailboxKind !== next.mailboxKind ||
previous.addressFieldId !== next.addressFieldId ||
previous.contextFieldId !== next.contextFieldId)) ||
(previous?.kind === "access" &&
next?.kind === "access" &&
(previous.referenceFieldId !== next.referenceFieldId ||
previous.resourceTypeId !== next.resourceTypeId ||
previous.systemId !== next.systemId ||
previous.environmentId !== next.environmentId))
) {
return Result.fail(new ResourceCatalogError("CONTEXT_FIXED"));
}
return Result.ok(
this.values.equals(values)
? this
: new Resource(
{
...this.props,
values,
history: [
...this.history,
ResourceRevision.create({
values,
specification: this.specification,
actorId,
recordedAt,
}),
],
},
this.id
)
);
}
}