Files
dbtype/docs/architecture/repo-adapter.md
glm-5.1 dd2ec9df3c Add SDD architecture docs for dbtype
Phase 0 architecture specification following the alkdev documentation
pattern from @alkdev/flowgraph. Documents the validated architecture
(UJSX elements → Type.Module → Drizzle hosts) based on e2e probe results.

Docs added:
- README: Project overview, architecture, current state
- architecture/README: Index, design decisions, relationships
- architecture/schema: Type.Module as bundle, construction, serialization
- architecture/hosts: HostConfig per dialect, column mapping, symbolic defaults
- architecture/elements: UJSX element types, props, function components
- architecture/module: Module mechanics, format registration, diffing
- architecture/repo-adapter: from-dbtype operations adapter (phase 2)
- architecture/build-distribution: Package structure, exports
- architecture/open-questions: 10 open questions across all topics
- ADRs 001-005: UJSX as IR, Type.Module, HostConfig, format, repo adapter
2026-05-22 11:34:58 +00:00

6.4 KiB

status, last_updated
status last_updated
draft 2026-05-22

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
  schema?: Record<string, TObject>              // Optional: Type.Module entries for select/insert/update
  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 host must also render relations, not just tables
  • Returning clauses are dialect-specific — PG supports .returning() on all mutations, MySQL does not. The adapter must handle this per-dialect.

Open Questions

  1. Per-dialect handler differences? PG has .returning() on all mutations, MySQL often doesn't. Should the adapter handle this transparently, or expose it in the config?

  2. Schema bundle or separate schemas? Should the adapter accept the Type.Module compiled bundle, or individual schemas? The module is convenient (everything in one place) but couples the adapter to TypeBox's module format.

  3. Relation rendering responsibility? Should the host render relations (new <relation> element), or should the adapter generate them from the module's relation entries?

References

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