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
This commit is contained in:
2026-05-22 11:34:58 +00:00
parent 6fe84e1a53
commit dd2ec9df3c
14 changed files with 1447 additions and 48 deletions

View File

@@ -0,0 +1,155 @@
---
status: draft
last_updated: 2026-05-22
---
# 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<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` |
```typescript
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`:
```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 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/operations``OperationSpec`, `OperationRegistry`, `OperationHandler`
- Drizzle-GraphQL reference: `/workspace/drizzle-graphql` — CRUD generator pattern
- Research: `docs/research/architecture.md` (validation schemas section)