You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Kotlin port of the table-per-hierarchy REST contract, mirroring the Java/C#/Python/TS
generated lanes. A discriminator base (`@discriminator`) now emits ONE self-contained
Kotlin Spring controller mounting the polymorphic collection plus a per-subtype CRUD set;
its subtypes (`@discriminatorValue`) are folded into the base's single Exposed table and
emit no standalone table/controller.
Routes (segment = `@discriminatorValue` lowercased): GET /api/<base>, GET /<base>/{id},
GET /<base>/<seg>, GET /<base>/<seg>/{id} (404 cross-subtype), POST /<base>/<seg>
(discriminator injected from URL, not body), PATCH|PUT /<base>/<seg>/{id} (partial patch,
discriminator immutable, 404 cross-subtype), DELETE /<base>/<seg>/{id} (404 cross-subtype).
- KotlinTphPlan — single source of truth: isTphSubtype / isTphBase / planFor /
collectSubtypeFields / routeSegment (mirrors TphPlan.java).
- KotlinExposedTableGenerator — the base table folds in every subtype column NULLABLE.
- KotlinEntityGenerator — the base data class is the UNION (all properties nullable +
defaulted, no validation) so it's a partial-PATCH-friendly wire body for every endpoint.
- KotlinSpringControllerGenerator.emitTph — the self-contained TPH controller, embedding
Exposed against the union table; the discriminator is the generated enum (scoped/injected
as `<Enum>.<Value>`); create forces the required base column non-null, update is a partial
COALESCE-style patch.
Generated lane (NO Postgres — kotlin-compile-testing → MockMvc over in-memory H2, seeded via
the controller's own per-subtype POST): the 4 tph api-contract scenarios pass against the
EMITTED AuthController. No regression: codegen-kotlin green; Author generated 20/0,
reference + codegen-matches green (TPH paths are gated to discriminator entities).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinExposedTableGenerator.kt
+15Lines changed: 15 additions & 0 deletions
Original file line number
Diff line number
Diff line change
@@ -351,6 +351,21 @@ open class KotlinExposedTableGenerator : MultiFileDirectGeneratorBase<MetaObject
351
351
val full =if (nullable) "$decorated.nullable()"else decorated
352
352
append(" val ${field.name} = $full\n")
353
353
}
354
+
// FR-017 TPH: a discriminator base's single table also carries every subtype-only
355
+
// column, emitted NULLABLE (a row of another subtype stores null there, even when the
356
+
// field is @required on its subtype). collectSubtypeFields is empty for a non-TPH entity.
357
+
for (field inKotlinTphPlan.collectSubtypeFields(entity, loader)) {
358
+
if (field isObjectField) continue
359
+
val baseSpec =if (field isEnumField) {
360
+
val enumName =KotlinTypeMapper.enumTypeName(field, entity)?.simpleName
361
+
?: error("enumTypeName returned null for EnumField '${field.name}' on ${entity.name}")
362
+
val colName =KotlinGenUtil.camelToSnake(field.name)
Copy file name to clipboardExpand all lines: server/java/codegen-kotlin/src/main/kotlin/com/metaobjects/generator/kotlin/KotlinSpringControllerGenerator.kt
+206-1Lines changed: 206 additions & 1 deletion
Original file line number
Diff line number
Diff line change
@@ -87,6 +87,9 @@ open class KotlinSpringControllerGenerator : MultiFileDirectGeneratorBase<MetaOb
87
87
if (entity.subType !=MetaObject.SUBTYPE_ENTITY) continue
88
88
// Abstract entities are inheritance scaffolding — never emit a CRUD controller.
89
89
if (KotlinGenUtil.isAbstractEntity(entity)) continue
90
+
// FR-017 TPH: a subtype is folded into its base's single table + base controller (it
91
+
// also carries no own source.rdb, so the guard below would skip it too).
92
+
if (KotlinTphPlan.isTphSubtype(entity)) continue
90
93
val sourceRdb = entity.children.filterIsInstance<RdbSource>().firstOrNull() ?:continue
91
94
val kind = sourceRdb.effectiveKind
92
95
// Only writable tables get a CRUD controller. View / materializedView are
@@ -116,7 +119,10 @@ open class KotlinSpringControllerGenerator : MultiFileDirectGeneratorBase<MetaOb
116
119
}
117
120
continue
118
121
}
119
-
emit(entity, outRoot, loader)
122
+
// FR-017 TPH: a discriminator base emits ONE controller mounting the polymorphic
123
+
// collection routes plus a full per-subtype CRUD set scoped by the discriminator.
124
+
val tph =KotlinTphPlan.planFor(entity, loader)
125
+
if (tph !=null) emitTph(entity, tph, outRoot) else emit(entity, outRoot, loader)
120
126
}
121
127
}
122
128
@@ -364,6 +370,205 @@ open class KotlinSpringControllerGenerator : MultiFileDirectGeneratorBase<MetaOb
364
370
Files.writeString(outFile, source)
365
371
}
366
372
373
+
/**
374
+
* FR-017 TPH: emit the discriminator-base controller — ONE self-contained `@RestController`
375
+
* at `/api/<base-plural>` mounting the polymorphic collection (`GET /`, `GET /{id}`) plus, per
376
+
* subtype `<seg>` (the `@discriminatorValue` lowercased), a full CRUD set scoped by the
0 commit comments