@metaobjectsdev/sdk 0.15.12 → 0.15.14-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -580,6 +580,44 @@ across files (same `package` + same `name` → merged; last-writer-wins on attr
580
580
  conflicts, structural children accumulate). Use `extends` to share shape between
581
581
  distinct entities; use `overlay` to split one entity's declaration across files.
582
582
 
583
+ ## Discriminator inheritance (TPH)
584
+
585
+ When several concrete entities are variants of one thing and should share a
586
+ **single table** (table-per-hierarchy / single-table inheritance), model it with a
587
+ **discriminator** rather than one table per variant:
588
+
589
+ - The **base** `object.entity` declares `@discriminator` naming a discriminator
590
+ field — typically a `field.enum` whose `@values` are the subtype tags.
591
+ - Each concrete **subtype** `extends` the base and declares `@discriminatorValue`
592
+ (one of those enum members).
593
+
594
+ All subtypes persist to the base's single table (subtype-only columns fold in
595
+ nullable). You author only the metadata; codegen emits the polymorphic surface —
596
+ per-subtype routes at `/<base>/<discriminatorValue lowercased>` where create
597
+ **injects** the discriminator from the URL, reads/updates/deletes are **scoped** to
598
+ the subtype (cross-subtype → 404), and the discriminator is **immutable**.
599
+ Supported + conformance-gated in all five ports (the repo's
600
+ `docs/features/abstracts-and-inheritance.md` has the full example and per-port
601
+ mapping).
602
+
603
+ ```yaml
604
+ - object.entity:
605
+ name: Auth # TPH base — owns the single `auths` table
606
+ discriminator: type
607
+ children:
608
+ - source.rdb: { table: auths }
609
+ - field.long: { name: id }
610
+ - field.enum: { name: type, values: ["Bridge", "Copay"] }
611
+ - identity.primary: { fields: id }
612
+
613
+ - object.entity:
614
+ name: BridgeAuth # subtype — folded into `auths`, tagged type="Bridge"
615
+ extends: Auth
616
+ discriminatorValue: Bridge
617
+ children:
618
+ - field.int: { name: quantity, required: true }
619
+ ```
620
+
583
621
  ---
584
622
 
585
623
  For non-trivial schema design, use `/superpowers:brainstorming` if installed;
@@ -82,9 +82,9 @@ first group together:
82
82
 
83
83
  | Generator | Output |
84
84
  |---|---|
85
- | `SpringControllerGenerator` | `<Entity>Controller.java` per writable entity (`source.rdb` `@kind="table"`) — Spring Web MVC, five CRUD endpoints on the cross-port REST contract (`?sort`, `?limit`/`?offset`, `?withCount=1` envelope, 404/400 envelopes) |
86
- | `SpringDtoGenerator` | `<Entity>Dto.java` as a Java 21 `record`; wrapped primitives (`Long`/`Integer`/`Boolean`) so missing JSON props deserialise to `null`; currency = `Long` (integer minor units) |
87
- | `SpringRepositoryGenerator` | `<Entity>Repository.java` — a hand-stubbed `interface` the consumer implements with their persistence layer (Spring Data JPA / jOOQ / JDBC) |
85
+ | `SpringControllerGenerator` | `<Entity>Controller.java` per writable entity (`source.rdb` `@kind="table"`) — Spring Web MVC, five CRUD endpoints on the cross-port REST contract (`?sort`, `?limit`/`?offset`, `?withCount=1` envelope, 404/400 envelopes). A TPH `@discriminator` base emits ONE controller: polymorphic `GET /<base>(+/{id})` plus a per-subtype CRUD set at `/<base>/<discriminatorValue lowercased>` — create injects the discriminator from the URL (never the body); get/update/delete scoped to the subtype (cross-subtype → 404); discriminator immutable. |
86
+ | `SpringDtoGenerator` | `<Entity>Dto.java` as a Java 21 `record`; wrapped primitives (`Long`/`Integer`/`Boolean`) so missing JSON props deserialise to `null`; currency = `Long` (integer minor units). A TPH `@discriminator` base's DTO is the **union** of every subtype's columns (subtype-only fields folded nullable, validation dropped), so one wire shape backs the polymorphic + per-subtype endpoints. |
87
+ | `SpringRepositoryGenerator` | `<Entity>Repository.java` — a hand-stubbed `interface` the consumer implements with their persistence layer (Spring Data JPA / jOOQ / JDBC). For a TPH base the interface is polymorphic + per-subtype-scoped (`listByType`/`findByIdAndType`/`createWithType`/`updateByIdAndType`/`deleteByIdAndType`) over the single table; subtype entities emit no own controller/DTO/repository — they fold into the base. |
88
88
  | `SpringPayloadGenerator` | a Java 21 `record` per template payload VO |
