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
+2 -1
View File
@@ -45,12 +45,13 @@
- the audit-history assistant helpers now live in their own domain module, keeping controller-side change-history reads out of the monolithic assistant router without changing the assistant contract - the audit-history assistant helpers now live in their own domain module, keeping controller-side change-history reads out of the monolithic assistant router without changing the assistant contract
- the import/export and staged Dispo assistant helpers now live in their own domain module, keeping file-bound export/import and batch-staging orchestration out of the monolithic assistant router without changing the assistant contract - the import/export and staged Dispo assistant helpers now live in their own domain module, keeping file-bound export/import and batch-staging orchestration out of the monolithic assistant router without changing the assistant contract
- the remaining estimate search, planning lookup, self-service timeline read, and navigation assistant helpers now live in their own domain module, keeping another mixed read-only cluster out of the monolithic assistant router without changing the assistant contract - the remaining estimate search, planning lookup, self-service timeline read, and navigation assistant helpers now live in their own domain module, keeping another mixed read-only cluster out of the monolithic assistant router without changing the assistant contract
- the country listing and country detail assistant helpers now live in their own domain module, keeping the remaining geo/readmodel lookups out of the monolithic assistant router without changing the assistant contract
## Next Up ## Next Up
Pin the next structural cleanup on the API side: Pin the next structural cleanup on the API side:
continue splitting `packages/api/src/router/assistant-tools.ts` into domain-oriented tool modules without changing the public tool contract. continue splitting `packages/api/src/router/assistant-tools.ts` into domain-oriented tool modules without changing the public tool contract.
The next clean slice should stay adjacent to the extracted domains and target one cohesive leftover block such as the remaining country read helpers or other small read-only assistant clusters still living in the monolithic router. The next clean slice should stay adjacent to the extracted domains and target one cohesive leftover block such as the remaining workflow helpers or other small assistant clusters still living in the monolithic router.
## Remaining Major Themes ## Remaining Major Themes
+11 -63
View File
@@ -85,6 +85,10 @@ import {
countryMetroAdminToolDefinitions, countryMetroAdminToolDefinitions,
createCountryMetroAdminExecutors, createCountryMetroAdminExecutors,
} from "./assistant-tools/country-metro-admin.js"; } from "./assistant-tools/country-metro-admin.js";
import {
countryReadmodelToolDefinitions,
createCountryReadmodelExecutors,
} from "./assistant-tools/country-readmodels.js";
import { import {
createUserSelfServiceExecutors, createUserSelfServiceExecutors,
userSelfServiceToolDefinitions, userSelfServiceToolDefinitions,
@@ -431,7 +435,6 @@ const LEGACY_MONOLITHIC_TOOL_ACCESS: Partial<Record<string, ToolAccessRequiremen
get_pending_vacation_approvals: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] }, get_pending_vacation_approvals: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
get_entitlement_summary: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] }, get_entitlement_summary: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
set_entitlement: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] }, set_entitlement: { allowedSystemRoles: [...MANAGER_ASSISTANT_ROLES] },
get_country: { requiresResourceOverview: true },
delete_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] }, delete_project: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
generate_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] }, generate_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
remove_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] }, remove_project_cover: { requiredPermissions: [PermissionKey.MANAGE_PROJECTS] },
@@ -2161,34 +2164,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = withToolAccess([
...clientMutationToolDefinitions, ...clientMutationToolDefinitions,
// ── ADMIN / CONFIG READ TOOLS ── // ── ADMIN / CONFIG READ TOOLS ──
{ ...countryReadmodelToolDefinitions,
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"],
},
},
},
...countryMetroAdminToolDefinitions, ...countryMetroAdminToolDefinitions,
...configReadmodelToolDefinitions, ...configReadmodelToolDefinitions,
...userAdminToolDefinitions, ...userAdminToolDefinitions,
@@ -2627,40 +2603,12 @@ const executors = {
// ── ADMIN / CONFIG ── // ── ADMIN / CONFIG ──
async list_countries(params: { includeInactive?: boolean; search?: string }, ctx: ToolContext) { ...createCountryReadmodelExecutors({
const caller = createCountryCaller(createScopedCallerContext(ctx)); createCountryCaller,
const countries = await caller.list( createScopedCallerContext,
params.includeInactive formatCountry,
? undefined toAssistantCountryNotFoundError,
: { 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);
},
...createCountryMetroAdminExecutors({ ...createCountryMetroAdminExecutors({
createCountryCaller, createCountryCaller,
createScopedCallerContext, 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);
},
};
}