feat(housekeeping): deliver economy vertical
This commit is contained in:
1 parent
1ec05cda39
commit
9c4b973faf
62 files changed
+6306
-1285
No files matched your search
@@ -0,0 +1,215 @@
|
||||
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;
|
||||
}
|
||||
Reference in new issue
Block a user