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
|
||||
Reference in New Issue
Block a user