263 lines
7.2 KiB
TypeScript
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
|
|
)
|
|
);
|
|
}
|
|
}
|