Skip to content

Commit 56366c4

Browse files
delchevclaude
andcommitted
feat(intent): generates.items supports a non-composition FK-referencing source item + decimal item defaults
An item create-from (generates with an items: block) assumed the item source is a composition child living in the source document's perspective package, and rendered numeric item defaults as bare literals. Two fixes so an item source that is a SEPARATE primary entity referencing the source document by FK (an aggregate document whose per-line detail is its own entity) works: - The source item's package resolves from its OWN perspective (a new fromItemPerspective, == fromPerspective for the composition-child case, so no change to existing create-froms); the template qualifies srcItem with it. - Item defaults use childAssignments (numeric -> BigDecimal), matching the schedule-children path, so a decimal line column (quantity/price) gets a compilable value. GlueGeneratesTest gains a non-composition-source case asserting the item perspective and the BigDecimal default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent c116a9a commit 56366c4

4 files changed

Lines changed: 82 additions & 3 deletions

File tree

  • components
    • engine/engine-intent/src
    • template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events
    • ui/service-generate/src/main/resources/META-INF/dirigible/service-generate/template

components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/GlueIntentGenerator.java

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -613,13 +613,25 @@ private static List<Map<String, Object>> buildGenerates(IntentModel model, Map<S
613613
if (hasItems) {
614614
e.put("fromItemEntity", items.getFrom());
615615
e.put("toItemEntity", items.getTo());
616+
// The SOURCE item's own perspective: a composition child resolves to its master's
617+
// perspective (== fromPerspective, so this is a no-op for the common document-item
618+
// case), but a source item that is a SEPARATE primary entity referencing the source
619+
// by FK (e.g. an aggregate document whose per-line detail is its own entity) lives in
620+
// its OWN package - the template must qualify srcItem with this, not the source
621+
// document's perspective. (The TARGET item stays on toPerspective: a create-from
622+
// always writes into the target document's own composition-item table.)
623+
e.put("fromItemPerspective", IntentEntities.resolvePerspective(items.getFrom(), compositionParents));
616624
// A document child's FK back to its master is, by convention, the master entity's name.
617625
e.put("srcFkProperty", IntentNaming.pascalCase(g.getFrom()));
618626
e.put("toFkProperty", IntentNaming.pascalCase(g.getTo()));
619-
e.put("itemFieldAssignments", assignments(items.getMap(), items.getDefaults(), "srcItem"));
627+
// childAssignments (not assignments) so a numeric item default renders as BigDecimal -
628+
// line-item columns (quantity/price/amount) are decimal, and a bare int literal does
629+
// not convert to the generated BigDecimal field.
630+
e.put("itemFieldAssignments", childAssignments(items.getMap(), items.getDefaults(), "srcItem"));
620631
} else {
621632
e.put("fromItemEntity", "");
622633
e.put("toItemEntity", "");
634+
e.put("fromItemPerspective", "");
623635
e.put("srcFkProperty", "");
624636
e.put("toFkProperty", "");
625637
e.put("itemFieldAssignments", new ArrayList<>());

components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/GlueGeneratesTest.java

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,4 +212,67 @@ void integerDecimalAndBooleanLiteralsRenderTyped() {
212212
assertTrue(fields.contains(Map.of("targetProp", "Active", "expr", "true")));
213213
assertFalse(fields.isEmpty());
214214
}
215+
216+
/**
217+
* The source item may be a SEPARATE primary entity that references the source document by FK (an
218+
* aggregate document whose per-line detail is its own entity), not a composition child. Its package
219+
* must resolve from its OWN perspective (not the source document's), and a numeric item default
220+
* must render as {@code BigDecimal} for the decimal line column.
221+
*/
222+
@SuppressWarnings("unchecked")
223+
@Test
224+
void nonCompositionSourceItemResolvesItsOwnPackageAndDecimalDefaults() {
225+
IntentModel model = IntentParser.parse("""
226+
name: work
227+
uses:
228+
- { model: kf-mod-sales-invoices }
229+
entities:
230+
- name: Sheet
231+
fields:
232+
- { name: id, type: integer, primaryKey: true, generated: true }
233+
- { name: number, type: string, documentTitle: true }
234+
relations:
235+
- { name: Customer, kind: manyToOne, to: Customer, model: kf-mod-sales-invoices }
236+
- name: Line
237+
fields:
238+
- { name: id, type: integer, primaryKey: true, generated: true }
239+
- { name: number, type: string }
240+
- { name: amount, type: decimal, precision: 18, scale: 2 }
241+
relations:
242+
- { name: Sheet, kind: manyToOne, to: Sheet, required: true }
243+
generates:
244+
- name: invoice-from-sheet
245+
from: Sheet
246+
to: SalesInvoice
247+
uses: kf-mod-sales-invoices
248+
forEntity: Sheet
249+
map:
250+
Customer: Customer
251+
items:
252+
from: Line
253+
to: SalesInvoiceItem
254+
map:
255+
name: number
256+
price: amount
257+
defaults:
258+
quantity: 1
259+
""");
260+
Map<String, Object> g = GlueIntentGenerator.buildGeneratesForTest(model)
261+
.get(0);
262+
assertEquals(true, g.get("hasItems"));
263+
assertEquals("Line", g.get("fromItemEntity"));
264+
// The source document's perspective is `Sheet`; the item's OWN perspective is `Line` - the
265+
// template must qualify srcItem with the latter (sanitized to the `line` package), or it
266+
// references a non-existent class. (For a composition-child item these two coincide.)
267+
assertEquals("Sheet", g.get("fromPerspective"));
268+
assertEquals("Line", g.get("fromItemPerspective"));
269+
// The FK the item loop queries by is the source document entity name.
270+
assertEquals("Sheet", g.get("srcFkProperty"));
271+
272+
List<Map<String, Object>> itemFields = (List<Map<String, Object>>) g.get("itemFieldAssignments");
273+
assertTrue(itemFields.contains(Map.of("targetProp", "Name", "expr", "srcItem.Number")));
274+
assertTrue(itemFields.contains(Map.of("targetProp", "Price", "expr", "srcItem.Amount")));
275+
// The decimal `quantity` default renders as BigDecimal (a bare `1` would not compile).
276+
assertTrue(itemFields.contains(Map.of("targetProp", "Quantity", "expr", "new java.math.BigDecimal(\"1\")")));
277+
}
215278
}

components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Generate.java.template

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ public class ${className}Generate {
4141
gen.${toGenFolder}.data.${toJavaPerspective}.${toEntity}Entity saved =
4242
new gen.${toGenFolder}.data.${toJavaPerspective}.${toEntity}Repository().save(target);
4343
#if($hasItems)
44-
for (gen.${javaGenFolderName}.data.${fromJavaPerspective}.${fromItemEntity}Entity srcItem :
45-
new gen.${javaGenFolderName}.data.${fromJavaPerspective}.${fromItemEntity}Repository()
44+
for (gen.${javaGenFolderName}.data.${fromItemJavaPerspective}.${fromItemEntity}Entity srcItem :
45+
new gen.${javaGenFolderName}.data.${fromItemJavaPerspective}.${fromItemEntity}Repository()
4646
.findAll(Criteria.create()
4747
.eq("${srcFkProperty}", req.id))) {
4848
gen.${toGenFolder}.data.${toJavaPerspective}.${toItemEntity}Entity item =

components/ui/service-generate/src/main/resources/META-INF/dirigible/service-generate/template/generateUtils.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -959,6 +959,10 @@ export function generateFiles(model, parameters, templateSources) {
959959
hasItems: g.hasItems,
960960
fromItemEntity: g.fromItemEntity,
961961
toItemEntity: g.toItemEntity,
962+
// The source item's own package (a non-composition primary source item
963+
// lives outside the source document's perspective); the glue resolves
964+
// it (== fromJavaPerspective for the common composition-child case).
965+
fromItemJavaPerspective: sanitizeJavaIdentifier(g.fromItemPerspective || g.fromPerspective),
962966
srcFkProperty: g.srcFkProperty,
963967
toFkProperty: g.toFkProperty,
964968
itemFieldAssignments: g.itemFieldAssignments,

0 commit comments

Comments
 (0)