feat(housekeeping): deliver content vertical
This commit is contained in:
1 parent
bdab924bdf
commit
fd68819d9b
67 files changed
+5742
-1916
No files matched your search
@@ -0,0 +1,210 @@
|
||||
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 CONTENT_MUTATION_OPERATIONS = [
|
||||
"article.change",
|
||||
"ad.change",
|
||||
"banner.change",
|
||||
"event-type.change",
|
||||
"event.change",
|
||||
"event-prize.change",
|
||||
"event-winner.add",
|
||||
"poll.change",
|
||||
"poll-question.change",
|
||||
"photo.delete",
|
||||
"media.upload",
|
||||
"media.delete",
|
||||
"navigation.update",
|
||||
"tag.change",
|
||||
"prefix.change",
|
||||
"prefix-blacklist.change",
|
||||
"prefix-settings.update",
|
||||
"help-question.change",
|
||||
"writeable-box.change",
|
||||
"email-template.change",
|
||||
"theme.update",
|
||||
"theme.apply-preset",
|
||||
"theme.custom-change",
|
||||
"theme.apply-custom",
|
||||
"favicon.save",
|
||||
"favicon.delete",
|
||||
"logo.save",
|
||||
"translation.cms.save",
|
||||
"translation.client.save",
|
||||
"translation.emulator.save",
|
||||
] as const;
|
||||
|
||||
export type ContentMutationOperation =
|
||||
(typeof CONTENT_MUTATION_OPERATIONS)[number];
|
||||
|
||||
export interface ContentMutationSnapshot {
|
||||
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 ContentMutationInvocation {
|
||||
readonly correlationId: string;
|
||||
readonly expectedActorId: number;
|
||||
readonly legacy?: boolean;
|
||||
}
|
||||
|
||||
export interface ContentMutationContext {
|
||||
readonly capability: HousekeepingCapabilityContext;
|
||||
readonly correlationId: string;
|
||||
readonly legacy: boolean;
|
||||
}
|
||||
|
||||
export interface ContentMutationAdapter {
|
||||
execute(
|
||||
operation: ContentMutationOperation,
|
||||
input: unknown,
|
||||
context: ContentMutationContext,
|
||||
): Promise<ContentMutationSnapshot>;
|
||||
}
|
||||
|
||||
export interface ContentMutationService {
|
||||
execute(
|
||||
invocation: ContentMutationInvocation,
|
||||
operation: ContentMutationOperation,
|
||||
input: unknown,
|
||||
): Promise<HousekeepingResult<ContentMutationSnapshot>>;
|
||||
}
|
||||
|
||||
export class ContentMutationFailure extends Error {
|
||||
constructor(
|
||||
readonly code: HousekeepingErrorCode,
|
||||
readonly messageKey: string,
|
||||
readonly fieldErrors?: Readonly<Record<string, readonly string[]>>,
|
||||
) {
|
||||
super(messageKey);
|
||||
this.name = "ContentMutationFailure";
|
||||
}
|
||||
}
|
||||
|
||||
const OPERATION_PERMISSION = Object.freeze({
|
||||
"article.change": PERMS.NEWS_EDIT,
|
||||
"ad.change": PERMS.PAGES_EDIT,
|
||||
"banner.change": PERMS.BANNERS_EDIT,
|
||||
"event-type.change": PERMS.EVENTS_EDIT,
|
||||
"event.change": PERMS.EVENTS_EDIT,
|
||||
"event-prize.change": PERMS.EVENTS_EDIT,
|
||||
"event-winner.add": PERMS.EVENTS_EDIT,
|
||||
"poll.change": PERMS.POLLS_EDIT,
|
||||
"poll-question.change": PERMS.POLLS_EDIT,
|
||||
"photo.delete": PERMS.PAGES_EDIT,
|
||||
"media.upload": PERMS.PAGES_EDIT,
|
||||
"media.delete": PERMS.PAGES_EDIT,
|
||||
"navigation.update": PERMS.SETTINGS_EDIT,
|
||||
"tag.change": PERMS.PAGES_EDIT,
|
||||
"prefix.change": PERMS.PREFIXES_EDIT,
|
||||
"prefix-blacklist.change": PERMS.PREFIXES_EDIT,
|
||||
"prefix-settings.update": PERMS.PREFIXES_EDIT,
|
||||
"help-question.change": PERMS.PAGES_EDIT,
|
||||
"writeable-box.change": PERMS.PAGES_EDIT,
|
||||
"email-template.change": PERMS.PAGES_EDIT,
|
||||
"theme.update": PERMS.SETTINGS_EDIT,
|
||||
"theme.apply-preset": PERMS.SETTINGS_EDIT,
|
||||
"theme.custom-change": PERMS.SETTINGS_EDIT,
|
||||
"theme.apply-custom": PERMS.SETTINGS_EDIT,
|
||||
"favicon.save": PERMS.SETTINGS_EDIT,
|
||||
"favicon.delete": PERMS.SETTINGS_EDIT,
|
||||
"logo.save": PERMS.SETTINGS_EDIT,
|
||||
"translation.cms.save": PERMS.SETTINGS_EDIT,
|
||||
"translation.client.save": PERMS.SETTINGS_EDIT,
|
||||
"translation.emulator.save": PERMS.SETTINGS_EDIT,
|
||||
} satisfies Record<ContentMutationOperation, string>);
|
||||
|
||||
export function contentMutationCapability(
|
||||
operation: ContentMutationOperation,
|
||||
) {
|
||||
return anyCapability(OPERATION_PERMISSION[operation]);
|
||||
}
|
||||
|
||||
export function createContentMutationService(
|
||||
adapter: ContentMutationAdapter,
|
||||
resolveCapabilityContext: () => Promise<HousekeepingCapabilityContext>,
|
||||
): ContentMutationService {
|
||||
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, contentMutationCapability(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 ContentMutationFailure) {
|
||||
return fail(
|
||||
error.code,
|
||||
error.messageKey,
|
||||
invocation.correlationId,
|
||||
error.fieldErrors,
|
||||
);
|
||||
}
|
||||
return fail(
|
||||
"DEPENDENCY_UNAVAILABLE",
|
||||
"errors.housekeeping.dependencyUnavailable",
|
||||
invocation.correlationId,
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createContentMutationInvocation(
|
||||
actor: { readonly id: number },
|
||||
correlationId: string,
|
||||
): ContentMutationInvocation {
|
||||
return { correlationId, expectedActorId: actor.id, legacy: true };
|
||||
}
|
||||
|
||||
const productionAdapter: ContentMutationAdapter = {
|
||||
async execute(operation, input, context) {
|
||||
const { contentProductionMutationAdapter } = await import(
|
||||
"./mutations-production"
|
||||
);
|
||||
return contentProductionMutationAdapter.execute(operation, input, context);
|
||||
},
|
||||
};
|
||||
|
||||
export const contentMutationService = createContentMutationService(
|
||||
productionAdapter,
|
||||
async () => {
|
||||
const { getHousekeepingCapabilityContext } = await import(
|
||||
"../../../foundation/server-capability-context"
|
||||
);
|
||||
return getHousekeepingCapabilityContext();
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user