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
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: truewithdefault) - 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:
- Reads relation entries from the module
- Generates the Drizzle
relations()calls from the rendered table objects - Passes the full
{ tables, relations }schema to the Drizzle relational query builder - Maps
withparameter types from the module to the DrizzlewithAPI
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
uuidcolumn gets different operators than abooleancolumn - The
withparameter for relations requires the Drizzlerelationsobject — 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
-
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? -
Schema bundle or separate schemas? Should the adapter accept the
Type.Modulecompiled bundle, or individual schemas? The module is convenient (everything in one place) but couples the adapter to TypeBox's module format. -
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/operations—OperationSpec,OperationRegistry,OperationHandler - Drizzle-GraphQL reference:
/workspace/drizzle-graphql— CRUD generator pattern - Research:
docs/research/architecture.md(validation schemas section)