refactor(api): extract assistant self-service slice

This commit is contained in:
2026-03-30 21:31:06 +02:00
parent 72394747f9
commit 7d3c6d978e
3 changed files with 322 additions and 261 deletions
+2 -1
View File
@@ -31,12 +31,13 @@
- the adjacent chargeability/computation read helpers now live in their own domain module, keeping the advanced financial transparency read models out of the monolithic router without changing the assistant contract
- the neighboring country and metro-city admin mutations now live in their own domain module, keeping more settings-side CRUD wiring out of the monolithic router without changing the assistant contract
- the adjacent management-level, utilization, calculation-rule, effort-rule, and experience-multiplier read helpers now live in their own domain module, further shrinking the monolithic assistant router without changing the assistant contract
- the authenticated user self-service assistant helpers now live in their own domain module, covering assignable users, dashboard preferences, favorites, column preferences, and MFA self-service without changing the assistant contract
## Next Up
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.
The next clean slice should stay adjacent to the extracted domains and target one cohesive block such as the authenticated user/self-service assistant tools, or the remaining estimate and project admin helper clusters that are still embedded in the monolithic router.
The next clean slice should stay adjacent to the extracted domains and target one cohesive block such as the remaining admin-user helper cluster around `list_users` and user mutations, or the embedded estimate and project admin helper clusters that are still in the monolithic router.
## Remaining Major Themes
+10 -260
View File
@@ -95,6 +95,10 @@ import {
countryMetroAdminToolDefinitions,
createCountryMetroAdminExecutors,
} from "./assistant-tools/country-metro-admin.js";
import {
createUserSelfServiceExecutors,
userSelfServiceToolDefinitions,
} from "./assistant-tools/user-self-service.js";
import type { ToolContext, ToolDef, ToolExecutor } from "./assistant-tools/shared.js";
import { getCommentToolEntityDescription, getCommentToolScopeSentence } from "../lib/comment-entity-registry.js";
@@ -2794,152 +2798,7 @@ export const TOOL_DEFINITIONS: ToolDef[] = [
},
},
},
{
type: "function",
function: {
name: "list_assignable_users",
description: "List lightweight users available for assignment workflows. Manager or admin role required.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_current_user",
description: "Get the authenticated user's own profile, role, and permission overrides.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_dashboard_layout",
description: "Get the authenticated user's saved dashboard widget layout and last update timestamp.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "save_dashboard_layout",
description: "Save the authenticated user's dashboard layout. Always confirm first.",
parameters: {
type: "object",
properties: {
layout: {
type: "array",
description: "Dashboard layout items as stored by the user router.",
items: { type: "object" },
},
},
required: ["layout"],
},
},
},
{
type: "function",
function: {
name: "get_favorite_project_ids",
description: "Get the authenticated user's favorite project IDs.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "toggle_favorite_project",
description: "Add or remove a project from the authenticated user's favorites. Always confirm first.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID." },
},
required: ["projectId"],
},
},
},
{
type: "function",
function: {
name: "get_column_preferences",
description: "Get the authenticated user's saved table column preferences for all supported views.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "set_column_preferences",
description: "Update the authenticated user's table column preferences for one view. Always confirm first.",
parameters: {
type: "object",
properties: {
view: {
type: "string",
enum: ["resources", "projects", "allocations", "vacations", "roles", "users", "blueprints"],
description: "View key to update.",
},
visible: {
type: "array",
items: { type: "string" },
description: "Visible column IDs.",
},
sort: {
type: ["object", "null"],
properties: {
field: { type: "string" },
dir: { type: "string", enum: ["asc", "desc"] },
},
description: "Sort state. Use null to clear it.",
},
rowOrder: {
type: ["array", "null"],
items: { type: "string" },
description: "Optional row order. Use null to clear it.",
},
},
required: ["view"],
},
},
},
{
type: "function",
function: {
name: "generate_totp_secret",
description: "Generate a new MFA TOTP secret and provisioning URI for the authenticated user. Always confirm first. The secret is sensitive.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "verify_and_enable_totp",
description: "Verify a 6-digit MFA TOTP token and enable MFA for the authenticated user. Always confirm first.",
parameters: {
type: "object",
properties: {
token: { type: "string", description: "6-digit TOTP token." },
},
required: ["token"],
},
},
},
{
type: "function",
function: {
name: "get_mfa_status",
description: "Get the authenticated user's MFA status.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_active_user_count",
description: "Get the number of users active in the last five minutes. Admin role required.",
parameters: { type: "object", properties: {} },
},
},
...userSelfServiceToolDefinitions,
{
type: "function",
function: {
@@ -5482,120 +5341,11 @@ const executors = {
const users = await caller.list();
return users.slice(0, Math.min(params.limit ?? 50, 100));
},
async list_assignable_users(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.listAssignable();
},
async get_current_user(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.me();
},
async get_dashboard_layout(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.getDashboardLayout();
},
async save_dashboard_layout(params: { layout: unknown[] }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const result = await caller.saveDashboardLayout({ layout: params.layout });
return {
__action: "invalidate",
scope: ["dashboard"],
success: true,
...result,
message: "Saved dashboard layout.",
};
},
async get_favorite_project_ids(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.getFavoriteProjectIds();
},
async toggle_favorite_project(params: { projectId: string }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const result = await caller.toggleFavoriteProject({ projectId: params.projectId });
return {
__action: "invalidate",
scope: ["project"],
success: true,
...result,
message: result.added ? "Added project to favorites." : "Removed project from favorites.",
};
},
async get_column_preferences(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.getColumnPreferences();
},
async set_column_preferences(params: {
view: "resources" | "projects" | "allocations" | "vacations" | "roles" | "users" | "blueprints";
visible?: string[];
sort?: { field: string; dir: "asc" | "desc" } | null;
rowOrder?: string[] | null;
}, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const result = await caller.setColumnPreferences({
view: params.view,
...(params.visible !== undefined ? { visible: params.visible } : {}),
...(params.sort !== undefined ? { sort: params.sort } : {}),
...(params.rowOrder !== undefined ? { rowOrder: params.rowOrder } : {}),
});
return {
__action: "invalidate",
scope: ["user"],
success: true,
...result,
message: `Updated column preferences for ${params.view}.`,
};
},
async generate_totp_secret(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const result = await caller.generateTotpSecret();
return {
__action: "invalidate",
scope: ["user"],
success: true,
...result,
message: "Generated a new MFA TOTP secret.",
};
},
async verify_and_enable_totp(params: { token: string }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
let result;
try {
result = await caller.verifyAndEnableTotp({ token: params.token });
} catch (error) {
const mapped = toAssistantTotpEnableError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate",
scope: ["user"],
success: true,
...result,
message: "Enabled MFA TOTP.",
};
},
async get_mfa_status(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.getMfaStatus();
},
async get_active_user_count(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.activeCount();
},
...createUserSelfServiceExecutors({
createUserCaller,
createScopedCallerContext,
toAssistantTotpEnableError,
}),
async create_user(params: {
email: string;
@@ -0,0 +1,310 @@
import type { TRPCContext } from "../../trpc.js";
import type { ToolContext, ToolDef, ToolExecutor } from "./shared.js";
type AssistantToolErrorResult = { error: string };
type ColumnPreferenceView =
| "resources"
| "projects"
| "allocations"
| "vacations"
| "roles"
| "users"
| "blueprints";
type UserSelfServiceDeps = {
createUserCaller: (ctx: TRPCContext) => {
listAssignable: () => Promise<unknown>;
me: () => Promise<unknown>;
getDashboardLayout: () => Promise<unknown>;
saveDashboardLayout: (params: { layout: unknown[] }) => Promise<Record<string, unknown>>;
getFavoriteProjectIds: () => Promise<unknown>;
toggleFavoriteProject: (
params: { projectId: string },
) => Promise<Record<string, unknown> & { added: boolean }>;
getColumnPreferences: () => Promise<unknown>;
setColumnPreferences: (params: {
view: ColumnPreferenceView;
visible?: string[];
sort?: { field: string; dir: "asc" | "desc" } | null;
rowOrder?: string[] | null;
}) => Promise<Record<string, unknown>>;
generateTotpSecret: () => Promise<Record<string, unknown>>;
verifyAndEnableTotp: (params: { token: string }) => Promise<Record<string, unknown>>;
getMfaStatus: () => Promise<unknown>;
activeCount: () => Promise<unknown>;
};
createScopedCallerContext: (ctx: ToolContext) => TRPCContext;
toAssistantTotpEnableError: (
error: unknown,
) => AssistantToolErrorResult | null;
};
export const userSelfServiceToolDefinitions: ToolDef[] = [
{
type: "function",
function: {
name: "list_assignable_users",
description: "List lightweight users available for assignment workflows. Manager or admin role required.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_current_user",
description: "Get the authenticated user's own profile, role, and permission overrides.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_dashboard_layout",
description: "Get the authenticated user's saved dashboard widget layout and last update timestamp.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "save_dashboard_layout",
description: "Save the authenticated user's dashboard layout. Always confirm first.",
parameters: {
type: "object",
properties: {
layout: {
type: "array",
description: "Dashboard layout items as stored by the user router.",
items: { type: "object" },
},
},
required: ["layout"],
},
},
},
{
type: "function",
function: {
name: "get_favorite_project_ids",
description: "Get the authenticated user's favorite project IDs.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "toggle_favorite_project",
description: "Add or remove a project from the authenticated user's favorites. Always confirm first.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID." },
},
required: ["projectId"],
},
},
},
{
type: "function",
function: {
name: "get_column_preferences",
description: "Get the authenticated user's saved table column preferences for all supported views.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "set_column_preferences",
description: "Update the authenticated user's table column preferences for one view. Always confirm first.",
parameters: {
type: "object",
properties: {
view: {
type: "string",
enum: ["resources", "projects", "allocations", "vacations", "roles", "users", "blueprints"],
description: "View key to update.",
},
visible: {
type: "array",
items: { type: "string" },
description: "Visible column IDs.",
},
sort: {
type: ["object", "null"],
properties: {
field: { type: "string" },
dir: { type: "string", enum: ["asc", "desc"] },
},
description: "Sort state. Use null to clear it.",
},
rowOrder: {
type: ["array", "null"],
items: { type: "string" },
description: "Optional row order. Use null to clear it.",
},
},
required: ["view"],
},
},
},
{
type: "function",
function: {
name: "generate_totp_secret",
description: "Generate a new MFA TOTP secret and provisioning URI for the authenticated user. Always confirm first. The secret is sensitive.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "verify_and_enable_totp",
description: "Verify a 6-digit MFA TOTP token and enable MFA for the authenticated user. Always confirm first.",
parameters: {
type: "object",
properties: {
token: { type: "string", description: "6-digit TOTP token." },
},
required: ["token"],
},
},
},
{
type: "function",
function: {
name: "get_mfa_status",
description: "Get the authenticated user's MFA status.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_active_user_count",
description: "Get the number of users active in the last five minutes. Admin role required.",
parameters: { type: "object", properties: {} },
},
},
];
export function createUserSelfServiceExecutors(
deps: UserSelfServiceDeps,
): Record<string, ToolExecutor> {
return {
async list_assignable_users(_params: Record<string, never>, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
return caller.listAssignable();
},
async get_current_user(_params: Record<string, never>, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
return caller.me();
},
async get_dashboard_layout(_params: Record<string, never>, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
return caller.getDashboardLayout();
},
async save_dashboard_layout(params: { layout: unknown[] }, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
const result = await caller.saveDashboardLayout({ layout: params.layout });
return {
__action: "invalidate" as const,
scope: ["dashboard"],
success: true,
...result,
message: "Saved dashboard layout.",
};
},
async get_favorite_project_ids(_params: Record<string, never>, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
return caller.getFavoriteProjectIds();
},
async toggle_favorite_project(params: { projectId: string }, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
const result = await caller.toggleFavoriteProject({ projectId: params.projectId });
return {
__action: "invalidate" as const,
scope: ["project"],
success: true,
...result,
message: result.added ? "Added project to favorites." : "Removed project from favorites.",
};
},
async get_column_preferences(_params: Record<string, never>, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
return caller.getColumnPreferences();
},
async set_column_preferences(params: {
view: ColumnPreferenceView;
visible?: string[];
sort?: { field: string; dir: "asc" | "desc" } | null;
rowOrder?: string[] | null;
}, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
const result = await caller.setColumnPreferences({
view: params.view,
...(params.visible !== undefined ? { visible: params.visible } : {}),
...(params.sort !== undefined ? { sort: params.sort } : {}),
...(params.rowOrder !== undefined ? { rowOrder: params.rowOrder } : {}),
});
return {
__action: "invalidate" as const,
scope: ["user"],
success: true,
...result,
message: `Updated column preferences for ${params.view}.`,
};
},
async generate_totp_secret(_params: Record<string, never>, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
const result = await caller.generateTotpSecret();
return {
__action: "invalidate" as const,
scope: ["user"],
success: true,
...result,
message: "Generated a new MFA TOTP secret.",
};
},
async verify_and_enable_totp(params: { token: string }, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
let result;
try {
result = await caller.verifyAndEnableTotp({ token: params.token });
} catch (error) {
const mapped = deps.toAssistantTotpEnableError(error);
if (mapped) {
return mapped;
}
throw error;
}
return {
__action: "invalidate" as const,
scope: ["user"],
success: true,
...result,
message: "Enabled MFA TOTP.",
};
},
async get_mfa_status(_params: Record<string, never>, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
return caller.getMfaStatus();
},
async get_active_user_count(_params: Record<string, never>, ctx: ToolContext) {
const caller = deps.createUserCaller(deps.createScopedCallerContext(ctx));
return caller.activeCount();
},
};
}