Files
storage/docs/architecture/decisions/048-operation-specs-as-repo-surface.md
glm-5.1 412ad98f11 Pivot: fold drizzlebox as utils, HonkerEventTarget, OperationSpecs as repo surface
- Update architecture docs to reflect pivot from @libsql/client to Honker
- Fold @alkdev/drizzlebox Phase 0 into src/sqlite/utils/ (ADR-046)
- Add HonkerEventTarget adapter for pubsub TypedEventTarget (ADR-047)
- Replace hand-written CRUD with OperationSpec generation (ADR-048)
- Resolved OQ-26: Honker replaces Redis for single-node pub/sub (POC validated)
- Updated OQ-17, OQ-18, OQ-19 for OperationSpec repository surface
- Added OQ-30 (composite event target), OQ-31 (consumer naming), OQ-32 (Drizzle Kit)
- POC results: adapter buildable, same-process pub/sub works, transactional
  outbox semantics confirmed, concurrent listeners/streams work
- Research doc at docs/research/pivot-honker-sqlite-adapter.md
2026-06-01 16:31:40 +00:00

3.1 KiB

status, date, supersedes
status date supersedes
accepted 2026-06-01 ADR-033 (partially — OQ-17, OQ-18, OQ-19 updated)

ADR-048: OperationSpecs as Repository Surface

Context

ADR-033 established JSON path queries with hand-written CRUD for v1. The question was whether the repository layer would be hand-written query functions, auto-generated from table definitions, or something else.

The @alkdev/operations package provides OperationSpec — a serializable descriptor for a query, mutation, or subscription operation. Hubs and spokes register specs in an OperationRegistry along with handlers. The operations runtime handles execution, call protocol routing, subscriptions, access control, and validation.

This presents a natural alternative to hand-written CRUD: storage outputs OperationSpec[] describing CRUD operations per table, and the consumer wires them into the operations registry.

Decision

Storage does not ship a "repository layer" of hand-written query functions. Instead, it outputs OperationSpec[] per table — flat arrays describing CRUD operations. The consumer (hub/spoke) imports these specs, registers handlers, and the operations runtime handles execution.

// Storage defines table + operation contracts
export const graphTypes = sqliteTable("graph_types", { ... });
export const graphTypeSpecs: OperationSpec[] = [
  { name: "create", namespace: "graph_types", type: "mutation", ... },
  { name: "find",   namespace: "graph_types", type: "query",     ... },
  { name: "list",   namespace: "graph_types", type: "query",     ... },
  { name: "update", namespace: "graph_types", type: "mutation", ... },
  { name: "delete", namespace: "graph_types", type: "mutation", ... },
];

// Hub registers specs + handlers
for (const spec of graphTypeSpecs) {
  registry.registerSpec(spec);
  registry.registerHandler(`${spec.namespace}.${spec.name}`, handler);
}

The handler is consumer-provided. Storage defines the contract (input/output schemas, operation type); the hub provides the execution (Drizzle queries, Honker transactions, notifications).

@alkdev/operations is a type-only peer dependency of storage. No circular dependency.

Consequences

  • No hand-written repository functions — storage avoids the maintenance burden of typed CRUD code. The contract is the OperationSpec.
  • Consumer owns execution — the hub decides how to handle each operation (raw Drizzle query, with Honker notification, with access control, etc.). Storage doesn't execute queries.
  • Subscriptions for free — OperationSpec supports type: "subscription". Table-level change streams become operation subscriptions rather than custom event handling.
  • Clean dependency graph — storage depends on operations only for the OperationSpec type (peer dep). No runtime dependency.
  • Attribute queries remain JSON path — for metagraph tables where attributes are dynamic JSON, json_extract() is still the query mechanism. Domain-specific tables with native columns produce simpler specs.
  • Supersedes ADR-033 partially — the "hand-written CRUD" part is replaced. The "JSON path queries for attributes" part still applies to metagraph tables.