import type { Transaction } from "@tailor-platform/sdk/kysely"; import { logger } from "@tailor-platform/sdk/runtime"; import type { InsufficientPermissionError } from "./errors"; import { UnauthenticatedError } from "./errors"; import { errorAttrs, inputAttr } from "./logAttrs"; import { requirePermission } from "./permissions"; import { err, type Result } from "./result"; import type { CallerContext, CommandContext } from "./types"; export type Command> = ( // oxlint-disable-next-line typescript/no-explicit-any -- generic Transaction type db: Transaction, input: TInput, ctx: CallerContext, ) => Promise; type CommandResult> = | TReturn | Result> | Result>; export function defineCommand< TDeps extends unknown[], TInput, TReturn extends Result, >( name: string, permission: string, impl: ( // oxlint-disable-next-line typescript/no-explicit-any -- generic Transaction type db: Transaction, input: TInput, ctx: CommandContext, ...deps: TDeps ) => Promise, ): (...deps: TDeps) => Command> { return (...deps) => async (db, input, ctx) => { if (!db.isTransaction) { throw new Error("Commands require a transaction database"); } // A command attributes its work to an actor — audit columns take the id — so // there is nothing sensible to run as when the caller is anonymous. const { actorId } = ctx; if (actorId === null) { logger.debug("command unauthenticated", { command: name }); return err(new UnauthenticatedError()); } const authenticated: CommandContext = { ...ctx, actorId, permissions: ctx.permissions ?? [] }; const check = requirePermission(authenticated, permission); if (!check.ok) { logger.debug("command permission denied", { command: name, permission }); return check; } logger.debug("command start", { command: name, input: inputAttr(input) }); const start = Date.now(); const result = await impl(db, input, authenticated, ...deps); const durationMs = Date.now() - start; if ("ok" in result && !result.ok) { logger.debug("command error", { command: name, durationMs, ...errorAttrs(result.error), }); } else { logger.debug("command ok", { command: name, durationMs }); } return result; }; }