metis-desk/modules/resources/src/api/domain/resource-catalog.aggregate.ts

201 lines
6.5 KiB
TypeScript

import type { UniqueID } from "@repo/rdx-ddd";
import { AggregateRoot } from "@repo/rdx-ddd";
import { Maybe, Result } from "@repo/rdx-utils";
import { ResourceCatalogError } from "./resource-definition";
import { CatalogAvailability, ResourceDefinition } from "./resource-definition.entity";
import type { ResourceDefinitionValues } from "./value-objects/resource-definition-values.vo";
type CatalogProps = {
definitions: readonly ResourceDefinition[];
availability: readonly CatalogAvailability[];
};
export class ResourceCatalog extends AggregateRoot<CatalogProps> {
private constructor(props: CatalogProps, id?: UniqueID) {
super(
{
definitions: Object.freeze([...props.definitions]),
availability: Object.freeze([...props.availability]),
},
id
);
}
static create() {
return new ResourceCatalog({ definitions: [], availability: [] });
}
static rehydrate(props: CatalogProps, id: UniqueID) {
return new ResourceCatalog(props, id);
}
get definitions() {
return this.props.definitions;
}
get availability() {
return this.props.availability;
}
visible(companyId: Maybe<UniqueID>) {
return this.definitions.filter(
(item) =>
!item.companyId.getOrUndefined() ||
item.companyId.getOrUndefined()?.equals(companyId.getOrUndefined())
);
}
active(item: ResourceDefinition, companyId: Maybe<UniqueID>) {
const override = this.availability.find(
(entry) =>
entry.companyId.equals(companyId.getOrUndefined()) && entry.definitionId.equals(item.id)
);
return override?.active.getOrUndefined() ?? item.active;
}
save(
companyId: Maybe<UniqueID>,
id: Maybe<UniqueID>,
values: ResourceDefinitionValues
): Result<ResourceCatalog, ResourceCatalogError> {
const previous = id.getOrUndefined()
? this.definitions.find((item) => item.id.equals(id.getOrUndefined()))
: undefined;
if (id.getOrUndefined() && !(previous && sameScope(previous.companyId, companyId))) {
return Result.fail(new ResourceCatalogError("DEFINITION_NOT_FOUND"));
}
const candidate = previous
? previous.update(values)
: Result.ok(ResourceDefinition.create(companyId, values, true));
if (!candidate.isSuccess) {
return candidate;
}
return this.replace(candidate.data, companyId, previous);
}
private replace(
candidate: ResourceDefinition,
scope: Maybe<UniqueID>,
previous?: ResourceDefinition
): Result<ResourceCatalog, ResourceCatalogError> {
const conflicts = scope.getOrUndefined() ? this.visible(scope) : this.definitions;
if (
conflicts.some(
(item) =>
!item.id.equals(candidate.id) &&
item.values.data.kind === candidate.values.data.kind &&
item.values.data.name.toLocaleLowerCase("es") ===
candidate.values.data.name.toLocaleLowerCase("es")
)
) {
return Result.fail(new ResourceCatalogError("NAME_EXISTS"));
}
const invalidField = this.invalidField(candidate, scope, previous);
if (invalidField) {
return Result.fail(new ResourceCatalogError("FIELD_UNAVAILABLE"));
}
if (candidate === previous) {
return Result.ok(this);
}
return Result.ok(
new ResourceCatalog(
{
...this.props,
definitions: previous
? this.definitions.map((item) => (item.id.equals(candidate.id) ? candidate : item))
: [...this.definitions, candidate],
},
this.id
)
);
}
private invalidField(
candidate: ResourceDefinition,
scope: Maybe<UniqueID>,
previous?: ResourceDefinition
) {
return candidate.values.fieldIds.some((id) => {
const field = this.visible(scope).find((item) => item.id.equals(id));
if (!field || field.values.data.kind !== "field") {
return true;
}
return !(
this.active(field, scope) ||
previous?.values.fieldIds.some((existing) => existing.equals(id))
);
});
}
seed(
stockKey: string,
values: ResourceDefinitionValues
): Result<ResourceCatalog, ResourceCatalogError> {
const previous = this.definitions.find((item) => item.stockKey.getOrUndefined() === stockKey);
if (previous) {
const upgraded = previous.initializeSections(values);
if (upgraded === previous) {
return Result.ok(this);
}
return Result.ok(
new ResourceCatalog(
{
...this.props,
definitions: this.definitions.map((item) =>
item.id.equals(previous.id) ? upgraded : item
),
},
this.id
)
);
}
return this.replace(
ResourceDefinition.create(Maybe.none(), values, true, Maybe.some(stockKey)),
Maybe.none()
);
}
setActive(
scope: Maybe<UniqueID>,
id: UniqueID,
active: boolean | null
): Result<ResourceCatalog, ResourceCatalogError> {
const item = this.visible(scope).find((entry) => entry.id.equals(id));
if (!item) {
return Result.fail(new ResourceCatalogError("DEFINITION_NOT_FOUND"));
}
const company = scope.getOrUndefined();
if (company && !item.companyId.getOrUndefined()) {
const previous = this.availability.find(
(entry) => entry.companyId.equals(company) && entry.definitionId.equals(id)
);
const value = active === null ? Maybe.none<boolean>() : Maybe.some(active);
const changed = previous
? previous.update(value)
: CatalogAvailability.create(company, id, value);
if (previous === changed || (!previous && active === null)) {
return Result.ok(this);
}
return Result.ok(
new ResourceCatalog(
{
...this.props,
availability: previous
? this.availability.map((entry) => (entry.id.equals(previous.id) ? changed : entry))
: [...this.availability, changed],
},
this.id
)
);
}
if (active === null) {
return Result.fail(new ResourceCatalogError("INVALID_REQUEST"));
}
const changed = item.setActive(active);
if (changed === item) {
return Result.ok(this);
}
return Result.ok(
new ResourceCatalog(
{
...this.props,
definitions: this.definitions.map((entry) => (entry.id.equals(id) ? changed : entry)),
},
this.id
)
);
}
}
function sameScope(a: Maybe<UniqueID>, b: Maybe<UniqueID>) {
return a.getOrUndefined()?.toPrimitive() === b.getOrUndefined()?.toPrimitive();
}