Skip to content

Add polymorphic aggregate root support via sealed interfaces#20

Merged
stijnvanbael merged 22 commits intomainfrom
copilot/analyse-task-13-polymorphic-aggregates
Apr 6, 2026
Merged

Add polymorphic aggregate root support via sealed interfaces#20
stijnvanbael merged 22 commits intomainfrom
copilot/analyse-task-13-polymorphic-aggregates

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Apr 2, 2026

  • Fix PolymorphicRestWriterTest failure: update expected ShapeController.java with OpenAPI annotations
  • Fix avro-example SaleIntegrationTest flakiness: schema registry using wrong Kafka bootstrap address (kafka:9092 → kafka:9095)
Original prompt

Analyse task 13 for polymorphic aggregate roots and entities. This should work on Postgres and MongoDB

…erfaces

Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/027deafc-27fc-4c7d-aa98-03ab24653ec2

Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
Copilot AI changed the title [WIP] Analysing task 13 for polymorphic aggregate roots and entities Add polymorphic aggregate root support via sealed interfaces Apr 2, 2026
Copilot AI requested a review from stijnvanbael April 2, 2026 09:51
…, runtime TODOs, open questions

Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/cf2390fd-1fcc-40e9-974d-d99d32450e5f

Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
Copilot AI and others added 2 commits April 3, 2026 09:14
…nse method

Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/86c44968-f68f-4250-a182-e18bc02c6008

Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
Copilot AI and others added 2 commits April 3, 2026 10:13
… + fix Kafka test setup

Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/98e50b41-f827-4ffe-9479-516dd0296e33

Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
…PersistentEntity classes

Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/98e50b41-f827-4ffe-9479-516dd0296e33

Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
@stijnvanbael stijnvanbael marked this pull request as ready for review April 3, 2026 10:25
…g Data 4.x compatibility

Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/5a6f7dcb-9c4f-46e9-85d4-a930a3a4c46a

Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
…erter, use direct cast

Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/8ac19811-cf87-48a1-8f3b-cfbdbfe3fda4

Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds end-to-end support for polymorphic aggregate roots modeled as @Aggregate sealed interfaces, spanning annotation-processor generation plus Postgres (Spring Data JDBC) and MongoDB runtime wiring.

Changes:

  • Generate repositories, REST layer, DB migrations (single-table + type discriminator), and JDBC reading converters for sealed-interface aggregates.
  • Wire Spring Data JDBC to (a) treat sealed interfaces as persistent entities and (b) route reads through generated polymorphic converters; also populate the type discriminator on insert.
  • Add MongoDB mapping-context support for sealed-interface aggregates and an example MongoDB integration test using the generated HTTP layer.

Reviewed changes

