128 lines
3.5 KiB
TypeScript
128 lines
3.5 KiB
TypeScript
export const INVALIDATION_TYPES = [
|
|
"directory.updated",
|
|
"page.updated",
|
|
"admin.updated",
|
|
] as const;
|
|
|
|
export type InvalidationType = (typeof INVALIDATION_TYPES)[number];
|
|
|
|
export type AdminEditorScope =
|
|
| "overview"
|
|
| "weapon"
|
|
| "artifact"
|
|
| "constellations"
|
|
| "team"
|
|
| "extras:list"
|
|
| `extra:${string}`;
|
|
|
|
export interface InvalidationEvent {
|
|
type: InvalidationType;
|
|
id: string;
|
|
version: number;
|
|
scope?: AdminEditorScope;
|
|
operation?: "overwrite";
|
|
actorName?: string;
|
|
}
|
|
|
|
export interface OutboxEventLike {
|
|
aggregateId: string;
|
|
eventType: string;
|
|
payload: Record<string, unknown>;
|
|
topic: string;
|
|
}
|
|
|
|
function isInvalidationType(value: string): value is InvalidationType {
|
|
return INVALIDATION_TYPES.some((type) => type === value);
|
|
}
|
|
|
|
function isAdminEditorScope(value: unknown): value is AdminEditorScope {
|
|
return typeof value === "string" && (
|
|
["overview", "weapon", "artifact", "constellations", "team", "extras:list"].includes(value)
|
|
|| /^extra:[0-9a-f-]{36}$/iu.test(value)
|
|
);
|
|
}
|
|
|
|
function overwriteDetails(payload: Record<string, unknown>) {
|
|
if (payload.operation === undefined && payload.actorName === undefined) return {};
|
|
if (
|
|
payload.operation !== "overwrite"
|
|
|| typeof payload.actorName !== "string"
|
|
|| !payload.actorName.trim()
|
|
|| payload.actorName.length > 100
|
|
) return null;
|
|
return { operation: "overwrite" as const, actorName: payload.actorName };
|
|
}
|
|
|
|
export function expectedTopic(event: InvalidationEvent): string {
|
|
switch (event.type) {
|
|
case "directory.updated":
|
|
return "directory";
|
|
case "page.updated":
|
|
return `page:${event.id}`;
|
|
case "admin.updated":
|
|
return "admin";
|
|
}
|
|
}
|
|
|
|
export function toInvalidationEvent(
|
|
record: OutboxEventLike,
|
|
): InvalidationEvent | null {
|
|
if (!isInvalidationType(record.eventType)) return null;
|
|
if (
|
|
record.eventType === "admin.updated"
|
|
&& record.payload.scope !== undefined
|
|
&& !isAdminEditorScope(record.payload.scope)
|
|
) return null;
|
|
const details = record.eventType === "admin.updated" ? overwriteDetails(record.payload) : {};
|
|
if (details === null) return null;
|
|
const id = record.payload.id;
|
|
const version = record.payload.version;
|
|
if (
|
|
typeof id !== "string" ||
|
|
id !== record.aggregateId ||
|
|
typeof version !== "number" ||
|
|
!Number.isSafeInteger(version) ||
|
|
version < 0
|
|
) {
|
|
return null;
|
|
}
|
|
const event: InvalidationEvent = {
|
|
type: record.eventType,
|
|
id,
|
|
version,
|
|
...(record.eventType === "admin.updated" && isAdminEditorScope(record.payload.scope)
|
|
? { scope: record.payload.scope }
|
|
: {}),
|
|
...details,
|
|
};
|
|
return expectedTopic(event) === record.topic ? event : null;
|
|
}
|
|
|
|
export function parseInvalidationEvent(value: string): InvalidationEvent | null {
|
|
try {
|
|
const parsed = JSON.parse(value) as Record<string, unknown>;
|
|
const type = parsed.type;
|
|
const id = parsed.id;
|
|
const version = parsed.version;
|
|
const scope = parsed.scope;
|
|
const details = type === "admin.updated" ? overwriteDetails(parsed) : {};
|
|
if (
|
|
typeof type !== "string" ||
|
|
!isInvalidationType(type) ||
|
|
typeof id !== "string" ||
|
|
typeof version !== "number" ||
|
|
!Number.isSafeInteger(version) ||
|
|
version < 0
|
|
) {
|
|
return null;
|
|
}
|
|
if (scope !== undefined && (type !== "admin.updated" || !isAdminEditorScope(scope))) {
|
|
return null;
|
|
}
|
|
if (details === null) return null;
|
|
return { type, id, version, ...(scope === undefined ? {} : { scope }), ...details };
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|