--- status: stable last_updated: 2026-05-23 --- # Repo Adapter: from-dbtype for @alkdev/operations How dbtype schemas produce CRUD `OperationSpec`s for the operations registry. ## Overview The `from-dbtype` adapter consumes the same element tree and Type.Module bundle that produces Drizzle tables and validation schemas, and generates a complete set of CRUD `OperationSpec`s: `findMany`, `findFirst`, `insertOne`, `insert`, `update`, `delete`. This is phase 2 of the project. Phase 1 delivers the core schema, module, and hosts. This document defines the interface and design so phase 1 doesn't paint us into a corner. ## Interface ```typescript interface FromDbTypeConfig { namespace: string // Operation namespace (e.g., 'db') db: AnyDrizzleDB // Drizzle database instance tables: Record // UJSX table elements host: HostConfig<...> // Rendered dialect host module: TModule // Compiled Type.Module bundle (accessed via M.Import()) operations?: Record // Per-table operation selection accessControl?: Record // Per-table access control } type OperationSelection = true | ('findMany' | 'findFirst' | 'insertOne' | 'insert' | 'update' | 'delete')[] type AccessControlMap = Record // operation name -> access control ``` Returns `OperationSpec[]` ready to register with `OperationRegistry`. ## Schema Derivation ### Select Schema The module entry as-is, with nullable columns wrapped in `Type.Union([inner, Type.Null()])`. ### Insert Schema Derived from column metadata: - Remove auto-generated primary keys (`primaryKey: true` with `default`) - Make columns with defaults `Type.Optional` - Make nullable columns `Type.Optional(Type.Union([inner, Type.Null()]))` - Keep required not-null columns mandatory ### Update Schema `Type.Partial(Type.Ref('TableName'))` — all columns optional. ### Filter Schema Per-column comparison operators derived from `DbColumnType`: | Column Type | Available Operators | |-------------|-------------------| | `uuid`, `string` | `eq`, `ne`, `like`, `notLike`, `ilike`, `inArray`, `notInArray`, `isNull`, `isNotNull` | | `integer`, `timestamp` | `eq`, `ne`, `lt`, `lte`, `gt`, `gte`, `inArray`, `notInArray`, `isNull`, `isNotNull` | | `boolean` | `eq`, `ne`, `isNull`, `isNotNull` | | `json`, `array`, `object` | `eq`, `isNull`, `isNotNull` | ```typescript function generateFilterSchema(tableMeta: TableMeta): TObject { const filterProps: Record = {} for (const [name, col] of Object.entries(tableMeta.columns)) { filterProps[name] = Type.Optional(generateColumnFilter(col)) } return Type.Object(filterProps) } ``` ## Handler Generation Each operation's handler uses the rendered Drizzle table and the Drizzle query builder: | Operation | Handler Pattern | |-----------|----------------| | `findMany` | `db.query[tableName].findMany({ where, orderBy, limit, offset, with })` | | `findFirst` | `db.query[tableName].findFirst({ where, orderBy, offset, with })` | | `insertOne` | `db.insert(table).values(input).returning()` | | `insert` | `db.insert(table).values(input.values).returning()` | | `update` | `db.update(table).set(input.set).where(filterToWhere(input.where)).returning()` | | `delete` | `db.delete(table).where(filterToWhere(input.where)).returning()` | `filterToWhere()` translates the TypeBox-validated filter object into Drizzle SQL operators (`eq()`, `and()`, `or()`, `like()`, etc.). ## Access Control The adapter accepts per-table, per-operation `AccessControl` specs that map directly to `OperationSpec.accessControl`: ```typescript accessControl: { users: { findMany: { requiredScopes: ['users:read'] }, insertOne: { requiredScopes: ['users:write'] }, update: { requiredScopes: ['users:write'] }, delete: { requiredScopes: ['users:admin'] }, } } ``` Operations called through `buildEnv()` are `trusted: true` — internal composition skips access control. ## Relations The module's relation entries (`UsersRelations`, `TasksRelations`) drive the `with` parameter in `findMany` and `findFirst`. The adapter: 1. Reads relation entries from the module 2. Generates the Drizzle `relations()` calls from the rendered table objects 3. Passes the full `{ tables, relations }` schema to the Drizzle relational query builder 4. Maps `with` parameter types from the module to the Drizzle `with` API ## Overrides Individual operations can be overridden: ```typescript overrides: { users: { delete: async (input, ctx) => { // Soft delete instead of hard delete return ctx.env.db.update(usersTable) .set({ deletedAt: new Date() }) .where(eq(usersTable.id, input.where.id)) } } } ``` Overrides replace the auto-generated handler but keep the auto-generated `OperationSpec` (input/output schemas, access control). ## Constraints - **Phase 2 concern** — this adapter is not part of phase 1. Phase 1 delivers the schema, module, and hosts. - **The adapter depends on both the element tree and the rendered Drizzle objects** — it needs the tree for schema derivation and the rendered tables for query execution - **Filter operators are column-type-dependent** — a `uuid` column gets different operators than a `boolean` column - **The `with` parameter for relations requires the Drizzle `relations` object** — the adapter generates relations from module entries and rendered table objects (ADR-013) - **Returning clauses use graceful fallback** — all mutations call `.returning()`. The adapter checks the dialect string from the host config (`root.ctx.dialect`) to determine whether to include `.returning()`. Dialects that support it (PG, SQLite) return full result rows. Dialects that don't (MySQL) fall back to validated input (inserts) or affected row count (updates/deletes). This keeps the operations API uniform. See [ADR-012](decisions/012-always-returning-graceful-fallback.md). - **Relations are generated by the adapter, not the host** — the adapter reads `FooRelations` entries from the module and generates Drizzle `relations()` calls using the rendered table objects. No `` element type is needed. See [ADR-013](decisions/013-relation-rendering-adapter.md). ## Open Questions 1. ~~**Per-dialect handler differences?**~~ **Resolved — ADR-012**. Always use `.returning()` with graceful fallback. Dialects that support it return data; dialects that don't fall back to validated input or affected row count. 2. ~~**Schema bundle or separate schemas?**~~ **Resolved — ADR-016**. The adapter accepts the `Type.Module` compiled bundle. `M.Import()` handles ref resolution automatically, which eliminates potential complications with separate schema wiring. Consistent with ADR-010 (derived schemas in the module). 3. ~~**Relation rendering responsibility?**~~ **Resolved — ADR-013**. The adapter generates relations from module entries and rendered table objects. No new `` element type is needed. ## References - Operations registry: `@alkdev/operations` — `OperationSpec`, `OperationRegistry`, `OperationHandler` - Drizzle-GraphQL reference: `/workspace/drizzle-graphql` — CRUD generator pattern - Research: `docs/research/architecture.md` (validation schemas section)