Files
EpicNext-Cms/src/features/housekeeping/domains/economy/search.ts
T
Simo 9c4b973faf
CI / check (pull_request) Successful in 40s
CI / release (pull_request) Skipped
CI / deploy (pull_request) Skipped
feat(housekeeping): deliver economy vertical
2026-08-30 13:47:06 +02:00

121 lines
3.4 KiB
TypeScript

import { PERMS } from "@/lib/permission-slugs";
import { authorizeHousekeeping } from "../../foundation/authorization";
import { satisfiesCapability } from "../../foundation/capability-context";
import {
anyCapability,
type CapabilityRequirement,
fail,
type HousekeepingCapabilityContext,
type HousekeepingSearchProvider,
ok,
} from "../../foundation/contracts";
import { isSafeHousekeepingHref } from "../../foundation/housekeeping-href";
export const ECONOMY_SEARCH_PROVIDER_IDS = [
"economy.catalog-pages",
"economy.items",
"economy.transactions",
] as const;
export interface EconomySearchCandidate {
readonly id: string;
readonly title: string;
readonly description?: string;
readonly href: string;
readonly capability: CapabilityRequirement;
}
type EconomySearchLoader = (
context: HousekeepingCapabilityContext,
term: string,
limit: number,
) => Promise<readonly EconomySearchCandidate[]>;
export interface EconomySearchAdapters {
readonly catalogPages: EconomySearchLoader;
readonly items: EconomySearchLoader;
readonly transactions: EconomySearchLoader;
}
function createProvider(
id: (typeof ECONOMY_SEARCH_PROVIDER_IDS)[number],
capability: CapabilityRequirement,
load: EconomySearchLoader,
): HousekeepingSearchProvider {
return {
id,
owner: "economy",
capability,
async search(context, input) {
const authorization = authorizeHousekeeping(context, capability);
if (!authorization.ok) return authorization;
const limit = Number.isFinite(input.limit)
? Math.min(25, Math.max(1, Math.trunc(input.limit)))
: 25;
const term = input.term.normalize("NFC").trim().slice(0, 128);
try {
const candidates = await load(context, term, limit);
return ok(
candidates
.filter(
(candidate) =>
isSafeHousekeepingHref(candidate.href) &&
satisfiesCapability(context, candidate.capability),
)
.slice(0, limit)
.map((candidate) => ({
...candidate,
domain: "economy" as const,
type: "entity" as const,
href: candidate.href as `/ase/${string}`,
})),
authorization.correlationId,
);
} catch {
return fail(
"DEPENDENCY_UNAVAILABLE",
"errors.housekeeping.dependencyUnavailable",
authorization.correlationId,
);
}
},
};
}
export function createEconomySearchProviders(
adapters: EconomySearchAdapters,
): readonly HousekeepingSearchProvider[] {
return [
createProvider(
"economy.catalog-pages",
anyCapability(PERMS.CATALOG_VIEW),
adapters.catalogPages,
),
createProvider(
"economy.items",
anyCapability(PERMS.CATALOG_VIEW),
adapters.items,
),
createProvider(
"economy.transactions",
anyCapability(PERMS.SHOP_VIEW),
adapters.transactions,
),
];
}
export const ECONOMY_SEARCH_PROVIDERS = createEconomySearchProviders({
async catalogPages(context, term, limit) {
const { loadEconomySearchCandidates } = await import("./search-production");
return loadEconomySearchCandidates("catalog-pages", context, term, limit);
},
async items(context, term, limit) {
const { loadEconomySearchCandidates } = await import("./search-production");
return loadEconomySearchCandidates("items", context, term, limit);
},
async transactions(context, term, limit) {
const { loadEconomySearchCandidates } = await import("./search-production");
return loadEconomySearchCandidates("transactions", context, term, limit);
},
});