refactor(api): extract assistant country read slice

This commit is contained in:
2026-03-30 22:53:59 +02:00
parent 0cc7b9805a
commit 45c25b17c1
3 changed files with 124 additions and 64 deletions
+11 -63
View File
@@ -85,6 +85,10 @@ import {
countryMetroAdminToolDefinitions,
createCountryMetroAdminExecutors,
} from "./assistant-tools/country-metro-admin.js";
import {
countryReadmodelToolDefinitions,
createCountryReadmodelExecutors,
} from "./assistant-tools/country-readmodels.js";
import {
createUserSelfServiceExecutors,
userSelfServiceToolDefinitions,
@@ -431,7 +435,6 @@ const LEGACY_MONOLITHIC_TOOL_ACCESS: Partial<Record<string, ToolAccessRequiremen
get_pending_vacation_approvals: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
get_entitlement_summary: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
set_entitlement: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
get_country: { requiresResourceOverview: true },
delete_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
generate_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
remove_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
@@ -2161,34 +2164,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess([
...clientMutationToolDefinitions,
// ── ADMIN / CONFIG READ TOOLS ──
{
type: "function",
function: {
name: "list_countries",
description: "List countries including working hours, schedule rules, active flag, and metro cities.",
parameters: {
type: "object",
properties: {
includeInactive: { type: "boolean", description: "Include inactive countries. Default: false." },
search: { type: "string", description: "Optional country code or name search." },
},
},
},
},
{
type: "function",
function: {
name: "get_country",
description: "Get one country with schedule rules, active flag, metro cities, and resource count. Accepts ID, code, or name.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Country ID, code, or name." },
},
required: ["identifier"],
},
},
},
...countryReadmodelToolDefinitions,
...countryMetroAdminToolDefinitions,
...configReadmodelToolDefinitions,
...userAdminToolDefinitions,
@@ -2627,40 +2603,12 @@ const executors = {
// ── ADMIN / CONFIG ──
async list_countries(params: { includeInactive?: boolean; search?: string }, ctx: ToolContext) {
const caller = createCountryCaller(createScopedCallerContext(ctx));
const countries = await caller.list(
params.includeInactive
? undefined
: { isActive: true },
);
const normalizedSearch = params.search?.trim().toLowerCase();
const filteredCountries = normalizedSearch
? countries.filter((country) =>
country.code.toLowerCase().includes(normalizedSearch)
|| country.name.toLowerCase().includes(normalizedSearch))
: countries;
return {
count: filteredCountries.length,
countries: filteredCountries.map(formatCountry),
};
},
async get_country(params: { identifier: string }, ctx: ToolContext) {
const caller = createCountryCaller(createScopedCallerContext(ctx));
let country;
try {
country = await caller.getByIdentifier({ identifier: params.identifier });
} catch (error) {
const mapped = toAssistantCountryNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
return formatCountry(country);
},
...createCountryReadmodelExecutors({
createCountryCaller,
createScopedCallerContext,
formatCountry,
toAssistantCountryNotFoundError,
}),
...createCountryMetroAdminExecutors({
createCountryCaller,
createScopedCallerContext,
@@ -0,0 +1,111 @@
import type { Prisma } from "@capakraken/db";
import type { TRPCContext } from "../../trpc.js";
import { withToolAccess, type ToolContext, type ToolDef, type ToolExecutor } from "./shared.js";
type AssistantToolErrorResult = { error: string };
type CountryRecord = {
id: string;
code: string;
name: string;
dailyWorkingHours: number;
scheduleRules?: Prisma.JsonValue | null;
isActive?: boolean | null;
metroCities?: Array<{ id: string; name: string }> | null;
_count?: { resources?: number | null } | null;
};
type CountryReadmodelsDeps = {
createCountryCaller: (ctx: TRPCContext) => {
list: (params?: { isActive: boolean }) => Promise<CountryRecord[]>;
getByIdentifier: (params: { identifier: string }) => Promise<CountryRecord>;
};
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
formatCountry: (country: CountryRecord) => unknown;
toAssistantCountryNotFoundError: (
error: unknown,
) => AssistantToolErrorResult | null;
};
export const countryReadmodelToolDefinitions: ToolDef[] = withToolAccess([
{
type: "function",
function: {
name: "list_countries",
description: "List countries including working hours, schedule rules, active flag, and metro cities.",
parameters: {
type: "object",
properties: {
includeInactive: { type: "boolean", description: "Include inactive countries. Default: false." },
search: { type: "string", description: "Optional country code or name search." },
},
},
},
},
{
type: "function",
function: {
name: "get_country",
description: "Get one country with schedule rules, active flag, metro cities, and resource count. Accepts ID, code, or name.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Country ID, code, or name." },
},
required: ["identifier"],
},
},
},
], {
get_country: {
requiresResourceOverview: true,
},
});
export function createCountryReadmodelExecutors(
deps: CountryReadmodelsDeps,
): Record<string, ToolExecutor> {
return {
async list_countries(
params: { includeInactive?: boolean; search?: string },
ctx: ToolContext,
) {
const caller = deps.createCountryCaller(deps.createScopedCallerContext(ctx));
const countries = await caller.list(
params.includeInactive
? undefined
: { isActive: true },
);
const normalizedSearch = params.search?.trim().toLowerCase();
const filteredCountries = normalizedSearch
? countries.filter((country) =>
country.code.toLowerCase().includes(normalizedSearch)
|| country.name.toLowerCase().includes(normalizedSearch))
: countries;
return {
count: filteredCountries.length,
countries: filteredCountries.map(deps.formatCountry),
};
},
async get_country(
params: { identifier: string },
ctx: ToolContext,
) {
const caller = deps.createCountryCaller(deps.createScopedCallerContext(ctx));
let country;
try {
country = await caller.getByIdentifier({ identifier: params.identifier });
} catch (error) {
const mapped = deps.toAssistantCountryNotFoundError(error);
if (mapped) {
return mapped;
}
throw error;
}
return deps.formatCountry(country);
},
};
}