4.9 KiB
Registry
The OperationRegistry is the central store for operation specs and handlers. It handles registration, validation, access control enforcement, and execution.
Operation Types
enum OperationType {
QUERY = "query",
MUTATION = "mutation",
SUBSCRIPTION = "subscription",
}
- Query — read-only, no side effects
- Mutation — writes state, side effects
- Subscription — streams results over time (async generator handler)
Defining an Operation
Every operation has a spec (serializable metadata) and optionally a handler (the function that runs).
import { Type } from "@alkdev/typebox";
import { OperationType } from "@alkdev/operations";
const createTask = {
name: "create",
namespace: "task",
version: "1.0.0",
type: OperationType.MUTATION,
description: "Create a new task",
inputSchema: Type.Object({
title: Type.String(),
priority: Type.Optional(Type.Union([Type.Literal("low"), Type.Literal("high")])),
}),
outputSchema: Type.Object({
id: Type.String(),
title: Type.String(),
}),
accessControl: {
requiredScopes: ["task:write"],
},
handler: async (input: { title: string; priority?: string }) => {
return { id: crypto.randomUUID(), title: input.title };
},
};
Spec Fields
| Field | Type | Required | Description |
|---|---|---|---|
name |
string |
yes | Operation name within its namespace |
namespace |
string |
yes | Grouping (e.g. "task", "user") |
version |
string |
yes | Semantic version |
type |
OperationType |
yes | query, mutation, or subscription |
description |
string |
yes | Human-readable description |
inputSchema |
TSchema |
yes | TypeBox schema for input validation |
outputSchema |
TSchema |
yes | TypeBox schema for output; use Type.Unknown() if untyped |
accessControl |
AccessControl |
yes | Scopes and resource requirements (see Access Control) |
title |
string |
no | Human-readable title |
tags |
string[] |
no | Tags for filtering/grouping |
errorSchemas |
ErrorDefinition[] |
no | Declared error codes and their schemas |
_meta |
Record<string, unknown> |
no | Arbitrary metadata |
Handler Signature
Query/Mutation handlers return a value (or ResponseEnvelope):
type OperationHandler<TInput, TOutput> = (
input: TInput,
context: OperationContext,
) => Promise<TOutput> | TOutput;
Subscription handlers must be async generators:
type SubscriptionHandler<TInput, TOutput> = (
input: TInput,
context: OperationContext,
) => AsyncGenerator<TOutput, void, unknown>;
Registering Operations
Combined registration
const registry = new OperationRegistry();
registry.register(createTask);
register() stores both the spec and the handler. The handler field is optional — you can register the spec first and add the handler later.
Batch registration
registry.registerAll([createTask, listTasks, deleteTask]);
Separate spec and handler
registry.registerSpec(mySpec);
registry.registerHandler("task.create", myHandler);
This is useful when specs come from one source (e.g., OpenAPI import) and handlers from another.
registerHandlerthrows if no spec exists for the operation ID.
Executing Operations
const envelope = await registry.execute(
"task.create",
{ title: "Ship it" },
{ identity: { id: "user-1", scopes: ["task:write"] } },
);
What execute() does:
- Looks up the spec by
namespace.name - Looks up the handler
- Enforces access control (unless
context.trustedistrue) - Validates input against the spec's
inputSchema - Runs the handler
- Wraps the result in a
ResponseEnvelopeif not already one - Casts the output through
outputSchemaand warns on validation errors
Returns ResponseEnvelope<TOutput>. See Response Envelopes.
Execution context
interface OperationContext {
requestId?: string;
parentRequestId?: string;
identity?: Identity;
trusted?: boolean; // set by buildEnv, not by callers
metadata?: Record<string, unknown>;
env?: OperationEnv; // injected by buildEnv for inter-op calls
}
Querying the Registry
registry.get("task.create"); // spec + handler, or undefined
registry.getSpec("task.create"); // spec only
registry.getHandler("task.create"); // handler only
registry.getByName("task", "create"); // same as get("task.create")
registry.list(); // all specs + handlers
registry.getAllSpecs(); // all specs only
Schema Adapters
By default, the registry expects TypeBox schemas. If you use Zod or Valibot, pass a schema adapter:
import { zodAdapter } from "@alkdev/operations/from-typemap";
const adapter = zodAdapter();
await adapter.init();
const registry = new OperationRegistry({ schemaAdapter: adapter });
See Adapters for details.