diff --git a/plugins/backend-application-default/src/main/java/io/zenwave360/sdk/plugins/BackendApplicationDefaultHelpers.java b/plugins/backend-application-default/src/main/java/io/zenwave360/sdk/plugins/BackendApplicationDefaultHelpers.java index c9be63e6..8e624a3e 100644 --- a/plugins/backend-application-default/src/main/java/io/zenwave360/sdk/plugins/BackendApplicationDefaultHelpers.java +++ b/plugins/backend-application-default/src/main/java/io/zenwave360/sdk/plugins/BackendApplicationDefaultHelpers.java @@ -5,9 +5,7 @@ import io.zenwave360.sdk.utils.NamingUtils; import io.zenwave360.sdk.zdl.ZDLFindUtils; -import io.zenwave360.sdk.zdl.ZDLHttpUtils; import io.zenwave360.sdk.zdl.ZDLJavaSignatureUtils; -import org.apache.commons.lang3.ObjectUtils; import org.apache.commons.lang3.StringUtils; import com.github.jknack.handlebars.Options; @@ -42,9 +40,38 @@ public boolean isCrudMethod(String crudMethodPrefix, Options options) { return isCrudMethod; } + public List serviceAggregates(Map service, Options options) { + var zdl = options.get("zdl"); + var aggregateNames = JSONPath.get(service, "aggregates", Collections.emptyList()); + return aggregateNames.stream() + .map(aggregateName -> JSONPath.get(zdl, "$.allEntitiesAndEnums." + aggregateName, Map.of())) + .filter(aggregate -> "aggregates".equals(aggregate.get("type"))) + .collect(Collectors.toList()); + } + + public boolean includeDomainEvents(Map service, Options options) { + var zdl = options.get("zdl"); + return !JSONPath.get(zdl, "$.aggregates[*].commands[*].withEvents", List.of()).isEmpty(); + } + + public List aggregateEvents(Map aggregate, Options options) { + var zdl = options.get("zdl"); + return ZDLFindUtils.aggregateEvents(aggregate).stream().map(event -> (Map) JSONPath.get(zdl, "$.events." + event)).toList(); + } + public Collection findAggregateInputs(Map aggregate, Options options) { + return new HashSet<>(JSONPath.get(aggregate, "$.commands[*].parameter", List.of())); + } + + public String findEntityAggregate(String entityName, Options options) { var zdl = options.get("zdl"); - var aggregateName = (String) aggregate.get("name"); + var aggregateNames = JSONPath.get(zdl, "$.aggregates[*][?(@.aggregateRoot == '" + entityName + "')].name", List.of()); + return aggregateNames.isEmpty()? null : (String) aggregateNames.get(0); + } + + public Collection findServiceInputs(Map entity, Options options) { + var zdl = options.get("zdl"); + var aggregateName = (String) entity.get("name"); var inputDTOSuffix = (String) options.get("inputDTOSuffix"); Set inputs = new HashSet(); inputs.addAll(JSONPath.get(zdl, "$.services[*][?('" + aggregateName + "' in @.aggregates)].methods[*].parameter")); @@ -305,14 +332,14 @@ public String validationPatternJava(String pattern, Options options) { return pattern.replace("\\", "").replace("\\", "\\\\"); } - public Object skipEntityRepository(Object context, Options options) { - Map entity = (Map) context; - return generator.skipEntityRepository.apply(Map.of("entity", entity)); + public Object skipEntityRepository(Map entity, Options options) { + var zdl = options.get("zdl"); + return generator.skipEntityRepository.apply(Map.of("zdl", zdl, "entity", entity)); }; - public Object skipEntityId(Object context, Options options) { - Map entity = (Map) context; - return generator.skipEntityId.apply(Map.of("entity", entity)); + public Object skipEntityId(Map entity, Options options) { + var zdl = options.get("zdl"); + return generator.skipEntityId.apply(Map.of("zdl", zdl, "entity", entity)); }; public Object addExtends(Object entity, Options options) { diff --git a/plugins/backend-application-default/src/main/java/io/zenwave360/sdk/plugins/BackendDefaultApplicationGenerator.java b/plugins/backend-application-default/src/main/java/io/zenwave360/sdk/plugins/BackendDefaultApplicationGenerator.java index 19426b3c..4b78c485 100644 --- a/plugins/backend-application-default/src/main/java/io/zenwave360/sdk/plugins/BackendDefaultApplicationGenerator.java +++ b/plugins/backend-application-default/src/main/java/io/zenwave360/sdk/plugins/BackendDefaultApplicationGenerator.java @@ -13,6 +13,7 @@ import io.zenwave360.sdk.options.PersistenceType; import io.zenwave360.sdk.options.ProgrammingStyle; import io.zenwave360.sdk.utils.JSONPath; +import io.zenwave360.sdk.zdl.ZDLFindUtils; /** * Generates a backend application with the following structure: @@ -48,6 +49,7 @@ public class BackendDefaultApplicationGenerator extends AbstractZDLProjectGenera public String configPackage = "{{basePackage}}.config"; public String entitiesPackage = "{{basePackage}}.core.domain"; + public String domainEventsPackage = "{{basePackage}}.core.domain.events"; public String inboundPackage = "{{basePackage}}.core.inbound"; public String inboundDtosPackage = "{{basePackage}}.core.inbound.dtos"; public String outboundPackage = "{{basePackage}}.core.outbound"; @@ -91,7 +93,7 @@ protected boolean is(Map model, String... annotations) { return !(JSONPath.get(model, "$.entity.options[?(" + annotationsFilter + ")]", List.of())).isEmpty(); } - protected Function, Boolean> skipEntityRepository = (model) -> !is(model, "aggregate"); + protected Function, Boolean> skipEntityRepository = (model) -> !(is(model, "aggregate") || ZDLFindUtils.isAggregateRoot(JSONPath.get(model, "zdl"), JSONPath.get(model, "$.entity.name"))); protected Function, Boolean> skipEntityId = (model) -> is(model, "embedded", "vo", "input", "abstract"); protected Function, Boolean> skipEntity = (model) -> is(model, "vo", "input"); protected Function, Boolean> skipEntityInput = (model) -> inputDTOSuffix == null || inputDTOSuffix.isEmpty(); @@ -102,6 +104,11 @@ protected boolean is(Map model, String... annotations) { protected ZDLProjectTemplates configureProjectTemplates() { var ts = new ZDLProjectTemplates("io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator"); + ts.addTemplate(ts.aggregateTemplates, "src/main/java","core/domain/common/Aggregate.java", + "{{asPackageFolder entitiesPackage}}/{{aggregate.name}}.java", JAVA, null, true); + ts.addTemplate(ts.domainEventsTemplates, "src/main/java","core/domain/common/DomainEvent.java", + "{{asPackageFolder domainEventsPackage}}/{{event.name}}.java", JAVA, null, true); + ts.addTemplate(ts.entityTemplates, "src/main/java","core/domain/{{persistence}}/Entity.java", "{{asPackageFolder entitiesPackage}}/{{entity.name}}.java", JAVA, skipEntity, false); ts.addTemplate(ts.entityTemplates, "src/main/java","core/outbound/{{persistence}}/{{style}}/EntityRepository.java", diff --git a/plugins/backend-application-default/src/main/java/io/zenwave360/sdk/plugins/BackendMultiModuleApplicationGenerator.java b/plugins/backend-application-default/src/main/java/io/zenwave360/sdk/plugins/BackendMultiModuleApplicationGenerator.java index 69c35ecb..a8c5779d 100644 --- a/plugins/backend-application-default/src/main/java/io/zenwave360/sdk/plugins/BackendMultiModuleApplicationGenerator.java +++ b/plugins/backend-application-default/src/main/java/io/zenwave360/sdk/plugins/BackendMultiModuleApplicationGenerator.java @@ -11,6 +11,11 @@ public class BackendMultiModuleApplicationGenerator extends BackendDefaultApplic protected ZDLProjectTemplates configureProjectTemplates() { var ts = new ZDLProjectTemplates("io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator"); + ts.addTemplate(ts.aggregateTemplates, "src/main/java","core/domain/common/Aggregate.java","{{mavenModulesPrefix}}-domain", + "{{asPackageFolder entitiesPackage}}/{{aggregate.name}}.java", JAVA, null, true); + ts.addTemplate(ts.domainEventsTemplates, "src/main/java","core/domain/common/DomainEvent.java","{{mavenModulesPrefix}}-domain", + "{{asPackageFolder domainEventsPackage}}/{{event.name}}.java", JAVA, null, true); + ts.addTemplate(ts.entityTemplates, "src/main/java","core/domain/{{persistence}}/Entity.java", "{{mavenModulesPrefix}}-domain", "{{asPackageFolder entitiesPackage}}/{{entity.name}}.java", JAVA, skipEntity, false); ts.addTemplate(ts.entityTemplates, "src/main/java","core/outbound/{{persistence}}/{{style}}/EntityRepository.java", "{{mavenModulesPrefix}}-domain", diff --git a/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/domain/common/Aggregate.java.hbs b/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/domain/common/Aggregate.java.hbs new file mode 100644 index 00000000..4fa352ee --- /dev/null +++ b/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/domain/common/Aggregate.java.hbs @@ -0,0 +1,61 @@ +package {{entitiesPackage}}; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import {{domainEventsPackage}}.*; +import {{inboundDtosPackage}}.*; + +import org.mapstruct.MappingTarget; +import org.mapstruct.factory.Mappers; + + +public class {{aggregate.name}} { + private static final Mapper mapper = Mappers.getMapper(Mapper.class); + + private final {{aggregate.aggregateRoot}} rootEntity; + + private final List events = new ArrayList<>(); + + public {{aggregate.name}}() { + this(new {{aggregate.aggregateRoot}}()); + } + public {{aggregate.name}}({{aggregate.aggregateRoot}} rootEntity) { + this.rootEntity = rootEntity; + } + + public String getId() { + return rootEntity.getId(); + } + + public {{aggregate.aggregateRoot}} getRootEntity() { + return rootEntity; + } + + public List getEvents() { + return Collections.unmodifiableList(events); + } + +{{#each aggregate.commands as |method|}} + {{~> (partial '../../implementation/partials/serviceMethodJavadoc')}} + public void {{method.name}}({{method.parameter}} input) { + // TODO: implement this command + mapper.update(rootEntity, input); + {{~#each (methodEvents method) as |event|}} + events.add(mapper.as{{event.name}}(rootEntity)); + {{~/each}} + } +{{/each}} + + @org.mapstruct.Mapper + interface Mapper { + {{~#each (findAggregateInputs aggregate) as |input|}} + {{aggregate.aggregateRoot}} update(@MappingTarget {{aggregate.aggregateRoot}} entity, {{input}} input); + {{~/each}} + + {{~#each (aggregateEvents aggregate) as |event|}} + {{event.className}} as{{event.name}}({{aggregate.aggregateRoot}} entity); + {{~/each}} + } +} diff --git a/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/domain/common/DomainEvent.java.hbs b/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/domain/common/DomainEvent.java.hbs new file mode 100644 index 00000000..501f7ed5 --- /dev/null +++ b/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/domain/common/DomainEvent.java.hbs @@ -0,0 +1,51 @@ +package {{domainEventsPackage}}; + +import java.io.Serializable; +import java.math.*; +import java.time.*; +import java.util.*; +import jakarta.validation.constraints.*; + +import {{entitiesPackage}}.*; + +/** +* {{event.comment}} +*/ +{{~#if useLombok}} +@lombok.Getter @lombok.Setter +{{~/if}} +public {{abstractClass event}} class {{event.className}} {{addExtends event}} implements Serializable { + + @java.io.Serial + private static final long serialVersionUID = 1L; + +{{#each event.fields as |field|}} + {{#each field.validations as |validation|~}} + // @{{validation.name}}("{{validation.value}}") + {{~/each}} + private {{{fieldType field}}} {{field.name}}; +{{/each}} + + +{{#each event.fields as |field|}} + {{~#if field.isArray}} + public {{event.className}} add{{capitalize field.name}}({{javaType field}} {{field.name}}) { + this.{{field.name}}.add({{field.name}}); + return this; + } + {{~/if}} +{{/each}} + +{{~#unless useLombok}} +{{#each event.fields as |field|}} + public {{{fieldType field}}} get{{capitalize field.name}}() { + return {{field.name}}; + } + + public {{event.className}} set{{capitalize field.name}}({{{fieldType field}}} {{field.name}}) { + this.{{field.name}} = {{field.name}}; + return this; + } +{{/each}} +{{~/unless}} +} diff --git a/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/implementation/jpa/imperative/ServiceImpl.java.hbs b/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/implementation/jpa/imperative/ServiceImpl.java.hbs index cf4b2d77..a3465def 100644 --- a/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/implementation/jpa/imperative/ServiceImpl.java.hbs +++ b/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/implementation/jpa/imperative/ServiceImpl.java.hbs @@ -8,7 +8,9 @@ import {{outboundPackage}}.jpa.*; {{#if includeEmitEventsImplementation}} import {{outboundEventsPackage}}.*; {{/if}} - +{{#if (includeDomainEvents service)}} +import {{domainEventsPackage}}.*; +{{/if}} import java.math.*; import java.time.*; @@ -22,6 +24,9 @@ import org.springframework.data.domain.Pageable; import java.util.concurrent.CompletableFuture; import org.springframework.scheduling.annotation.Async; {{~/if}} +{{~#if (includeEmitEventsImplementation service)}} +import org.springframework.context.ApplicationEventPublisher; +{{~/if}} import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -48,6 +53,7 @@ public class {{service.name}}Impl implements {{service.name}} { {{#if (includeEmitEventsImplementation service)}} private final EventsMapper eventsMapper = EventsMapper.INSTANCE; private final {{eventsProducerInterface service.name}} eventsProducer; + private final ApplicationEventPublisher applicationEventPublisher; {{/if}} {{#unless useLombok~}} @@ -55,12 +61,14 @@ public class {{service.name}}Impl implements {{service.name}} { * Constructor. */ public {{service.name}}Impl({{#joinWithTemplate service.entities delimiter=", "}}{{#unless (skipEntityRepository this)}}{{className}}Repository {{instanceName}}Repository{{/unless}}{{/joinWithTemplate}} - {{#if (includeEmitEventsImplementation service)}}, {{eventsProducerInterface service.name}} eventsProducer{{/if}}) { + {{#if (includeEmitEventsImplementation service)}}, {{eventsProducerInterface service.name}} eventsProducer, ApplicationEventPublisher applicationEventPublisher{{/if}} + ) { {{~#joinWithTemplate service.entities ~}} {{#unless (skipEntityRepository this)}}this.{{instanceName}}Repository = {{instanceName}}Repository;{{/unless}} {{~/joinWithTemplate~}} {{~#if (includeEmitEventsImplementation service)}} this.eventsProducer = eventsProducer; + this.applicationEventPublisher = applicationEventPublisher; {{~/if}} } {{/unless~}} @@ -72,4 +80,26 @@ public class {{service.name}}Impl implements {{service.name}} { } {{/each}} +{{#each (serviceAggregates service) as |aggregate|}} + private {{aggregate.name}} persistAndEmitEvents({{aggregate.name}} {{asInstanceName aggregate.name}}) { + var {{asInstanceName aggregate.aggregateRoot}} = {{asInstanceName aggregate.aggregateRoot}}Repository.save({{asInstanceName aggregate.name}}.getRootEntity()); + {{asInstanceName aggregate.name}}.getEvents().forEach(event -> { + {{#each (aggregateEvents aggregate) as |event|}} + if (event instanceof {{event.className}}) { + {{~#if event.options.asyncapi }} + {{~#if includeEmitEventsImplementation }} + eventsProducer.{{operationNameForEvent event.name}}({{asInstanceName event.name}}); + {{~else}} + // TODO: set 'includeEmitEventsImplementation' to generate this + // eventsProducer.{{operationNameForEvent event.name}}({{asInstanceName event.name}}); + {{~/if}} + {{~else}} + applicationEventPublisher.publishEvent(event); + {{~/if}} + } + {{/each}} + }); + return {{asInstanceName aggregate.name}}; + } +{{/each}} } diff --git a/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/implementation/mappers/ServiceMapper.java.hbs b/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/implementation/mappers/ServiceMapper.java.hbs index d9196d6b..b50efc5c 100644 --- a/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/implementation/mappers/ServiceMapper.java.hbs +++ b/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/implementation/mappers/ServiceMapper.java.hbs @@ -15,10 +15,10 @@ import org.springframework.data.domain.Page; public interface {{service.name}}Mapper { {{service.name}}Mapper INSTANCE = Mappers.getMapper({{service.name}}Mapper.class); -{{~#each service.aggregates as |entityName|}} +{{~#each service.entityNames as |entityName|}} {{~assign "entity" (findEntity entityName)}} - {{~#each (findAggregateInputs entity) as |input|}} + {{~#each (findServiceInputs entity) as |input|}} {{~#if (not (eq entity.className input))}} // {{entity.className}} as{{entity.className}}({{mapperInputSignature input}}); {{~/if}} diff --git a/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/implementation/mongodb/imperative/ServiceImpl.java.hbs b/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/implementation/mongodb/imperative/ServiceImpl.java.hbs index 68a9c9ba..29659b6c 100644 --- a/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/implementation/mongodb/imperative/ServiceImpl.java.hbs +++ b/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/implementation/mongodb/imperative/ServiceImpl.java.hbs @@ -8,6 +8,9 @@ import {{outboundPackage}}.mongodb.*; {{#if includeEmitEventsImplementation}} import {{outboundEventsPackage}}.*; {{/if}} +{{#if (includeDomainEvents service)}} +import {{domainEventsPackage}}.*; +{{/if}} import java.math.*; import java.time.*; @@ -21,6 +24,9 @@ import org.springframework.data.domain.Pageable; import java.util.concurrent.CompletableFuture; import org.springframework.scheduling.annotation.Async; {{~/if}} +{{~#if (includeEmitEventsImplementation service)}} +import org.springframework.context.ApplicationEventPublisher; +{{~/if}} import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -47,6 +53,7 @@ public class {{service.name}}Impl implements {{service.name}} { {{#if (includeEmitEventsImplementation service)}} private final EventsMapper eventsMapper = EventsMapper.INSTANCE; private final {{eventsProducerInterface service.name}} eventsProducer; + private final ApplicationEventPublisher applicationEventPublisher; {{/if}} {{#unless useLombok~}} @@ -54,12 +61,14 @@ public class {{service.name}}Impl implements {{service.name}} { * Constructor. */ public {{service.name}}Impl({{#joinWithTemplate service.entities delimiter=", "}}{{#unless (skipEntityRepository this)}}{{className}}Repository {{instanceName}}Repository{{/unless}}{{/joinWithTemplate}} - {{#if (includeEmitEventsImplementation service)}}, {{eventsProducerInterface service.name}} eventsProducer{{/if}}) { + {{#if (includeEmitEventsImplementation service)}}, {{eventsProducerInterface service.name}} eventsProducer, ApplicationEventPublisher applicationEventPublisher{{/if}} + ) { {{~#joinWithTemplate service.entities ~}} {{#unless (skipEntityRepository this)}}this.{{instanceName}}Repository = {{instanceName}}Repository;{{/unless}} {{~/joinWithTemplate~}} {{~#if (includeEmitEventsImplementation service)}} this.eventsProducer = eventsProducer; + this.applicationEventPublisher = applicationEventPublisher; {{~/if}} } {{/unless~}} @@ -70,4 +79,27 @@ public class {{service.name}}Impl implements {{service.name}} { {{~> (partial '../../partials/mongodbMethodBody')~}} } {{/each}} + +{{#each (serviceAggregates service) as |aggregate|}} + private {{aggregate.name}} persistAndEmitEvents({{aggregate.name}} {{asInstanceName aggregate.name}}) { + var {{asInstanceName aggregate.aggregateRoot}} = {{asInstanceName aggregate.aggregateRoot}}Repository.save({{asInstanceName aggregate.name}}.getRootEntity()); + {{asInstanceName aggregate.name}}.getEvents().forEach(event -> { + {{#each (aggregateEvents aggregate) as |event|}} + if (event instanceof {{event.className}}) { + {{~#if event.options.asyncapi }} + {{~#if includeEmitEventsImplementation }} + eventsProducer.{{operationNameForEvent event.name}}({{asInstanceName event.name}}); + {{~else}} + // TODO: set 'includeEmitEventsImplementation' to generate this + // eventsProducer.{{operationNameForEvent event.name}}({{asInstanceName event.name}}); + {{~/if}} + {{~else}} + applicationEventPublisher.publishEvent(event); + {{~/if}} + } + {{/each}} + }); + return {{asInstanceName aggregate.name}}; + } +{{/each}} } diff --git a/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/implementation/partials/mongodbMethodBody.hbs b/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/implementation/partials/mongodbMethodBody.hbs index 527922ca..8ebbd762 100644 --- a/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/implementation/partials/mongodbMethodBody.hbs +++ b/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/implementation/partials/mongodbMethodBody.hbs @@ -75,8 +75,9 @@ {{~/if}} {{~else}} log.debug("Request {{method.name}}"); - // var {{entity.instanceName}} = new {{entity.className}}(); + var {{entity.instanceName}}; // = new {{entity.className}}(); {{~/if}} + // TODO: implement this method {{~#if method.returnType}} {{~#if (eq entity.name returnEntity.name)}} @@ -89,6 +90,7 @@ {{~/if}} {{~/if}} {{~else~}} + // TODO: implement this method {{~> (partial 'withEvents')}} {{~#if method.returnType}} diff --git a/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/outbound/mongodb/imperative/EntityRepository.java.hbs b/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/outbound/mongodb/imperative/EntityRepository.java.hbs index 74115288..bf03ea91 100644 --- a/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/outbound/mongodb/imperative/EntityRepository.java.hbs +++ b/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/main/java/core/outbound/mongodb/imperative/EntityRepository.java.hbs @@ -1,13 +1,26 @@ -package {{basePackage}}.core.outbound.mongodb; +package {{outboundPackage}}.mongodb; +{{~assign 'aggregate' (findEntityAggregate entity.name)}} -import {{basePackage}}.core.domain.{{entity.className}}; +import {{entitiesPackage}}.{{entity.className}}; +{{~#if aggregate}} +import {{entitiesPackage}}.{{aggregate}}; +{{~/if}} import org.springframework.data.mongodb.repository.MongoRepository; import org.springframework.data.mongodb.repository.Query; import org.springframework.stereotype.Repository; +import java.util.Optional; + /** * Spring Data MongoDB repository for the {{entity.className}} entity. */ @SuppressWarnings("unused") @Repository -public interface {{entity.className}}Repository extends MongoRepository<{{entity.className}}, String> {} +public interface {{entity.className}}Repository extends MongoRepository<{{entity.className}}, String> { + +{{~#if aggregate}} + default Optional<{{aggregate}}> find{{aggregate}}ById({{idJavaType}} id) { + return findById(id).map({{aggregate}}::new); + } +{{~/if}} +} diff --git a/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/test/java/config/ServicesInMemoryConfig.java.hbs b/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/test/java/config/ServicesInMemoryConfig.java.hbs index 06887834..2c90b839 100644 --- a/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/test/java/config/ServicesInMemoryConfig.java.hbs +++ b/plugins/backend-application-default/src/main/resources/io/zenwave360/sdk/plugins/BackendApplicationDefaultGenerator/src/test/java/config/ServicesInMemoryConfig.java.hbs @@ -9,6 +9,9 @@ import {{coreImplementationPackage}}.*; {{#if includeEmitEventsImplementation}} import {{outboundEventsPackage}}.*; {{/if}} +{{~#if (includeEmitEventsImplementation service)}} +import org.springframework.context.ApplicationEventPublisher; +{{~/if}} import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Profile; @@ -32,7 +35,7 @@ public class ServicesInMemoryConfig extends RepositoriesInMemoryConfig { {{#unless (skipEntityRepository entity)}}{{entity.instanceName}}Repository(){{/unless}} {{~/joinWithTemplate~}} {{#if (includeEmitEventsImplementation service)}} - , eventsProducerInMemoryContext.{{eventsProducerInstance service.name}}() + , eventsProducerInMemoryContext.{{eventsProducerInstance service.name}}(), applicationEventPublisher {{/if}} ); {{~/each}} @@ -56,4 +59,19 @@ public class ServicesInMemoryConfig extends RepositoriesInMemoryConfig { {{entity.className}}Repository().saveAll({{entity.instanceNamePlural}}); {{~/each}} } + +{{~#if (includeEmitEventsImplementation service)}} + private List publishedEvents = new ArrayList<>(); + + public List getPublishedEvents() { + return publishedEvents; + } + + private ApplicationEventPublisher applicationEventPublisher = new ApplicationEventPublisher() { + @Override + public void publishEvent(Object event) { + publishedEvents.add(event); + } + }; +{{~/if}} } diff --git a/plugins/backend-application-default/src/test/java/io/zenwave360/sdk/plugins/BackendApplicationMongoImperativeGeneratorTest.java b/plugins/backend-application-default/src/test/java/io/zenwave360/sdk/plugins/BackendApplicationMongoImperativeGeneratorTest.java index d0f0feba..514a8d13 100644 --- a/plugins/backend-application-default/src/test/java/io/zenwave360/sdk/plugins/BackendApplicationMongoImperativeGeneratorTest.java +++ b/plugins/backend-application-default/src/test/java/io/zenwave360/sdk/plugins/BackendApplicationMongoImperativeGeneratorTest.java @@ -79,4 +79,26 @@ public void test_generator_hexagonal_mongodb_order_faults_attachments() throws E Assertions.assertEquals(0, exitCode); } + @Test + public void test_generator_hexagonal_mongodb_orders_with_aggregate() throws Exception { + String targetFolder = "target/zdl/test_generator_hexagonal_mongodb_orders_with_aggregate"; + Plugin plugin = new BackendApplicationDefaultPlugin() + .withSpecFile("classpath:io/zenwave360/sdk/resources/zdl/orders-with-aggregate.zdl") + .withTargetFolder(targetFolder) + .withOption("basePackage", "io.zenwave360.example") + .withOption("persistence", PersistenceType.mongodb) + .withOption("style", ProgrammingStyle.imperative) + .withOption("forceOverwrite", true) + .withOption("haltOnFailFormatting", false); + + new MainGenerator().generate(plugin); + + List logs = logCaptor.getLogs(); + // Assertions.assertTrue(logs.contains("Writing template with targetFile: io/example/integration/test/api/provider_for_commands_reactive/DoCreateProductConsumer.java")); + // Assertions.assertTrue(logs.contains("Writing template with targetFile: io/example/integration/test/api/provider_for_commands_reactive/DoCreateProductService.java")); + + int exitCode = MavenCompiler.copyPomAndCompile("src/test/resources/mongodb-elasticsearch-scs3-pom.xml", targetFolder); + Assertions.assertEquals(0, exitCode); + } + } diff --git a/pom.xml b/pom.xml index 92e81c0b..cf5ebb6a 100644 --- a/pom.xml +++ b/pom.xml @@ -74,7 +74,7 @@ 1.7 2.38.0 0.0.39 - 4.13.2 + 5.10.1 2.6.7 0.10.2 5.8.2 diff --git a/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/generators/AbstractZDLProjectGenerator.java b/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/generators/AbstractZDLProjectGenerator.java index 78aa22c8..58f0aa25 100644 --- a/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/generators/AbstractZDLProjectGenerator.java +++ b/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/generators/AbstractZDLProjectGenerator.java @@ -5,6 +5,7 @@ import io.zenwave360.sdk.templating.*; import io.zenwave360.sdk.utils.JSONPath; +import io.zenwave360.sdk.zdl.ZDLFindUtils; public abstract class AbstractZDLProjectGenerator extends AbstractZDLGenerator { @@ -31,6 +32,33 @@ public List generate(Map contextModel) { var templateOutputList = new ArrayList(); var apiModel = getZDLModel(contextModel); + Map> aggregates = (Map) apiModel.get("aggregates"); + Set> domainEvents = new HashSet<>(); + for (Map aggregate : aggregates.values()) { + for (TemplateInput template : templates.aggregateTemplates) { + templateOutputList.addAll(generateTemplateOutput(contextModel, template, Map.of("aggregate", aggregate))); + } + var events = ZDLFindUtils.aggregateEvents(aggregate); + for (String eventName : events) { + var event = JSONPath.get(apiModel, "$.events." + eventName); + if(event != null) { + domainEvents.add((Map) event); + } + } + } + + // include all events not annotated with @asyncapi + domainEvents.addAll((List) JSONPath.get(apiModel, "$.events[*][?(!@.options.asyncapi && !@.options.embedded)]", List.of())); + // include all events referenced by fields + JSONPath.get(domainEvents, "$..fields[*].type", List.of()).stream().map(type -> (Map) JSONPath.get(apiModel, "$.events." + type)).filter(Objects::nonNull).forEach(domainEvents::add); + + for (Map domainEvent : domainEvents) { + for (TemplateInput template : templates.domainEventsTemplates) { + templateOutputList.addAll(generateTemplateOutput(contextModel, template, Map.of("event", domainEvent))); + } + } + + Map> entities = (Map) apiModel.get("entities"); for (Map entity : entities.values()) { if (!isGenerateEntity(entity)) { @@ -106,7 +134,8 @@ protected List> getEntitiesByService(Map ser if (entityNames.size() == 1 && "*".equals(entityNames.get(0))) { entityNames = JSONPath.get(apiModel, "$.entities[*].name"); } - List> entitiesByService = (List>) entityNames.stream().map(e -> JSONPath.get(apiModel, "$.entities." + e)).collect(Collectors.toList()); + entityNames = entityNames.stream().map(entity -> JSONPath.get(apiModel, "$.aggregates." + entity + ".aggregateRoot", entity)).toList(); + List> entitiesByService = (List>) entityNames.stream().map(e -> JSONPath.get(apiModel, "$.entities." + e)).toList(); List excludedNames = ((List) service.get("excludedNames")); if (excludedNames != null && excludedNames.size() > 0) { entitiesByService = entitiesByService.stream().filter(e -> !excludedNames.contains(e.get("name"))).collect(Collectors.toList()); diff --git a/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/generators/ZDLProjectTemplates.java b/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/generators/ZDLProjectTemplates.java index c4be39d5..3b636f4e 100644 --- a/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/generators/ZDLProjectTemplates.java +++ b/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/generators/ZDLProjectTemplates.java @@ -17,6 +17,9 @@ public ZDLProjectTemplates(String templatesFolder) { this.templatesFolder = templatesFolder; } + public List aggregateTemplates = new ArrayList<>(); + + public List domainEventsTemplates = new ArrayList<>(); public List entityTemplates = new ArrayList<>(); public List enumTemplates = new ArrayList<>(); diff --git a/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/processors/ZDL2JDLProcessor.java b/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/processors/ZDL2JDLProcessor.java index 81a5881a..e60140e9 100644 --- a/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/processors/ZDL2JDLProcessor.java +++ b/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/processors/ZDL2JDLProcessor.java @@ -26,6 +26,9 @@ public Map process(Map contextModel) { var entityNames = (List) service.get("aggregates"); for (Object entityName : entityNames) { var entity = JSONPath.get(model, "$.entities['" + entityName + "']"); + if(entity == null) { + continue; + } var options = JSONPath.get(entity, "$.options", new HashMap<>()); options.put("service", service.get("name")); JSONPath.set(entity, "$.options", options); diff --git a/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/processors/ZDLProcessor.java b/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/processors/ZDLProcessor.java index d83a887b..d21cbc6e 100644 --- a/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/processors/ZDLProcessor.java +++ b/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/processors/ZDLProcessor.java @@ -51,7 +51,12 @@ public void processServiceName(Map zdlModel) { for (Map.Entry service : services.entrySet()) { var aggregates = JSONPath.get(service.getValue(), "$.aggregates", List.of()); for (Object aggregate : aggregates) { - JSONPath.set(zdlModel, "$.entities." + aggregate + ".options.service", service.getKey()); + if(JSONPath.get(zdlModel, "$.entities." + aggregate) != null) { + JSONPath.set(zdlModel, "$.entities." + aggregate + ".options.service", service.getKey()); + } +// if(JSONPath.get(zdlModel, "$.aggregates." + aggregate) != null) { +// JSONPath.set(zdlModel, "$.aggregates." + aggregate + ".options.service", service.getKey()); +// } } } } diff --git a/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/zdl/ZDLFindUtils.java b/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/zdl/ZDLFindUtils.java index 4cb6ebb4..f63d3835 100644 --- a/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/zdl/ZDLFindUtils.java +++ b/zenwave-sdk-cli/src/main/java/io/zenwave360/sdk/zdl/ZDLFindUtils.java @@ -94,6 +94,20 @@ public static Map findServiceMethod(String operationId, Map aggregateEvents(Map aggregate) { + var allEvents = new HashSet(); + var methods = JSONPath.get(aggregate, "$.commands[*]", List.of()); + for (var method : methods) { + allEvents.addAll(methodEventsFlatList(method)); + } + return allEvents; + } + public static List methodEventsFlatList(Map method) { var events = (List) method.getOrDefault("withEvents", List.of()); List allEvents = new ArrayList<>(); diff --git a/zenwave-sdk-cli/src/test/java/io/zenwave360/sdk/zdl/ZDLFindUtilsTest.java b/zenwave-sdk-cli/src/test/java/io/zenwave360/sdk/zdl/ZDLFindUtilsTest.java index 52371081..f81c1851 100644 --- a/zenwave-sdk-cli/src/test/java/io/zenwave360/sdk/zdl/ZDLFindUtilsTest.java +++ b/zenwave-sdk-cli/src/test/java/io/zenwave360/sdk/zdl/ZDLFindUtilsTest.java @@ -4,7 +4,9 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; +import io.zenwave360.sdk.utils.JSONPath; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -89,6 +91,21 @@ public void testFindServiceMethod() throws Exception { Assertions.assertEquals("AttachmentService", method.get("serviceName")); } + @Test + public void isAggregateRoot() throws Exception { + var model = loadZDL("classpath:io/zenwave360/sdk/resources/zdl/orders-with-aggregate.zdl"); + Assertions.assertTrue(ZDLFindUtils.isAggregateRoot(model, "CustomerOrder")); + Assertions.assertFalse(ZDLFindUtils.isAggregateRoot(model, "Restaurant")); + } + + @Test + public void aggregateEvents() throws Exception { + var model = loadZDL("classpath:io/zenwave360/sdk/resources/zdl/orders-with-aggregate.zdl"); + var aggregate = JSONPath.get(model, "$.aggregates.CustomerOrderAggregate", Map.of()); + var events = ZDLFindUtils.aggregateEvents(aggregate); + Assertions.assertEquals(Set.of("OrderEvent", "OrderStatusUpdated"), events); + } + @Test public void methodEventsFlatList() throws Exception { var model = loadZDL("classpath:io/zenwave360/sdk/resources/zdl/customer-address.zdl"); diff --git a/zenwave-sdk-test-resources/src/main/resources/io/zenwave360/sdk/resources/zdl/orders-with-aggregate.zdl b/zenwave-sdk-test-resources/src/main/resources/io/zenwave360/sdk/resources/zdl/orders-with-aggregate.zdl new file mode 100644 index 00000000..da377f4e --- /dev/null +++ b/zenwave-sdk-test-resources/src/main/resources/io/zenwave360/sdk/resources/zdl/orders-with-aggregate.zdl @@ -0,0 +1,208 @@ +/** + * ZenWave Online Food Delivery - Orders Module. + */ +config { + title "ZenWave Online Food Delivery - Orders Module" + basePackage "io.zenwave360.example.orders" + targetFolder "modules/orders" + persistence mongodb + + // these are code generation plugins for ZenWave IntelliJ Plugin, for models you can skip to 'entities' section + plugins { + + ZDLToOpenAPIPlugin { + idType string + targetFile "{{targetFolder}}/src/main/resources/apis/openapi.yml" + } + + ZDLToAsyncAPIPlugin { + asyncapiVersion v3 + idType string + targetFile "{{targetFolder}}/src/main/resources/apis/asyncapi.yml" + } + + BackendApplicationDefaultPlugin { + useLombok true + includeEmitEventsImplementation true + // --force // overwite all files + } + + OpenAPIControllersPlugin { + formatter google // comments in one line are better for demos + // TODO fix this: specFile "{{targetFolder}}/src/main/resources/apis/openapi.yml" + specFile "modules/orders/src/main/resources/apis/openapi.yml" + zdlFile "models/orders.zdl" + + // these should match the values of openapi-generator-maven-plugin + openApiApiPackage "{{basePackage}}.adapters.web" + openApiModelPackage "{{basePackage}}.adapters.web.model" + openApiModelNameSuffix DTO + } + + SpringCloudStreams3AdaptersPlugin { + apiId "restaurants" + role client + specFile "modules/restaurants/src/main/resources/apis/asyncapi.yml" + modelPackage "{{basePackage}}.client.{{apiId}}.events.dtos" + consumerApiPackage "{{basePackage}}.client.{{apiId}}.events.consumer" + } + SpringCloudStreams3AdaptersPlugin { + apiId "delivery" + role client + specFile "modules/delivery/src/main/resources/apis/asyncapi.yml" + modelPackage "{{basePackage}}.client.{{apiId}}.events.dtos" + consumerApiPackage "{{basePackage}}.client.{{apiId}}.events.consumer" + } + + } +} + +apis { + asyncapi(provider) default { + uri "orders/src/main/resources/apis/asyncapi.yml" + } + asyncapi(client) RestaurantsAsyncAPI { + uri "restaurants/src/main/resources/apis/asyncapi.yml" + } + asyncapi(client) DeliveryAsyncAPI { + uri "delivery/src/main/resources/apis/asyncapi.yml" + } +} + + +aggregate CustomerOrderAggregate(CustomerOrder) { + createOrder(CustomerOrderInput) withEvents OrderEvent + updateOrder(CustomerOrderInput) withEvents OrderEvent OrderStatusUpdated + updateKitchenStatus(KitchenStatusInput) withEvents OrderEvent OrderStatusUpdated + updateDeliveryStatus(DeliveryStatusInput) withEvents OrderEvent OrderStatusUpdated + cancelOrder(CancelOrderInput) withEvents OrderEvent OrderStatusUpdated +} + +// == Entities ============================= +// @aggregate +entity CustomerOrder { + orderTime Instant + status OrderStatus + customerDetails Customer { + customerId String required + firstName String required + lastName String required + email String required + phone String required + address Address { + street String required + city String + state String + zip String + } + } + restaurantDetails Restaurant { + restaurantId String required + name String required + phone String required + addresses Address { + street String required + city String + state String + zip String + } + } + orderItems OrderItemInput[] { + menuItemId String required + name String required + description String + price BigDecimal required + quantity Integer required + } +} + +enum OrderStatus { + RECEIVED, KITCHEN_ACCEPTED, DELIVERY_ACCEPTED, CONFIRMED, + KITCHEN_IN_PROGRESS, KITCHEN_READY, KITCHEN_DELIVERED, + ON_DELIVERY, DELIVERED, CANCELLED +} + +// == Serices ============================= + +input CustomerOrderInput { + orderTime Instant + status OrderStatus + customerId String required + restaurantId String required + addressIdentifier String required + orderItems OrderItem[] { + menuItemId String required + name String required + description String + price BigDecimal required + quantity Integer required + } +} + +input OrdersFilter { + status OrderStatus + customerName String + restaurantName String +} + +input KitchenStatusInput { + kitchenOrderId String + kitchenStatus KitchenStatus +} + +input DeliveryStatusInput { + deliveryOrderId String + deliveryStatus DeliveryStatus +} + +input CancelOrderInput { + id String + reason String +} + +@input +enum KitchenStatus { + REJECTED, ACCEPTED, IN_PROGRESS, READY, DELIVERED, CANCELLED +} + +@input +enum DeliveryStatus { + REJECTED, ACCEPTED, IN_PROGRESS, DELIVERED, CANCELLED +} + +@rest("/orders") +service OrdersService for (CustomerOrderAggregate) { + @get("/{orderId}") + getCustomerOrder(id) CustomerOrder? + @post + createOrder(CustomerOrderInput) CustomerOrder withEvents OrderEvent + @put("/{orderId}") + updateOrder(id, CustomerOrderInput) CustomerOrder withEvents OrderEvent OrderStatusUpdated + + @asyncapi({api: RestaurantsAsyncAPI, channel: "KitchenOrdersStatusChannel"}) + updateKitchenStatus(id, KitchenStatusInput) CustomerOrder withEvents OrderEvent OrderStatusUpdated + @asyncapi({api: DeliveryAsyncAPI, channel: "DeliveryStatusChannel"}) + updateDeliveryStatus(id, DeliveryStatusInput) CustomerOrder withEvents OrderEvent OrderStatusUpdated + + @asyncapi({channel: "CancelOrdersChannel", topic: "orders.cancel_orders"}) + @put("/{orderId}/cancel") + cancelOrder(id, CancelOrderInput) CustomerOrder withEvents OrderEvent OrderStatusUpdated + + @post("/search") + searchOrders(OrdersFilter) CustomerOrder[] +} + +@copy(CustomerOrder) +@asyncapi({channel: "OrdersChannel", topic: "orders.orders"}) +event OrderEvent { + id String + // + all fields from CustomerOrder (carried state transfer) +} + +@asyncapi({channel: "OrderUpdatesChannel", topic: "orders.order_updates"}) +event OrderStatusUpdated { + id String + dateTime Instant + status OrderStatus + previousStatus OrderStatus +}