89
89
  | `SpringOutputParserGenerator` | the `template.output` parser-on-receipt (see the prompts reference) |
90
90
  | `SpringFilterAllowlistGenerator` | per-entity filter allowlist |
@@ -92,3 +92,19 @@ first group together:
92
92
  Metadata lives under `src/main/metaobjects/` in the same canonical JSON the other
93
93
  ports read — fused-key form, `source.rdb` + `@table`, `@column` for a renamed
94
94
  physical column.
95
+
96
+ ## Discriminator inheritance (TPH)
97
+
98
+ `codegen-spring` fully supports **table-per-hierarchy (TPH) inheritance**. `TphPlan`
99
+ is the shared descriptor every TPH-aware generator reads: an `object.entity`
100
+ carrying `@discriminator` (naming a `field.enum`) is the base; concrete entities
101
+ that `extends` it and declare `@discriminatorValue` are its subtypes, all persisted
102
+ to the base's **single** table (single-table inheritance). `SpringDtoGenerator`
103
+ emits the base DTO as the union of subtype columns (each folded nullable);
104
+ `SpringControllerGenerator` mounts polymorphic reads + per-subtype CRUD scoped by
105
+ the discriminator (inject on create, subtype-scope + cross-subtype 404 on
106
+ get/update/delete, immutable discriminator); `SpringRepositoryGenerator` emits the
107
+ polymorphic + per-subtype-scoped repository seam the consumer implements against
108
+ Spring Data JPA / JDBC. Conformance-gated by `fixtures/api-contract-conformance/tph`
109
+ (HTTP wire shape) and `fixtures/persistence-conformance/tph-*` (single-table
110
+ runtime semantics).
@@ -94,10 +94,10 @@ All live in `metaobjects-codegen-kotlin` under
94
94
 
95
95
  | Generator | Output |
96
96
  |---|---|
97
- | `KotlinEntityGenerator` | `<Entity>.kt` — `@Serializable data class` per `object.entity` / `object.value` |
98
- | `KotlinExposedTableGenerator` | `<Entity>Table.kt` — Exposed `Table` object (PK + FK + `@storage` columns) for entities with `source.rdb` |
97
+ | `KotlinEntityGenerator` | `<Entity>.kt` — `@Serializable data class` per `object.entity` / `object.value`. A TPH `@discriminator` base's data class is the **union** of every subtype's columns (each folded nullable, validation dropped) so one wire shape backs the polymorphic + per-subtype endpoints. |
98
+ | `KotlinExposedTableGenerator` | `<Entity>Table.kt` — Exposed `Table` object (PK + FK + `@storage` columns) for entities with `source.rdb`. A TPH `@discriminator` base emits ONE `Table` for the whole hierarchy — every subtype-only column folded in `.nullable()` (a row of another subtype stores null there) — single-table inheritance; subtype entities emit no table of their own. |
99
99
  | `KotlinRelationsGenerator` | `<Entity>Relations.kt` — extension fns for `@cardinality="many"` query helpers |
100
- | `KotlinSpringControllerGenerator` | `<Entity>Controller.kt` — Spring `@RestController`, five CRUD endpoints on the cross-port REST contract, for writable entities (`source.rdb` `@kind="table"`) |
100
+ | `KotlinSpringControllerGenerator` | `<Entity>Controller.kt` — Spring `@RestController`, five CRUD endpoints on the cross-port REST contract, for writable entities (`source.rdb` `@kind="table"`). A TPH `@discriminator` base emits ONE controller: polymorphic `GET /<base>(+/{id})` plus a per-subtype CRUD set at `/<base>/<discriminatorValue lowercased>` — create injects the discriminator from the URL (never the body); get/update/delete are scoped to the subtype (cross-subtype → 404); the discriminator is immutable. |
101
101
  | `KotlinPayloadGenerator` | `<Template>Payload.kt` — `@Serializable` payload data class from a template's `@payloadRef` |
102
102
  | `KotlinOutputParserGenerator` | the `template.output` parser-on-receipt (see the prompts reference) |
103
103
  | `KotlinValidatorGenerator` | `MetadataStartupValidator.kt` + `ExposedTableValidator.kt` (once per project) |
@@ -108,3 +108,19 @@ All live in `metaobjects-codegen-kotlin` under
108
108
  Metadata lives under `src/main/metaobjects/` in the same canonical JSON the other
109
109
  ports read — fused-key form, `source.rdb` + `@table`, `@column` for a renamed
