Files
dbtype/docs/architecture/repo-adapter.md
glm-5.1 da65f7b693 docs: resolve OQ-11 and OQ-12 with ADR-015 and ADR-016
ADR-015: <table> accepts extraConfig prop mirroring Drizzle's third
callback argument for composite unique constraints and other
column-reference-based features.

ADR-016: Adapter accepts Type.Module compiled bundle (not individual
schemas). M.Import() handles ref resolution automatically, eliminating
potential complications with separate schema wiring.

Update FromDbTypeConfig interface to use module instead of schema,
update element types table with extraConfig prop.
2026-05-23 13:02:23 +00:00

7.2 KiB

status, last_updated
status last_updated
stable 2026-05-23

Repo Adapter: from-dbtype for @alkdev/operations

How dbtype schemas produce CRUD OperationSpecs 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 OperationSpecs: 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

interface FromDbTypeConfig {
  namespace: string                              // Operation namespace (e.g., 'db')
  db: AnyDrizzleDB                              // Drizzle database instance
  tables: Record<string, UElement>              // UJSX table elements
  host: HostConfig<...>                          // Rendered dialect host
  module: TModule                                // Compiled Type.Module bundle (accessed via M.Import())
  operations?: Record<string, OperationSelection> // Per-table operation selection
  accessControl?: Record<string, AccessControlMap> // Per-table access control
}

type OperationSelection = true | ('findMany' | 'findFirst' | 'insertOne' | 'insert' | 'update' | 'delete')[]

type AccessControlMap = Record<string, AccessControl>  // 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
function generateFilterSchema(tableMeta: TableMeta): TObject {
  const filterProps: Record<string, any> = {}
  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:

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:

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.
  • 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 <relation> element type is needed. See ADR-013.

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 <relation> element type is needed.

References

  • Operations registry: @alkdev/operationsOperationSpec, OperationRegistry, OperationHandler
  • Drizzle-GraphQL reference: /workspace/drizzle-graphql — CRUD generator pattern
  • Research: docs/research/architecture.md (validation schemas section)