Files
Epicnabbo-Catalogus-Updated…/src/features/housekeeping/domains/economy/queries/catalog.ts
T

151 lines
4.2 KiB
TypeScript

import { authorizeHousekeeping } from "../../../foundation/authorization";
import {
fail,
type HousekeepingQuery,
ok,
} from "../../../foundation/contracts";
import { isSafeHousekeepingHref } from "../../../foundation/housekeeping-href";
import {
type EconomyRouteGroup,
type EconomyRouteId,
economyRouteById,
economyRouteGroup,
} from "../routes";
export interface EconomyQueryInput {
readonly routeId: EconomyRouteId;
readonly params?: Readonly<Record<string, string>>;
readonly list?: Readonly<{
search?: string;
pageSize?: number;
offset?: number;
}>;
}
export interface EconomyQueryItem {
readonly id: string;
readonly title: string;
readonly status?: string;
readonly amount?: string;
readonly href?: string;
readonly description?: string;
readonly updatedAt?: string | null;
}
export interface EconomyQueryData {
readonly kind: EconomyRouteGroup;
readonly items: readonly EconomyQueryItem[];
readonly total: number;
readonly partialDependencies: readonly string[];
}
export interface EconomyQueryAdapters {
load(input: NormalizedEconomyQueryInput): Promise<EconomyQueryData>;
}
export interface NormalizedEconomyQueryInput {
readonly routeId: EconomyRouteId;
readonly params: Readonly<Record<string, string>>;
readonly list: Readonly<{ search: string; pageSize: number; offset: number }>;
}
function boundedInteger(
value: unknown,
fallback: number,
minimum: number,
maximum: number,
): number {
const parsed = Number(value);
if (!Number.isSafeInteger(parsed)) return fallback;
return Math.min(maximum, Math.max(minimum, parsed));
}
function normalizeInput(input: EconomyQueryInput): NormalizedEconomyQueryInput {
return {
routeId: input.routeId,
params: Object.fromEntries(
Object.entries(input.params ?? {}).map(([key, value]) => [
key.normalize("NFC").trim().slice(0, 128),
value.normalize("NFC").trim().slice(0, 128),
]),
),
list: {
search: String(input.list?.search ?? "")
.normalize("NFC")
.trim()
.slice(0, 128),
pageSize: boundedInteger(input.list?.pageSize, 25, 1, 100),
offset: boundedInteger(input.list?.offset, 0, 0, 100_000),
},
};
}
function isValidData(
data: EconomyQueryData,
input: NormalizedEconomyQueryInput,
): boolean {
if (
data.kind !== economyRouteGroup(input.routeId) ||
!Array.isArray(data.items) ||
!Number.isSafeInteger(data.total) ||
data.total < 0 ||
!Array.isArray(data.partialDependencies) ||
data.partialDependencies.length !== 0
) {
return false;
}
return data.items.every(
(item) =>
typeof item.id === "string" &&
item.id.length > 0 &&
typeof item.title === "string" &&
item.title.length > 0 &&
(item.href === undefined || isSafeHousekeepingHref(item.href)),
);
}
export function createEconomyQuery(
adapters: EconomyQueryAdapters,
): HousekeepingQuery<EconomyQueryInput, EconomyQueryData> {
return {
id: "economy.query",
owner: "economy",
capability: economyRouteById("economy.catalog.overview").capability,
async run(context, rawInput) {
const route = economyRouteById(rawInput.routeId);
const authorization = authorizeHousekeeping(context, route.capability);
if (!authorization.ok) return authorization;
const input = normalizeInput(rawInput);
try {
const data = await adapters.load(input);
if (!isValidData(data, input)) throw new Error("invalid Economy query");
return ok(data, authorization.correlationId);
} catch {
return fail(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
authorization.correlationId,
);
}
},
};
}
export async function loadEconomyCatalogQuery(
input: NormalizedEconomyQueryInput,
): Promise<EconomyQueryData> {
const { loadEconomyDatabaseQuery } = await import("./economy-production");
return loadEconomyDatabaseQuery(input);
}
export const economyQuery = createEconomyQuery({
async load(input) {
const group = economyRouteGroup(input.routeId);
if (group === "catalog" || group === "items") {
return loadEconomyCatalogQuery(input);
}
if (group === "commerce" || group === "history") {
const { loadEconomyCommerceQuery } = await import("./commerce");
return loadEconomyCommerceQuery(input);
}
const { loadEconomyValueQuery } = await import("./value");
return loadEconomyValueQuery(input);
},
});