Skip to content

feat(intent): cross-model entity references, n:m, and faithful field attributes - #6089

Merged
delchev merged 3 commits into
masterfrom
feat/intent-cross-model-references
Jun 27, 2026
Merged

feat(intent): cross-model entity references, n:m, and faithful field attributes#6089
delchev merged 3 commits into
masterfrom
feat/intent-cross-model-references

Conversation

@delchev

@delchev delchev commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Context

Today an .intent model is self-contained: a relation's to: must target an entity in the same model, manyToMany is parsed but never materialized, and each model prefixes its own tables. There was no way to author the real-world pattern the codbex apps use, where each domain (UoM, Country, Currency, Customer, SalesInvoice, CustomerPayment) is its own project and entities reference each other across projects.

The codbex apps solve this with PROJECTION entities: a consumer stores an integer FK to the owner's Id and renders a dropdown sourced from the owner's REST service; the projection generates no table / DAO / controller locally. The owner model owns the single physical table. That mechanism existed only in the visual EDM editor — this PR lifts it into the .intent DSL, adds n:m via an explicit intermediate entity, and adds the field attributes needed for faithful models.

What changed

Tranche A — cross-model references

  • New top-level uses: block (UsesIntent) + optional model: on a relation.
  • Parser allows a to-one relation to target an entity in a declared uses model; rejects undeclared models and forbids cross-model composition.
  • EdmIntentGenerator emits a PROJECTION entity per distinct cross-model target plus an integer FK + dropdown on the consuming side. CrossModelSupport reads the owner's already-generated .model for the exact perspective / PK / table (convention fallback when the owner isn't generated yet). Projections carry a blank perspective so they stay out of the consuming app's navigation.
  • workspaceName plumbed through endpoint → service → context for the projectionReferencedModel path.
  • No template changesparameterUtils.js / generateUtils.js and the schema template already handle type=PROJECTION (table skipped; cross-model FK resolves to the owner's table in the shared DB).

Tranche B — n:m via an explicit intermediate entity (composition to one side, cross-model manyToOne to the other, plus bridge fields like Amount for partial allocations). Reuses the composition + cross-model code paths; no extra generator logic. manyToMany stays non-materializing.

Tranche C — faithful field attributes: FieldIntent gains unique, precision, scale, calculatedOnCreate, calculatedOnUpdate; entities gain audit: true (emits the four standard audit columns). EdmIntentGenerator emits dataUnique, precision/scale, isCalculatedProperty + calculatedPropertyExpression*, and auditType — the exact attribute names the DAO/schema templates consume.

Examples + docs + tests

  • 6 Billing example .intent files (uoms, countries, currencies, customers, customer-payments, sales-invoices) under engine-intent test resources, including the cross-model SalesInvoiceCustomerPayment n:m intermediate.
  • intent-assistant-guide.md documents uses:, cross-model relations, the n:m intermediate pattern, and the new field attributes.
  • Parser tests (cross-model accept/reject, cross-model composition reject, unique/calculated parse) + a new EdmIntentGeneratorTest asserting projection shape, FK metadata, no-perspective-leakage, n:m, calculated, and audit. 21/21 pass.

Target runtime

The intent scaffold defaults the code-gen recipe to template-application-ui-harmonia-java (Java DAO/REST + Harmonia UI), which sets javaRuntime=true, so cross-model dropdowns generate as /services/java/{ownerProject}/gen/{ownerModel}/api/{Perspective}/{Entity}Controller. The .model is template-agnostic, so switching a project to the AngularJS/TypeScript recipe emits the /services/ts/... form with no intent changes.

Known limitations (documented, not fixed)

  • The cross-model dropdown URL has no workspace segment, so all referenced projects must be published to the same runtime (the codbex deployment model).
  • The Java DAO injects a calculated expression as raw Java, so codbex's JS NumberGeneratorService does not apply to the Java path; the example uses a compilable java.util.UUID.randomUUID().toString() and documents the swap.

Verification

  • Unit: mvn -pl components/engine/engine-intent test (21 tests pass).
  • End-to-end (requires a running Dirigible, not run here): generate the 6 projects leaf-first, then assert CUSTOMERS_CUSTOMER has integer CUSTOMER_COUNTRY/CUSTOMER_CURRENCY, SALES_INVOICES_SALES_INVOICE_CUSTOMER_PAYMENT has the composition FK + integer ..._CUSTOMER_PAYMENT + decimal ..._AMOUNT, no Country/Currency/Customer/CustomerPayment table is created by consuming models, and the cross-model dropdowns + n:m allocation work in the UI.

🤖 Generated with Claude Code

…attributes

Lets a multi-project app (e.g. Billing: uoms, countries, currencies, customers,
customer-payments, sales-invoices) be authored as separate .intent models that
reference each other cross-model, reusing the proven PROJECTION mechanism.

Tranche A - cross-model references:
- New top-level `uses:` block (UsesIntent) + optional `model:` on a relation.
- Parser allows a to-one relation to target an entity in a declared uses model
  (forbids cross-model composition; rejects undeclared models).
- EdmIntentGenerator emits a PROJECTION entity per cross-model target (owner
  table + PK resolved from the owner .model when present, else by convention)
  plus an integer FK + dropdown; projections carry no perspective so they stay
  out of the consuming app's nav. No template changes (existing parameterUtils /
  generateUtils / schema template already handle PROJECTION).
