151 lines
4.2 KiB
TypeScript
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);
|
|
},
|
|
});
|