216 lines
5.7 KiB
TypeScript
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;
|
|
}
|