110
110
  physical column.
111
+
112
+ ## Discriminator inheritance (TPH)
113
+
114
+ `codegen-kotlin` fully supports **table-per-hierarchy (TPH) inheritance**.
115
+ `KotlinTphPlan` is the shared descriptor every TPH-aware generator reads: an
116
+ `object.entity` carrying `@discriminator` (naming a `field.enum`) is the base;
117
+ concrete entities that `extends` it and declare `@discriminatorValue` are its
118
+ subtypes, all persisted to the base's **single** Exposed `Table` (single-table
119
+ inheritance). `KotlinExposedTableGenerator` folds each subtype's own columns into
120
+ that table as `.nullable()`; `KotlinEntityGenerator` builds the base data class as
121
+ the union of subtype columns; `KotlinFilterAllowlistGenerator` unions the
122
+ subtypes' filterable columns; `KotlinSpringControllerGenerator` mounts the
123
+ polymorphic reads + per-subtype CRUD scoped by the discriminator (inject on create,
124
+ subtype-scope + cross-subtype 404 on get/update/delete, immutable discriminator).
125
+ Conformance-gated by `fixtures/api-contract-conformance/tph` (HTTP wire shape) and
126
+ `fixtures/persistence-conformance/tph-*` (single-table runtime semantics).
@@ -37,12 +37,28 @@ a renamed physical column).
37
37
 
38
38
  | Stable name | Output |
39
39
  |---|---|
40
- | `entity` | one **Pydantic model** per `object.entity` / projection (the `entity-model` generator): typed fields from the metadata, nullability from `@required`, `@maxLength`/validators, enum fields → a Python `Enum`. This is the typed data model. |
41
- | `routes` | a **FastAPI `APIRouter`** per writable entity (`source.rdb @kind="table"`) on the cross-port REST contract (`?filter[field][op]=`, `?sort=field:asc`, `?limit`/`?offset`, `?withCount=1` envelope, 400/404 envelopes). The router declares a repository **`Protocol`** you implement and inject. |
40
+ | `entity` | one **Pydantic model** per `object.entity` / projection (the `entity-model` generator): typed fields from the metadata, nullability from `@required`, `@maxLength`/validators, enum fields → a Python `Enum`. This is the typed data model. A TPH concrete subtype (`@discriminatorValue`) pins the inherited `@discriminator` field to a `Literal[...]` so the model rejects a foreign-subtype tag. |
41
+ | `routes` | a **FastAPI `APIRouter`** per writable entity (`source.rdb @kind="table"`) on the cross-port REST contract (`?filter[field][op]=`, `?sort=field:asc`, `?limit`/`?offset`, `?withCount=1` envelope, 400/404 envelopes). The router declares a repository **`Protocol`** you implement and inject. A TPH `@discriminator` base emits ONE polymorphic router: `GET /<base>(+/{id})` plus a per-subtype CRUD set at `/<base>/<discriminatorValue lowercased>` — create injects the discriminator from the URL (never the body); get/update/delete scoped to the subtype (cross-subtype → 404); discriminator immutable. Its repository `Protocol` is subtype-keyed (`subtype=None` for the polymorphic base) so your implementation applies the single-table discriminator scope. |
42
42
  | `filter-allowlist` | per-entity filter allowlist (FR-009 — the server-side field+operator allowlist the routes validate against). |
43
43
  | `payload` / `output-parser` / `output-prompt` / `extractor` / `render-helper` / `trace-helper` | the `template.output` prompt-pillar artifacts — see the **prompts** reference. |
44
44
  | `template` | the generic Mustache `template` primitive. |
45
45
 
46
+ ## Discriminator inheritance (TPH)
47
+
48
+ Python codegen fully supports **table-per-hierarchy (TPH) inheritance**
49
+ (`tph_plan.py` is the shared descriptor): an `object.entity` carrying
50
+ `@discriminator` (naming a `field.enum`) is the base; concrete entities that
51
+ `extends` it and declare `@discriminatorValue` are its subtypes, all persisted to
52
+ the base's **single** table (single-table inheritance). The `entity` generator
53
+ pins each subtype's inherited discriminator to a `Literal`; the `routes` generator
54
+ emits the polymorphic router + per-subtype CRUD scoped by the discriminator (inject
55
+ on create, subtype-scope + cross-subtype 404 on get/update/delete, immutable
56
+ discriminator). Because Python owns no ORM (see below), your repository — keyed by
57
+ subtype — applies the single-table discriminator scope (idiomatically a
58
+ SQLAlchemy polymorphic/single-table mapping). Conformance-gated by
59
+ `fixtures/api-contract-conformance/tph` (HTTP wire shape) and
60
+ `fixtures/persistence-conformance/tph-*` (single-table runtime semantics).
61
+
46
62
  ## No ORM — you own persistence (unlike the C# port)
