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

74 lines
3.1 KiB
Markdown

---
status: accepted
date: 2026-06-01
supersedes: 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.
```ts
// 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.