127 lines
3.6 KiB
TypeScript
127 lines
3.6 KiB
TypeScript
import type { UniqueID } from "@repo/rdx-ddd";
|
|
import { AggregateRoot } from "@repo/rdx-ddd";
|
|
import type { Maybe } from "@repo/rdx-utils";
|
|
import { Result } from "@repo/rdx-utils";
|
|
|
|
import type { Person } from "../person.entity";
|
|
|
|
import { AccessError } from "./access-error";
|
|
import type { AccessItem, AuditContext } from "./access-item.entity";
|
|
import {
|
|
AccessPolicy,
|
|
accessConflict,
|
|
checkCatalog,
|
|
checkConfiguration,
|
|
checkRelations,
|
|
} from "./access-policy";
|
|
import type { AccessValues } from "./access-values.vo";
|
|
|
|
export type AccessContext = Readonly<{
|
|
catalog: readonly AccessItem[];
|
|
people: readonly Person[];
|
|
today: string;
|
|
}>;
|
|
|
|
export class AccessWorkspace extends AggregateRoot<{
|
|
companyId: Maybe<UniqueID>;
|
|
items: readonly AccessItem[];
|
|
}> {
|
|
protected constructor(
|
|
props: { companyId: Maybe<UniqueID>; items: readonly AccessItem[] },
|
|
id: UniqueID
|
|
) {
|
|
super({ ...props, items: Object.freeze([...props.items]) }, id);
|
|
}
|
|
|
|
static create(companyId: Maybe<UniqueID>, id: UniqueID) {
|
|
return new AccessWorkspace({ companyId, items: [] }, id);
|
|
}
|
|
|
|
static rehydrate(companyId: Maybe<UniqueID>, items: readonly AccessItem[], id: UniqueID) {
|
|
return new AccessWorkspace({ companyId, items }, id);
|
|
}
|
|
|
|
get companyId() {
|
|
return this.props.companyId;
|
|
}
|
|
|
|
get items() {
|
|
return this.props.items;
|
|
}
|
|
|
|
save(
|
|
candidate: AccessItem,
|
|
context: AccessContext,
|
|
previous?: AccessItem
|
|
): Result<AccessWorkspace, AccessError> {
|
|
const value = candidate.values.data;
|
|
|
|
if (
|
|
this.companyId.getOrUndefined()?.toPrimitive() !==
|
|
candidate.companyId.getOrUndefined()?.toPrimitive()
|
|
) {
|
|
return Result.fail(new AccessError("SCOPE_MISMATCH"));
|
|
}
|
|
const global = ["system", "resource_type", "permission", "template"];
|
|
|
|
if (!(this.companyId.getOrUndefined() || global.includes(value.kind))) {
|
|
return Result.fail(new AccessError("SCOPE_MISMATCH"));
|
|
}
|
|
if (value.kind === "template" && this.companyId.getOrUndefined()) {
|
|
return Result.fail(new AccessError("SCOPE_MISMATCH"));
|
|
}
|
|
const all = [...context.catalog, ...this.items.filter((item) => !item.id.equals(candidate.id))];
|
|
const isNewOrReopen =
|
|
!previous ||
|
|
("endDate" in value &&
|
|
value.endDate === null &&
|
|
"endDate" in previous.values.data &&
|
|
previous.values.data.endDate !== null);
|
|
const policy = new AccessPolicy(this.companyId, all, context, isNewOrReopen);
|
|
const failure =
|
|
checkCatalog(value, policy) ??
|
|
checkRelations(value, policy) ??
|
|
checkConfiguration(value, previous);
|
|
|
|
if (failure) {
|
|
return failure;
|
|
}
|
|
const duplicates = all.some((other) => accessConflict(candidate, other, all));
|
|
|
|
if (duplicates) {
|
|
return Result.fail(new AccessError("ACCESS_CONFLICT"));
|
|
}
|
|
if (previous === candidate) {
|
|
return Result.ok(this);
|
|
}
|
|
|
|
return Result.ok(
|
|
new AccessWorkspace(
|
|
{
|
|
companyId: this.companyId,
|
|
items: previous
|
|
? this.items.map((item) => (item.id.equals(candidate.id) ? candidate : item))
|
|
: [...this.items, candidate],
|
|
},
|
|
this.id
|
|
)
|
|
);
|
|
}
|
|
|
|
update(
|
|
id: UniqueID,
|
|
values: AccessValues,
|
|
audit: AuditContext,
|
|
context: AccessContext
|
|
): Result<AccessWorkspace, AccessError> {
|
|
const previous = this.items.find((item) => item.id.equals(id));
|
|
|
|
if (!previous) {
|
|
return Result.fail(new AccessError("ACCESS_NOT_FOUND"));
|
|
}
|
|
const candidate = previous.update(values, audit);
|
|
|
|
return candidate.isSuccess ? this.save(candidate.data, context, previous) : candidate;
|
|
}
|
|
}
|