47
63
 
48
64
  Python codegen emits the **Pydantic models + the FastAPI routers**, but **no ORM /
@@ -69,13 +69,31 @@ From `@metaobjectsdev/codegen-ts/generators` (server-side, framework-neutral):
69
69
 
70
70
  | Generator | Emits per entity |
71
71
  |---|---|
72
- | `entityFile()` | `<Entity>.ts` — Drizzle table + FK `.references()` + `relations()` + inferred types + Zod insert/update schemas + `<Entity>FilterAllowlist` / `<Entity>SortAllowlist` |
72
+ | `entityFile()` | `<Entity>.ts` — Drizzle table + FK `.references()` + `relations()` + inferred types + Zod insert/update schemas + `<Entity>FilterAllowlist` / `<Entity>SortAllowlist`. A TPH `@discriminator` base folds every subtype's columns into ONE Drizzle table (subtype-only columns nullable, no default — single-table inheritance) and emits a discriminated-union type + per-subtype Zod schemas + a `parse<Base>` dispatcher; subtype entities emit no table of their own. |
73
73
  | `queriesFile()` | `<Entity>.queries.ts` — typed CRUD (`findPostById`, `listPosts`, `createPost`, `updatePost`, `deletePostById`) |
74
- | `routesFile()` | `<Entity>.routes.ts` — Fastify CRUD routes on the cross-port REST contract. `routesFileHono()` is the Hono/Workers variant |
74
+ | `routesFile()` | `<Entity>.routes.ts` — Fastify CRUD routes on the cross-port REST contract. `routesFileHono()` is the Hono/Workers variant. A TPH `@discriminator` base mounts polymorphic `GET /<base>(+/:id)` plus a per-subtype CRUD set at `<basePath>/<discriminatorValue lowercased>` — create omits the discriminator (the URL names the subtype; the runtime injects it); get/update/delete scoped to the subtype (cross-subtype → 404); discriminator immutable via the runtime `discriminator` option. |
75
75
  | `barrel()` | `index.ts` re-exporting each `<Entity>.ts` (one-shot, not per-entity) |
76
76
  | `promptRender()` | `render<Name>()` per `template.prompt` |
77
77
  | `outputParser()` | `<Name>.output.ts` (`parse*` / `safeParse*`) per `template.output` |
78
78
 
79
+ ## Discriminator inheritance (TPH)
80
+
81
+ The TS reference implementation fully supports **table-per-hierarchy (TPH)
82
+ inheritance** (`tph-discriminator.ts` is the shared descriptor): an `object.entity`
83
+ carrying `@discriminator` (naming a `field.enum`) is the base; concrete entities
84
+ that `extends` it and declare `@discriminatorValue` are its subtypes, all persisted
85
+ to the base's **single** Drizzle table (single-table inheritance). `entityFile()`
86
+ folds each subtype's columns into that table nullable and emits the
87
+ discriminated-union type + per-subtype Zod schemas + a `parse<Base>` dispatcher;
88
+ `routesFile()` mounts polymorphic reads + per-subtype CRUD scoped by the
89
+ discriminator. At runtime, `@metaobjectsdev/runtime-ts`'s ObjectManager enforces the
90
+ subtype contract: it injects the discriminator on create, scopes every
91
+ read/update/delete to the subtype (a foreign-subtype row is invisible), and treats
92
+ the discriminator as immutable — mirroring the generated per-subtype route's
93
+ cross-subtype 404. Conformance-gated by `fixtures/api-contract-conformance/tph`
94
+ (HTTP wire shape) and `fixtures/persistence-conformance/tph-*` (single-table
95
+ runtime semantics).
96
+
79
97
  ## Docs — `meta docs` (one door, two surfaces)
80
98
 
81
99
  Documentation is NOT a `meta gen` generator. The single door is the `meta docs`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metaobjectsdev/sdk",
3
- "version": "0.15.12",
3
+ "version": "0.15.14-rc.1",
4
4
  "description": "Workspace helpers and agent-docs utilities for MetaObjects projects.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -56,7 +56,7 @@
56
56
  "access": "public"
57
57
  },
58
58
  "dependencies": {
59
- "@metaobjectsdev/metadata": "0.15.12",
59
+ "@metaobjectsdev/metadata": "0.15.14-rc.1",
60
60
  "zod": "^3.23.0"
61
61
  },
62
62
  "devDependencies": {