- workspaceName plumbed through endpoint -> service -> context for the
  projectionReferencedModel path.

Tranche B - n:m via an explicit intermediate entity (composition to one side,
cross-model manyToOne to the other, plus bridge fields like Amount). Reuses the
composition + cross-model paths; no extra generator logic.

Tranche C - faithful field attributes: FieldIntent unique / precision / scale /
calculatedOnCreate / calculatedOnUpdate, and entity-level `audit: true` (emits the
four standard audit columns). EdmIntentGenerator emits dataUnique, precision/scale,
isCalculatedProperty + calculatedPropertyExpression*, and auditType.

Includes the 6 Billing example .intent files (test resources), parser + EDM
generator unit tests (21 pass), and intent-assistant-guide.md docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment on lines +78 to +80
LOGGER.info(
"Cross-model target [{}] of model [{}] not yet generated at [{}] - using convention fallbacks; regenerate after [{}] is generated",
targetEntity, alias, modelPath, alias);
return new TargetInfo(true, perspective, tableDataName, keyField, keyColumn, labelField, fkType);
}
LOGGER.warn("Cross-model target entity [{}] not found in owner model [{}] - using convention fallbacks", targetEntity,
modelPath);
Comment on lines +117 to +118
LOGGER.warn("Failed to read owner model [{}] for cross-model target [{}] - using convention fallbacks", modelPath, targetEntity,
e);
delchev pushed a commit to dirigiblelabs/sample-intent-multi-model that referenced this pull request Jun 27, 2026
Tag each domain entity with a `group:` (master-data / sales / payments / settings)
so the generated apps contribute grouped perspectives to the platform's shared
application shell instead of forcing the user between per-project UIs. Add a
`navigation` project that defines the four navigation groups once
(getPerspectiveGroup per id, registered to application-perspectives).

Requires the intent shared-shell support (eclipse-dirigible/dirigible#6089, #6090).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
delchev and others added 2 commits June 27, 2026 21:41
…6090)

Multi-project intent apps generated a standalone Harmonia shell each, forcing the
user to jump between per-domain UIs. This makes each project ALSO contribute its
entities as perspectives to the platform's shared application shell, grouped, so
they appear as one app. The standalone per-project shell is unchanged (still ideal
for an all-in-one model and for testing a domain in isolation).

- New entity-level `group:` (EntityIntent.group); EdmIntentGenerator sets the
  perspective's groupId (perspectiveNavId) from it.
- Harmonia template emits a per-entity perspective .js + .extension registered to
  `application-perspectives` (namespaced id, groupId, path = this project's own SPA
  route in embedded mode), across the list/manage/master/setting collections.
