Files
EpicNext-Cms/src/features/housekeeping/domains/economy/services/mutations.ts
T
Simo 9c4b973faf
CI / check (pull_request) Successful in 40s
CI / release (pull_request) Skipped
CI / deploy (pull_request) Skipped
feat(housekeeping): deliver economy vertical
2026-08-30 13:47:06 +02:00

216 lines
5.7 KiB
TypeScript

import { PERMS } from "@/lib/permission-slugs";
import { satisfiesCapability } from "../../../foundation/capability-context";
import {
anyCapability,
fail,
type HousekeepingCapabilityContext,
type HousekeepingErrorCode,
type HousekeepingPartialCompletion,
type HousekeepingResult,
ok,
} from "../../../foundation/contracts";
export const ECONOMY_MUTATION_OPERATIONS = [
"catalog-page.change",
"catalog-page.reorder",
"catalog-page.delete-tree",
"bc-page.change",
"bc-page.reorder",
"bc-page.delete-tree",
"bc-item.change",
"catalog-item.change",
"catalog-item.bulk-create",
"catalog-item.move",
"catalog-item.reorder",
"catalog-item.translate",
"maintenance.fix-offers",
"maintenance.fix-everything",
"maintenance.fix-sprite-ids",
"maintenance.reconcile-ids",
"maintenance.align-ids",
"maintenance.remove-duplicates",
"items-base.update",
"shop-article.change",
"marketplace.cancel",
"voucher.change",
"rare-category.change",
"rare-value.change",
"badge.give",
"badge.upload",
"soundtrack.change",
] as const;
export type EconomyMutationOperation =
(typeof ECONOMY_MUTATION_OPERATIONS)[number];
export interface EconomyMutationSnapshot {
readonly before: Readonly<Record<string, unknown>> | null;
readonly after: Readonly<Record<string, unknown>> | null;
readonly output?: Readonly<Record<string, unknown>>;
readonly completion?: HousekeepingPartialCompletion;
}
export interface EconomyMutationInvocation {
readonly correlationId: string;
readonly expectedActorId: number;
readonly legacy?: boolean;
}
export interface EconomyMutationContext {
readonly capability: HousekeepingCapabilityContext;
readonly correlationId: string;
readonly legacy: boolean;
}
export interface EconomyMutationAdapter {
execute(
operation: EconomyMutationOperation,
input: unknown,
context: EconomyMutationContext,
): Promise<EconomyMutationSnapshot>;
}
export interface EconomyMutationService {
execute(
invocation: EconomyMutationInvocation,
operation: EconomyMutationOperation,
input: unknown,
): Promise<HousekeepingResult<EconomyMutationSnapshot>>;
}
export class EconomyMutationFailure extends Error {
constructor(
readonly code: HousekeepingErrorCode,
readonly messageKey: string,
readonly fieldErrors?: Readonly<Record<string, readonly string[]>>,
) {
super(messageKey);
this.name = "EconomyMutationFailure";
}
}
const OPERATION_PERMISSION = Object.freeze(
Object.fromEntries(
ECONOMY_MUTATION_OPERATIONS.map((operation) => [
operation,
operation.startsWith("shop-") ||
operation.startsWith("marketplace.") ||
operation.startsWith("voucher.") ||
operation.startsWith("rare-")
? PERMS.SHOP_EDIT
: PERMS.CATALOG_EDIT,
]),
) as Record<EconomyMutationOperation, string>,
);
export function economyMutationCapability(operation: EconomyMutationOperation) {
return anyCapability(OPERATION_PERMISSION[operation]);
}
export function createEconomyMutationService(
adapter: EconomyMutationAdapter,
resolveCapabilityContext: () => Promise<HousekeepingCapabilityContext>,
): EconomyMutationService {
return {
async execute(invocation, operation, input) {
let capability: HousekeepingCapabilityContext;
try {
capability = await resolveCapabilityContext();
} catch {
return fail(
"UNAUTHENTICATED",
"errors.housekeeping.unauthenticated",
invocation.correlationId,
);
}
if (
capability.actor.id !== invocation.expectedActorId ||
!satisfiesCapability(capability, economyMutationCapability(operation))
) {
return fail(
"FORBIDDEN",
"errors.housekeeping.forbidden",
invocation.correlationId,
);
}
try {
const snapshot = await adapter.execute(operation, input, {
capability,
correlationId: invocation.correlationId,
legacy: invocation.legacy === true,
});
return ok(snapshot, invocation.correlationId, snapshot.completion);
} catch (error) {
if (error instanceof EconomyMutationFailure) {
return fail(
error.code,
error.messageKey,
invocation.correlationId,
error.fieldErrors,
);
}
return fail(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
invocation.correlationId,
);
}
},
};
}
export function createEconomyMutationInvocation(
actor: { readonly id: number },
correlationId: string,
): EconomyMutationInvocation {
return { correlationId, expectedActorId: actor.id, legacy: true };
}
const productionAdapter: EconomyMutationAdapter = {
async execute(operation, input, context) {
const { economyProductionMutationAdapter } = await import(
"./mutations-production"
);
return economyProductionMutationAdapter.execute(operation, input, context);
},
};
export const economyMutationService = createEconomyMutationService(
productionAdapter,
async () => {
const { getHousekeepingCapabilityContext } = await import(
"../../../foundation/server-capability-context"
);
return getHousekeepingCapabilityContext();
},
);
export async function executeLegacyEconomyMutation(
actor: { readonly id: number },
operation: EconomyMutationOperation,
input: unknown,
): Promise<EconomyMutationSnapshot> {
const { createCorrelationId } = await import(
"../../../foundation/correlation"
);
const result = await economyMutationService.execute(
{
correlationId: createCorrelationId(),
expectedActorId: actor.id,
legacy: true,
},
operation,
input,
);
if (!result.ok) {
throw new EconomyMutationFailure(
result.error.code,
result.error.messageKey,
result.error.fieldErrors,
);
}
if (result.completion?.external === "failed") {
throw new EconomyMutationFailure(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
);
}
return result.data;
}