Copilot reviewed 38 out of 39 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
test/src/main/java/be/appify/prefab/test/kafka/KafkaTestAutoConfiguration.java Adjust schema-registry bootstrap server port for testcontainers wiring.
postgres/src/main/java/be/appify/prefab/postgres/spring/PrefabJdbcConfiguration.java Auto-register generated polymorphic JDBC reading converters in JdbcCustomConversions.
postgres/src/main/java/be/appify/prefab/postgres/spring/data/jdbc/SealedInterfacePersistentEntity.java New JDBC persistent entity for sealed @Aggregate interfaces (table + id/isNew delegation).
postgres/src/main/java/be/appify/prefab/postgres/spring/data/jdbc/PrefabPersistentEntity.java Derive table name from sealed @Aggregate interface for subtypes (single-table mapping).
postgres/src/main/java/be/appify/prefab/postgres/spring/data/jdbc/PrefabMappingJdbcConverter.java Delegate reads of sealed-interface aggregates to registered converters.
postgres/src/main/java/be/appify/prefab/postgres/spring/data/jdbc/PrefabJdbcMappingContext.java Create SealedInterfacePersistentEntity for sealed @Aggregate interfaces.
postgres/src/main/java/be/appify/prefab/postgres/spring/data/jdbc/PrefabDataAccessStrategy.java Populate type discriminator on insert for polymorphic aggregate subtypes.
mongodb/src/main/java/be/appify/prefab/mongodb/spring/PrefabMongoConfiguration.java Add a custom MongoMappingContext bean intended to support sealed-interface aggregates.
mongodb/src/main/java/be/appify/prefab/mongodb/spring/data/mongodb/SealedInterfaceMongoPersistentEntity.java New Mongo persistent entity for sealed @Aggregate interfaces (id/isNew delegation).
mongodb/src/main/java/be/appify/prefab/mongodb/spring/data/mongodb/PrefabMongoMappingContext.java New mapping context allowing sealed @Aggregate interfaces as managed Mongo entities.
examples/mongodb/src/main/java/be/appify/prefab/example/mongodb/shape/Shape.java Example polymorphic sealed-interface aggregate for MongoDB.
examples/mongodb/src/test/java/be/appify/prefab/example/mongodb/shape/ShapeIntegrationTest.java Integration test verifying round-trip + HTTP responses for polymorphic MongoDB aggregate.
examples/avro/src/test/resources/application-test.yml Formatting tweak for schema-registry test property.
core/src/main/java/be/appify/prefab/core/spring/data/jdbc/PolymorphicReadingConverter.java Marker interface for generated polymorphic JDBC reading converters.
backlog/tasks/task-013 - Polymorphism.md Update task status/notes and document the approach and acceptance criteria.
annotation-processor/src/main/java/be/appify/prefab/processor/PrefabProcessor.java Detect sealed @Aggregate types and route through polymorphic generation pipeline.
annotation-processor/src/main/java/be/appify/prefab/processor/PrefabPlugin.java Extend plugin API to receive polymorphic manifests for combined generation.
annotation-processor/src/main/java/be/appify/prefab/processor/PolymorphicAggregateManifest.java New manifest for sealed-interface aggregates and subtype/common-field discovery.
annotation-processor/src/main/java/be/appify/prefab/processor/PolymorphicJdbcConverterWriter.java Generate JDBC @ReadingConverter for sealed-interface aggregates based on type.
annotation-processor/src/main/java/be/appify/prefab/processor/PersistenceWriter.java Generate repositories for polymorphic aggregates.
annotation-processor/src/main/java/be/appify/prefab/processor/HttpWriter.java Generate polymorphic controller + sealed response type with Jackson type info.
annotation-processor/src/main/java/be/appify/prefab/processor/ApplicationWriter.java Generate application services for polymorphic aggregates.
annotation-processor/src/main/java/be/appify/prefab/processor/rest/ControllerUtil.java Add response-type helper for polymorphic manifests.
annotation-processor/src/main/java/be/appify/prefab/processor/rest/getbyid/GetByIdPlugin.java Enable GetById generation for polymorphic aggregates.
annotation-processor/src/main/java/be/appify/prefab/processor/rest/getbyid/GetByIdControllerWriter.java Generate GetById controller method for polymorphic aggregates.
annotation-processor/src/main/java/be/appify/prefab/processor/rest/getbyid/GetByIdServiceWriter.java Generate GetById service method for polymorphic aggregates.
annotation-processor/src/main/java/be/appify/prefab/processor/rest/getlist/GetListPlugin.java Enable GetList generation for polymorphic aggregates.
annotation-processor/src/main/java/be/appify/prefab/processor/rest/getlist/GetListControllerWriter.java Generate GetList controller method for polymorphic aggregates.
annotation-processor/src/main/java/be/appify/prefab/processor/rest/getlist/GetListServiceWriter.java Generate GetList service method for polymorphic aggregates.
annotation-processor/src/main/java/be/appify/prefab/processor/dbmigration/DbMigrationWriter.java Generate single-table STI schema for polymorphic aggregates incl. type column.
annotation-processor/src/main/java/be/appify/prefab/processor/dbmigration/DbMigrationPlugin.java Pass polymorphic manifests to migration generation.
annotation-processor/src/test/java/be/appify/prefab/processor/persistence/PolymorphicConverterWriterTest.java New compilation/snapshot tests for polymorphic persistence generation.
annotation-processor/src/test/java/be/appify/prefab/processor/rest/PolymorphicRestWriterTest.java New compilation/snapshot tests for polymorphic REST generation.
annotation-processor/src/test/resources/persistence/polymorphic/source/Shape.java Test input for polymorphic persistence generation.
annotation-processor/src/test/resources/persistence/polymorphic/expected/ShapeReadingConverter.java Snapshot of expected generated polymorphic JDBC reading converter.
annotation-processor/src/test/resources/rest/polymorphic/source/Shape.java Test input for polymorphic REST generation.
annotation-processor/src/test/resources/rest/polymorphic/expected/ShapeController.java Snapshot of expected generated controller.
annotation-processor/src/test/resources/rest/polymorphic/expected/ShapeResponse.java Snapshot of expected generated response type.
annotation-processor/src/test/resources/rest/polymorphic/expected/ShapeService.java Snapshot of expected generated service.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +97 to +105
return CodeBlock.of("$T.fromId(($T) row.get($S))", Reference.class, innerType.asTypeName(), columnName);
} else if (fieldType.isSingleValueType()) {
var innerType = fieldType.fields().getFirst().type();
return CodeBlock.of("row.get($S) != null ? new $T(($T) row.get($S)) : null",
columnName, fieldType.asTypeName(), innerType.asTypeName());
}
return CodeBlock.of("($T) row.get($S)", fieldType.asBoxed().asTypeName(), columnName);
}

Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The generated reading converter currently does raw casts for scalar fields (e.g. casting DECIMAL columns to Double). In this codebase, double/float fields are migrated as DECIMAL(19,4) (see DataType.typeOf(...)), so JDBC will typically return a BigDecimal (or at least a Number), causing ClassCastException at runtime. Please convert numeric values via Number (e.g. ((Number)v).doubleValue()/longValue()) and handle nulls, and update the expected ShapeReadingConverter snapshot accordingly.