- Harmonia shell gains an embedded (chromeless) mode: when loaded with ?embedded
  (how the shared shell hosts a perspective) it hides its own sidebar so the shared
  shell provides the single chrome. Standalone mode unaffected.
- Navigation groups are defined once (a dedicated navigation-groups project that
  exports getPerspectiveGroup per id); entities only reference the id - avoids the
  shell's duplicate-group-id drop.
- Billing example models tagged with groups (master-data / sales / payments /
  settings); guide documents `group:`.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…ab table-name fix (#6091)

* feat(intent): document (header-items) layout + aggregate totals + kebab table-name fix

- IntentNaming.upperSnake now collapses non-alphanumeric separators (-, space, ., /)
  to a single underscore, so a kebab-case intent/project name yields a valid SQL
  identifier (sales-invoices -> SALES_INVOICES, not the invalid SALES-INVOICES that
  breaks table creation). Pure-identifier entity/field names are unaffected.
- Document (header-items) layout: a master that owns a composition child whose name
  ends in "Item" (SalesInvoice -> SalesInvoiceItem) is emitted with layoutType
  MANAGE_DOCUMENT + documentItemsEntity, so it renders as a document (header form,
  inline items table, totals footer) instead of the default master-detail. The items
  child stays a DEPENDENT detail. generateUtils gains a uiDocumentModels collection.
- New field attribute `aggregate: true` -> emits an "aggregate" render hint so a
  (typically calculated) total shows in the document's totals footer, not the header
  form. Presentational only.
- Billing sample: SalesInvoice totals marked aggregate. Tests cover the document
  layout, documentItemsEntity, and the aggregate hint.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Add document-layout UI templates + IntentNaming test

The document (header-items) view templates and the kebab-fix unit test that were not
captured by the earlier diff: document.js manifest, document-page.js + document-view
templates, and IntentNamingTest (3 cases). NOTE: document.js is not yet imported in
template/ui/template.js and index.html.template has no MANAGE_DOCUMENT route branch,
so the document UI does not generate yet - wiring is the remaining step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@delchev
delchev merged commit 414230c into master Jun 27, 2026
1 check passed
@delchev
delchev deleted the feat/intent-cross-model-references branch June 27, 2026 18:45
delchev added a commit that referenced this pull request Jun 28, 2026
…n shell

Root CLAUDE.md: the cross-model intent DSL (uses:/model:, PROJECTION FKs, n:m
intermediate entity, new field/entity attributes, document layout), the shared
model-independent shell runtime under application-core, and the pure-Harmonia
application shell (resources-application). PRs #6089-#6094.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
delchev added a commit that referenced this pull request Jun 28, 2026
… record selection (#6095)

* feat(resources): Harmonia application shell (dirigible-components-resources-application)

A pure Harmonia (Alpine.js) shell for the application layer, replacing the AngularJS
dashboard's role of hosting application perspectives - the app layer is going pure
Java + Harmonia (the IDE stays AngularJS + BlimpKit for now). Embedding Harmonia
forms inside an AngularJS shell was the wrong layering.

Served at /services/web/application/. It aggregates the `application-perspectives`
extension point via the platform-core perspectives service (the same point the
generated apps already contribute to), renders the grouped sidebar (groups + items +
utilities), and hosts the selected perspective in an iframe. Generated Harmonia apps
expose their routes in embedded mode, so their own sidebar is hidden and this shell
provides the single chrome. Light/dark via Harmonia's colour-scheme API; responsive
sidebar-to-drawer; deep-linkable selection via the URL hash.

New module wired into components/pom.xml (modules + dependencyManagement) and
group-ui. Assets reuse the existing webjars/application-core URLs - no CDN.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(intent): emit perspectives for MANAGE_DOCUMENT entities (shared shell)

A document-layout master (e.g. SalesInvoice, layoutType MANAGE_DOCUMENT) was absent
from the shared application shell because the Harmonia perspective emission did not
include the uiDocumentModels collection. Add it so document masters appear (grouped)
alongside list/manage/master/setting entities.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(resources): share the Harmonia shell runtime; application shell reuses it (no AngularJS)

The Harmonia application shell was iframing the platform's AngularJS perspectives
(e.g. Settings), which break inside a pure-Harmonia shell. Instead, reuse the existing
Harmonia shell pages.

- Move the model-independent Harmonia shell runtime (stores, services, base/page
  components, Inbox/Documents/Reports/notfound views, css) into application-core,
  served at /services/web/application-core/shell/* - a single shared runtime both the
  generated per-project shell and the application shell can load (Stage A: the
  application shell consumes it; repointing the generated template follows in Stage B).
- Rebuild resources-application to reuse the shared runtime: Dashboard (generic
  landing) / Inbox / Documents / Reports as native Harmonia routes, and aggregate
  `application-perspectives` for the domain apps, hosting each in an embedded iframe.
  Only named perspective groups are shown, so the legacy AngularJS utility perspectives
  (the old Settings) are excluded. Drops the AngularJS-iframe approach and the local
  theme store (now shared).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(harmonia): generated shell reuses the shared runtime (Stage B)

The generated per-project Harmonia shell no longer bundles its own copy of the
model-independent runtime; it loads it once from the shared location
/services/web/application-core/shell (the same runtime the Harmonia application shell
uses), so there is a single source of truth.

- index.html.template: the css link, the Inbox/Documents/Reports/notfound route
  targets, and the app/services/stores/base-page/appShell script tags now point at
  /services/web/application-core/shell/*. config.js, dashboardPage.js, the Settings and
  Dashboard views, and the per-entity pages/views stay generated locally.
- shell.js: stop copying the model-independent files; only generate index.html,
  config.js, _settings.html, dashboardPage.js and _dashboard.html.
- Remove the now-duplicated shell .template files from the template module.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* Harmonia document calculated fields + totals roll-up

Calculated fields are authored once as a neutral arithmetic expression and
evaluated identically on the Java server and the JS client:
- New SDK org.eclipse.dirigible.sdk.utils.Calc (server evaluator; double math
  to match JS, returns BigDecimal at the field scale).
- template-application-dao-java emits entity.X = Calc.eval("<expr>", entity,
  scale)[.xxxValue()] for numeric calculated fields (non-numeric stay verbatim).
- Harmonia document item dialog previews them live via the mirror
  harmoniaCalcEval + recalcDraft (detail-register carries calc/scale).
- Calculated fields render read-only.

Document header totals are summed from the line items by name convention.
A single grouped handler per document (template-application-events-java
DocumentRollup) recomputes ALL aggregate sums in one read-modify-write, so
per-field updates cannot clobber one another; emitted via the documentRollups
glue collection (GlueIntentGenerator) on child create/update/delete. The
document footer also sums the loaded items client-side so the UI is instant and
never races the eventually-consistent server roll-up.

ProcessId is hidden from the manage + document forms (kept in the model so it
round-trips on edit / stays null on create).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(harmonia): reflect hosted app + record selection in the shell URL

Application shell (resources-application):
- Host domain apps via a real route /app/<perspective-id>[/<inner-route>] instead
  of only swapping the iframe src, so opening an app updates the browser URL.
- Mirror the embedded app's own hash route into the top address bar via the iframe's
  pinecone:end/hashchange, keeping the URL in sync and deep-linkable (back/forward work).

Generated apps (template-application-ui-harmonia-java):
- Add a selection route /<Entity>/:id rendering the manage-list view (MANAGE, SETTING,
  DOCUMENT layouts).
- Row selection now updates /<Entity>/<id> in place via replaceState (no list reload)
  and notifies the embedding shell; deep-link/reload pre-selects the row.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs: document multi-model intent, shared Harmonia shell + application shell

Root CLAUDE.md: the cross-model intent DSL (uses:/model:, PROJECTION FKs, n:m
intermediate entity, new field/entity attributes, document layout), the shared
model-independent shell runtime under application-core, and the pure-Harmonia
application shell (resources-application). PRs #6089-#6094.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants