eva4j 1.0.17 ā 1.0.18
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.
- package/AGENTS.md +2 -0
- package/DOMAIN_YAML_GUIDE.md +3 -1
- package/QUICK_REFERENCE.md +8 -4
- package/bin/eva4j.js +70 -2
- package/config/defaults.json +1 -0
- package/docs/CAMUNDA_DMN_GUIDE.md +1380 -0
- package/docs/KAFKA_PRODUCTION_CONFIG.md +441 -0
- package/docs/RABBITMQ_PRODUCTION_CONFIG.md +227 -0
- package/docs/commands/ADD_RABBITMQ_CLIENT.md +192 -0
- package/docs/commands/EVALUATE_SYSTEM.md +272 -8
- package/docs/commands/GENERATE_RABBITMQ_EVENT.md +341 -0
- package/docs/commands/GENERATE_RABBITMQ_LISTENER.md +595 -0
- package/docs/commands/GENERATE_TEMPORAL_FLOW.md +52 -12
- package/docs/commands/INDEX.md +27 -3
- package/docs/prototype/TEMPORAL_COMMUNICATION_PATTERNS.md +731 -0
- package/docs/prototype/TEMPORAL_DESIGN_METHODOLOGY.md +740 -0
- package/docs/prototype/system/RISKS.md +277 -0
- package/docs/prototype/system/customers.yaml +133 -0
- package/docs/prototype/system/inventory.yaml +109 -0
- package/docs/prototype/system/notifications.yaml +131 -0
- package/docs/prototype/system/orders.yaml +241 -0
- package/docs/prototype/system/payments.yaml +256 -0
- package/docs/prototype/system/products.yaml +168 -0
- package/docs/prototype/system/system.yaml +269 -0
- package/examples/domain-endpoints-multi-aggregate.yaml +140 -0
- package/examples/domain-read-models.yaml +2 -2
- package/examples/system/customer.yaml +89 -0
- package/examples/system/orders.yaml +119 -0
- package/examples/system/product.yaml +27 -0
- package/examples/system/system.yaml +80 -0
- package/package.json +1 -1
- package/src/agents/design-gap-analyst-temporal.agent.md +452 -0
- package/src/agents/design-gap-analyst.agent.md +383 -0
- package/src/agents/design-reviewer-temporal.agent.md +412 -0
- package/src/agents/design-reviewer.agent.md +31 -5
- package/src/agents/implement-use-cases.prompt.md +179 -0
- package/src/agents/ux-gap-analyst.agent.md +412 -0
- package/src/commands/add-rabbitmq-client.js +261 -0
- package/src/commands/add-temporal-client.js +22 -2
- package/src/commands/build.js +267 -11
- package/src/commands/evaluate-system.js +700 -13
- package/src/commands/generate-entities.js +308 -16
- package/src/commands/generate-rabbitmq-event.js +665 -0
- package/src/commands/generate-rabbitmq-listener.js +205 -0
- package/src/commands/generate-temporal-activity.js +968 -34
- package/src/commands/generate-temporal-flow.js +95 -38
- package/src/commands/generate-temporal-system.js +708 -0
- package/src/skills/build-system-yaml/SKILL.md +222 -2
- package/src/skills/build-system-yaml/references/domain-yaml-spec.md +50 -4
- package/src/skills/build-system-yaml/references/module-spec.md +57 -8
- package/src/skills/build-temporal-system/SKILL.md +752 -0
- package/src/skills/build-temporal-system/references/temporal-communication-patterns.md +167 -0
- package/src/skills/build-temporal-system/references/temporal-domain-yaml-spec.md +449 -0
- package/src/skills/build-temporal-system/references/temporal-module-spec.md +353 -0
- package/src/skills/build-temporal-system/references/temporal-system-yaml-spec.md +326 -0
- package/src/skills/implement-use-case/SKILL.md +350 -0
- package/src/skills/implement-use-case/references/use-case-patterns.md +980 -0
- package/src/skills/requirements-elicitation/SKILL.md +228 -0
- package/src/skills/requirements-elicitation/references/interview-framework.md +260 -0
- package/src/skills/requirements-elicitation/references/output-templates.md +368 -0
- package/src/utils/bounded-context-diagram.js +844 -0
- package/src/utils/domain-validator.js +266 -4
- package/src/utils/naming.js +10 -0
- package/src/utils/system-validator.js +169 -11
- package/src/utils/system-yaml-parser.js +318 -0
- package/src/utils/temporal-validator.js +497 -0
- package/src/utils/yaml-to-entity.js +10 -7
- package/templates/aggregate/DomainEventHandler.java.ejs +116 -22
- package/templates/aggregate/JpaAggregateRoot.java.ejs +2 -2
- package/templates/aggregate/JpaEntity.java.ejs +2 -2
- package/templates/base/docker/rabbitmq-services.yaml.ejs +12 -0
- package/templates/base/resources/parameters/develop/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/develop/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/develop/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/local/kafka.yaml.ejs +5 -0
- package/templates/base/resources/parameters/local/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/local/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/production/kafka.yaml.ejs +39 -8
- package/templates/base/resources/parameters/production/rabbitmq.yaml.ejs +32 -0
- package/templates/base/resources/parameters/production/temporal.yaml.ejs +0 -3
- package/templates/base/resources/parameters/test/kafka.yaml.ejs +12 -6
- package/templates/base/resources/parameters/test/rabbitmq.yaml.ejs +15 -0
- package/templates/base/resources/parameters/test/temporal.yaml.ejs +0 -3
- package/templates/base/root/AGENTS.md.ejs +1 -1
- package/templates/crud/EndpointsController.java.ejs +1 -1
- package/templates/crud/ScaffoldCommand.java.ejs +5 -2
- package/templates/crud/ScaffoldCommandHandler.java.ejs +3 -1
- package/templates/crud/ScaffoldQuery.java.ejs +5 -2
- package/templates/crud/ScaffoldQueryHandler.java.ejs +3 -1
- package/templates/crud/SubEntityRemoveCommand.java.ejs +1 -1
- package/templates/evaluate/report.html.ejs +1447 -90
- package/templates/kafka-event/KafkaConfigBean.java.ejs +1 -1
- package/templates/kafka-event/KafkaMessageBroker.java.ejs +3 -3
- package/templates/ports/PortAclMapper.java.ejs +35 -0
- package/templates/ports/PortFeignAdapter.java.ejs +7 -22
- package/templates/ports/PortFeignClient.java.ejs +4 -0
- package/templates/ports/PortResponseDto.java.ejs +1 -1
- package/templates/rabbitmq-event/RabbitConfigBean.java.ejs +33 -0
- package/templates/rabbitmq-event/RabbitConfigExchange.java.ejs +12 -0
- package/templates/rabbitmq-event/RabbitMessageBroker.java.ejs +35 -0
- package/templates/rabbitmq-event/RabbitMessageBrokerMethod.java.ejs +9 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerBean.java.ejs +33 -0
- package/templates/rabbitmq-listener/RabbitConfigConsumerExchange.java.ejs +12 -0
- package/templates/rabbitmq-listener/RabbitListenerClass.java.ejs +82 -0
- package/templates/rabbitmq-listener/RabbitListenerSimple.java.ejs +56 -0
- package/templates/read-model/ReadModelJpa.java.ejs +1 -1
- package/templates/read-model/ReadModelJpaRepository.java.ejs +2 -0
- package/templates/read-model/ReadModelRabbitListener.java.ejs +71 -0
- package/templates/read-model/ReadModelRepositoryImpl.java.ejs +9 -5
- package/templates/read-model/ReadModelSyncHandler.java.ejs +2 -0
- package/templates/shared/configurations/kafkaConfig/KafkaConfig.java.ejs +18 -4
- package/templates/shared/configurations/rabbitmqConfig/RabbitMQConfig.java.ejs +100 -0
- package/templates/shared/configurations/temporalConfig/TemporalConfig.java.ejs +2 -64
- package/templates/shared/configurations/temporalConfig/TemporalWorkerFactoryLifecycle.java.ejs +41 -0
- package/templates/temporal-activity/ActivityImpl.java.ejs +68 -2
- package/templates/temporal-activity/ActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/ActivityInterface.java.ejs +7 -1
- package/templates/temporal-activity/ActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/NestedType.java.ejs +12 -0
- package/templates/temporal-activity/SharedActivityInput.java.ejs +14 -0
- package/templates/temporal-activity/SharedActivityInterface.java.ejs +15 -0
- package/templates/temporal-activity/SharedActivityOutput.java.ejs +14 -0
- package/templates/temporal-activity/SharedNestedType.java.ejs +12 -0
- package/templates/temporal-flow/ModuleHeavyActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleLightActivity.java.ejs +6 -0
- package/templates/temporal-flow/ModuleTemporalWorkerConfig.java.ejs +58 -0
- package/templates/temporal-flow/WorkFlowImpl.java.ejs +172 -12
- package/templates/temporal-flow/WorkFlowInput.java.ejs +11 -0
- package/templates/temporal-flow/WorkFlowInterface.java.ejs +5 -4
- package/templates/temporal-flow/WorkFlowService.java.ejs +42 -12
- package/COMMAND_EVALUATION.md +0 -911
- package/test-c2010.js +0 -49
- package/test-update-compat.js +0 -109
- package/test-update-lifecycle.js +0 -121
|
@@ -3,12 +3,499 @@ const ora = require('ora');
|
|
|
3
3
|
const chalk = require('chalk');
|
|
4
4
|
const path = require('path');
|
|
5
5
|
const fs = require('fs-extra');
|
|
6
|
+
const yaml = require('js-yaml');
|
|
6
7
|
const ConfigManager = require('../utils/config-manager');
|
|
7
8
|
const { isEva4jProject, moduleExists } = require('../utils/validator');
|
|
8
|
-
const { toPackagePath, toPascalCase, toCamelCase } = require('../utils/naming');
|
|
9
|
+
const { toPackagePath, toPascalCase, toCamelCase, toScreamingSnakeCase } = require('../utils/naming');
|
|
9
10
|
const { renderAndWrite } = require('../utils/template-engine');
|
|
10
11
|
const ChecksumManager = require('../utils/checksum-manager');
|
|
11
12
|
|
|
13
|
+
// āāā Java type ā import mapping āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
14
|
+
const JAVA_TYPE_IMPORTS = {
|
|
15
|
+
BigDecimal: 'java.math.BigDecimal',
|
|
16
|
+
LocalDateTime: 'java.time.LocalDateTime',
|
|
17
|
+
LocalDate: 'java.time.LocalDate',
|
|
18
|
+
LocalTime: 'java.time.LocalTime',
|
|
19
|
+
Instant: 'java.time.Instant',
|
|
20
|
+
UUID: 'java.util.UUID',
|
|
21
|
+
List: 'java.util.List',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Resolve Java imports for a list of typed fields.
|
|
26
|
+
* @param {Array<{javaType: string}>} fields
|
|
27
|
+
* @returns {string[]} sorted import statements
|
|
28
|
+
*/
|
|
29
|
+
function resolveFieldImports(fields) {
|
|
30
|
+
const imports = new Set();
|
|
31
|
+
for (const field of fields) {
|
|
32
|
+
for (const [type, imp] of Object.entries(JAVA_TYPE_IMPORTS)) {
|
|
33
|
+
if (field.javaType && field.javaType.includes(type)) {
|
|
34
|
+
imports.add(`import ${imp};`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return Array.from(imports).sort();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse the `activities:` section from a module's domain.yaml.
|
|
43
|
+
* Returns null if the file does not exist or has no activities.
|
|
44
|
+
* @param {string} domainYamlPath
|
|
45
|
+
* @returns {Array|null}
|
|
46
|
+
*/
|
|
47
|
+
async function parseActivitiesFromYaml(domainYamlPath) {
|
|
48
|
+
if (!(await fs.pathExists(domainYamlPath))) return null;
|
|
49
|
+
const content = await fs.readFile(domainYamlPath, 'utf-8');
|
|
50
|
+
const data = yaml.load(content);
|
|
51
|
+
if (!data || !Array.isArray(data.activities) || data.activities.length === 0) return null;
|
|
52
|
+
return data.activities;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extract aggregate info (root entity name + fields) from domain.yaml data.
|
|
57
|
+
* Returns null if no aggregate with a root entity is found.
|
|
58
|
+
* @param {string} domainYamlPath
|
|
59
|
+
* @returns {object|null} { aggregateName, rootEntityName, rootFieldNames: Set<string>, idFieldName, idFieldType }
|
|
60
|
+
*/
|
|
61
|
+
async function parseAggregateInfo(domainYamlPath) {
|
|
62
|
+
if (!(await fs.pathExists(domainYamlPath))) return null;
|
|
63
|
+
const content = await fs.readFile(domainYamlPath, 'utf-8');
|
|
64
|
+
const data = yaml.load(content);
|
|
65
|
+
if (!data || !Array.isArray(data.aggregates) || data.aggregates.length === 0) return null;
|
|
66
|
+
|
|
67
|
+
for (const agg of data.aggregates) {
|
|
68
|
+
const entities = Array.isArray(agg.entities) ? agg.entities : [];
|
|
69
|
+
const root = entities.find((e) => e.isRoot === true);
|
|
70
|
+
if (!root || !Array.isArray(root.fields)) continue;
|
|
71
|
+
|
|
72
|
+
const aggregateName = toPascalCase(agg.name);
|
|
73
|
+
const rootEntityName = toPascalCase(root.name);
|
|
74
|
+
const rootFieldNames = new Set(root.fields.map((f) => f.name));
|
|
75
|
+
const rootFieldTypes = new Map(root.fields.map((f) => [f.name, f.type || 'String']));
|
|
76
|
+
const idField = root.fields.find((f) => f.name === 'id');
|
|
77
|
+
|
|
78
|
+
// Build voFieldsMap: PascalCase VO name ā raw fields array
|
|
79
|
+
const voFieldsMap = new Map();
|
|
80
|
+
const voList = Array.isArray(agg.valueObjects) ? agg.valueObjects : [];
|
|
81
|
+
for (const vo of voList) {
|
|
82
|
+
voFieldsMap.set(toPascalCase(vo.name), Array.isArray(vo.fields) ? vo.fields : []);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Build lazyRelationshipFields: camelCase field name ā { relType, target, jpaFieldName }
|
|
86
|
+
// OneToMany / ManyToMany are LAZY by JPA default; OneToOne / ManyToOne are EAGER by default.
|
|
87
|
+
const lazyRelationshipFields = new Map();
|
|
88
|
+
const relationships = Array.isArray(root.relationships) ? root.relationships : [];
|
|
89
|
+
for (const rel of relationships) {
|
|
90
|
+
const relType = (rel.type || '').trim();
|
|
91
|
+
const fetchType = (rel.fetch || '').toUpperCase();
|
|
92
|
+
const isToMany = relType === 'OneToMany' || relType === 'ManyToMany';
|
|
93
|
+
// LAZY when: *ToMany (unless explicitly EAGER) OR any type explicitly marked LAZY
|
|
94
|
+
const isLazy = isToMany ? fetchType !== 'EAGER' : fetchType === 'LAZY';
|
|
95
|
+
if (!isLazy) continue;
|
|
96
|
+
const targetPascal = toPascalCase(rel.target || '');
|
|
97
|
+
if (!targetPascal) continue;
|
|
98
|
+
const fieldBase = targetPascal.charAt(0).toLowerCase() + targetPascal.slice(1);
|
|
99
|
+
const fieldName = isToMany ? fieldBase + 's' : fieldBase; // simple plural for *ToMany
|
|
100
|
+
lazyRelationshipFields.set(fieldName, { relType, target: targetPascal, jpaFieldName: fieldName });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
aggregateName,
|
|
105
|
+
rootEntityName,
|
|
106
|
+
rootFieldNames,
|
|
107
|
+
rootFieldTypes,
|
|
108
|
+
voFieldsMap,
|
|
109
|
+
lazyRelationshipFields,
|
|
110
|
+
idFieldName: idField ? idField.name : 'id',
|
|
111
|
+
idFieldType: idField ? (idField.type || 'String') : 'String',
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if a field name represents an entity ID for the given aggregate.
|
|
119
|
+
* Matches: 'id', '{entityName}Id' (e.g. 'shoppingCartId' for ShoppingCart),
|
|
120
|
+
* or the abbreviated last-word form: '{lastWord}Id' (e.g. 'cartId' for ShoppingCart).
|
|
121
|
+
* @param {string} fieldName
|
|
122
|
+
* @param {object} aggregateInfo
|
|
123
|
+
* @returns {boolean}
|
|
124
|
+
*/
|
|
125
|
+
function isIdField(fieldName, aggregateInfo) {
|
|
126
|
+
const entityLower = aggregateInfo.rootEntityName.charAt(0).toLowerCase() + aggregateInfo.rootEntityName.slice(1);
|
|
127
|
+
// Split camelCase to get the last word: 'shoppingCart' ā ['shopping','Cart'] ā 'cart'
|
|
128
|
+
const nameParts = entityLower.split(/(?=[A-Z])/);
|
|
129
|
+
const lastWord = nameParts[nameParts.length - 1].toLowerCase();
|
|
130
|
+
return fieldName === 'id'
|
|
131
|
+
|| fieldName === `${entityLower}Id`
|
|
132
|
+
|| (nameParts.length > 1 && fieldName === `${lastWord}Id`);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Map a single YAML field definition {name, type} to {name, javaType}.
|
|
137
|
+
* @param {{name: string, type: string}} field
|
|
138
|
+
* @returns {{name: string, javaType: string}}
|
|
139
|
+
*/
|
|
140
|
+
function mapField(field) {
|
|
141
|
+
return { name: field.name, javaType: field.type || 'String' };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Build the template context for a single activity from its YAML definition.
|
|
146
|
+
* @param {object} activity - Raw activity object from domain.yaml
|
|
147
|
+
* @param {string} packageName
|
|
148
|
+
* @param {string} moduleName
|
|
149
|
+
* @param {boolean} shared - Whether this activity's contracts live in shared/
|
|
150
|
+
* @param {object|null} aggregateInfo - Aggregate info for query activity detection
|
|
151
|
+
* @returns {object} context for templates
|
|
152
|
+
*/
|
|
153
|
+
function buildActivityContext(activity, packageName, moduleName, shared = false, aggregateInfo = null, sharedNestedTypes = new Set(), existingSharedTypes = new Map()) {
|
|
154
|
+
const activityPascalCase = toPascalCase(activity.name);
|
|
155
|
+
const modulePascalCase = toPascalCase(moduleName);
|
|
156
|
+
const categoryType = (activity.type || 'light').toLowerCase() === 'heavy' ? 'HeavyActivity' : 'LightActivity';
|
|
157
|
+
const activityCategory = `${modulePascalCase}${categoryType}`;
|
|
158
|
+
|
|
159
|
+
const inputFields = Array.isArray(activity.input) ? activity.input.map(mapField) : [];
|
|
160
|
+
const outputFields = Array.isArray(activity.output) ? activity.output.map(mapField) : [];
|
|
161
|
+
const hasInput = inputFields.length > 0;
|
|
162
|
+
const hasOutput = outputFields.length > 0;
|
|
163
|
+
|
|
164
|
+
// āā Query activity detection āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
165
|
+
// An activity is a "query activity" when:
|
|
166
|
+
// 1. Name starts with "Get"
|
|
167
|
+
// 2. Exactly 1 input field that is an entity ID
|
|
168
|
+
// 3. Has output fields
|
|
169
|
+
// 4. Module has an aggregate with root entity
|
|
170
|
+
let isQueryActivity = false;
|
|
171
|
+
let queryMeta = null;
|
|
172
|
+
|
|
173
|
+
if (aggregateInfo
|
|
174
|
+
&& activityPascalCase.startsWith('Get')
|
|
175
|
+
&& inputFields.length === 1
|
|
176
|
+
&& hasOutput
|
|
177
|
+
&& isIdField(inputFields[0].name, aggregateInfo)) {
|
|
178
|
+
isQueryActivity = true;
|
|
179
|
+
const idAccessor = `input.${inputFields[0].name}()`;
|
|
180
|
+
const entityLower = aggregateInfo.rootEntityName.charAt(0).toLowerCase() + aggregateInfo.rootEntityName.slice(1);
|
|
181
|
+
const entityIdAlias = `${entityLower}Id`; // e.g. 'customerId' for Customer
|
|
182
|
+
const SIMPLE_TYPES = new Set(['String', 'Integer', 'Long', 'Boolean', 'BigDecimal', 'LocalDate', 'LocalDateTime', 'LocalTime', 'Instant', 'UUID', 'Double', 'Float']);
|
|
183
|
+
// Build nestedTypesMap: PascalCase name ā raw fields array (for VOānestedType detection)
|
|
184
|
+
const nestedTypesMap = new Map();
|
|
185
|
+
if (Array.isArray(activity.nestedTypes)) {
|
|
186
|
+
for (const nt of activity.nestedTypes) {
|
|
187
|
+
nestedTypesMap.set(toPascalCase(nt.name), Array.isArray(nt.fields) ? nt.fields : []);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const outputMappings = outputFields.map((f) => {
|
|
191
|
+
// Match {entityName}Id ā entity.getId()
|
|
192
|
+
if (f.name === entityIdAlias && aggregateInfo.rootFieldNames.has('id')) {
|
|
193
|
+
return { fieldName: f.name, accessor: 'entity.getId()' };
|
|
194
|
+
}
|
|
195
|
+
if (aggregateInfo.rootFieldNames.has(f.name)) {
|
|
196
|
+
const getter = 'entity.get' + f.name.charAt(0).toUpperCase() + f.name.slice(1) + '()';
|
|
197
|
+
const entityFieldType = aggregateInfo.rootFieldTypes ? aggregateInfo.rootFieldTypes.get(f.name) : null;
|
|
198
|
+
// Enum/VO ā String: append .name() when output expects String but entity field is not a simple type
|
|
199
|
+
if (f.javaType === 'String' && entityFieldType && !SIMPLE_TYPES.has(entityFieldType)) {
|
|
200
|
+
return { fieldName: f.name, accessor: getter + '.name()' };
|
|
201
|
+
}
|
|
202
|
+
// VO ā nestedType: when entity field is a VO and output expects a declared nestedType
|
|
203
|
+
const voFields = entityFieldType && aggregateInfo.voFieldsMap ? aggregateInfo.voFieldsMap.get(entityFieldType) : null;
|
|
204
|
+
const ntFields = nestedTypesMap.get(f.javaType);
|
|
205
|
+
if (voFields && ntFields) {
|
|
206
|
+
const ntArgs = ntFields.map((ntField) => {
|
|
207
|
+
const voField = voFields.find((vf) => vf.name === ntField.name);
|
|
208
|
+
return voField
|
|
209
|
+
? `${getter}.get${ntField.name.charAt(0).toUpperCase() + ntField.name.slice(1)}()`
|
|
210
|
+
: `null /* TODO: derive ${ntField.name} */`;
|
|
211
|
+
});
|
|
212
|
+
return {
|
|
213
|
+
fieldName: f.name,
|
|
214
|
+
accessor: f.name,
|
|
215
|
+
preDeclaration: { varType: f.javaType, varName: f.name, getter, ntArgs },
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
return { fieldName: f.name, accessor: getter };
|
|
219
|
+
}
|
|
220
|
+
return { fieldName: f.name, accessor: `null /* TODO: derive ${f.name} from entity */` };
|
|
221
|
+
});
|
|
222
|
+
const voMappingImports = outputMappings
|
|
223
|
+
.filter((m) => m.preDeclaration)
|
|
224
|
+
.map((m) => (shared
|
|
225
|
+
? `import ${packageName}.shared.domain.contracts.${moduleName}.${m.preDeclaration.varType};`
|
|
226
|
+
: `import ${packageName}.${moduleName}.application.dtos.temporal.${m.preDeclaration.varType};`));
|
|
227
|
+
|
|
228
|
+
// āā Eager load detection āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
229
|
+
// If any output field is a List<...> whose name matches a LAZY relationship,
|
|
230
|
+
// we must use findByIdWith{Field} (JOIN FETCH) instead of findById.
|
|
231
|
+
let needsEagerLoad = false;
|
|
232
|
+
let eagerLoadField = null;
|
|
233
|
+
const lazyMap = aggregateInfo.lazyRelationshipFields;
|
|
234
|
+
if (lazyMap && lazyMap.size > 0) {
|
|
235
|
+
for (const f of outputFields) {
|
|
236
|
+
if (!f.javaType.startsWith('List<')) continue;
|
|
237
|
+
for (const [lazyFieldName, lazyInfo] of lazyMap.entries()) {
|
|
238
|
+
const lazyFieldPascal = lazyFieldName.charAt(0).toUpperCase() + lazyFieldName.slice(1);
|
|
239
|
+
const outputFieldPascal = f.name.charAt(0).toUpperCase() + f.name.slice(1);
|
|
240
|
+
// Direct name match OR the lazy field name ends with the output field name (PascalCase suffix)
|
|
241
|
+
if (f.name === lazyFieldName || lazyFieldPascal.endsWith(outputFieldPascal)) {
|
|
242
|
+
needsEagerLoad = true;
|
|
243
|
+
eagerLoadField = { jpaFieldName: lazyInfo.jpaFieldName, fieldPascal: lazyFieldPascal };
|
|
244
|
+
break;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (needsEagerLoad) break;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
queryMeta = {
|
|
252
|
+
aggregateName: aggregateInfo.aggregateName,
|
|
253
|
+
idFieldAccessor: idAccessor,
|
|
254
|
+
outputMappings,
|
|
255
|
+
voMappingImports,
|
|
256
|
+
needsEagerLoad,
|
|
257
|
+
eagerLoadField,
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
packageName,
|
|
263
|
+
moduleName,
|
|
264
|
+
modulePascalCase,
|
|
265
|
+
activityPascalCase,
|
|
266
|
+
activityCategory,
|
|
267
|
+
categoryType,
|
|
268
|
+
hasInput,
|
|
269
|
+
hasOutput,
|
|
270
|
+
inputFields,
|
|
271
|
+
outputFields,
|
|
272
|
+
inputImports: resolveFieldImports(inputFields),
|
|
273
|
+
outputImports: resolveFieldImports(outputFields),
|
|
274
|
+
description: activity.description || '',
|
|
275
|
+
compensation: activity.compensation || null,
|
|
276
|
+
timeout: activity.timeout || null,
|
|
277
|
+
shared,
|
|
278
|
+
targetModule: moduleName,
|
|
279
|
+
isQueryActivity,
|
|
280
|
+
queryMeta,
|
|
281
|
+
...buildNestedTypesContext(activity, packageName, moduleName, shared, inputFields, outputFields, sharedNestedTypes, existingSharedTypes),
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Extract custom type names referenced in field javaTypes that are NOT
|
|
287
|
+
* standard Java types (not in JAVA_TYPE_IMPORTS and not primitives).
|
|
288
|
+
* E.g., `List<OrderItemDetail>` ā `OrderItemDetail`.
|
|
289
|
+
* @param {Array<{javaType: string}>} fields
|
|
290
|
+
* @returns {Set<string>} custom type names found
|
|
291
|
+
*/
|
|
292
|
+
function extractCustomTypeNames(fields) {
|
|
293
|
+
const BUILTIN = new Set(['String', 'Integer', 'Long', 'Boolean', 'Double', 'Float', 'int', 'long', 'boolean', 'double', 'float', 'void',
|
|
294
|
+
...Object.keys(JAVA_TYPE_IMPORTS)]);
|
|
295
|
+
const customNames = new Set();
|
|
296
|
+
const genericRe = /<([A-Z][A-Za-z0-9]*)>/;
|
|
297
|
+
for (const field of fields) {
|
|
298
|
+
if (!field.javaType) continue;
|
|
299
|
+
// Check inside generics: List<OrderItemDetail> ā OrderItemDetail
|
|
300
|
+
const match = field.javaType.match(genericRe);
|
|
301
|
+
if (match && !BUILTIN.has(match[1])) {
|
|
302
|
+
customNames.add(match[1]);
|
|
303
|
+
}
|
|
304
|
+
// Check bare type: OrderItemDetail (not inside generic)
|
|
305
|
+
if (!match && !BUILTIN.has(field.javaType) && /^[A-Z]/.test(field.javaType)) {
|
|
306
|
+
customNames.add(field.javaType);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
return customNames;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Build nestedTypes context from an activity's `nestedTypes` YAML section.
|
|
314
|
+
* Returns properties to merge into the activity context.
|
|
315
|
+
* @param {object} activity - Raw activity YAML
|
|
316
|
+
* @param {string} packageName
|
|
317
|
+
* @param {string} moduleName
|
|
318
|
+
* @param {boolean} shared
|
|
319
|
+
* @param {Array} inputFields - Already-mapped input fields
|
|
320
|
+
* @param {Array} outputFields - Already-mapped output fields
|
|
321
|
+
* @returns {object} { nestedTypes, nestedTypeImportLines }
|
|
322
|
+
*/
|
|
323
|
+
function buildNestedTypesContext(activity, packageName, moduleName, shared, inputFields, outputFields, sharedNestedTypes = new Set(), existingSharedTypes = new Map()) {
|
|
324
|
+
const rawNested = Array.isArray(activity.nestedTypes) ? activity.nestedTypes : [];
|
|
325
|
+
const rawExternal = Array.isArray(activity.externalTypes) ? activity.externalTypes : [];
|
|
326
|
+
|
|
327
|
+
if (rawNested.length === 0 && rawExternal.length === 0) {
|
|
328
|
+
return { nestedTypes: [], nestedTypeImportLines: [], externalTypeMap: {} };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const nestedTypes = rawNested.map((nt) => {
|
|
332
|
+
const name = toPascalCase(nt.name);
|
|
333
|
+
const fields = Array.isArray(nt.fields) ? nt.fields.map(mapField) : [];
|
|
334
|
+
return {
|
|
335
|
+
name,
|
|
336
|
+
fields,
|
|
337
|
+
imports: resolveFieldImports(fields),
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Build external type map: typeName ā sourceModule (camelCase)
|
|
342
|
+
const externalTypeMap = {};
|
|
343
|
+
for (const ext of rawExternal) {
|
|
344
|
+
const typeName = toPascalCase(ext.name);
|
|
345
|
+
const sourceModule = toCamelCase(ext.module);
|
|
346
|
+
externalTypeMap[typeName] = sourceModule;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Determine which custom types are actually referenced by input/output fields
|
|
350
|
+
const allFields = [...inputFields, ...outputFields];
|
|
351
|
+
const referencedNames = extractCustomTypeNames(allFields);
|
|
352
|
+
const nestedTypeNames = new Set(nestedTypes.map((nt) => nt.name));
|
|
353
|
+
|
|
354
|
+
// Build import lines for custom types referenced in input/output
|
|
355
|
+
const importLines = [];
|
|
356
|
+
for (const typeName of referencedNames) {
|
|
357
|
+
if (nestedTypeNames.has(typeName)) {
|
|
358
|
+
// Check if an identical type already lives in shared/domain/contracts/ ā if so, prefer it.
|
|
359
|
+
const alreadySharedModule = existingSharedTypes.get(typeName);
|
|
360
|
+
if (sharedNestedTypes.has(typeName) || shared) {
|
|
361
|
+
// Prefer the module that actually owns the type in shared (first-writer-wins).
|
|
362
|
+
// This ensures Input/Output files in module B import from module A's contracts
|
|
363
|
+
// when both declared the same nestedType but A was processed first.
|
|
364
|
+
const ownerModule = existingSharedTypes.get(typeName) || moduleName;
|
|
365
|
+
importLines.push(`import ${packageName}.shared.domain.contracts.${ownerModule}.${typeName};`);
|
|
366
|
+
} else if (alreadySharedModule) {
|
|
367
|
+
// Type already generated into shared by another activity ā point there, not locally.
|
|
368
|
+
importLines.push(`import ${packageName}.shared.domain.contracts.${alreadySharedModule}.${typeName};`);
|
|
369
|
+
} else {
|
|
370
|
+
importLines.push(`import ${packageName}.${moduleName}.application.dtos.temporal.${typeName};`);
|
|
371
|
+
}
|
|
372
|
+
} else if (externalTypeMap[typeName]) {
|
|
373
|
+
// External type ā always from source module's shared contracts
|
|
374
|
+
const sourceModule = externalTypeMap[typeName];
|
|
375
|
+
importLines.push(`import ${packageName}.shared.domain.contracts.${sourceModule}.${typeName};`);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return {
|
|
380
|
+
nestedTypes,
|
|
381
|
+
nestedTypeImportLines: importLines.sort(),
|
|
382
|
+
externalTypeMap,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Parse system.yaml and return a Set of activity names that are referenced
|
|
388
|
+
* cross-module (i.e. appear as activity or compensation in workflow steps).
|
|
389
|
+
* @param {string} projectDir
|
|
390
|
+
* @returns {Set<string>} activity names (PascalCase) that are cross-module
|
|
391
|
+
*/
|
|
392
|
+
async function parseCrossModuleActivities(projectDir) {
|
|
393
|
+
const systemYamlPath = path.join(projectDir, 'system', 'system.yaml');
|
|
394
|
+
if (!(await fs.pathExists(systemYamlPath))) return new Set();
|
|
395
|
+
|
|
396
|
+
const content = await fs.readFile(systemYamlPath, 'utf-8');
|
|
397
|
+
const data = yaml.load(content);
|
|
398
|
+
if (!data || !Array.isArray(data.workflows)) return new Set();
|
|
399
|
+
|
|
400
|
+
const crossModuleSet = new Set();
|
|
401
|
+
|
|
402
|
+
for (const wf of data.workflows) {
|
|
403
|
+
if (!Array.isArray(wf.steps)) continue;
|
|
404
|
+
const hostModule = wf.trigger && wf.trigger.module
|
|
405
|
+
? toCamelCase(wf.trigger.module)
|
|
406
|
+
: null;
|
|
407
|
+
for (const step of wf.steps) {
|
|
408
|
+
if (step.activity && step.target) {
|
|
409
|
+
const targetModule = toCamelCase(step.target);
|
|
410
|
+
// Only mark as cross-module if target differs from the workflow host
|
|
411
|
+
if (targetModule !== hostModule) {
|
|
412
|
+
crossModuleSet.add(toPascalCase(step.activity));
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (step.compensation) {
|
|
416
|
+
// Compensation targets same module as the step it compensates
|
|
417
|
+
const compTarget = step.target ? toCamelCase(step.target) : hostModule;
|
|
418
|
+
if (compTarget !== hostModule) {
|
|
419
|
+
crossModuleSet.add(toPascalCase(step.compensation));
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
return crossModuleSet;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Generate shared activity contracts (Interface + Input + Output) into
|
|
430
|
+
* shared/domain/contracts/{module}/. Called by YAML-driven generation
|
|
431
|
+
* and also exported for use by `eva g temporal-system`.
|
|
432
|
+
*
|
|
433
|
+
* @param {object} actCtx - Activity context from buildActivityContext()
|
|
434
|
+
* @param {string} sharedBasePath - Path to shared/ Java directory
|
|
435
|
+
* @param {object} writeOptions
|
|
436
|
+
* @returns {string[]} list of generated file paths (relative)
|
|
437
|
+
*/
|
|
438
|
+
async function generateSharedContracts(actCtx, sharedBasePath, writeOptions, existingSharedTypes = new Map()) {
|
|
439
|
+
const templatesDir = path.join(__dirname, '..', '..', 'templates', 'temporal-activity');
|
|
440
|
+
const contractsDir = path.join(sharedBasePath, 'domain', 'contracts', actCtx.targetModule);
|
|
441
|
+
const generated = [];
|
|
442
|
+
|
|
443
|
+
// 1. Shared Input record
|
|
444
|
+
if (actCtx.hasInput) {
|
|
445
|
+
await renderAndWrite(
|
|
446
|
+
path.join(templatesDir, 'SharedActivityInput.java.ejs'),
|
|
447
|
+
path.join(contractsDir, `${actCtx.activityPascalCase}Input.java`),
|
|
448
|
+
{ ...actCtx, imports: actCtx.inputImports },
|
|
449
|
+
writeOptions
|
|
450
|
+
);
|
|
451
|
+
generated.push(`shared/domain/contracts/${actCtx.targetModule}/${actCtx.activityPascalCase}Input.java`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// 2. Shared Output record
|
|
455
|
+
if (actCtx.hasOutput) {
|
|
456
|
+
await renderAndWrite(
|
|
457
|
+
path.join(templatesDir, 'SharedActivityOutput.java.ejs'),
|
|
458
|
+
path.join(contractsDir, `${actCtx.activityPascalCase}Output.java`),
|
|
459
|
+
{ ...actCtx, imports: actCtx.outputImports },
|
|
460
|
+
writeOptions
|
|
461
|
+
);
|
|
462
|
+
generated.push(`shared/domain/contracts/${actCtx.targetModule}/${actCtx.activityPascalCase}Output.java`);
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
// 3. Shared ActivityInterface
|
|
466
|
+
await renderAndWrite(
|
|
467
|
+
path.join(templatesDir, 'SharedActivityInterface.java.ejs'),
|
|
468
|
+
path.join(contractsDir, `${actCtx.activityPascalCase}Activity.java`),
|
|
469
|
+
actCtx,
|
|
470
|
+
writeOptions
|
|
471
|
+
);
|
|
472
|
+
generated.push(`shared/domain/contracts/${actCtx.targetModule}/${actCtx.activityPascalCase}Activity.java`);
|
|
473
|
+
|
|
474
|
+
// 4. Shared NestedType records
|
|
475
|
+
if (Array.isArray(actCtx.nestedTypes) && actCtx.nestedTypes.length > 0) {
|
|
476
|
+
for (const nt of actCtx.nestedTypes) {
|
|
477
|
+
// Skip if an identical type already lives in *another* module's shared contracts.
|
|
478
|
+
// This prevents incompatible-types errors when the same nestedType is declared
|
|
479
|
+
// in multiple module YAMLs (e.g. CartItemDetail in carts.yaml AND products.yaml).
|
|
480
|
+
const existingOwner = existingSharedTypes.get(nt.name);
|
|
481
|
+
if (existingOwner && existingOwner !== actCtx.targetModule) continue;
|
|
482
|
+
const ntPath = path.join(contractsDir, `${nt.name}.java`);
|
|
483
|
+
if (await fs.pathExists(ntPath)) continue; // Deduplicate within same module
|
|
484
|
+
await renderAndWrite(
|
|
485
|
+
path.join(templatesDir, 'SharedNestedType.java.ejs'),
|
|
486
|
+
ntPath,
|
|
487
|
+
{ packageName: actCtx.packageName, targetModule: actCtx.targetModule, typeName: nt.name, fields: nt.fields, imports: nt.imports },
|
|
488
|
+
writeOptions
|
|
489
|
+
);
|
|
490
|
+
generated.push(`shared/domain/contracts/${actCtx.targetModule}/${nt.name}.java`);
|
|
491
|
+
// Update the map so subsequent activities in the same run see this type as already written.
|
|
492
|
+
existingSharedTypes.set(nt.name, actCtx.targetModule);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
return generated;
|
|
497
|
+
}
|
|
498
|
+
|
|
12
499
|
async function generateTemporalActivityCommand(moduleName, activityName, options = {}) {
|
|
13
500
|
const projectDir = process.cwd();
|
|
14
501
|
|
|
@@ -46,7 +533,7 @@ async function generateTemporalActivityCommand(moduleName, activityName, options
|
|
|
46
533
|
const { packageName } = projectConfig;
|
|
47
534
|
const packagePath = toPackagePath(packageName);
|
|
48
535
|
|
|
49
|
-
// Normalise module name to camelCase
|
|
536
|
+
// Normalise module name to camelCase
|
|
50
537
|
moduleName = toCamelCase(moduleName);
|
|
51
538
|
|
|
52
539
|
if (!(await configManager.moduleExists(moduleName))) {
|
|
@@ -60,6 +547,349 @@ async function generateTemporalActivityCommand(moduleName, activityName, options
|
|
|
60
547
|
process.exit(1);
|
|
61
548
|
}
|
|
62
549
|
|
|
550
|
+
const moduleBasePath = path.join(projectDir, 'src', 'main', 'java', packagePath, moduleName);
|
|
551
|
+
const checksumManager = new ChecksumManager(moduleBasePath);
|
|
552
|
+
await checksumManager.load();
|
|
553
|
+
const writeOptions = { force: options.force, checksumManager };
|
|
554
|
+
|
|
555
|
+
// āā Try YAML-driven generation āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
556
|
+
const domainYamlPath = path.join(moduleBasePath, 'domain.yaml');
|
|
557
|
+
const yamlActivities = await parseActivitiesFromYaml(domainYamlPath);
|
|
558
|
+
|
|
559
|
+
if (yamlActivities) {
|
|
560
|
+
await generateFromYaml(yamlActivities, activityName, {
|
|
561
|
+
projectDir, packageName, packagePath, moduleName, moduleBasePath, checksumManager, writeOptions, options,
|
|
562
|
+
});
|
|
563
|
+
} else {
|
|
564
|
+
await generateInteractive(activityName, {
|
|
565
|
+
projectDir, packageName, packagePath, moduleName, moduleBasePath, checksumManager, writeOptions, options,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
await checksumManager.save();
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// āāā YAML-driven generation āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Walk shared/domain/contracts/ and return a Map from PascalCase type simple name
|
|
576
|
+
* ā camelCase sourceModule for every *.java file found.
|
|
577
|
+
* Used to detect when a local nestedType duplicates a type that already lives in shared.
|
|
578
|
+
* @param {string} sharedBasePath - absolute path to shared/ Java root
|
|
579
|
+
* @returns {Map<string, string>} simpleName ā sourceModule
|
|
580
|
+
*/
|
|
581
|
+
async function scanExistingSharedTypes(sharedBasePath) {
|
|
582
|
+
const result = new Map();
|
|
583
|
+
const contractsRoot = path.join(sharedBasePath, 'domain', 'contracts');
|
|
584
|
+
if (!(await fs.pathExists(contractsRoot))) return result;
|
|
585
|
+
let moduleDirs;
|
|
586
|
+
try { moduleDirs = await fs.readdir(contractsRoot); } catch { return result; }
|
|
587
|
+
for (const mod of moduleDirs) {
|
|
588
|
+
const modDir = path.join(contractsRoot, mod);
|
|
589
|
+
let stat;
|
|
590
|
+
try { stat = await fs.stat(modDir); } catch { continue; }
|
|
591
|
+
if (!stat.isDirectory()) continue;
|
|
592
|
+
let files;
|
|
593
|
+
try { files = await fs.readdir(modDir); } catch { continue; }
|
|
594
|
+
for (const file of files) {
|
|
595
|
+
if (!file.endsWith('.java')) continue;
|
|
596
|
+
const simpleName = file.replace('.java', '');
|
|
597
|
+
if (!result.has(simpleName)) result.set(simpleName, mod);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
return result;
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Scan all module domain.yaml files in the system/ directory for `externalTypes`
|
|
605
|
+
* references. Returns a Map from sourceModule (camelCase) ā Set of type names
|
|
606
|
+
* (PascalCase) that are referenced externally and thus need shared contracts.
|
|
607
|
+
*/
|
|
608
|
+
async function collectExternalTypeRefs(projectDir) {
|
|
609
|
+
const systemDir = path.join(projectDir, 'system');
|
|
610
|
+
if (!(await fs.pathExists(systemDir))) return new Map();
|
|
611
|
+
|
|
612
|
+
const refs = new Map();
|
|
613
|
+
let files;
|
|
614
|
+
try {
|
|
615
|
+
files = await fs.readdir(systemDir);
|
|
616
|
+
} catch {
|
|
617
|
+
return refs;
|
|
618
|
+
}
|
|
619
|
+
for (const file of files) {
|
|
620
|
+
if (file === 'system.yaml' || !file.endsWith('.yaml')) continue;
|
|
621
|
+
try {
|
|
622
|
+
const content = await fs.readFile(path.join(systemDir, file), 'utf-8');
|
|
623
|
+
const data = yaml.load(content);
|
|
624
|
+
if (!data || !Array.isArray(data.activities)) continue;
|
|
625
|
+
for (const act of data.activities) {
|
|
626
|
+
if (!Array.isArray(act.externalTypes)) continue;
|
|
627
|
+
for (const et of act.externalTypes) {
|
|
628
|
+
const sourceModule = toCamelCase(et.module);
|
|
629
|
+
const typeName = toPascalCase(et.name);
|
|
630
|
+
if (!refs.has(sourceModule)) refs.set(sourceModule, new Set());
|
|
631
|
+
refs.get(sourceModule).add(typeName);
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
} catch {
|
|
635
|
+
// Skip malformed YAML files
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return refs;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Inject a `findByIdWith{Field}` method (with JOIN FETCH) into the 3 repository
|
|
643
|
+
* files when a query activity needs eager loading of a LAZY relationship.
|
|
644
|
+
* Idempotent: skips injection if the method already exists in the file.
|
|
645
|
+
*
|
|
646
|
+
* @param {object} actCtx - Activity context (must have queryMeta.eagerLoadField)
|
|
647
|
+
* @param {string} moduleBasePath
|
|
648
|
+
* @param {string} packageName
|
|
649
|
+
* @param {string} moduleName
|
|
650
|
+
*/
|
|
651
|
+
async function injectEagerLoadMethods(actCtx, moduleBasePath, packageName, moduleName) {
|
|
652
|
+
const { aggregateName, eagerLoadField } = actCtx.queryMeta;
|
|
653
|
+
const { jpaFieldName, fieldPascal } = eagerLoadField;
|
|
654
|
+
const methodName = `findByIdWith${fieldPascal}`;
|
|
655
|
+
const entityLower = aggregateName.charAt(0).toLowerCase() + aggregateName.slice(1);
|
|
656
|
+
|
|
657
|
+
// 1. domain/repositories/{Entity}Repository.java
|
|
658
|
+
const repoPath = path.join(moduleBasePath, 'domain', 'repositories', `${aggregateName}Repository.java`);
|
|
659
|
+
if (await fs.pathExists(repoPath)) {
|
|
660
|
+
let content = await fs.readFile(repoPath, 'utf-8');
|
|
661
|
+
if (!content.includes(methodName)) {
|
|
662
|
+
const lastBrace = content.lastIndexOf('}');
|
|
663
|
+
const injection = `\n Optional<${aggregateName}> ${methodName}(String id);\n`;
|
|
664
|
+
content = content.slice(0, lastBrace) + injection + content.slice(lastBrace);
|
|
665
|
+
await fs.writeFile(repoPath, content, 'utf-8');
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// 2. infrastructure/database/repositories/{Entity}JpaRepository.java
|
|
670
|
+
const jpaRepoPath = path.join(moduleBasePath, 'infrastructure', 'database', 'repositories', `${aggregateName}JpaRepository.java`);
|
|
671
|
+
if (await fs.pathExists(jpaRepoPath)) {
|
|
672
|
+
let content = await fs.readFile(jpaRepoPath, 'utf-8');
|
|
673
|
+
if (!content.includes(methodName)) {
|
|
674
|
+
// Inject @Query, @Param, and Optional imports if missing
|
|
675
|
+
const queryImport = 'import org.springframework.data.jpa.repository.Query;';
|
|
676
|
+
const paramImport = 'import org.springframework.data.repository.query.Param;';
|
|
677
|
+
const optionalImport = 'import java.util.Optional;';
|
|
678
|
+
if (!content.includes(queryImport)) {
|
|
679
|
+
content = content.replace(/^(import .+;)$/m, `$1\n${queryImport}`);
|
|
680
|
+
}
|
|
681
|
+
if (!content.includes(paramImport)) {
|
|
682
|
+
content = content.replace(/^(import .+;)$/m, `$1\n${paramImport}`);
|
|
683
|
+
}
|
|
684
|
+
if (!content.includes(optionalImport)) {
|
|
685
|
+
content = content.replace(/^(import .+;)$/m, `$1\n${optionalImport}`);
|
|
686
|
+
}
|
|
687
|
+
const lastBrace = content.lastIndexOf('}');
|
|
688
|
+
const queryStr = `SELECT ${entityLower} FROM ${aggregateName}Jpa ${entityLower} JOIN FETCH ${entityLower}.${jpaFieldName} WHERE ${entityLower}.id = :id`;
|
|
689
|
+
const injection = `\n @Query("${queryStr}")\n Optional<${aggregateName}Jpa> ${methodName}(@Param("id") String id);\n`;
|
|
690
|
+
content = content.slice(0, lastBrace) + injection + content.slice(lastBrace);
|
|
691
|
+
await fs.writeFile(jpaRepoPath, content, 'utf-8');
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// 3. infrastructure/database/repositories/{Entity}RepositoryImpl.java
|
|
696
|
+
const implPath = path.join(moduleBasePath, 'infrastructure', 'database', 'repositories', `${aggregateName}RepositoryImpl.java`);
|
|
697
|
+
if (await fs.pathExists(implPath)) {
|
|
698
|
+
let content = await fs.readFile(implPath, 'utf-8');
|
|
699
|
+
if (!content.includes(methodName)) {
|
|
700
|
+
const lastBrace = content.lastIndexOf('}');
|
|
701
|
+
const injection = [
|
|
702
|
+
'',
|
|
703
|
+
' @Override',
|
|
704
|
+
` public Optional<${aggregateName}> ${methodName}(String id) {`,
|
|
705
|
+
` return jpaRepository.${methodName}(id).map(mapper::toDomain);`,
|
|
706
|
+
' }',
|
|
707
|
+
'',
|
|
708
|
+
].join('\n');
|
|
709
|
+
content = content.slice(0, lastBrace) + injection + content.slice(lastBrace);
|
|
710
|
+
await fs.writeFile(implPath, content, 'utf-8');
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async function generateFromYaml(yamlActivities, activityName, ctx) {
|
|
716
|
+
const { projectDir, packageName, packagePath, moduleName, moduleBasePath, writeOptions } = ctx;
|
|
717
|
+
|
|
718
|
+
// Detect which activities are cross-module from system.yaml
|
|
719
|
+
const crossModuleSet = await parseCrossModuleActivities(projectDir);
|
|
720
|
+
|
|
721
|
+
// Detect nestedTypes from this module that are referenced as externalTypes in other modules
|
|
722
|
+
const externalTypeRefs = await collectExternalTypeRefs(projectDir);
|
|
723
|
+
const sharedNestedTypes = externalTypeRefs.get(moduleName) || new Set();
|
|
724
|
+
|
|
725
|
+
// Load aggregate info for query activity detection
|
|
726
|
+
const domainYamlPath = path.join(moduleBasePath, 'domain.yaml');
|
|
727
|
+
const aggregateInfo = await parseAggregateInfo(domainYamlPath);
|
|
728
|
+
|
|
729
|
+
let activitiesToGenerate;
|
|
730
|
+
|
|
731
|
+
if (activityName) {
|
|
732
|
+
const match = yamlActivities.find(
|
|
733
|
+
(a) => toPascalCase(a.name) === toPascalCase(activityName)
|
|
734
|
+
);
|
|
735
|
+
if (!match) {
|
|
736
|
+
console.error(chalk.red(`ā Activity '${activityName}' not found in domain.yaml`));
|
|
737
|
+
console.error(chalk.gray('Available activities: ' + yamlActivities.map((a) => a.name).join(', ')));
|
|
738
|
+
process.exit(1);
|
|
739
|
+
}
|
|
740
|
+
activitiesToGenerate = [match];
|
|
741
|
+
} else if (ctx.options && ctx.options.generateAll) {
|
|
742
|
+
// Non-interactive: generate all activities (used by eva build)
|
|
743
|
+
activitiesToGenerate = yamlActivities;
|
|
744
|
+
} else {
|
|
745
|
+
// Prompt: generate all or select
|
|
746
|
+
const { selection } = await inquirer.prompt([{
|
|
747
|
+
type: 'list',
|
|
748
|
+
name: 'selection',
|
|
749
|
+
message: `Found ${yamlActivities.length} activities in domain.yaml:`,
|
|
750
|
+
choices: [
|
|
751
|
+
{ name: 'Generate ALL activities', value: 'all' },
|
|
752
|
+
...yamlActivities.map((a) => ({ name: ` ${a.name} (${a.type || 'light'})`, value: a.name })),
|
|
753
|
+
],
|
|
754
|
+
}]);
|
|
755
|
+
activitiesToGenerate = selection === 'all'
|
|
756
|
+
? yamlActivities
|
|
757
|
+
: [yamlActivities.find((a) => a.name === selection)];
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const spinner = ora(`Generating ${activitiesToGenerate.length} activity(ies)...`).start();
|
|
761
|
+
|
|
762
|
+
try {
|
|
763
|
+
const templatesDir = path.join(__dirname, '..', '..', 'templates', 'temporal-activity');
|
|
764
|
+
const sharedBasePath = path.join(projectDir, 'src', 'main', 'java', packagePath, 'shared');
|
|
765
|
+
|
|
766
|
+
// Scan shared/domain/contracts/ for types already generated by other activities/modules.
|
|
767
|
+
// Used to prevent local nestedType files that duplicate a shared type (causing import collisions).
|
|
768
|
+
const existingSharedTypes = await scanExistingSharedTypes(sharedBasePath);
|
|
769
|
+
|
|
770
|
+
const generated = [];
|
|
771
|
+
const sharedGenerated = [];
|
|
772
|
+
|
|
773
|
+
for (const activity of activitiesToGenerate) {
|
|
774
|
+
const isShared = crossModuleSet.has(toPascalCase(activity.name));
|
|
775
|
+
const actCtx = buildActivityContext(activity, packageName, moduleName, isShared, aggregateInfo, sharedNestedTypes, existingSharedTypes);
|
|
776
|
+
spinner.text = `Generating ${actCtx.activityPascalCase}Activity${isShared ? ' (shared)' : ''}...`;
|
|
777
|
+
|
|
778
|
+
if (isShared) {
|
|
779
|
+
// Interface + Input + Output ā shared/domain/contracts/{module}/
|
|
780
|
+
// Pass existingSharedTypes so generateSharedContracts can skip cross-module duplicates
|
|
781
|
+
// and update the map in-place for subsequent activities in the same run.
|
|
782
|
+
const sharedFiles = await generateSharedContracts(actCtx, sharedBasePath, writeOptions, existingSharedTypes);
|
|
783
|
+
sharedGenerated.push(...sharedFiles);
|
|
784
|
+
} else {
|
|
785
|
+
// Interface + Input + Output ā {module}/application/
|
|
786
|
+
if (actCtx.hasInput) {
|
|
787
|
+
await renderAndWrite(
|
|
788
|
+
path.join(templatesDir, 'ActivityInput.java.ejs'),
|
|
789
|
+
path.join(moduleBasePath, 'application', 'dtos', 'temporal', `${actCtx.activityPascalCase}Input.java`),
|
|
790
|
+
{ ...actCtx, imports: actCtx.inputImports },
|
|
791
|
+
writeOptions
|
|
792
|
+
);
|
|
793
|
+
}
|
|
794
|
+
if (actCtx.hasOutput) {
|
|
795
|
+
await renderAndWrite(
|
|
796
|
+
path.join(templatesDir, 'ActivityOutput.java.ejs'),
|
|
797
|
+
path.join(moduleBasePath, 'application', 'dtos', 'temporal', `${actCtx.activityPascalCase}Output.java`),
|
|
798
|
+
{ ...actCtx, imports: actCtx.outputImports },
|
|
799
|
+
writeOptions
|
|
800
|
+
);
|
|
801
|
+
}
|
|
802
|
+
await renderAndWrite(
|
|
803
|
+
path.join(templatesDir, 'ActivityInterface.java.ejs'),
|
|
804
|
+
path.join(moduleBasePath, 'application', 'ports', `${actCtx.activityPascalCase}Activity.java`),
|
|
805
|
+
actCtx,
|
|
806
|
+
writeOptions
|
|
807
|
+
);
|
|
808
|
+
// Local NestedType records (or shared if externally referenced / already in shared)
|
|
809
|
+
if (Array.isArray(actCtx.nestedTypes) && actCtx.nestedTypes.length > 0) {
|
|
810
|
+
for (const nt of actCtx.nestedTypes) {
|
|
811
|
+
if (sharedNestedTypes.has(nt.name)) {
|
|
812
|
+
// Generate as shared contract ā referenced by other modules via externalTypes
|
|
813
|
+
const contractsDir = path.join(sharedBasePath, 'domain', 'contracts', moduleName);
|
|
814
|
+
const ntPath = path.join(contractsDir, `${nt.name}.java`);
|
|
815
|
+
if (await fs.pathExists(ntPath)) continue; // Deduplicate
|
|
816
|
+
await renderAndWrite(
|
|
817
|
+
path.join(templatesDir, 'SharedNestedType.java.ejs'),
|
|
818
|
+
ntPath,
|
|
819
|
+
{ packageName: actCtx.packageName, targetModule: actCtx.moduleName, typeName: nt.name, fields: nt.fields, imports: nt.imports },
|
|
820
|
+
writeOptions
|
|
821
|
+
);
|
|
822
|
+
sharedGenerated.push(`shared/domain/contracts/${moduleName}/${nt.name}.java`);
|
|
823
|
+
} else if (existingSharedTypes.has(nt.name)) {
|
|
824
|
+
// A type with the same simple name already lives in shared/domain/contracts/.
|
|
825
|
+
// Skip writing a local duplicate ā the import in Output/Input already points to shared.
|
|
826
|
+
continue;
|
|
827
|
+
} else {
|
|
828
|
+
// Generate locally
|
|
829
|
+
const ntPath = path.join(moduleBasePath, 'application', 'dtos', 'temporal', `${nt.name}.java`);
|
|
830
|
+
if (await fs.pathExists(ntPath)) continue; // Deduplicate
|
|
831
|
+
await renderAndWrite(
|
|
832
|
+
path.join(templatesDir, 'NestedType.java.ejs'),
|
|
833
|
+
ntPath,
|
|
834
|
+
{ packageName: actCtx.packageName, moduleName: actCtx.moduleName, typeName: nt.name, fields: nt.fields, imports: nt.imports },
|
|
835
|
+
writeOptions
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
// ActivityImpl always goes in the module
|
|
843
|
+
await renderAndWrite(
|
|
844
|
+
path.join(templatesDir, 'ActivityImpl.java.ejs'),
|
|
845
|
+
path.join(moduleBasePath, 'infrastructure', 'adapters', 'activities', `${actCtx.activityPascalCase}ActivityImpl.java`),
|
|
846
|
+
actCtx,
|
|
847
|
+
writeOptions
|
|
848
|
+
);
|
|
849
|
+
|
|
850
|
+
// Inject eager-load repository methods when a LAZY relationship is projected
|
|
851
|
+
if (actCtx.isQueryActivity && actCtx.queryMeta && actCtx.queryMeta.needsEagerLoad) {
|
|
852
|
+
await injectEagerLoadMethods(actCtx, moduleBasePath, packageName, moduleName);
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
generated.push(actCtx);
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
spinner.succeed(chalk.green(`ā
${generated.length} activity(ies) generated successfully`));
|
|
859
|
+
|
|
860
|
+
if (sharedGenerated.length > 0) {
|
|
861
|
+
console.log(chalk.blue('\nš Shared contracts (cross-module):'));
|
|
862
|
+
for (const f of sharedGenerated) {
|
|
863
|
+
console.log(chalk.gray(` ${f}`));
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
console.log(chalk.blue('\nš Module files:'));
|
|
868
|
+
for (const actCtx of generated) {
|
|
869
|
+
const name = actCtx.activityPascalCase;
|
|
870
|
+
if (!actCtx.shared) {
|
|
871
|
+
if (actCtx.hasInput) {
|
|
872
|
+
console.log(chalk.gray(` ${moduleName}/application/dtos/temporal/${name}Input.java`));
|
|
873
|
+
}
|
|
874
|
+
if (actCtx.hasOutput) {
|
|
875
|
+
console.log(chalk.gray(` ${moduleName}/application/dtos/temporal/${name}Output.java`));
|
|
876
|
+
}
|
|
877
|
+
console.log(chalk.gray(` ${moduleName}/application/ports/${name}Activity.java`));
|
|
878
|
+
}
|
|
879
|
+
console.log(chalk.gray(` ${moduleName}/infrastructure/adapters/activities/${name}ActivityImpl.java`));
|
|
880
|
+
}
|
|
881
|
+
} catch (error) {
|
|
882
|
+
spinner.fail(chalk.red('Failed to generate temporal activity'));
|
|
883
|
+
console.error(chalk.red(error.message));
|
|
884
|
+
process.exit(1);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
// āāā Interactive generation (fallback when no domain.yaml activities) āāāāāāāā
|
|
889
|
+
|
|
890
|
+
async function generateInteractive(activityName, ctx) {
|
|
891
|
+
const { projectDir, packageName, packagePath, moduleName, moduleBasePath, checksumManager, writeOptions, options } = ctx;
|
|
892
|
+
|
|
63
893
|
// 1. Prompt activity name
|
|
64
894
|
if (!activityName) {
|
|
65
895
|
const answer = await inquirer.prompt([{
|
|
@@ -84,35 +914,83 @@ async function generateTemporalActivityCommand(moduleName, activityName, options
|
|
|
84
914
|
],
|
|
85
915
|
}]);
|
|
86
916
|
|
|
87
|
-
// 3.
|
|
917
|
+
// 3. Prompt for input fields
|
|
918
|
+
const inputFields = await promptFields('Input');
|
|
919
|
+
const hasInput = inputFields.length > 0;
|
|
920
|
+
|
|
921
|
+
// 4. Prompt for output fields
|
|
922
|
+
const outputFields = await promptFields('Output');
|
|
923
|
+
const hasOutput = outputFields.length > 0;
|
|
924
|
+
|
|
925
|
+
// 5. Discover existing workflows across all modules
|
|
88
926
|
const javaRoot = path.join(projectDir, 'src', 'main', 'java', packagePath);
|
|
89
927
|
const workflows = await findExistingWorkflows(javaRoot);
|
|
90
928
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
929
|
+
let selectedWorkflow = null;
|
|
930
|
+
if (workflows.length > 0) {
|
|
931
|
+
const { doRegister } = await inquirer.prompt([{
|
|
932
|
+
type: 'confirm',
|
|
933
|
+
name: 'doRegister',
|
|
934
|
+
message: 'Register activity in an existing workflow?',
|
|
935
|
+
default: true,
|
|
936
|
+
}]);
|
|
96
937
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
938
|
+
if (doRegister) {
|
|
939
|
+
const answer = await inquirer.prompt([{
|
|
940
|
+
type: 'list',
|
|
941
|
+
name: 'selectedWorkflow',
|
|
942
|
+
message: 'Register activity in workflow:',
|
|
943
|
+
choices: workflows.map((w) => ({ name: `${w.moduleName} / ${w.implClass}`, value: w })),
|
|
944
|
+
}]);
|
|
945
|
+
selectedWorkflow = answer.selectedWorkflow;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
103
948
|
|
|
104
|
-
const
|
|
105
|
-
const
|
|
106
|
-
|
|
949
|
+
const modulePascalCase = toPascalCase(moduleName);
|
|
950
|
+
const moduleActivityCategory = `${modulePascalCase}${activityCategory}`;
|
|
951
|
+
const context = {
|
|
952
|
+
packageName,
|
|
953
|
+
moduleName,
|
|
954
|
+
modulePascalCase,
|
|
955
|
+
activityPascalCase,
|
|
956
|
+
activityCategory: moduleActivityCategory,
|
|
957
|
+
categoryType: activityCategory,
|
|
958
|
+
hasInput,
|
|
959
|
+
hasOutput,
|
|
960
|
+
inputFields,
|
|
961
|
+
outputFields,
|
|
962
|
+
inputImports: resolveFieldImports(inputFields),
|
|
963
|
+
outputImports: resolveFieldImports(outputFields),
|
|
964
|
+
};
|
|
107
965
|
|
|
108
966
|
const spinner = ora(`Generating ${activityPascalCase}Activity...`).start();
|
|
109
967
|
|
|
110
968
|
try {
|
|
111
|
-
const context = { packageName, moduleName, activityPascalCase, activityCategory };
|
|
112
969
|
const templatesDir = path.join(__dirname, '..', '..', 'templates', 'temporal-activity');
|
|
113
|
-
const writeOptions = { force: options.force, checksumManager };
|
|
114
970
|
|
|
115
|
-
//
|
|
971
|
+
// Generate Input record
|
|
972
|
+
if (hasInput) {
|
|
973
|
+
spinner.text = `Generating ${activityPascalCase}Input...`;
|
|
974
|
+
await renderAndWrite(
|
|
975
|
+
path.join(templatesDir, 'ActivityInput.java.ejs'),
|
|
976
|
+
path.join(moduleBasePath, 'application', 'dtos', 'temporal', `${activityPascalCase}Input.java`),
|
|
977
|
+
{ ...context, imports: context.inputImports },
|
|
978
|
+
writeOptions
|
|
979
|
+
);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
// Generate Output record
|
|
983
|
+
if (hasOutput) {
|
|
984
|
+
spinner.text = `Generating ${activityPascalCase}Output...`;
|
|
985
|
+
await renderAndWrite(
|
|
986
|
+
path.join(templatesDir, 'ActivityOutput.java.ejs'),
|
|
987
|
+
path.join(moduleBasePath, 'application', 'dtos', 'temporal', `${activityPascalCase}Output.java`),
|
|
988
|
+
{ ...context, imports: context.outputImports },
|
|
989
|
+
writeOptions
|
|
990
|
+
);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
// Generate ActivityInterface
|
|
116
994
|
spinner.text = `Generating ${activityPascalCase}Activity interface...`;
|
|
117
995
|
await renderAndWrite(
|
|
118
996
|
path.join(templatesDir, 'ActivityInterface.java.ejs'),
|
|
@@ -121,7 +999,7 @@ async function generateTemporalActivityCommand(moduleName, activityName, options
|
|
|
121
999
|
writeOptions
|
|
122
1000
|
);
|
|
123
1001
|
|
|
124
|
-
//
|
|
1002
|
+
// Generate ActivityImpl
|
|
125
1003
|
spinner.text = `Generating ${activityPascalCase}ActivityImpl...`;
|
|
126
1004
|
await renderAndWrite(
|
|
127
1005
|
path.join(templatesDir, 'ActivityImpl.java.ejs'),
|
|
@@ -130,25 +1008,29 @@ async function generateTemporalActivityCommand(moduleName, activityName, options
|
|
|
130
1008
|
writeOptions
|
|
131
1009
|
);
|
|
132
1010
|
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
1011
|
+
// Register activity stub in selected WorkFlowImpl
|
|
1012
|
+
if (selectedWorkflow) {
|
|
1013
|
+
spinner.text = `Registering activity in ${selectedWorkflow.implClass}...`;
|
|
1014
|
+
await registerActivityInWorkflow(
|
|
1015
|
+
selectedWorkflow.filePath,
|
|
1016
|
+
packageName,
|
|
1017
|
+
moduleName,
|
|
1018
|
+
activityPascalCase,
|
|
1019
|
+
activityCategory
|
|
1020
|
+
);
|
|
1021
|
+
}
|
|
142
1022
|
|
|
143
1023
|
spinner.succeed(chalk.green(`ā
${activityPascalCase}Activity generated successfully`));
|
|
144
1024
|
|
|
145
1025
|
console.log(chalk.blue('\nš Generated files:'));
|
|
1026
|
+
if (hasInput) console.log(chalk.gray(` ${moduleName}/application/dtos/temporal/${activityPascalCase}Input.java`));
|
|
1027
|
+
if (hasOutput) console.log(chalk.gray(` ${moduleName}/application/dtos/temporal/${activityPascalCase}Output.java`));
|
|
146
1028
|
console.log(chalk.gray(` ${moduleName}/application/ports/${activityPascalCase}Activity.java`));
|
|
147
1029
|
console.log(chalk.gray(` ${moduleName}/infrastructure/adapters/activities/${activityPascalCase}ActivityImpl.java`));
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
1030
|
+
if (selectedWorkflow) {
|
|
1031
|
+
console.log(chalk.blue('\nš Updated files:'));
|
|
1032
|
+
console.log(chalk.gray(` ${selectedWorkflow.moduleName}/application/usecases/${selectedWorkflow.implClass}.java`));
|
|
1033
|
+
}
|
|
152
1034
|
} catch (error) {
|
|
153
1035
|
spinner.fail(chalk.red('Failed to generate temporal activity'));
|
|
154
1036
|
console.error(chalk.red(error.message));
|
|
@@ -156,6 +1038,50 @@ async function generateTemporalActivityCommand(moduleName, activityName, options
|
|
|
156
1038
|
}
|
|
157
1039
|
}
|
|
158
1040
|
|
|
1041
|
+
// āāā Helpers āāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā
|
|
1042
|
+
|
|
1043
|
+
const COMMON_JAVA_TYPES = ['String', 'Integer', 'Long', 'Boolean', 'BigDecimal', 'LocalDateTime', 'LocalDate', 'UUID', 'List'];
|
|
1044
|
+
|
|
1045
|
+
async function promptFields(label) {
|
|
1046
|
+
const fields = [];
|
|
1047
|
+
const { addFields } = await inquirer.prompt([{
|
|
1048
|
+
type: 'confirm',
|
|
1049
|
+
name: 'addFields',
|
|
1050
|
+
message: `Define ${label} fields?`,
|
|
1051
|
+
default: true,
|
|
1052
|
+
}]);
|
|
1053
|
+
|
|
1054
|
+
if (!addFields) return fields;
|
|
1055
|
+
|
|
1056
|
+
let adding = true;
|
|
1057
|
+
while (adding) {
|
|
1058
|
+
const { fieldName, fieldType, more } = await inquirer.prompt([
|
|
1059
|
+
{
|
|
1060
|
+
type: 'input',
|
|
1061
|
+
name: 'fieldName',
|
|
1062
|
+
message: ` ${label} field name:`,
|
|
1063
|
+
validate: (v) => (v.trim() ? true : 'Field name is required'),
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
type: 'list',
|
|
1067
|
+
name: 'fieldType',
|
|
1068
|
+
message: ` ${label} field type:`,
|
|
1069
|
+
choices: COMMON_JAVA_TYPES,
|
|
1070
|
+
},
|
|
1071
|
+
{
|
|
1072
|
+
type: 'confirm',
|
|
1073
|
+
name: 'more',
|
|
1074
|
+
message: ` Add another ${label} field?`,
|
|
1075
|
+
default: false,
|
|
1076
|
+
},
|
|
1077
|
+
]);
|
|
1078
|
+
fields.push({ name: fieldName.trim(), javaType: fieldType });
|
|
1079
|
+
adding = more;
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
return fields;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
159
1085
|
async function findExistingWorkflows(javaRoot) {
|
|
160
1086
|
const results = [];
|
|
161
1087
|
if (!(await fs.pathExists(javaRoot))) return results;
|
|
@@ -229,3 +1155,11 @@ async function registerActivityInWorkflow(workflowImplPath, packageName, moduleN
|
|
|
229
1155
|
}
|
|
230
1156
|
|
|
231
1157
|
module.exports = generateTemporalActivityCommand;
|
|
1158
|
+
module.exports.generateSharedContracts = generateSharedContracts;
|
|
1159
|
+
module.exports.buildActivityContext = buildActivityContext;
|
|
1160
|
+
module.exports.scanExistingSharedTypes = scanExistingSharedTypes;
|
|
1161
|
+
module.exports.parseActivitiesFromYaml = parseActivitiesFromYaml;
|
|
1162
|
+
module.exports.parseCrossModuleActivities = parseCrossModuleActivities;
|
|
1163
|
+
module.exports.collectExternalTypeRefs = collectExternalTypeRefs;
|
|
1164
|
+
module.exports.resolveFieldImports = resolveFieldImports;
|
|
1165
|
+
module.exports.mapField = mapField;
|