@beignet/core
Version:
Core framework primitives for Beignet
370 lines • 12.9 kB
TypeScript
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