Suggested change
return CodeBlock.of("$T.fromId(($T) row.get($S))", Reference.class, innerType.asTypeName(), columnName);
} else if (fieldType.isSingleValueType()) {
var innerType = fieldType.fields().getFirst().type();
return CodeBlock.of("row.get($S) != null ? new $T(($T) row.get($S)) : null",
columnName, fieldType.asTypeName(), innerType.asTypeName());
}
return CodeBlock.of("($T) row.get($S)", fieldType.asBoxed().asTypeName(), columnName);
}
return CodeBlock.of("$T.fromId($L)", Reference.class, readValueFromRow(innerType, columnName));
} else if (fieldType.isSingleValueType()) {
var innerType = fieldType.fields().getFirst().type();
return CodeBlock.of("row.get($S) != null ? new $T($L) : null",
columnName, fieldType.asTypeName(), readValueFromRow(innerType, columnName));
}
return readValueFromRow(fieldType, columnName);
}
private CodeBlock readValueFromRow(TypeManifest fieldType, String columnName) {
var boxedType = fieldType.asBoxed().asTypeName();
if (boxedType.equals(ClassName.get(Double.class))) {
return numericConversion(columnName, "doubleValue");
} else if (boxedType.equals(ClassName.get(Float.class))) {
return numericConversion(columnName, "floatValue");
} else if (boxedType.equals(ClassName.get(Long.class))) {
return numericConversion(columnName, "longValue");
} else if (boxedType.equals(ClassName.get(Integer.class))) {
return numericConversion(columnName, "intValue");
} else if (boxedType.equals(ClassName.get(Short.class))) {
return numericConversion(columnName, "shortValue");
} else if (boxedType.equals(ClassName.get(Byte.class))) {
return numericConversion(columnName, "byteValue");
}
return CodeBlock.of("($T) row.get($S)", boxedType, columnName);
}
private CodeBlock numericConversion(String columnName, String methodName) {
return CodeBlock.of("row.get($S) != null ? (($T) row.get($S)).$L() : null",
columnName, Number.class, columnName, methodName);
}

Copilot uses AI. Check for mistakes.
.toList();
var polymorphicAggregates = environment.getElementsAnnotatedWith(Aggregate.class)
.stream()
.filter(element -> element.getModifiers().contains(Modifier.SEALED))
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

polymorphicAggregates currently includes any @Aggregate element with the sealed modifier, including sealed classes. The runtime JDBC/Mongo wiring added in this PR explicitly assumes a sealed interface (checks type.isInterface()), so processing sealed classes here would generate repositories/converters that cannot work consistently. Consider filtering to element.getKind().isInterface() (and optionally also excluding abstract sealed classes) to keep the processor aligned with the supported model.

Suggested change
.filter(element -> element.getModifiers().contains(Modifier.SEALED))
.filter(element -> element.getKind().isInterface() && element.getModifiers().contains(Modifier.SEALED))

Copilot uses AI. Check for mistakes.
Comment on lines +57 to +61
@Bean
@ConditionalOnMissingBean
public MongoMappingContext mongoMappingContext() {
return new PrefabMongoMappingContext();
}
Copy link

Copilot AI Apr 5, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new MongoMappingContext bean is created as new PrefabMongoMappingContext() without being configured with the application’s MongoCustomConversions (simple type holder) and other defaults normally applied by Spring Boot’s MongoDB auto-configuration (initial entity set, field naming strategy, auto-index creation, etc.). This can break mapping of Prefab single-value types (e.g. Reference) and potentially entity scanning/index behavior. Consider wiring the Prefab mapping context the same way as the default one (at minimum call setSimpleTypeHolder(mongoCustomConversions.getSimpleTypeHolder()), and ensure the initial entity set is populated) rather than returning a bare instance.

Copilot uses AI. Check for mistakes.
@stijnvanbael
Copy link
Copy Markdown
Owner

Build fails please fix

Copilot AI and others added 2 commits April 5, 2026 16:29
…nstructor call

Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/bef0ff60-9f21-4470-8112-bba628023554

Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
…nstructor

Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/bef0ff60-9f21-4470-8112-bba628023554

Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
…MigrationWriterTest conflict

Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
…DbMigrationWriter

Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/c0aff1f8-4e91-4d7b-8e7f-e09d484532f1

Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
…nnotations

Agent-Logs-Url: https://github.com/stijnvanbael/prefab/sessions/0ad0d76f-063e-4d95-adbb-93004fefc631

Co-authored-by: stijnvanbael <1050148+stijnvanbael@users.noreply.github.com>
@stijnvanbael stijnvanbael merged commit f72cb93 into main Apr 6, 2026
1 check passed
@stijnvanbael stijnvanbael deleted the copilot/analyse-task-13-polymorphic-aggregates branch April 6, 2026 05:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants