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:
155
docs/architecture/repo-adapter.md
Normal file
155
docs/architecture/repo-adapter.md
Normal 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)
|
||||
Reference in New Issue
Block a user