diff --git a/docs/architecture/decisions/015-table-extraconfig-props.md b/docs/architecture/decisions/015-table-extraconfig-props.md new file mode 100644 index 0000000..1fa80bb --- /dev/null +++ b/docs/architecture/decisions/015-table-extraconfig-props.md @@ -0,0 +1,86 @@ +# ADR-015: `` 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 `
` element only has a `name` prop. The `` and `` 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 `` and `` 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 `
`** — Add an `extraConfig` prop that accepts a function `(columns: Record) => Record`, 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 ``, ``, etc. as new intrinsic elements. This covers more cases but bloats the element vocabulary. + +## Decision + +Use Option B: the `
` 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 +
({ + uniqueEmail: unique().on(t.email), +})}> + + +
+``` + +## 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. **`` and `` remain the primary way**: For simple cases (single-column indexes, basic FK constraints), `` and `` 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 `` (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 ``) +- 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 `` and `` elements can. The host renders them opaquely. +- Two ways to define some features (e.g., `` 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 `` +- [open-questions.md](../open-questions.md) — OQ-11 \ No newline at end of file diff --git a/docs/architecture/decisions/016-adapter-accepts-module-bundle.md b/docs/architecture/decisions/016-adapter-accepts-module-bundle.md new file mode 100644 index 0000000..0a89e97 --- /dev/null +++ b/docs/architecture/decisions/016-adapter-accepts-module-bundle.md @@ -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 \ No newline at end of file diff --git a/docs/architecture/elements.md b/docs/architecture/elements.md index fe43142..0e6d377 100644 --- a/docs/architecture/elements.md +++ b/docs/architecture/elements.md @@ -20,6 +20,7 @@ The top-level schema element. Contains ``, ``, and `` childre | Prop | Type | Required | Description | |------|------|----------|-------------| | `name` | `string` | yes | Table name in the database | +| `extraConfig` | `(columns: Record) => Record` | 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 `` 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 `
` accept `extraConfig` props?**~~ **Resolved — ADR-015**. Yes — the `
` 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 ``. 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'`. diff --git a/docs/architecture/open-questions.md b/docs/architecture/open-questions.md index 36ad7a2..ed3cdec 100644 --- a/docs/architecture/open-questions.md +++ b/docs/architecture/open-questions.md @@ -100,16 +100,18 @@ Architectural questions for dbtype, organized by theme. Resolved questions refer ### OQ-11: Should `
` 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 `` element handles most index cases, but composite unique constraints using the Drizzle callback style may not be expressible via ``. How should this map to element props, or is it needed at all? +- **Resolution**: Yes — the `
` 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 ``. 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 | \ No newline at end of file +| 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 | \ No newline at end of file diff --git a/docs/architecture/repo-adapter.md b/docs/architecture/repo-adapter.md index 1fd1220..23321c6 100644 --- a/docs/architecture/repo-adapter.md +++ b/docs/architecture/repo-adapter.md @@ -21,7 +21,7 @@ interface FromDbTypeConfig { db: AnyDrizzleDB // Drizzle database instance tables: Record // UJSX table elements host: HostConfig<...> // Rendered dialect host - schema?: Record // Optional: Type.Module entries for select/insert/update + module: TModule // Compiled Type.Module bundle (accessed via M.Import()) operations?: Record // Per-table operation selection accessControl?: Record // 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 `` element type is needed.