docs: resolve OQ-11 and OQ-12 with ADR-015 and ADR-016
ADR-015: <table> accepts extraConfig prop mirroring Drizzle's third callback argument for composite unique constraints and other column-reference-based features. ADR-016: Adapter accepts Type.Module compiled bundle (not individual schemas). M.Import() handles ref resolution automatically, eliminating potential complications with separate schema wiring. Update FromDbTypeConfig interface to use module instead of schema, update element types table with extraConfig prop.
This commit is contained in:
86
docs/architecture/decisions/015-table-extraconfig-props.md
Normal file
86
docs/architecture/decisions/015-table-extraconfig-props.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# ADR-015: `<table>` Accepts `extraConfig` Props
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
Drizzle tables accept a third callback argument that receives a column builder and returns an object for indexes, unique constraints, and computed columns defined in terms of column references:
|
||||
|
||||
```typescript
|
||||
pgTable('users', {
|
||||
id: uuid('id').primaryKey(),
|
||||
email: varchar('email').notNull(),
|
||||
name: varchar('name'),
|
||||
}, (t) => ({
|
||||
uniqueEmail: unique().on(t.email),
|
||||
compositeKey: unique().on(t.email, t.name),
|
||||
}))
|
||||
```
|
||||
|
||||
The current `<table>` element only has a `name` prop. The `<index>` and `<fk>` elements cover basic index and foreign key definitions, but composite unique constraints, partial indexes, and other Drizzle callbacks that reference column builders cannot be expressed through existing element types.
|
||||
|
||||
Options considered:
|
||||
|
||||
**Option A: No `extraConfig` support** — Only support what `<index>` and `<fk>` elements can express. Composite unique constraints and other Drizzle callback features would require consumers to add them manually after rendering.
|
||||
|
||||
**Option B: `extraConfig` function prop on `<table>`** — Add an `extraConfig` prop that accepts a function `(columns: Record<string, any>) => Record<string, any>`, mirroring Drizzle's third argument. The host passes the rendered column builders to this function during finalization.
|
||||
|
||||
**Option C: New element types for all Drizzle callback features** — Add `<unique>`, `<compositeIndex>`, etc. as new intrinsic elements. This covers more cases but bloats the element vocabulary.
|
||||
|
||||
## Decision
|
||||
|
||||
Use Option B: the `<table>` element accepts an `extraConfig` prop that mirrors Drizzle's third callback argument.
|
||||
|
||||
The `extraConfig` prop is a function that receives the rendered column builders and returns an object for indexes, unique constraints, and other column-reference-based features:
|
||||
|
||||
```typescript
|
||||
// h() call style
|
||||
h('table', { name: 'users', extraConfig: (t) => ({
|
||||
uniqueEmail: unique().on(t.email),
|
||||
}) }, ...columns)
|
||||
|
||||
// JSX style
|
||||
<table name="users" extraConfig={(t) => ({
|
||||
uniqueEmail: unique().on(t.email),
|
||||
})}>
|
||||
<column name="id" type="uuid" primaryKey default="uuid" />
|
||||
<column name="email" type="varchar" notNull />
|
||||
</table>
|
||||
```
|
||||
|
||||
## Rationale
|
||||
|
||||
1. **Full Drizzle coverage**: The `extraConfig` prop provides a straightforward escape hatch for everything Drizzle's third argument supports — composite unique constraints, partial indexes, computed columns, and any future Drizzle features that reference column builders. No element vocabulary expansion needed.
|
||||
|
||||
2. **Mirrors Drizzle's API**: The prop directly maps to Drizzle's `pgTable(name, columns, extraConfig)` pattern. Consumers familiar with Drizzle will immediately understand it.
|
||||
|
||||
3. **Minimal implementation cost**: The host's `finalizeInstance` already has access to the rendered column builders. Passing them to `extraConfig` is a small addition — no new element types, no new rendering pipeline steps.
|
||||
|
||||
4. **`<index>` and `<fk>` remain the primary way**: For simple cases (single-column indexes, basic FK constraints), `<index>` and `<fk>` are still the right elements. `extraConfig` is for Drizzle features that need column references — composite uniques, partial indexes, etc.
|
||||
|
||||
5. **Consistent with the escape hatch pattern**: Like the `inner` prop on `<column>` (ADR-007) which provides an escape hatch for TypeBox validation that flat props can't express, `extraConfig` is an escape hatch for Drizzle table features that element props can't express. The 90% case is covered by elements; the 10% case has an escape hatch.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Full coverage of Drizzle's table definition features
|
||||
- No new intrinsic element types to implement and maintain
|
||||
- Familiar API for Drizzle users
|
||||
- Consistent with the existing escape hatch pattern (`inner` on `<column>`)
|
||||
- The host only needs to pass column builders to `extraConfig` during finalization
|
||||
|
||||
### Negative
|
||||
|
||||
- `extraConfig` exposes Drizzle's API directly — it couples the element tree to Drizzle-specific constructs. This is acceptable since dbtype's rendering target is Drizzle.
|
||||
- `extraConfig` functions are not serializable in JSON Schema — they can't be introspected by `extractTable()` the way `<index>` and `<fk>` elements can. The host renders them opaquely.
|
||||
- Two ways to define some features (e.g., `<index unique>` vs `extraConfig: (t) => ({ myIdx: unique().on(t.email) })`). Guidelines should recommend elements for simple cases and `extraConfig` for column-reference-based features.
|
||||
|
||||
## References
|
||||
|
||||
- [elements.md](../elements.md) — Table element definition
|
||||
- [hosts.md](../hosts.md) — Rendering pipeline and `finalizeInstance`
|
||||
- [ADR-007](007-inner-escape-hatch.md) — The `inner` escape hatch pattern on `<column>`
|
||||
- [open-questions.md](../open-questions.md) — OQ-11
|
||||
@@ -0,0 +1,54 @@
|
||||
# ADR-016: Adapter Accepts the Type.Module Bundle
|
||||
|
||||
## Status
|
||||
|
||||
Accepted
|
||||
|
||||
## Context
|
||||
|
||||
The `from-dbtype` repo adapter needs access to select, insert, update, and filter schemas for each table to generate CRUD `OperationSpec`s. The question is what format the adapter accepts:
|
||||
|
||||
**Option A: Type.Module compiled bundle** — The adapter accepts a compiled `Type.Module` instance and uses `M.Import(key)` to access any schema (select, insert, update, filter) by name. This couples the adapter to TypeBox's module format.
|
||||
|
||||
**Option B: Individual schemas** — The adapter accepts individual TypeBox schemas (`select: TObject`, `insert: TObject`, etc.) per table, passed explicitly by the consumer. This decouples from the module format but requires more configuration.
|
||||
|
||||
**Option C: Module bundle as default, individual schemas as fallback** — The adapter primarily works with the module bundle but can also accept individual schemas for consumers who don't use the module pattern.
|
||||
|
||||
## Decision
|
||||
|
||||
Use Option A: the adapter accepts the Type.Module compiled bundle.
|
||||
|
||||
With ADR-010 establishing that all derived schemas (insert, update, filter) are included in the module by default, the module bundle contains everything the adapter needs. Accessing schemas via `M.Import('Users')`, `M.Import('InsertUsers')`, etc. is the natural interface.
|
||||
|
||||
## Rationale
|
||||
|
||||
1. **Less configuration**: The adapter only needs one input — the compiled module — instead of separate schemas per table per operation type. With 20 tables and 4 operation types, that's 80 individual schemas vs. one module.
|
||||
|
||||
2. **`M.Import()` handles refs and recursion automatically**: The module's `Import()` method resolves `Type.Ref` references within the module namespace. This means nested schemas (e.g., `InsertUsers` referencing `UsersRelations` via `Type.Ref`) work without manual wiring. Accepting individual schemas would require the consumer to handle ref resolution themselves.
|
||||
|
||||
3. **Consistent with ADR-010**: If derived schemas are in the module (as decided), passing individual schemas would mean extracting them from the module first and then passing them separately — an unnecessary round-trip.
|
||||
|
||||
4. **The adapter is already part of the dbtype ecosystem**: It depends on `@alkdev/typebox` for schema access and `@alkdev/ujsx` for element trees. Coupling to TypeBox's module format is acceptable because the adapter is tightly integrated with the dbtype stack already.
|
||||
|
||||
5. **Avoiding potential complications**: Separate schemas would need ref/resolution handling that the module already provides. The `M.Import()` mechanism handles recursive and cross-table references transparently. Extracting and passing individual schemas risks missing these dependencies.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Positive
|
||||
|
||||
- Single input parameter (the compiled module) instead of many individual schemas
|
||||
- `M.Import()` auto-resolves `Type.Ref` references — no manual ref wiring
|
||||
- Consistent with the "everything in the module" pattern from ADR-010
|
||||
- Less consumer configuration
|
||||
|
||||
### Negative
|
||||
|
||||
- The adapter is coupled to TypeBox's `Type.Module` format. If dbtype ever supports a different schema system, the adapter interface would need to change.
|
||||
- `M.Import()` embeds all `$defs` in every import, which increases JSON Schema size for individual operations. This is a known TypeBox module behavior and is acceptable for the adapter's use case.
|
||||
|
||||
## References
|
||||
|
||||
- [repo-adapter.md](../repo-adapter.md) — Adapter interface and schema derivation
|
||||
- [ADR-010](010-one-module-per-database.md) — One module per database, include derived schemas
|
||||
- [module.md](../module.md) — Type.Module mechanics and `M.Import()` behavior
|
||||
- [open-questions.md](../open-questions.md) — OQ-12
|
||||
@@ -20,6 +20,7 @@ The top-level schema element. Contains `<column>`, `<index>`, and `<fk>` childre
|
||||
| Prop | Type | Required | Description |
|
||||
|------|------|----------|-------------|
|
||||
| `name` | `string` | yes | Table name in the database |
|
||||
| `extraConfig` | `(columns: Record<string, any>) => Record<string, any>` | no | Drizzle third-argument callback. Receives rendered column builders, returns an object for composite unique constraints, partial indexes, and other column-reference-based features. See [ADR-015](decisions/015-table-extraconfig-props.md). |
|
||||
|
||||
Children are column, index, and foreign key elements. Function component children that return column elements are transparent (their output is used, not the component itself).
|
||||
|
||||
@@ -235,7 +236,7 @@ interface TableMeta {
|
||||
|
||||
1. ~~Should column elements support `inner` TypeBox schemas?~~ **Resolved — Yes. ADR-007: flat props for common cases, `inner` as escape hatch for custom validation.** The `inner` prop is now documented in the column props table.
|
||||
|
||||
2. **Should `<table>` accept `extraConfig` props?** Drizzle tables accept a third callback argument for indexes and unique constraints defined in terms of column references. How does this map to element props? (See [OQ-11](open-questions.md))
|
||||
2. ~~**Should `<table>` accept `extraConfig` props?**~~ **Resolved — ADR-015**. Yes — the `<table>` element accepts an `extraConfig` prop that mirrors Drizzle's third callback argument. It receives the rendered column builders and returns an object for composite unique constraints, partial indexes, and other column-reference-based features. Consistent with the `inner` escape hatch pattern on `<column>`.
|
||||
|
||||
3. ~~**Should we support JSX file extensions?**~~ **Resolved — ADR-011**. JSX/TSX is supported as an ergonomic authoring layer. JSX desugars to `h()` calls via `jsxImportSource: '@alkdev/ujsx'`. The `h()` API remains the universal fallback. TSConfig must set `jsx: 'react-jsx'` and `jsxImportSource: '@alkdev/ujsx'`.
|
||||
|
||||
|
||||
@@ -100,16 +100,18 @@ Architectural questions for dbtype, organized by theme. Resolved questions refer
|
||||
### OQ-11: Should `<table>` accept `extraConfig` props?
|
||||
|
||||
- **Origin**: [elements.md](elements.md)
|
||||
- **Status**: Open
|
||||
- **Priority**: Low
|
||||
- **Status**: Resolved
|
||||
- **Priority**: ~~Low~~ Resolved
|
||||
- **Context**: Drizzle tables accept a third callback argument for indexes and unique constraints defined in terms of column references (e.g., `t => ({ uniqueEmail: unique().on(t.email) })`). The `<index>` element handles most index cases, but composite unique constraints using the Drizzle callback style may not be expressible via `<index unique>`. How should this map to element props, or is it needed at all?
|
||||
- **Resolution**: Yes — the `<table>` element accepts an `extraConfig` prop that mirrors Drizzle's third callback argument. It receives the rendered column builders and returns an object for indexes, unique constraints, and other column-reference-based features. This is the escape hatch pattern, consistent with `inner` on `<column>`. See [ADR-015](decisions/015-table-extraconfig-props.md).
|
||||
|
||||
### OQ-12: Should the adapter accept the Type.Module bundle or individual schemas?
|
||||
|
||||
- **Origin**: [repo-adapter.md](repo-adapter.md)
|
||||
- **Status**: Open
|
||||
- **Priority**: Low
|
||||
- **Status**: Resolved
|
||||
- **Priority**: ~~Low~~ Resolved
|
||||
- **Context**: The adapter needs access to select, insert, update, and filter schemas. Accepting the `Type.Module` compiled bundle is convenient (everything in one place, accessible via `M.Import()`) but couples the adapter to TypeBox's module format. Accepting individual schemas is more flexible but requires more config. With ADR-010 resolving that derived schemas are included in the module, the module bundle approach is the natural default, but the adapter could expose both interfaces.
|
||||
- **Resolution**: Accept the Type.Module compiled bundle. `M.Import()` handles ref resolution and recursion automatically, which eliminates potential complications with separate schema wiring. Consistent with ADR-010 (derived schemas in the module) and requires less configuration. See [ADR-016](decisions/016-adapter-accepts-module-bundle.md).
|
||||
|
||||
## Summary Table
|
||||
|
||||
@@ -125,5 +127,5 @@ Architectural questions for dbtype, organized by theme. Resolved questions refer
|
||||
| OQ-08 | Per-dialect handler differences | repo-adapter.md | ~~Medium~~ Resolved | **Resolved** — ADR-012 |
|
||||
| OQ-09 | Relation rendering responsibility | repo-adapter.md | ~~Medium~~ Resolved | **Resolved** — ADR-013 |
|
||||
| OQ-10 | Migration diffing scope | module.md | ~~Low~~ Resolved | **Resolved** — ADR-014 |
|
||||
| OQ-11 | Table extraConfig props | elements.md | Low | Open |
|
||||
| OQ-12 | Module bundle vs. individual schemas | repo-adapter.md | Low | Open |
|
||||
| OQ-11 | Table extraConfig props | elements.md | ~~Low~~ Resolved | **Resolved** — ADR-015 |
|
||||
| OQ-12 | Module bundle vs. individual schemas | repo-adapter.md | ~~Low~~ Resolved | **Resolved** — ADR-016 |
|
||||
@@ -21,7 +21,7 @@ interface FromDbTypeConfig {
|
||||
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
|
||||
module: TModule // Compiled Type.Module bundle (accessed via M.Import())
|
||||
operations?: Record<string, OperationSelection> // Per-table operation selection
|
||||
accessControl?: Record<string, AccessControlMap> // Per-table access control
|
||||
}
|
||||
@@ -145,7 +145,7 @@ Overrides replace the auto-generated handler but keep the auto-generated `Operat
|
||||
|
||||
1. ~~**Per-dialect handler differences?**~~ **Resolved — ADR-012**. Always use `.returning()` with graceful fallback. Dialects that support it return data; dialects that don't fall back to validated input or affected row count.
|
||||
|
||||
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. (See [OQ-12](open-questions.md))
|
||||
2. ~~**Schema bundle or separate schemas?**~~ **Resolved — ADR-016**. The adapter accepts the `Type.Module` compiled bundle. `M.Import()` handles ref resolution automatically, which eliminates potential complications with separate schema wiring. Consistent with ADR-010 (derived schemas in the module).
|
||||
|
||||
3. ~~**Relation rendering responsibility?**~~ **Resolved — ADR-013**. The adapter generates relations from module entries and rendered table objects. No new `<relation>` element type is needed.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user