UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

370 lines 12.9 kB
import type { StandardSchemaV1 } from "@standard-schema/spec"; import { type EventPublishOptions } from "../events/index.js"; /** * Any Standard Schema compatible validator. */ export type StandardSchema = StandardSchemaV1<unknown, unknown>; /** * Infer the parsed output type from a Standard Schema. */ export type InferOutput<T extends StandardSchemaV1> = StandardSchemaV1.InferOutput<T>; /** * Infer the input type accepted by a Standard Schema. */ export type InferInput<T extends StandardSchemaV1> = StandardSchemaV1.InferInput<T>; type SchemaOutput<T> = T extends StandardSchemaV1 ? InferOutput<T> : never; /** * Boundary phase that failed use-case schema validation. */ export type UseCaseValidationPhase = "input" | "output"; /** * Error thrown when a use case input or output fails schema validation. */ export declare class UseCaseValidationError extends Error { readonly name = "UseCaseValidationError"; readonly useCaseName: string; readonly phase: UseCaseValidationPhase; readonly issues: readonly StandardSchemaV1.Issue[]; constructor(args: { useCaseName: string; phase: UseCaseValidationPhase; issues: readonly StandardSchemaV1.Issue[]; }); } /** * Error thrown when a use case tries to emit an event it did not declare with * `.emits(...)`. */ export declare class UseCaseEventDeclarationError extends Error { readonly name = "UseCaseEventDeclarationError"; readonly useCaseName: string; readonly eventName: string; readonly declaredEventNames: readonly string[]; constructor(args: { useCaseName: string; eventName: string; declaredEventNames: readonly string[]; }); } /** * Error thrown when a use-case event helper fails to validate an event payload * before recording or publishing it. */ export declare class UseCaseEventValidationError extends Error { readonly name = "UseCaseEventValidationError"; readonly useCaseName: string; readonly eventName: string; readonly issues: readonly StandardSchemaV1.Issue[]; constructor(args: { useCaseName: string; eventName: string; issues: readonly StandardSchemaV1.Issue[]; }); } /** * Minimal domain event definition accepted by use-case event helpers. * * This structurally matches events from `@beignet/core/events` and compatible * app-owned definitions. */ export interface DomainEventLike { /** * Stable event name. */ name: string; /** * Standard Schema payload validator. */ payload: StandardSchema; } /** * Infer the output payload type from a use-case event definition. */ export type InferUseCaseEventPayload<E extends DomainEventLike> = E["payload"] extends StandardSchemaV1<unknown, infer Output> ? Output : never; /** * Minimal recorder shape accepted by use-case event helpers. */ export interface UseCaseEventRecorderTarget { /** * Record a domain event payload. */ record<E extends DomainEventLike>(event: E, payload: InferUseCaseEventPayload<E>, options?: EventPublishOptions): Promise<void> | void; } /** * Minimal event-bus shape accepted by use-case event helpers. */ export interface UseCaseEventBusTarget { /** * Publish a domain event payload. */ publish<E extends DomainEventLike>(event: E, payload: InferUseCaseEventPayload<E>, options?: EventPublishOptions): Promise<void> | void; } /** * Event helper scoped to the events declared by a use case. */ export interface UseCaseEventHelpers<Emits extends readonly DomainEventLike[]> { /** * The exact event definitions declared with `.emits(...)`. */ readonly declared: Emits; /** * Return whether an event is declared by this use case. */ isDeclared(event: DomainEventLike): boolean; /** * Throw if an event is not declared by this use case. */ assertDeclared(event: DomainEventLike): void; /** * Validate and record a declared event into a transaction-scoped recorder. */ record<E extends Emits[number]>(recorder: UseCaseEventRecorderTarget, event: E, payload: InferUseCaseEventPayload<E>): Promise<void>; /** * Validate and publish a declared event directly through an event bus. */ publish<E extends Emits[number]>(eventBus: UseCaseEventBusTarget, event: E, payload: InferUseCaseEventPayload<E>): Promise<void>; } /** * Use case kind - distinguishes commands (write/side-effect) from queries (read-only) */ export type UseCaseKind = "command" | "query"; /** * Symbol key for the trusted run path attached to finalized use cases. * * The server route binder calls this method instead of `run` when the route's * input was already validated by the exact same schema object at the HTTP * boundary. It behaves like `run` but skips the input parse; output * validation, instrumentation, events, and `onRun` are unchanged. * * The key uses `Symbol.for(...)` so the binder and the application builder * agree on the key even across separately bundled copies of the package. */ export declare const USE_CASE_TRUSTED_RUN: unique symbol; /** * Finalized use case definition. * * Use cases validate their input before `run(...)` executes and validate their * output before returning, unless validation is disabled on the builder. */ export interface UseCaseDef<Ctx, Name extends string, Kind extends UseCaseKind, InputSchema extends StandardSchemaV1, OutputSchema extends StandardSchemaV1, Emits extends readonly DomainEventLike[] = readonly []> { /** * Stable use-case name, usually namespaced by feature. */ name: Name; /** * Whether this use case is a command or query. */ kind: Kind; /** Input schema, suitable for reuse in HTTP contracts and forms. */ inputSchema: InputSchema; /** Output schema, suitable for reuse in HTTP contracts and clients. */ outputSchema: OutputSchema; /** * Domain events this use case is allowed to record or publish through the * scoped `events` helper. */ emits: Emits; /** * Execute the use case with application context and typed input. */ run: (args: { ctx: Ctx; input: InferInput<InputSchema>; }) => Promise<InferOutput<OutputSchema>>; } /** * Event passed to the `onRun` hook for instrumentation. */ export interface UseCaseRunEvent<Ctx> { /** * Use-case name. */ name: string; /** * Use-case kind. */ kind: UseCaseKind; /** * Execution phase being observed. */ phase: "start" | "end" | "error"; /** * Elapsed time for end/error events. */ durationMs?: number; /** * Error captured for error events. */ error?: unknown; /** * Application context used for the run. */ ctx: Ctx; } /** * Options for `createUseCase(...)`. */ export interface CreateUseCaseOptions<Ctx> { /** * Optional app-owned observer called on use case start, end, and error. * * Observers run in addition to the built-in instrumentation. */ onRun?: (event: UseCaseRunEvent<Ctx>) => void | Promise<void>; /** * Built-in use-case instrumentation. * * By default every run records `usecase` lifecycle events (plus `error` * events for failed runs) into the provider instrumentation port resolved * from `ctx.ports` (`ports.instrumentation`, then `ports.devtools`). When no * port is installed, runs stay silent. Pass `false` to opt out. * * @default true */ instrumentation?: boolean; /** * Enable or disable schema validation for use case boundaries. * * Defaults to validating both input and output. Pass `false` to opt out, or * configure phases independently with `{ input: boolean, output: boolean }`. */ validate?: boolean | { input?: boolean; output?: boolean; }; } type ValidationOptions = { input: boolean; output: boolean; }; /** * Internal configuration for the use case builder */ interface UseCaseBuilderConfig<Name extends string, Kind extends UseCaseKind, InputSchema extends StandardSchemaV1 | undefined, OutputSchema extends StandardSchemaV1 | undefined, Emits extends readonly DomainEventLike[]> { name: Name; kind: Kind; input?: InputSchema; output?: OutputSchema; emits: Emits; } /** * Fluent builder for creating use cases */ declare class UseCaseBuilder<Ctx, Name extends string, Kind extends UseCaseKind, InputSchema extends StandardSchemaV1 | undefined, OutputSchema extends StandardSchemaV1 | undefined, Emits extends readonly DomainEventLike[] = readonly []> { private readonly config; private readonly onRun?; private readonly validation; private readonly instrumented; constructor(config: UseCaseBuilderConfig<Name, Kind, InputSchema, OutputSchema, Emits>, onRun?: ((event: UseCaseRunEvent<Ctx>) => void | Promise<void>) | undefined, validation?: ValidationOptions, instrumented?: boolean); /** * Define the input schema for this use case */ input<I extends StandardSchemaV1>(schema: I): UseCaseBuilder<Ctx, Name, Kind, I, OutputSchema, Emits>; /** * Define the output schema for this use case */ output<O extends StandardSchemaV1>(schema: O): UseCaseBuilder<Ctx, Name, Kind, InputSchema, O, Emits>; /** * Define the domain events that this use case may emit. */ emits<E extends readonly DomainEventLike[]>(events: E): UseCaseBuilder<Ctx, Name, Kind, InputSchema, OutputSchema, E>; /** * Define the run function and finalize the use case definition */ run(fn: InputSchema extends StandardSchemaV1 ? OutputSchema extends StandardSchemaV1 ? (args: { ctx: Ctx; input: SchemaOutput<InputSchema>; events: UseCaseEventHelpers<Emits>; }) => Promise<SchemaOutput<OutputSchema>> | SchemaOutput<OutputSchema> : never : never): InputSchema extends StandardSchemaV1 ? OutputSchema extends StandardSchemaV1 ? UseCaseDef<Ctx, Name, Kind, InputSchema, OutputSchema, Emits> : never : never; } /** * Root builder returned by createUseCase. */ export interface UseCaseBuilderRoot<Ctx> { /** * Create a command use case (write/side-effect path) */ command<Name extends string>(name: Name): UseCaseBuilder<Ctx, Name, "command", undefined, undefined, readonly []>; /** * Create a query use case (read-only path) */ query<Name extends string>(name: Name): UseCaseBuilder<Ctx, Name, "query", undefined, undefined, readonly []>; } /** * Infer the application context type from a finalized use case. */ export type UseCaseContext<TUseCase> = TUseCase extends { run: (args: { ctx: infer Ctx; input: infer _Input; }) => Promise<infer _Output>; } ? Ctx : never; /** * Infer the public input type accepted by a finalized use case. */ export type UseCaseInput<TUseCase> = TUseCase extends { run: (args: { ctx: infer _Ctx; input: infer Input; }) => Promise<infer _Output>; } ? Input : never; /** * Infer the public output type returned by a finalized use case. */ export type UseCaseOutput<TUseCase> = TUseCase extends { run: (args: { ctx: infer _Ctx; input: infer _Input; }) => Promise<infer Output>; } ? Output : never; type MaybePromise<T> = T | Promise<T>; /** * Small test harness for running use cases with typed inputs. */ export interface UseCaseTester<Ctx> { /** * Create a fresh test context. */ ctx(): Promise<Ctx>; /** * Run a use case with a typed input and either a fresh or explicit context. */ run<Input, Output>(useCase: { run(args: { ctx: Ctx; input: Input; }): Promise<Output>; }, input: Input, options?: { ctx?: Ctx; }): Promise<Output>; } /** * Create a small test harness for use cases. * * Pass a context factory when tests mutate ports or state. Pass a fixed context * for simple, immutable tests. */ export declare function createUseCaseTester<Ctx>(createContext: Ctx | (() => MaybePromise<Ctx>)): UseCaseTester<Ctx>; /** * Create a use case builder with a specific context type. * * Create this once in app code, usually in `lib/use-case.ts`, then import that * configured builder from feature use-case modules. * * @example * ```ts * export const useCase = createUseCase<AppContext>(); * * export const createTodo = useCase * .command("todos.create") * .input(CreateTodoInput) * .output(CreateTodoOutput) * .run(async ({ ctx, input }) => ctx.ports.todos.create(input)); * ``` * * @param options - Optional instrumentation and validation configuration. * @returns A root builder for command and query use cases. */ export declare function createUseCase<Ctx>(options?: CreateUseCaseOptions<Ctx>): UseCaseBuilderRoot<Ctx>; export {}; //# sourceMappingURL=index.d.ts.map