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:
51
docs/architecture/decisions/001-ujsx-as-ir.md
Normal file
51
docs/architecture/decisions/001-ujsx-as-ir.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# ADR-001: UJSX Element Tree as the IR
|
||||
|
||||
## Status
|
||||
|
||||
Proposed
|
||||
|
||||
## Context
|
||||
|
||||
The research docs (`docs/research/architecture.md`) proposed a `DbTypeBuilder` class with methods like `DbType.String()`, `DbType.Table()` — a programmatic API that produces `TDbColumn` objects wrapping TypeBox schemas with database metadata. This is the same pattern used by drizzle-orm itself (builder methods returning column objects).
|
||||
|
||||
Meanwhile, `@alkdev/ujsx` provides an element-based system where `<table>`, `<column>` trees carry the same metadata in props, compose via function components, and render to dialect-specific outputs via `HostConfig`.
|
||||
|
||||
These two approaches produce the same output (TypeBox schemas + Drizzle column builders) but differ in authoring surface and composability.
|
||||
|
||||
## Decision
|
||||
|
||||
Use UJSX element trees as the IR, not the `DbTypeBuilder` API.
|
||||
|
||||
## Rationale
|
||||
|
||||
1. **Composition**: Function components (`IdColumn`, `AuditColumns`) compose naturally in the element tree. A `DbTypeBuilder` API would need spread patterns or explicit column merging to achieve the same result.
|
||||
|
||||
2. **No duplication**: The `DbTypeBuilder` pattern would create a parallel API to what UJSX elements already provide. Both would carry type, notNull, primaryKey, default metadata. The element tree IS the IR.
|
||||
|
||||
3. **Future extensibility**: The UJSX reconciler (`prepareUpdate`/`commitUpdate`) can diff old and new element trees for schema migration. A builder API has no diffing capability — you'd need to compare two separate builder call sites.
|
||||
|
||||
4. **Already proven**: The e2e probe (`scripts/probe-e2e.ts`) validates the full pipeline: UJSX elements → extractTable → Type.Module → Value.Check → Drizzle rendering. All 28 assertions pass.
|
||||
|
||||
5. **Consistency with sibling projects**: Flowgraph uses UJSX for workflow templates. Using the same pattern for dbtype creates a unified mental model across the alkdev stack.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Single authoring surface (elements) instead of two (elements + builder)
|
||||
- Function components for reusable column groups (the storage_sqlite `commonCols` pattern, but as composable components)
|
||||
- Reconciler-ready for future migration support
|
||||
- Leverages existing `@alkdev/ujsx` infrastructure
|
||||
|
||||
### Negative
|
||||
|
||||
- Requires `@alkdev/ujsx` as a peer dependency (adds a dependency)
|
||||
- JSX syntax requires build configuration (`jsxImportSource`), though `h()` calls work without it
|
||||
- Less familiar to developers used to drizzle's builder API
|
||||
- Function component resolution must happen before host rendering — the tree must be "resolved" (function components called) before the host processes it. The probe validates this works.
|
||||
|
||||
## References
|
||||
|
||||
- Probe validation: `scripts/probe-e2e.ts`
|
||||
- Research comparison: `docs/research/architecture.md` (DbTypeBuilder section)
|
||||
- UJSX architecture: `@alkdev/ujsx/docs/architecture/`
|
||||
50
docs/architecture/decisions/002-type-module-as-bundle.md
Normal file
50
docs/architecture/decisions/002-type-module-as-bundle.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# ADR-002: Type.Module as the Schema Bundle
|
||||
|
||||
## Status
|
||||
|
||||
Proposed
|
||||
|
||||
## Context
|
||||
|
||||
Schemas need to be stored, validated, related, and serialized. The research docs proposed a custom `TransformRegistry` and `DbType` kind system for this. `@alkdev/typebox` already provides `Type.Module` with `Type.Ref` resolution, `Value.Check` validation, and JSON Schema serialization — exactly what we need.
|
||||
|
||||
## Decision
|
||||
|
||||
Use `Type.Module` as the schema bundle. No custom registry or kind system.
|
||||
|
||||
## Rationale
|
||||
|
||||
1. **Ref resolution works**: `Type.Ref('TableName')` inside a module resolves correctly for forward references, circular references, and mutual references. No import-order management, no separate `relations.ts`.
|
||||
|
||||
2. **Validation works**: `Value.Check(M.Import('Users'), userData)` validates after format registration. The module carries all `$defs` and resolves `$ref` pointers.
|
||||
|
||||
3. **Serialization works**: `JSON.stringify(M.Import('Users'))` produces valid JSON Schema with `$defs`. This enables migration diffing via `Value.Diff`.
|
||||
|
||||
4. **Incremental construction**: Build the defs map as a plain `Record<string, TSchema>`, mutate it, then compile with `Type.Module`. No special API needed.
|
||||
|
||||
5. **Derived schemas**: `Type.Partial(Type.Ref('Users'))` produces an update schema. Insert schemas can be added as explicit module entries. No custom derivation logic needed.
|
||||
|
||||
6. **No new abstraction**: Using what TypeBox already provides means less code to maintain and fewer custom kind registrations.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- No custom kind system or registry to maintain
|
||||
- Circular/mutual references handled natively
|
||||
- Free JSON Schema serialization for migration diffing
|
||||
- Free validation via Value.Check
|
||||
- Free derived schemas via Type.Partial + Type.Ref
|
||||
|
||||
### Negative
|
||||
|
||||
- Module keys are a flat namespace — no nested paths like `"tables/Users"` (acceptable, table names are already unique within a database)
|
||||
- `Type.Ref` outside a module has `static: unknown` — always use `M.Import(key)` for type inference
|
||||
- Every `Import()` call embeds the full `$defs` — increases JSON Schema payload size for individual schema exports (acceptable for our use case)
|
||||
- Custom column metadata (`notNull`, `primaryKey`, `default`) is not in the TypeBox schema — it lives in the element props and column metadata extracted during tree walking. The module carries validation types, not database constraints.
|
||||
|
||||
## References
|
||||
|
||||
- Probe validation: `scripts/probe-e2e.ts`
|
||||
- TypeBox Module API: `@alkdev/typebox/src/type/module/module.ts`
|
||||
- Research comparison: `docs/research/architecture.md` (DbType IR section), `docs/research/typedef-kind-pattern.md`
|
||||
49
docs/architecture/decisions/003-hostconfig-for-dialects.md
Normal file
49
docs/architecture/decisions/003-hostconfig-for-dialects.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# ADR-003: HostConfig for Dialect Rendering
|
||||
|
||||
## Status
|
||||
|
||||
Proposed
|
||||
|
||||
## Context
|
||||
|
||||
The research docs proposed a `TransformRegistry` with priority-sorted rules matching `DbType:*` kinds to dialect-specific Drizzle column builders. UJSX provides `HostConfig` — a similar pattern where `createInstance(tag, props, ctx)` maps element types to output instances.
|
||||
|
||||
These solve the same problem: dispatch a schema element to the appropriate dialect-specific builder.
|
||||
|
||||
## Decision
|
||||
|
||||
Use `HostConfig` for dialect rendering, not a custom `TransformRegistry`.
|
||||
|
||||
## Rationale
|
||||
|
||||
1. **Same pattern, more capabilities**: Both `HostConfig.createInstance` and `TransformRule.transform` map an input type to an output instance. HostConfig adds `appendChild` (for composing columns into tables), `finalizeInstance` (for calling `sqliteTable()` after all columns are ready), and `prepareUpdate`/`commitUpdate` (for future migration diffing).
|
||||
|
||||
2. **Function components are free composition**: `<IdColumn />`, `<AuditColumns />` compose via the UJSX reconciliation. A TransformRegistry would need its own composition mechanism.
|
||||
|
||||
3. **Reconciler for free**: The UJSX reconciler handles element diffing, key-based matching, and child reordering. For future schema migration, `prepareUpdate`/`commitUpdate` can diff old and new element props to compute ALTER TABLE statements.
|
||||
|
||||
4. **Context for dialect config**: `createRootContext` carries dialect-specific configuration (naming conventions, type mappings, enum pre-declarations).
|
||||
|
||||
5. **Already proven**: The probe scripts use HostConfig to render element trees to Drizzle SQLite tables. The pattern works end-to-end.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Leverages existing UJSX infrastructure
|
||||
- Future migration support via reconciler
|
||||
- Function component composition for common column patterns
|
||||
- Context propagation for dialect-specific config
|
||||
|
||||
### Negative
|
||||
|
||||
- `createTextInstance` is required by HostConfig but semantically wrong for DB schemas — must throw or noop
|
||||
- More infrastructure than a simple TransformRegistry (fiber tree, reconciler)
|
||||
- HostConfig is defined in `@alkdev/ujsx`, creating a tighter coupling between the packages
|
||||
- The transform registry from the research docs could handle cross-dialect conversion (PG → SQLite), which HostConfig doesn't address — but we don't need cross-dialect conversion because the element tree IS dialect-agnostic
|
||||
|
||||
## References
|
||||
|
||||
- Probe host rendering: `scripts/probe-e2e.ts`
|
||||
- UJSX HostConfig: `@alkdev/ujsx/docs/architecture/host-config.md`
|
||||
- Research comparison: `docs/research/architecture.md` (Transform Registry section), `docs/research/typemap-architecture.md` (module structure)
|
||||
46
docs/architecture/decisions/004-format-annotation-only.md
Normal file
46
docs/architecture/decisions/004-format-annotation-only.md
Normal file
@@ -0,0 +1,46 @@
|
||||
# ADR-004: Column Formats Are Annotations
|
||||
|
||||
## Status
|
||||
|
||||
Proposed
|
||||
|
||||
## Context
|
||||
|
||||
When a column has `type: 'uuid'` or `type: 'string'` with `format: 'email'`, the `format` field in the TypeBox schema (`Type.String({ format: 'uuid' })`) serves two purposes:
|
||||
1. **Validation metadata** — should `Value.Check` enforce the format?
|
||||
2. **Database metadata** — should the PG host render it as `uuid()` instead of `text()`?
|
||||
|
||||
TypeBox follows JSON Schema semantics: `format` is annotation-only by default. `Value.Check` does not enforce format strings unless validators are registered via `FormatRegistry.Set`.
|
||||
|
||||
## Decision
|
||||
|
||||
Treat `format` as annotation-only in the TypeBox schema. Register specific format validators when validation is needed. Use the column `type` prop (not `format`) for dialect-specific mapping.
|
||||
|
||||
## Rationale
|
||||
|
||||
1. **Probing confirmed this behavior**: `Type.String({ format: 'uuid' })` fails `Value.Check` until `FormatRegistry.Set('uuid', validator)` is called. This is correct JSON Schema behavior.
|
||||
|
||||
2. **Dialect mapping uses `type`, not `format`**: The PG host maps `<column type="uuid">` to `uuid()`. The SQLite host maps the same to `text()`. The `format` field is an additional hint for validation, not the driver for database rendering.
|
||||
|
||||
3. **Explicit is better than implicit**: Requiring `FormatRegistry.Set` for custom formats makes it clear when validation is active. No silent enforcement of formats the application doesn't care about.
|
||||
|
||||
4. **`format` is still useful**: It provides machine-readable metadata in the JSON Schema serialization. API generators (OpenAPI, GraphQL) can use `format: 'uuid'` or `format: 'email'` for documentation and client-side validation.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Correct JSON Schema semantics
|
||||
- No surprise validation failures
|
||||
- `format` still carries metadata for non-validation purposes
|
||||
- Dialect mapping is explicit (based on `type` prop)
|
||||
|
||||
### Negative
|
||||
|
||||
- Applications must explicitly register format validators to get format enforcement
|
||||
- Two fields (`type` and `format`) encode overlapping information (e.g., `type: 'uuid'` implies `format: 'uuid'`)
|
||||
|
||||
## References
|
||||
|
||||
- Format registration: `scripts/probe-e2e.ts`
|
||||
- TypeBox FormatRegistry: `@alkdev/typebox/src/type/registry/format.ts`
|
||||
47
docs/architecture/decisions/005-repo-as-adapter.md
Normal file
47
docs/architecture/decisions/005-repo-as-adapter.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# ADR-005: CRUD Generation as an Operations Adapter
|
||||
|
||||
## Status
|
||||
|
||||
Proposed
|
||||
|
||||
## Context
|
||||
|
||||
drizzle-graphql auto-generates CRUD operations (findMany, insert, update, delete) from Drizzle schemas. The dbtype architecture has enough information (element tree → module → schemas + rendered Drizzle tables) to do the same, but outputting `OperationSpec`s for `@alkdev/operations` instead of GraphQL resolvers.
|
||||
|
||||
This could be a core feature of dbtype, or a separate adapter package.
|
||||
|
||||
## Decision
|
||||
|
||||
CRUD generation is a separate adapter (`@alkdev/dbtype/repo` or a future `@alkdev/dbtype-operations` package), not a core feature.
|
||||
|
||||
## Rationale
|
||||
|
||||
1. **Separation of concerns**: dbtype's core job is "define once, validate and deploy anywhere" — schema definition, validation, and dialect rendering. CRUD generation is a different concern (API generation from schemas).
|
||||
|
||||
2. **The adapter consumes the same inputs**: The `FromDbType` adapter takes the element tree, the Type.Module, and the rendered Drizzle tables — all produced by dbtype core. It's a consumer, not a producer.
|
||||
|
||||
3. **Optional dependency**: Not every dbtype user needs CRUD operations. Keeping it in core would force `@alkdev/operations` as a dependency.
|
||||
|
||||
4. **The adapter pattern is proven**: `@alkdev/operations` already has `from-openapi`, `from-mcp`, and `from-typemap` adapters. `from-dbtype` follows the same pattern.
|
||||
|
||||
5. **Phase ordering**: Core (phase 1) must be stable before building the adapter (phase 2). The adapter depends on stable schema extraction APIs, which don't exist yet.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Core package stays focused on schema + rendering
|
||||
- Users who only need validation and Drizzle rendering don't pull in operations
|
||||
- The adapter can evolve independently
|
||||
- Follows the established adapter pattern from @alkdev/operations
|
||||
|
||||
### Negative
|
||||
|
||||
- Two packages to install for the full experience
|
||||
- The adapter needs access to both Type.Module entries (for schemas) and rendered Drizzle tables (for query execution), which means the rendering pipeline must expose both outputs
|
||||
|
||||
## References
|
||||
|
||||
- Operations adapters: `@alkdev/operations/from-openapi`, `@alkdev/operations/from-mcp`
|
||||
- drizzle-GraphQL reference: `/workspace/drizzle-graphql`
|
||||
- Repo adapter design: [repo-adapter.md](repo-adapter.md)
|
||||
Reference in New Issue
Block a user