From 8805ad9b3e56e7015f6f33ac58ce21f63a8faf90 Mon Sep 17 00:00:00 2001 From: delchev Date: Fri, 24 Jul 2026 09:59:50 +0300 Subject: [PATCH] feat(intent+templates): namespace generated handlers + bean/entity names per module Two pre-existing cross-module collisions surfaced by a whole-suite deploy of modules built from the same recipe: Fix A - the shared gen.events package: every generated event handler now lands in gen.events. (files under gen/events//), being the sanitized intent name via the new IntentNaming.javaModule/eventsPackage (exact mirror of the template engine's sanitizeJavaIdentifier). Producer (BpmnIntentGenerator handler FQNs, transition/generate endpoint URLs) and consumer (the events template's renames + package lines, the Harmonia print-feeder paths) derive the same segment. The module segment goes UNDER gen/events - not gen//events - so glue keeps surviving the per-model gen/ regeneration wipe. An authored "delegate: gen.events." (no further dot) is rewritten to this module's events package, so existing intents binding generated delegates keep working. Fix B - simple-class-name keying: every generated bean carries a module-qualified @Component("_") (rest-java controllers, dao-java repository, all events-template handlers), the generated @Entity declares name="_Entity" (the entity-name key in the one shared dynamic-map SessionFactory), and the four hand-built HQL sites in the rest-java controllers reference the qualified root entity name. New IntentCrossModuleCollisionIT deploys two modules authoring a same-named entity AND a same-named reaction together and asserts both modules fully live over REST; IntentEngineIT/IntentEmissionCoverageIT updated to the module-scoped layout; IntentNamingTest pins the sanitizer contract. Co-Authored-By: Claude Fable 5 --- CLAUDE.md | 2 +- components/engine/engine-intent/CLAUDE.md | 32 +-- .../intent/generator/IntentNaming.java | 44 ++++ .../intent/generator/SnapshotSupport.java | 13 +- .../generator/bpmn/BpmnIntentGenerator.java | 70 ++++-- .../generates/GeneratesIntentGenerator.java | 11 +- .../TransitionsIntentGenerator.java | 11 +- .../main/resources/intent-assistant-guide.md | 9 +- .../intent/generator/IntentNamingTest.java | 25 ++ .../data/Entity.java.template | 2 +- .../data/Repository.java.template | 2 + .../events/Abort.java.template | 4 +- .../events/Expansion.java.template | 4 +- .../events/FieldLoader.java.template | 2 +- .../events/Generate.java.template | 6 +- .../events/Integration.java.template | 4 +- .../events/Job.java.template | 4 +- .../events/Notification.java.template | 4 +- .../events/Numbering.java.template | 2 +- .../events/Posting.java.template | 4 +- .../events/PrintFeeder.java.template | 6 +- .../events/Resolver.java.template | 2 +- .../events/Rollup.java.template | 4 +- .../events/SetField.java.template | 2 +- .../events/SettlementOnInvoice.java.template | 2 +- .../events/SettlementOnPayment.java.template | 4 +- .../events/Snapshot.java.template | 6 +- .../events/TimerLoader.java.template | 2 +- .../events/Transition.java.template | 6 +- .../events/Trigger.java.template | 4 +- .../events/Wait.java.template | 4 +- .../events/Webhook.java.template | 6 +- .../events/Writer.java.template | 2 +- .../template/template.js | 44 ++-- .../api/EntityController.java.template | 6 +- .../api/EntityMyController.java.template | 4 +- .../api/EntityPartnerController.java.template | 4 +- .../api/reportFileEntity.java.template | 2 + .../document/document-page.js.template | 4 +- .../perspective/manage/form-page.js.template | 4 +- .../api/IntentCrossModuleCollisionIT.java | 235 ++++++++++++++++++ .../tests/api/IntentEmissionCoverageIT.java | 24 +- .../integration/tests/api/IntentEngineIT.java | 71 +++--- 43 files changed, 526 insertions(+), 177 deletions(-) create mode 100644 tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentCrossModuleCollisionIT.java diff --git a/CLAUDE.md b/CLAUDE.md index 54af558de5d..ef6ed351585 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -183,7 +183,7 @@ Client `.java` under `/registry/public//...` is synchronized by `JavaSy A single `app.intent` YAML file at a project root is the source of truth one altitude above the model files. **The intent is an authoring artifact, not a runtime artifact** — like the `.edm` it has an editor and an explicit Generate, and (like the `.edm`) it has **no synchronizer**. Double-clicking any `*.intent` file opens the Intent Editor (`components/ui/editor-intent`): editable YAML left, live read-only diagram right (mxGraph ER + per-process flowcharts, the same engine the EDM/schema/mapping modelers use), validation inline; the Generate button runs six generators that write `.edm`/`.model`, `.bpmn`, `.form`, `.report`, `.roles` and `.csvim`/`.csv` **into the developer's workspace project at the project root** (the layout of real-world Dirigible application projects) — nothing touches the registry until normal publish, after which the per-artefact synchronizers bring the runtime live as for any project. Services: `POST /services/ide/intent/parse` and `POST /services/ide/intent/generate`. A third editor pane is a **Claude AI assistant** (`POST /services/ide/intent/agent`): it proposes the complete updated `app.intent` via the Anthropic API (key server-side in `DirigibleConfig.INTENT_AI_*`, never sent to the browser), the editor shows a Monaco diff, and Accept merges it into the buffer — the agent never writes disk or runs Generate. Developed on PR [#6017](https://github.com/eclipse-dirigible/dirigible/pull/6017). -**Detailed guide:** [`components/engine/engine-intent/CLAUDE.md`](components/engine/engine-intent/CLAUDE.md). Read it before changing anything under that module — it covers the editor-first architecture and altitude contract (model files only, never code), the YAML schema and its semantics (integer-only primary keys, `composition: true` to-one = DEPENDENT master-detail while `required` alone is just a NOT NULL FK, PascalCase property names with UPPER_SNAKE columns, decision `then`/`else`, intent-prefixed table names via `IntentNaming`), the `writeModelFile`-only write surface with the stale-output scrub, the wrong turns already made (wrong altitude, template-output paths, registry-relative vs repository-absolute paths, the `JsonHelper` Gson pitfall, **and the synchronizer-based first incarnation — do not reintroduce it**), and the follow-up list (chaining model-to-code via `.gen` descriptors, `/custom/` escape hatch). Process triggers (`trigger: { onCreate: }`) are wired: the EDM adds a `ProcessId` field + a `triggers` collection to the `.model`, and the `template-application-events-java` template generates a `gen/events/Trigger.java` listener that starts the process on create. That persisted `ProcessId` is in turn **consumed by the generated entity-view UI**: a shared `ProcessTasks` module (`components/resources/resources-dashboard/.../dashboard/services/process-tasks.js`) surfaces the record's actionable BPM user tasks inline via an `` directive (correlating `entity.ProcessId === task.processInstanceId`), wired into every generated view gated on a `hasProcess` flag; the task form completes via the permission-checked `/services/inbox/tasks/{id}` and self-closes (#6074). `IntentEngineIT` is the HTTP-only end-to-end test (~1 minute, no sync cycles). The editor's diagram pane is **mxGraph** (replacing Mermaid, which had unfixable light/dark theming bugs) with a fixed brand-colour palette that reads on both themes — see the module guide's "Intent Editor diagram = mxGraph" section before touching `editor-intent/js/editor.js`. +**Detailed guide:** [`components/engine/engine-intent/CLAUDE.md`](components/engine/engine-intent/CLAUDE.md). Read it before changing anything under that module — it covers the editor-first architecture and altitude contract (model files only, never code), the YAML schema and its semantics (integer-only primary keys, `composition: true` to-one = DEPENDENT master-detail while `required` alone is just a NOT NULL FK, PascalCase property names with UPPER_SNAKE columns, decision `then`/`else`, intent-prefixed table names via `IntentNaming`), the `writeModelFile`-only write surface with the stale-output scrub, the wrong turns already made (wrong altitude, template-output paths, registry-relative vs repository-absolute paths, the `JsonHelper` Gson pitfall, **and the synchronizer-based first incarnation — do not reintroduce it**), and the follow-up list (chaining model-to-code via `.gen` descriptors, `/custom/` escape hatch). Process triggers (`trigger: { onCreate: }`) are wired: the EDM adds a `ProcessId` field + a `triggers` collection to the `.model`, and the `template-application-events-java` template generates a `gen/events//Trigger.java` listener (module-scoped package `gen.events.` — two modules authoring same-named reactions no longer collide by FQN; generated beans/entities carry module-qualified names for the same reason) that starts the process on create. That persisted `ProcessId` is in turn **consumed by the generated entity-view UI**: a shared `ProcessTasks` module (`components/resources/resources-dashboard/.../dashboard/services/process-tasks.js`) surfaces the record's actionable BPM user tasks inline via an `` directive (correlating `entity.ProcessId === task.processInstanceId`), wired into every generated view gated on a `hasProcess` flag; the task form completes via the permission-checked `/services/inbox/tasks/{id}` and self-closes (#6074). `IntentEngineIT` is the HTTP-only end-to-end test (~1 minute, no sync cycles). The editor's diagram pane is **mxGraph** (replacing Mermaid, which had unfixable light/dark theming bugs) with a fixed brand-colour palette that reads on both themes — see the module guide's "Intent Editor diagram = mxGraph" section before touching `editor-intent/js/editor.js`. **Multi-model + layout additions (PRs [#6089](https://github.com/eclipse-dirigible/dirigible/pull/6089)-[#6092](https://github.com/eclipse-dirigible/dirigible/pull/6092)):** the DSL now supports building an app from **several intent models that reference each other cross-model** - a top-level `uses:` block names other models, and a relation gains an optional `model:` alias; a cross-model `manyToOne`/`oneToOne` is emitted as a read-only **PROJECTION** entity + integer FK + dropdown (the codbex cross-project pattern - no local table/DAO/controller for the target), resolved against the owner's already-generated `.model` (leaf-first generation; convention fallback otherwise). **n:m** is an explicit **intermediate entity** (composition to one side + `manyToOne` to the other, which may be cross-model, plus bridge fields like `amount`) - `manyToMany` is parsed but never materialized. New field attributes: `unique`, `precision`/`scale`, `calculatedOnCreate`/`calculatedOnUpdate` (a neutral arithmetic expression for numeric totals, else emitted verbatim into the runtime), `calculatedActionOnCreate`/`calculatedActionOnUpdate` (server-side call-out to a hand-written `@Component implements org.eclipse.dirigible.sdk.db.CalculatedField`, invoked as `Beans.get(.class).calculate(entity)`, taking precedence over the expression — for logic too custom to model, e.g. number generation); field `readOnly: true` (not editable; rendered in the Harmonia form's read-only details block — Label:Value above the buttons — via `isReadOnlyProperty`; `ProcessId`/audit columns/`uuid` are auto-flagged read-only, `status`-style fields opt in); field `major: false` (kept off the entity **list** table — the model's `widgetIsMajor="false"` — still shown in forms + the record details pane; defaults true); entity `imports:` (Java `import` lines injected into the generated repository so a calculated action can be referenced by simple name — Base64-encoded into the `.model`'s `importsCode`, which the Java DAO template emits; the editor's entity-level Imports tab is the model-editor equivalent); entity `audit: true` (the four standard audit columns); entity `group:` (the perspective's nav-group id in the shared application shell). **Depends-On** is exposed as `dependsOn: { relation, valueFrom?, filterBy? }` on a to-one relation (cascading/narrowed dropdown) or a field (auto-populated value) — emitted as the EDM `widgetDependsOn*` attributes (the AngularJS stacks consume them as-is; the Harmonia runtime — form/document watchers + the metadata-driven item-dialog cascade — was added alongside); defaults are the respective primary keys, names are the target's authored property names, cross-model triggers/targets supported. **Multi-language data** (the TS-era `multilingual` port): entity `multilingual: true` → the schema layer generates a sibling `_LANG` table (`GUID, Id, , Language` — the codbex-uoms-data convention) and the generated Java repository overlays translated values on every read for the request's `Accept-Language` (SDK `Translator`, name-based merge); the supported language set is a PLATFORM concern (`DIRIGIBLE_APPLICATION_LANGUAGES`, default `en,bg`) — the Harmonia **Region & Language** Settings entry always offers that set (an Alpine `locale` store, localStorage `codbex.harmonia.language`, sent as `Accept-Language` by the shared fetch client — one flag drives UI, data, and the Print default), while the top-level `languages: [en, bg]` only declares which languages the module PROVIDES translations for; the application shell warns about modules missing a platform language, and untranslated content falls back to the default; translations are authored as seeds with `language: bg`, and large data sets reference an authored CSV via seed `file: data/x.csv` (subfolder mandatory — root `.csv` is scrub-owned) instead of inline rows. A master owning an `*Item` composition child renders as the **document (header-items) layout** (`MANAGE_DOCUMENT` + `documentItemsEntity`, `uiDocumentModels`), with `aggregate: true` fields shown in the totals footer. `IntentNaming.upperSnake` collapses kebab/space/`.`/`/` separators so a hyphenated model name yields a valid SQL identifier (`sales-invoices` -> `SALES_INVOICES`). Worked example: `dirigiblelabs/sample-intent-multi-model` (six interdependent projects + a navigation-groups project). diff --git a/components/engine/engine-intent/CLAUDE.md b/components/engine/engine-intent/CLAUDE.md index 0f162c1afd5..0399262f337 100644 --- a/components/engine/engine-intent/CLAUDE.md +++ b/components/engine/engine-intent/CLAUDE.md @@ -164,7 +164,7 @@ Web resources under `components/ui/*` and `components/.../resources` are bundled Six concrete generators currently live in-module: - [`EdmIntentGenerator`](src/main/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGenerator.java) writes `.edm` (XML) plus `.model` (JSON twin) from the entities + relations declared in the intent. Each entity is fleshed out with EDM editor defaults (icons, menu keys, layout type, perspective metadata, widget types) derived from the entity / field names so the produced model is a complete, openable EDM document. Conventions follow the canonical Dirigible model conventions: **property `name`s are PascalCase** (`id`->`Id`, `loanedOn`->`LoanedOn`, FK `member`->`Member`) via `IntentNaming.pascalCase`, while the physical column **`dataName` stays UPPER_SNAKE** and intent-prefixed (`ORDERS_COUNTRY`) - authoring stays lower camelCase, only the generated model names are PascalCased; every property carries **`auditType="NONE"`**, and a required field/FK also carries **`isRequiredProperty="true"`** (the generated REST controller's required-value validation keys on it, not on `dataNullable`). **Every to-one FK property carries the full relationship metadata the generation reads** (the `.model` has no separate relations array, so it must live on the property): `relationshipType` (`COMPOSITION` for `composition: true`, else `ASSOCIATION`), `relationshipCardinality` (`1_n` composition, `n_1` manyToOne association, `1_1` oneToOne association), `relationshipName` = the FK constraint name `_` (e.g. `Loan_Member` - used as the DB FK constraint name in the schema template), `relationshipEntityName` = target entity, `relationshipEntityPerspectiveName` = target's resolved perspective, `relationshipEntityPerspectiveLabel="Entities"`. **The dropdown's data-service URL (`api//Service.ts`) and the create-detail dialog are built from `relationshipEntityName` + `relationshipEntityPerspectiveName`** - omitting them generated `api/undefined/undefinedController.ts` and a dead dropdown. A `composition: true` relation additionally makes the owner DEPENDENT/MANAGE_DETAILS, inheriting its transitively-resolved parent perspective; every other to-one stays a PRIMARY association DROPDOWN. Dropdown key/value and `referencedProperty` come from the target entity's actual PK and `name`-like fields (PascalCased); the `.model` JSON carries `entities`/`perspectives`/`navigations` (no `relations` key - relations are XML-only, interleaved with their owning ``). The `.edm` also carries an **`mxGraphModel`** diagram with a `style="entity"` vertex per entity (carrying an `` value), a child vertex per property (carrying a `` value), and an edge per FK relation - the EDM editor renders the canvas *exclusively* by decoding `mxGraphModel`, so without it the editor opens empty. Entities are placed in a fixed grid for deterministic output. -- [`BpmnIntentGenerator`](src/main/java/org/eclipse/dirigible/components/intent/generator/bpmn/BpmnIntentGenerator.java) writes one `.bpmn` per process. Minimal Flowable-flavoured BPMN 2.0 - one start event, one end event, the declared steps, and the sequence flows that connect them. Decisions emit an exclusiveGateway with a conditioned outgoing flow to `args.then` and a default flow to `args.else` (falling back to the next step in the chain when omitted). The `trigger` block keeps the BPMN with a plain none-start event; the runtime auto-start is the `template-application-events-java` listener/handler under `gen/events` (driven off the EDM generator's `triggers` collection), not a BPMN start event. Emits the **`bpmndi:BPMNDiagram`** block (plus the `omgdc`/`omgdi` namespaces): the Flowable/Oryx modeler renders the canvas *only* from the diagram interchange - a process with no `BPMNShape`s opens empty. Nodes are laid out left-to-right along the linear chain at a fixed lane; edges connect source-right to target-left. The layout is deterministic (byte-stable across regenerations); the modeler re-routes on first manual edit. **Naming:** element **ids are uniform lower camelCase** — authored step names already are (`librarianReview`), and the injected decision-resolver task id is the lower-camel form of its handler (`resolveBookPrice`) while the delegate still resolves the PascalCase class `gen.events.ResolveBookPrice`. Task / gateway / process **`name`s are humanized** from the id via `IntentNaming.humanize` (`librarianReview → "Librarian Review"`, `LoanApproval → "Loan Approval"`); the process **id** stays the compact `LoanApproval`. +- [`BpmnIntentGenerator`](src/main/java/org/eclipse/dirigible/components/intent/generator/bpmn/BpmnIntentGenerator.java) writes one `.bpmn` per process. Minimal Flowable-flavoured BPMN 2.0 - one start event, one end event, the declared steps, and the sequence flows that connect them. Decisions emit an exclusiveGateway with a conditioned outgoing flow to `args.then` and a default flow to `args.else` (falling back to the next step in the chain when omitted). The `trigger` block keeps the BPMN with a plain none-start event; the runtime auto-start is the `template-application-events-java` listener/handler under `gen/events` (driven off the EDM generator's `triggers` collection), not a BPMN start event. Emits the **`bpmndi:BPMNDiagram`** block (plus the `omgdc`/`omgdi` namespaces): the Flowable/Oryx modeler renders the canvas *only* from the diagram interchange - a process with no `BPMNShape`s opens empty. Nodes are laid out left-to-right along the linear chain at a fixed lane; edges connect source-right to target-left. The layout is deterministic (byte-stable across regenerations); the modeler re-routes on first manual edit. **Naming:** element **ids are uniform lower camelCase** — authored step names already are (`librarianReview`), and the injected decision-resolver task id is the lower-camel form of its handler (`resolveBookPrice`) while the delegate still resolves the PascalCase class `gen.events..ResolveBookPrice`. Task / gateway / process **`name`s are humanized** from the id via `IntentNaming.humanize` (`librarianReview → "Librarian Review"`, `LoanApproval → "Loan Approval"`); the process **id** stays the compact `LoanApproval`. - [`FormIntentGenerator`](src/main/java/org/eclipse/dirigible/components/intent/generator/form/FormIntentGenerator.java) writes one `
.form` per form. Controls are typed by looking up each declared field against the bound entity (string/uuid -> input-textfield, text -> input-textarea, integer/decimal -> input-number, boolean -> input-checkbox, date -> input-date, timestamp -> input-datetime-local). A plain field's control `model` binds to the **PascalCase** entity property (`loanedOn` -> `LoanedOn`) to match the EDM. A **`relation.field` form field** (`book.price` on a form bound to `Loan`) is a one-hop to-one relation of the bound entity; its control binds `model` to the **`_` process variable** (`book_price`), is typed from the **target** entity's field (so `book.price` is an input-number), is `readonly`, and is labelled by the humanized path ("Book Price"). This is the form counterpart of the decision resolver: a BPM task form's model is the process variables, which hold the `Book` FK id but not the book's own fields, so a resolver step (see [`ProcessResolverSupport`](#)) must load it - **the form does not fetch the related entity itself** (it is a standalone iframe with only the process variables). A `relation.field` field on a form *not* used by a user task generates no resolver and stays empty (documented limitation; the resolver only exists inside a process). Actions become buttons in a trailing `container-hbox`; the button colour is inferred from the action name (approve -> positive, reject/decline/delete/cancel -> negative, save/submit -> emphasized). A stub controller code block declares `onClicked` handlers as TODOs - wiring to a backend is left to the downstream template engine or a hand-authored override under `custom/`. - [`ReportIntentGenerator`](src/main/java/org/eclipse/dirigible/components/intent/generator/report/ReportIntentGenerator.java) writes one `.report` per report in the **Dirigible `.report` shape**: `name` / `alias` (the source entity, the base-table alias) / `table` (intent-prefixed physical table via `IntentNaming.tableName`) / `columns` / a fully-materialised SQL **`query`** / `conditions` / `security`. The report is rooted at `source`; each dimension/measure resolves to a physical column - a plain field (`dueOn`) -> a source column; a **`relation.field` path (`member.name`) -> an `INNER JOIN` to the related entity** plus a column on it (this is how a report shows a parent's columns); a **bare to-one relation (`member`) -> an `INNER JOIN` showing the target's label (`name`-like) field, not the raw FK id** (so "group by member" displays the member's name; use `member.id` for the id); a measure `count(*)`/`sum(total)`/`avg`/`min`/`max` -> an aggregate column (dimensions then become the `GROUP BY`). `filter` becomes the `WHERE`, with intent field names rewritten to qualified physical columns (`dueOn <= CURRENT_DATE` -> `Loan."LOAN_DUE_ON" <= CURRENT_DATE`); operators / literals / `CURRENT_DATE` pass through. `security` is `{generateDefaultRoles, roleRead: .Report.ReadOnly}`. Column physical names + the base table mirror `EdmIntentGenerator` so the report never drifts from the model. (The earlier version left `query` empty and used a non-standard shape - reports did not run.) **All physical table and column identifiers in the `query` are double-quoted** (`"LIBRARY_LOAN"`, `Loan."LOAN_DUE_ON"`); PostgreSQL folds *unquoted* identifiers to lower case and would never match the quoted UPPER_SNAKE objects the platform creates, so an unquoted query runs on H2 but fails on Postgres. Table **aliases** stay unquoted (they fold consistently on both sides). The `quote(...)` helper + the JSON-escaping of those quotes inside the `.report` `query` string (assert `\"` in tests) are the two gotchas. **Caveat:** the base-table alias is the entity name; a reserved-word entity name (`Order`) yields an unquoted reserved alias - keep entity names non-reserved, as the standard Dirigible apps do. - [`PermissionIntentGenerator`](src/main/java/org/eclipse/dirigible/components/intent/generator/permission/PermissionIntentGenerator.java) writes `.roles` from the intent's `permissions` block (deduped by role name). It deliberately does NOT emit `.access` constraints - URL-shaped access rules belong to whichever downstream template materializes the UI for an entity / form / report, because only that template knows the paths it will publish. The `can: [Resource:action, ...]` tokens on each permission are an authoring hint to downstream UI generators about which actions each role may invoke; the actual `` mapping is the downstream template's contract, not intent's. @@ -273,14 +273,14 @@ Semantics worth knowing: - **`kind: setting` on an entity marks it as nomenclature / configuration.** `EntityIntent.kind` (default null = a regular managed entity); `kind: setting` makes `EdmIntentGenerator` emit the entity with `type="SETTING"` (and `entityType="SETTING"` in the mxGraph cell) instead of PRIMARY. The template engine keys on `entity.type === "SETTING"` (`service-generate/template/generateUtils.js`) to route it under the dashboard's global **Settings** perspective (it nulls the layout and sets `perspectiveName = "Settings"`), so a setting entity does NOT get its own generated perspective. Crucially the EDM generator also resolves any relation **targeting** a setting entity to the `Settings` perspective (`perspectiveFor(...)`), so an FK dropdown to a setting points at `api/Settings/` rather than a missing per-entity perspective. Settings are still real entities (own table, CSVIM seeds, FK columns) - only their UI placement differs. - **Calculated-field actions + entity `imports:` — server-side call-out for logic too custom to model.** Besides the neutral arithmetic `calculatedOnCreate`/`calculatedOnUpdate` expression (run by the SDK `Calc` evaluator, previewed live in the UI), a field may declare `calculatedActionOnCreate`/`calculatedActionOnUpdate` naming a Java class — a `@Component implements org.eclipse.dirigible.sdk.db.CalculatedField` (`T calculate(E entity)`). `EdmIntentGenerator.propertyMap` emits these as `calculatedActionOnCreate`/`OnUpdate` on the property (and `isCalculated()` now counts an action, so the property is marked calculated even with no expression); the **Java DAO template** (`template-application-dao-java/data/Repository.java.template`) gives the action **precedence** over the expression per slot and emits `entity. = Beans.get(.class).calculate(entity);`, importing `Beans` only when an action is present and `Calc` only when an expression is. An action runs **server-side only** (no client mirror). To reference the action by simple name, the entity declares `imports:` (a multi-line string of Java `import ...;` lines); `EdmIntentGenerator` Base64-encodes it into the `.model` entity's `importsCode` (matching the EDM editor's serialization), which the DAO template's `parameterUtils` decodes and emits into the repository's import block. The implementation is **hand-written under the project's `custom/` folder** (never `gen/`) — the intent layer emits no Java. The model-editor equivalents are the entity **Imports** tab and the property **Calculation** tab's *Action class* inputs (`editor-entity`). Worked example: `dirigiblelabs/sample-intent-multi-model` `sales-invoices` — `SalesInvoice.number` calls `custom/sales_invoices/SalesInvoiceNumberAction.java` (replacing the old inline `UUID.randomUUID()` expression). The SDK interface ships in `api-modules-java` (`org.eclipse.dirigible.sdk.db.CalculatedField`). - **Decision steps**: `if` + `then` are mandatory; `else` is optional and receives the gateway-default flow (so the conditioned branch can actually be skipped - without `else` the default falls through to the next step in the chain). `then`/`else` must name a declared step or the literal `end`; the parser validates this so a typo fails at parse time instead of producing BPMN Flowable rejects. -- **`setField` service task + `next` step routing (declarative field-set glue).** A `serviceTask` with `args: { setField: , value: }` sets a `string`/`text` field of the process's **trigger entity** to a literal value, generated as a `gen/events/.java` `JavaDelegate` (`SetFieldSupport` → the `setters` glue collection → `SetField.java.template`) instead of scaffolding a hand-written `custom.` stub - it persists the set column via the targeted single-column `updateProperty(id, "", value)` (a workflow write, not a user edit, so it must not re-fire `onUpdate` reactions; only the set column is in the UPDATE statement, so a concurrent write to any other column cannot be reverted). The canonical use is an approve/reject outcome: the form completes the task with the chosen `action` as a process variable, a `decision` branches on `action == 'approve'`, and the two branches are `setField` tasks (`status=ACTIVE` / `status=REJECTED`). **`args: { next: }`** on any step overrides its linear successor - needed because the BPMN generator builds a **linear** chain, so without it the first branch (`activate`) would fall through into the second (`reject`); `next: done` makes the branches converge. The `then`/`else` fall-through is deliberately NOT auto-converted to a diamond (LoanApproval's `curatorReview` relies on falling through to `notifyMember`), so convergence is explicit via `next`. Scope: literal string values only (the parser validates `setField` is a string/text field of the trigger entity and that `value` is present; `next` must name a declared step or `end`). Non-string fields and expression values are future work. +- **`setField` service task + `next` step routing (declarative field-set glue).** A `serviceTask` with `args: { setField: , value: }` sets a `string`/`text` field of the process's **trigger entity** to a literal value, generated as a `gen/events//.java` `JavaDelegate` (`SetFieldSupport` → the `setters` glue collection → `SetField.java.template`) instead of scaffolding a hand-written `custom.` stub - it persists the set column via the targeted single-column `updateProperty(id, "", value)` (a workflow write, not a user edit, so it must not re-fire `onUpdate` reactions; only the set column is in the UPDATE statement, so a concurrent write to any other column cannot be reverted). The canonical use is an approve/reject outcome: the form completes the task with the chosen `action` as a process variable, a `decision` branches on `action == 'approve'`, and the two branches are `setField` tasks (`status=ACTIVE` / `status=REJECTED`). **`args: { next: }`** on any step overrides its linear successor - needed because the BPMN generator builds a **linear** chain, so without it the first branch (`activate`) would fall through into the second (`reject`); `next: done` makes the branches converge. The `then`/`else` fall-through is deliberately NOT auto-converted to a diamond (LoanApproval's `curatorReview` relies on falling through to `notifyMember`), so convergence is explicit via `next`. Scope: literal string values only (the parser validates `setField` is a string/text field of the trigger entity and that `value` is present; `next` must name a declared step or `end`). Non-string fields and expression values are future work. - **`setRelationField` (set a status modelled as a to-one relation).** The generic counterpart to `setField` for a status that is a **FK to a settings/nomenclature entity** (e.g. `Status`) rather than a string column: `args: { setRelationField: , value: }` sets the relation's FK property to the integer **seed id** (unquoted — `entity. = ;`), via the **same** `SetFieldSupport` → `setters` glue → `SetField.java.template` path (the `Setter` carries a `relation` flag; the template branches `#if($relation == "true")` to emit the unquoted assignment). Unlike `setField` (serviceTask only), `setRelationField` is allowed on a **serviceTask** (bound directly by `appendServiceTask`, like `setField`) **and** on a **userTask** (the BPMN inserts the setter `JavaDelegate` right after the task — exactly like the Writer — so e.g. the Approve user task sets `Status=APPROVED` the moment it completes; `BpmnIntentGenerator` builds a `setterByProcessTask` map of user-task setters and `augmentWithResolvers` appends them in a `[writer, setter]` after-task chain, carrying `next` onto the last delegate). The parser validates the relation is a `manyToOne`/`oneToOne` of the trigger entity and that `value` is an integer id. This replaced an unimplemented `setStatus` idea — there is no `setStatus` keyword. **The `-transitioned` topic:** both setter shapes persist via the targeted `updateProperty` (no `-updated` re-fire, deliberately) but DO publish the fresh entity JSON (re-loaded after the write) on `---transitioned` — the dedicated status-reached channel. **Publication is deferred to the end of the synchronous BPMN chain** (`Process.executeAfterCommit` — a Flowable COMMITTED transaction listener): service tasks that follow the setter in the same chain (a number-generation delegate) complete before any consumer can react, so a consumer that re-loads the source by id observes their writes instead of racing them (an auto-posted journal entry used to catch the create-time UUID placeholder as `documentNumber`). **On a mid-chain FAILURE this is correct by design, not a lost event:** the status write commits in its own session (client-Java writes are per-operation — cloud-native, no ambient cross-step transaction), while the deferred publish only fires on the Flowable COMMITTED event; if a later task rolls the chain back, the publish deliberately does NOT fire, and the transition is unwound by the flow's compensation / error path (a compensating status set), never by a DB rollback. Checks + Saga-style compensation, NOT distributed transactions, are the consistency model here — do not "fix" this by enrolling the entity write in the BPMN transaction. Reactions/notifications keep binding `-updated`/create topics and never see it (no loops); a posting-glue or integration consumer binds `-transitioned` to observe workflow transitions that are otherwise event-silent (the Wave-0 accounting spike's core finding: an Issue step was invisible to every entity event). Emitted by `SetField.java.template`; covered by the `set_field_glue_...` IT assertion. **Placement rule (applies to `setField` too, documented for the editor AI in `intent-assistant-guide.md`):** when a task is **followed by a decision** (Approve/Reject), put the status set on a **`serviceTask` on the chosen branch**, NOT on the user task — setting it on the task makes a Reject transition `DRAFT → APPROVED → CANCELLED` (an artificial APPROVED hop) before the cancel branch overrides it. Set on the task **only** for a **single-action** task with no following decision (no branch ⇒ no transient state). The `sales-invoices` showcase follows this (Approve task has no set; an `activate` serviceTask sets APPROVED on the approve branch; single-action `issue`/`send` set on the task). -- **`delegate` service task (call a reusable, author-named client `JavaDelegate`).** `args: { delegate: , fields: { : , ... } }` binds a serviceTask to a hand-written client delegate via **`flowable:class`** (NOT the `${JavaTask}` dispatcher). This is the *fourth* service-task shape alongside `setField`/`setRelationField` (→ generated `gen.events.` via `${JavaTask}`+`handler`), `call` (→ `${JSTask}` TS handler), and the bare fallback (→ `custom.` + a scaffolded stub). Why `flowable:class` and not `${JavaTask}`: `DirigibleJavaCallDelegate` (`${JavaTask}`) reads only its `handler` field and instantiates the target with a **no-arg constructor**, so it can't pass parameters; `flowable:class` (resolved through `BpmFlowableConfig`'s `ClientAwareClassLoader`, which consults the client class loader) lets Flowable **inject** the declared `fields` as delegate fields — so one *general* delegate serves many steps. `BpmnIntentGenerator.appendServiceTask` branches to `appendDelegateServiceTask` (emitting `flowable:class` + one `` per `fields` entry, in declaration order); `ServiceTaskHandlerGenerator` **skips** delegate steps (no `custom/` stub — the developer owns the class, which may live in *another* published project since client Java compiles in one cross-project batch). Parser: `delegate` is serviceTask-only, mutually exclusive with `setField`/`setRelationField`/`call`, and `fields` values must be scalars. **Worked example:** `sample-intent-multi-model` — `sales-invoices`' `generateNumber` step (after `issue`) binds `custom.sales_invoices.DocumentNumberGeneratorDelegate` with `fields: { type: "Sales Invoice" }`. **The delegate lives in the document's OWN project** (sales-invoices), because it must load/save the invoice through the generated `SalesInvoiceRepository` (validations, events, i18n — NEVER the generic `Store`; see the engine-java guide's repository-only rule): it reads the record id from the `Id` process variable, `findById`s it, asks the reusable `custom.numbers.DocumentNumberGenerator` (in the `numbers` project — a codbex-number-generator port over its own `NumberRepository`, entity-agnostic) for the next formatted number of the injected `type`, sets `entity.Number`, and persists via `updateWithoutEvent` (workflow write). Only the entity-agnostic generator is shared; the entity-touching delegate is per-project. Covered by `IntentEngineIT.delegate_service_task_binds_a_client_java_delegate_via_flowable_class_with_injected_fields`. +- **`delegate` service task (call a reusable, author-named client `JavaDelegate`).** `args: { delegate: , fields: { : , ... } }` binds a serviceTask to a hand-written client delegate via **`flowable:class`** (NOT the `${JavaTask}` dispatcher). This is the *fourth* service-task shape alongside `setField`/`setRelationField` (→ generated `gen.events..` via `${JavaTask}`+`handler`), `call` (→ `${JSTask}` TS handler), and the bare fallback (→ `custom.` + a scaffolded stub). Why `flowable:class` and not `${JavaTask}`: `DirigibleJavaCallDelegate` (`${JavaTask}`) reads only its `handler` field and instantiates the target with a **no-arg constructor**, so it can't pass parameters; `flowable:class` (resolved through `BpmFlowableConfig`'s `ClientAwareClassLoader`, which consults the client class loader) lets Flowable **inject** the declared `fields` as delegate fields — so one *general* delegate serves many steps. `BpmnIntentGenerator.appendServiceTask` branches to `appendDelegateServiceTask` (emitting `flowable:class` + one `` per `fields` entry, in declaration order); `ServiceTaskHandlerGenerator` **skips** delegate steps (no `custom/` stub — the developer owns the class, which may live in *another* published project since client Java compiles in one cross-project batch). Parser: `delegate` is serviceTask-only, mutually exclusive with `setField`/`setRelationField`/`call`, and `fields` values must be scalars. **Worked example:** `sample-intent-multi-model` — `sales-invoices`' `generateNumber` step (after `issue`) binds `custom.sales_invoices.DocumentNumberGeneratorDelegate` with `fields: { type: "Sales Invoice" }`. **The delegate lives in the document's OWN project** (sales-invoices), because it must load/save the invoice through the generated `SalesInvoiceRepository` (validations, events, i18n — NEVER the generic `Store`; see the engine-java guide's repository-only rule): it reads the record id from the `Id` process variable, `findById`s it, asks the reusable `custom.numbers.DocumentNumberGenerator` (in the `numbers` project — a codbex-number-generator port over its own `NumberRepository`, entity-agnostic) for the next formatted number of the injected `type`, sets `entity.Number`, and persists via `updateWithoutEvent` (workflow write). Only the entity-agnostic generator is shared; the entity-touching delegate is per-project. Covered by `IntentEngineIT.delegate_service_task_binds_a_client_java_delegate_via_flowable_class_with_injected_fields`. - **`wait` step = park the process on an entity event (message catch + correlation glue).** `- { name: awaitReply, kind: wait, args: { onCreate: CaseMessage, via: case, when: "internal == 0", next: work } }` — the process parks on a BPMN `` + `intermediateCatchEvent` (message name = `` PascalCase) until the named entity event resumes it. Exactly one of `onCreate`/`onUpdate` (never `onDelete` — a deleted record cannot resume a wait) naming a declared entity; `via:` is the **event** entity's to-one relation walking to the trigger entity (required when they differ, forbidden when the event entity IS the trigger entity; same-model only); `when:` is the single-comparison guard grammar over the EVENT record (`NotificationSupport.guard`). Glue: `ProcessWaitSupport` → the `waits` collection in `.glue` → `Wait.java.template` — a self-describing `MessageHandler` on the event entity's topic that resolves the ProcessId-carrying record (via the FK, or `findById` of the event record itself in the direct case — re-loaded because the payload snapshot may predate the trigger's ProcessId write-back), and calls `Process.correlateMessageEvent(processId, message, Map.of())` wrapped fail-soft (an instance not parked on the message is a no-op, never an error). The process MUST have a trigger entity — its stamped `ProcessId` is the whole correlation contract (no new bookkeeping). Requested/shaped in upstream discussion #6327. - **`timeout:` / `expire:` on a userTask = boundary timers (reminders, SLAs, date-driven expiry).** Two optional map args on a user task, each with a `then:` routed like a decision branch (declared step or `end`; route the main flow around the branch steps with `next`, as with decision branches): `timeout: { after: P3D, then: remind }` emits a **non-cancelling** `boundaryEvent` (`cancelActivity="false"`) with a literal `timeDuration` — the task stays claimable while the reminder/escalation branch runs; `expire: { until: validUntil, then: markExpired }` emits a **cancelling** one (`cancelActivity="true"`) with `timeDate` bound to the `${__ExpireDate}` process variable — the task is withdrawn and the flow continues at `then`. The expire date is **re-read at task entry**: `ProcessTimerSupport` derives a loader `JavaDelegate` (the `timerLoaders` glue collection → `TimerLoader.java.template`) that `augmentWithResolvers` inserts right before the task, loading the trigger entity by id and publishing the field as a `java.util.Date` (Flowable arms the timer from the object directly — no string parsing). A `date` field expires at the start of the day AFTER it (the field names the last valid day); a `timestamp` at its instant; a `null` field or missing record arms 9999-12-31 so the timer never effectively fires. Parser: userTask-only, `after:` must parse as an ISO-8601 `Duration`/`Period`, `until:` must be a `date`/`timestamp` field of the trigger entity, `then:` validated like a decision target. The boundary shapes ride the host task's bottom edge in the DI; the layered layout ranks each boundary branch through a pseudo-flow from the HOST task. The Flowable async executor is already active (`BpmFlowableConfig.setAsyncExecutorActivate(true)`), so timer jobs fire with no new runtime. Caveat: a loop/jump straight into the user task (a reject-loop `else: approve`) re-enters the task WITHOUT re-running the inserted loader — the expire variable keeps its previous value for that pass. Requested/shaped in upstream discussion #6328. - **`abortOn:` on a process = cancel the in-flight instance when the document transitions into a terminal status (BPM events wave 2).** `abortOn: { status: [4, 5], then: markVoid }` — a `-transitioned` of the trigger entity into any listed EntityStatus seed id cancels the whole running instance (pending user tasks, parked waits, armed boundary timers). Emitted as an **interrupting message event subprocess** (`` with an `isInterrupting="true"` message start on `Abort` → optional cleanup serviceTask → `terminateEventDefinition`), NOT by wrapping the main flow — chosen over the proposal's subProcess-wrap sketch because it needs no restructuring of the flat step layout and still kills everything in scope. Glue: `ProcessAbortSupport` → the `aborts` collection in `.glue` → `Abort.java.template` — a `MessageHandler` on the entity's `-transitioned` topic (the channel transitions/setters already publish) that matches the status list (`entity. == || …`) and correlates `Abort` on the stamped `ProcessId`, fail-soft. `then:` omitted or `end` = terminate; a declared `serviceTask` cleanup (setField/setRelationField) is **abort-only** — `BpmnIntentGenerator` filters it out of the main linear chain (`steps.removeIf`) and re-emits it inside the event subprocess (its setter glue is still generated by `SetFieldSupport`). Parser (`validateAbortOn`): integer `status` (scalar or list), trigger entity with a `function: EntityStatus` relation, `then` = `end`/a setField-setRelationField serviceTask that is NOT explicitly routed to from the main flow (`next`/`then`/`else`). DI: the event subprocess is a fixed-placement container box below the main lane (BPMN-2.0 expanded-subprocess children carry absolute plane coordinates). Requested/shaped in upstream discussion #6340. **Consumers:** the orphaned-Inbox-task hole (cancel a SalesOrder mid-confirm), and the structural replacement for a cancelling `expire:` guard (kf quotations drops its `custom/` guard delegate). Caveat: `then` cleanup is one serviceTask (a multi-step cleanup chain is future work). - **`init: ` on a to-one relation = the FK's DB-level default (`RelationIntent.init` → `dataDefaultValue` on the FK property, in both `relationProperty` and `crossModelRelationProperty`).** The relation analogue of a field's `defaultValue`; a new row gets this FK on insert when the column is left unset (e.g. `Status` defaults to the DRAFT seed, `PaymentMethod` to Bank, `SentMethod` to E-mail). **Use `init` for an INITIAL status, never a process step.** A DB default is applied at insert — atomic, no ordering to reason about — while a start-step setter is extra moving parts for the same effect. (Historical note: a start-step `setRelationField` also used to be *clobbered* by the trigger's `ProcessId` write-back, which was a full-row `updateWithoutEvent` merge of a stale snapshot — confirmed live: the invoice reached the Approve task but `Status` stayed null. The trigger now persists `ProcessId` — and a minted business key — via the targeted single-column `repository.updateProperty(...)` (SDK `JavaRepository`/`JavaEntityStore`), so that race is gone; `init:` remains the right modeling for an initial status.) `setRelationField` is for *transitions* (after a user task / on a decision branch). -- **`trigger: { onCreate|onUpdate|onDelete: , when: "" }` starts the process on the named `` lifecycle event** - fully wired (Java). Any of the three events is supported: `onCreate` binds the entity's base topic, `onUpdate`/`onDelete` the `-updated`/`-deleted` topics the Java DAO publishes (`TriggerSupport` + `EventBinding`); an optional `when` guard (a single `field ==|!= literal`, via `NotificationSupport.guard`) gates `Process.start`. Three parts: (1) the parser validates at most one event kind and that the target is a declared entity; (2) the EDM generator adds a `ProcessId` back-reference property (VARCHAR) to that entity and a `triggers` collection to the `.model` (`TriggerSupport` + `EdmIntentGenerator.buildTriggers`); (3) the **`template-application-events-java`** template (intent-driven, like the other language templates) reads that `triggers` collection and emits one **`gen/events/Trigger.java`** per trigger - a client-Java self-describing `MessageHandler` (a `@Component` whose `destination()` is the entity's per-operation topic via `topicSuffix` and whose `kind()` is `TOPIC`) that loads the entity, applies the `when` guard, calls `Process.start(, businessKey, )`, and writes the instance id back to `ProcessId` (so it starts at most once). The Java DAO template (`template-application-dao-java`) now publishes the create event (`Producer.sendToTopic('${projectName}-${perspectiveName}-${name}', json)`) the way the TS DAO does - that's the topic the handler binds to. `gen/events` is a sibling of `gen/`, so it survives the per-model regeneration wipe. The events template iterates the model's `triggers` via a new **`triggers` collection case in `service-generate/template/generateUtils.js`** (the engine's collection switch is hardcoded; the case has its own loop because triggers are not entity-shaped). The BPM **business key** defaults to the entity's primary key but is **configurable**: `trigger: { ..., businessKey: }` names which trigger-entity field becomes the started instance's business key (the listener still loads the entity by its PK via `findById`; only the business key differs — a separate `businessKeyProperty` in `.glue`). An optional `businessKeyStrategy: timestamp` mints a `yyyyMMddHHmmss` value into that field when it is blank and persists it via the listener's existing update — the simple "for now" generator and the **extension point** for richer pluggable number generators later (sequential, zero-padded, config-prefixed invoice numbers); the parser validates the field exists, the strategy is supported, and (for `timestamp`) the field is `string`/`text`. `TriggerSupport.triggerBusinessKey`/`triggerBusinessKeyStrategy` read them; `GlueIntentGenerator` emits `businessKeyProperty` + `generateBusinessKey`; `Trigger.java.template` renders the mint-if-blank block. `onSchedule` is still unmodelled. **Casing subtlety in the generated handler:** its `import gen..data..{Entity,Repository}` must use the **lowercased** Java package segment (`javaPerspective` = `sanitizeJavaIdentifier(perspective)`, matching the DAO/entity templates' `javaPerspectiveName` folder), while the `destination()` topic (`"--"`) keeps the **raw** perspective so it matches the topic the DAO publishes to (`${projectName}-${perspectiveName}-${name}`). The `triggers` collection case in `generateUtils.js` supplies both (`javaPerspective` for the import, `perspective` for the topic). Using the raw perspective in the import compiled on macOS (case-insensitive FS) but failed `javac` with "package gen.x.data.Member does not exist" because the entity files declare the lowercased package. +- **`trigger: { onCreate|onUpdate|onDelete: , when: "" }` starts the process on the named `` lifecycle event** - fully wired (Java). Any of the three events is supported: `onCreate` binds the entity's base topic, `onUpdate`/`onDelete` the `-updated`/`-deleted` topics the Java DAO publishes (`TriggerSupport` + `EventBinding`); an optional `when` guard (a single `field ==|!= literal`, via `NotificationSupport.guard`) gates `Process.start`. Three parts: (1) the parser validates at most one event kind and that the target is a declared entity; (2) the EDM generator adds a `ProcessId` back-reference property (VARCHAR) to that entity and a `triggers` collection to the `.model` (`TriggerSupport` + `EdmIntentGenerator.buildTriggers`); (3) the **`template-application-events-java`** template (intent-driven, like the other language templates) reads that `triggers` collection and emits one **`gen/events//Trigger.java`** per trigger - a client-Java self-describing `MessageHandler` (a `@Component` whose `destination()` is the entity's per-operation topic via `topicSuffix` and whose `kind()` is `TOPIC`) that loads the entity, applies the `when` guard, calls `Process.start(, businessKey, )`, and writes the instance id back to `ProcessId` (so it starts at most once). The Java DAO template (`template-application-dao-java`) now publishes the create event (`Producer.sendToTopic('${projectName}-${perspectiveName}-${name}', json)`) the way the TS DAO does - that's the topic the handler binds to. `gen/events/` (the `` segment = the sanitized intent name, `IntentNaming.javaModule`) is a sibling of `gen/`, so it survives the per-model regeneration wipe. The events template iterates the model's `triggers` via a new **`triggers` collection case in `service-generate/template/generateUtils.js`** (the engine's collection switch is hardcoded; the case has its own loop because triggers are not entity-shaped). The BPM **business key** defaults to the entity's primary key but is **configurable**: `trigger: { ..., businessKey: }` names which trigger-entity field becomes the started instance's business key (the listener still loads the entity by its PK via `findById`; only the business key differs — a separate `businessKeyProperty` in `.glue`). An optional `businessKeyStrategy: timestamp` mints a `yyyyMMddHHmmss` value into that field when it is blank and persists it via the listener's existing update — the simple "for now" generator and the **extension point** for richer pluggable number generators later (sequential, zero-padded, config-prefixed invoice numbers); the parser validates the field exists, the strategy is supported, and (for `timestamp`) the field is `string`/`text`. `TriggerSupport.triggerBusinessKey`/`triggerBusinessKeyStrategy` read them; `GlueIntentGenerator` emits `businessKeyProperty` + `generateBusinessKey`; `Trigger.java.template` renders the mint-if-blank block. `onSchedule` is still unmodelled. **Casing subtlety in the generated handler:** its `import gen..data..{Entity,Repository}` must use the **lowercased** Java package segment (`javaPerspective` = `sanitizeJavaIdentifier(perspective)`, matching the DAO/entity templates' `javaPerspectiveName` folder), while the `destination()` topic (`"--"`) keeps the **raw** perspective so it matches the topic the DAO publishes to (`${projectName}-${perspectiveName}-${name}`). The `triggers` collection case in `generateUtils.js` supplies both (`javaPerspective` for the import, `perspective` for the topic). Using the raw perspective in the import compiled on macOS (case-insensitive FS) but failed `javac` with "package gen.x.data.Member does not exist" because the entity files declare the lowercased package. - **`dependsOn` on a to-one relation or a field = the EDM Depends-On feature (cascading dropdowns + auto-populated fields).** `dependsOn: { relation: , valueFrom?: , filterBy?: }` — the widget reacts to the sibling trigger: the generated form loads the trigger's selected record, reads `valueFrom` (default: the trigger target's PK), then a **relation** re-filters its dropdown options where its own target's `filterBy` (default: that target's PK) equals the value (`POST /search` with an EQ condition; a single remaining option auto-selects), while a **field** copies the value (auto-population; `valueFrom` mandatory, `filterBy` rejected). Emitted by `EdmIntentGenerator.putDependsOn` as the four scalar `widgetDependsOn*` property attributes the AngularJS stacks already consume (so those work for free); the Harmonia runtime was added in the same pass (`form-page.js.template` per-property watcher + `applyDependsOn` methods covering manage/master-detail/allocation forms; `document-page.js.template` header watchers + a generic metadata-driven `applyDraftDependsOn` for the line-item dialog off `detail-register.js.template`'s `editColumns[].dependsOn`, with filtered options in a separate `draftOptions` store so the items table's label resolution keeps the full set; `parameterUtils.js` precomputes `widgetDependsOnControllerUrl` from the trigger sibling). `valueFrom`/`filterBy` use the target's **authored** property names (field lower-camel / relation as declared); same-model references are parse-validated, cross-model ones generation-validated against the resolved owner model (`CrossModelSupport.TargetInfo.propertyNames`). A `documentStatus` relation can neither declare nor trigger a dependsOn. Canonical cases (the `codbex-sample-model-depends-on` set): Country→City cascade (`filterBy` only), Product→UoM narrow-to-referenced (`valueFrom` only), Product→price auto-populate (field). - **`postings:` (top-level) = declarative posting (source-document status → generated local document + computed items).** The accounting "documents → ledger" capability, generalized (spike-derived; see the KeyFolders catalog's spike findings). `PostingIntent` + parser `validatePostings` (creates = local document owning a composition items child; backReference = its to-one to the source, the at-most-once guard; event `when: " == "`; item cells = `rule()` refs into a single-selector rule entity or Calc arithmetic over the source; row `when: ==|!= `). `GlueIntentGenerator.buildPostings` pre-renders EVERYTHING as Java expressions (the expansions convention — the template stays shape-only): topic + re-load coordinates via `CrossModelSupport`, guard, header assignments (copy / literal / `{placeholder}` concat), `ruleRow.` refs, `Calc.eval("", source, )` amounts with the scale from the LOCAL item field, null-safe Calc row guards. `postings` glue collection → `generateUtils.js` case (source gen folder = sanitized model alias, topic keeps the RAW perspective) → `Posting.java.template`: a `MessageHandler` on `---transitioned` (#6220's channel) that re-loads the source by id (the payload lacks later-step data — the stamped number), guards, resolves the rule row (missing row / null referenced column → SKIP, the unposted worklist), and writes target + items through the repositories — so numbering / status `init:` / `checks:` fire on the created document. **Idempotent + resumable, not transactional** (the cloud-native consistency model — there is NO cross-step DB rollback; each write commits on its own): the back-reference identifies an existing post, so a redelivery of a COMPLETE post (item count ≥ the derived `expectedItems`) is a no-op, and a redelivery of a HALF-post (an item write failed after the target was saved) clears the partial items and rebuilds the full set — it never throws or half-posts. Concurrent-redelivery de-duplication is best-effort (a check-then-act on the back-reference) until a real UNIQUE key on the back-reference lands with schema constraint emission. Storno/negation mode LANDED as **`reverses:`** (paired with the `transitions:` void primitive - the "void-document event" is a transition into the void status): a reversal posting inherits creates/backReference/rule/map/items from the reversed sibling, negates every item amount expression on the SAME side (`Calc.eval("-()", ...)` - red storno), locates the original through the empty `storno:` self-link (none -> fail-soft skip), stamps the link on its creation, and both handlers' idempotency guards discriminate by that link (reversal counts linked rows, the sibling counts unlinked ones - `stornoProperty`/`stornoFilterProperty` in the glue). The explicit manual Reverse action (no source void) remains a follow-up. Compensation, not a transaction, is how a bad post is unwound. - **`immutableWhen:` / `immutable:` on an entity = user-write immutability.** `immutableWhen: "Status == 2"` (a boolean expression over EntityStatus seed ids, terms joined with `||`) makes update/delete through the generated REST controller answer 409 CONFLICT while the record's `function: EntityStatus` FK satisfies it; `immutable: true` is the unconditional append-only variant (mutually exclusive with `immutableWhen`; a non-existent id still yields 404, not 409). Emitted as the entity-level `immutableStatusProperty` + `immutableStatusValues` (or `immutableAlways`) model attrs; `requireMutable` fetches the existing row before writing. Repository writes are deliberately unaffected — the workflow (storno generation, roll-ups, ProcessId write-back) keeps working; this guards the USER surface, per the accounting audit-trail requirement (corrections are reversals, never edits). Parser requires an EntityStatus relation. Alongside it (no DSL): every generated controller now maps a **database constraint violation on DELETE to 409** ("referenced by other records") instead of a 500. Caveat discovered while verifying: the generated schema currently emits **no FK constraints at all** (`constraints: []` on every table — same-model included), so this mapping only engages once constraint emission lands; whether to emit them (a suite-wide data-integrity semantics change: deploy order, CSVIM import order, existing tables unaffected by ALTER) is a separate decision, raised with the accounting findings. Date-based period locking (records whose date falls in a Locked period) is deliberately NOT part of this — its shape needs the real fiscal-period module and follows as its own PR. @@ -288,7 +288,7 @@ Semantics worth knowing: - **`where` on a user-picked to-one relation = a static dropdown option filter.** `where: { : }` — a single constant condition that permanently narrows the relation's option list to matching target rows (the canonical case: a stock line's Product picker showing only `Type: 1` real products, never services). The `dependsOn` sibling for conditions that do not react to anything. Parser (`validateWhere`): to-one only, never on a composition parent (preset by the layout) or an `EntityStatus`, exactly one pair, scalar literal, same-model property checked immediately (cross-model at generation, like the relation target). Emitted by `EdmIntentGenerator.putOptionsFilter` as two scalar attrs `widgetOptionsFilterBy` (PascalCased) / `widgetOptionsFilterValue` (a YAML integer renders without the Gson `.0` — `stripTrailingZero`); `parameterUtils.js` pre-renders `widgetOptionsFilterValueJs` (numeric stays bare, else quoted+escaped) so the templates emit it verbatim. Harmonia consumption: the **chooser** dropdowns load via `POST /search` with the EQ condition (`form-page` + `document-page` header `loadOptions`), the item dialog keeps a third `filteredOptions` store (`dialogOptionsFor` prefers `draftOptions || filteredOptions || itemOptions`; metadata via `detail-register`'s `#optionsFilterMeta` → `col.filter`), and a combined `dependsOn` + `where` search sends BOTH conditions — while **label-resolution** lookups (list/master/table columns, `itemOptions`) deliberately keep the full set so historical rows referencing now-filtered-out targets still resolve. AngularJS stacks ignore the attrs (Harmonia-only for now — noted in the PR). - **`hierarchy:` on an entity + `leafOnly:` on a to-one relation = tree entities (chart-of-accounts shape).** `hierarchy: Parent` names the entity's own optional to-one self-relation as the tree edge (parser `validateHierarchy`: self-target, non-composition, optional — a required parent leaves no way to author a root). Emitted as the entity-level `hierarchyProperty` (PascalCase FK property) on the `.edm`/`.model`; `CrossModelSupport.TargetInfo` gained a `hierarchyProperty` component so a cross-model referencing module can read it off the owner's `.model`. `leafOnly: true` on a relation targeting a hierarchical entity emits `widgetLeafOnly` + `widgetHierarchyProperty` (the TARGET's edge property) on the FK; parse-checked same-model, generation-checked (loud) for a resolved cross-model target. Enforcement is dual-layer like `where:`: the generated REST controller gains `validateReferences` (leafOnly = child-count via the target's repository — `parameterUtils.leafOnlyRepositoryClass`, a cross-model Java import that resolves because client-Java compiles registry-wide; plus a walk-up cycle guard for the entity's own edge, depth-capped at 100), while the Harmonia pickers mirror it (`hierarchizeOptions`: depth-first order, em-space indentation, leaves only — form page, document header, and the item dialog via `detail-register`'s `#hierMeta` → `col.hier` + the `filteredOptions` store). The manage LIST renders as a Harmonia `x-h-tree` when the entity declares a hierarchy — fixed-depth markup recursion (6 levels; Alpine has no recursive template), search/column-filters fall back to the flat table so matches in collapsed branches stay findable. Plain (non-leafOnly) pickers to hierarchical targets stay flat — indentation rides the leafOnly metadata only (documented v1 scope). - **`order:` on an entity = explicit UI control order.** A list of property names (fields + to-one relations interleaved, matched case-insensitively against the authored names) that sequences the generated form inputs / list columns / detail rows. Default (no `order`) is fields-in-declaration-order then to-one relations, which pushes every relation last — bad UX for a line-item form where `Product`/`UoM` want to sit next to `Name`/`Quantity`. `EdmIntentGenerator.applyOrder` reorders the built `properties` list (which the templates AND the mxGraph diagram both consume) before emitting; a **partial** order is honoured (unlisted properties keep their relative position, appended after the listed ones), and system properties (`ProcessId`/audit columns) are simply left unlisted. The parser (`validateOrders`) checks every listed name resolves to a declared field/relation and rejects duplicates. Worked example: `sample-intent-multi-model` `SalesInvoiceItem` (`order: [Id, SalesInvoice, Product, Name, Quantity, UoM, Price, Discount, Net, Vat, Total]`). -- **`transitions:` (top-level) = guarded on-demand status flip (void / cancel / close / reopen).** The missing affordance for a document whose create-time process has ENDED: process triggers fire only on create/update/delete, and `actions:` only opens a custom page - nothing declarative could transition a finished document again. `TransitionIntent` + parser `validateTransitions` (forEntity must declare a `function: EntityStatus` relation; `from:` = non-empty list of allowed source seed ids; `setStatus:` = target seed id not in `from`; optional `when: " ==|!= "` guard over an own field, resolved case-insensitively - the identifier follows the Calc PascalCase convention). Two halves, the `generates` pattern: `TransitionsIntentGenerator` (`@Order(470)`) contributes the per-record button (`-transition-action.extension`/`.js` on `-custom-action`, descriptor carries `endpoint`); `GlueIntentGenerator.buildTransitions` pre-renders EVERYTHING (the `allowedExpr` over an `int currentStatus` local, the `when` guard as a full `Calc.eval(...).compareTo(...)` expression - null field reads as 0) into the `transitions` glue collection -> `generateUtils.js` case -> `Transition.java.template`: a `@Controller` at `gen/events/Transition/run` that re-loads the record, returns **409** (via `sdk.http.Response.setStatus`) with the reason when a guard fails, flips ONLY the status column via the targeted `updateProperty` (no `-updated` re-fire - no onUpdate reactions), re-loads, and publishes `-transitioned` - the SAME channel the workflow setters and `generates.sourceStatus` publish, so `postings:` glue observes a manual void exactly like a workflow transition. This realizes the "guarded transition" half of the Tier-2 `lifecycle:` sketch below for the post-process case. Covered by `TransitionsIntentTest` + `GlueTransitionsTest` + the `IntentEmissionCoverageIT` transitions assertions. +- **`transitions:` (top-level) = guarded on-demand status flip (void / cancel / close / reopen).** The missing affordance for a document whose create-time process has ENDED: process triggers fire only on create/update/delete, and `actions:` only opens a custom page - nothing declarative could transition a finished document again. `TransitionIntent` + parser `validateTransitions` (forEntity must declare a `function: EntityStatus` relation; `from:` = non-empty list of allowed source seed ids; `setStatus:` = target seed id not in `from`; optional `when: " ==|!= "` guard over an own field, resolved case-insensitively - the identifier follows the Calc PascalCase convention). Two halves, the `generates` pattern: `TransitionsIntentGenerator` (`@Order(470)`) contributes the per-record button (`-transition-action.extension`/`.js` on `-custom-action`, descriptor carries `endpoint`); `GlueIntentGenerator.buildTransitions` pre-renders EVERYTHING (the `allowedExpr` over an `int currentStatus` local, the `when` guard as a full `Calc.eval(...).compareTo(...)` expression - null field reads as 0) into the `transitions` glue collection -> `generateUtils.js` case -> `Transition.java.template`: a `@Controller` at `gen/events//Transition/run` that re-loads the record, returns **409** (via `sdk.http.Response.setStatus`) with the reason when a guard fails, flips ONLY the status column via the targeted `updateProperty` (no `-updated` re-fire - no onUpdate reactions), re-loads, and publishes `-transitioned` - the SAME channel the workflow setters and `generates.sourceStatus` publish, so `postings:` glue observes a manual void exactly like a workflow transition. This realizes the "guarded transition" half of the Tier-2 `lifecycle:` sketch below for the post-process case. Covered by `TransitionsIntentTest` + `GlueTransitionsTest` + the `IntentEmissionCoverageIT` transitions assertions. - **`multilingual: true` on an entity + `language:`/`file:` seeds + top-level `languages:` = the multi-language data stack.** A multilingual entity's translatable (string-typed) properties may carry per-language values in a sibling `
_LANG` table (`GUID, Id, , Language` — the codbex-uoms-data convention). `EdmIntentGenerator` emits the EDM `multilingual="true"` entity attribute (the same one the EDM editor writes); the schema template generates the language table from it; the Java DAO template overrides every finder to overlay translations via the SDK `org.eclipse.dirigible.sdk.db.Translator` for the caller's `Accept-Language` (thread-bound `User.getLanguage()`; null → no-op, so listeners/jobs read base values). Translations are authored as **seeds with a `language: bg` code** → `CsvimIntentGenerator` writes them into `
_LANG` (`GUID` auto-numbered, `Language` constant; parser validates the entity is multilingual and row keys are `id` + string/text fields). **Large data sets stay out of the intent**: a seed may reference an authored CSV via `file: data/countries.csv` (exactly one of `file`/`rows`; the path MUST be in a subfolder — root-level `.csv` files are intent-owned and scrubbed) — only the `.csvim` is generated, pointing at the developer-owned file. Top-level `languages: [en, bg]` declares which languages this module PROVIDES translations for (landing on the `.model` root → Harmonia `config.js` `languages`) — it never defines what the stack supports: the **Region & Language** picker always offers the PLATFORM's set (`DIRIGIBLE_APPLICATION_LANGUAGES`, default `en,bg`, served by `platform-core/services/application-languages.js`), backed by the shared `locale` Alpine store (localStorage `codbex.harmonia.language`) whose value the shared fetch client sends as `Accept-Language` on every call — one flag drives the backend translation, and the document Print flow prefers it too. The application shell compares each app's provided set against the platform set and lists gaps as warnings in Settings; untranslated content falls back to the default language. Caveat (TS parity): editing a record while a non-base language is active saves the displayed (translated) values into the base table — translations are maintained via seeds/DB, not through the generated UI. - **`label:` on an entity = the stored display name.** `label: "{number} - {date|yyyy MMMM} - {Customer.name}"` synthesizes a read-only `Name` VARCHAR(512) property (`labelNameProperty`) recomputed by the generated repository on save/update/`updateWithoutEvent` (`computeName` in `Repository.java.template` - the system path included, because workflow writes stamp label inputs like the document number; `updateProperty`/`recalculate` deliberately skip it). Tokens parse via `LabelExpression` (shared parser/generator): literals + `{field}` + `{Relation.field}` (ONE hop; `|format` = a `DateTimeFormatter` pattern for temporals) - deeper paths are rejected with a compose hint, since labels COMPOSE by referencing the related entity's generated `Name` (`{ProjectTimesheet.Name}`). Emitted as entity `labelExpression` + `labelParts` (List - .model only); parameterUtils sets `p.targetRepositoryClass` on every FK and merges it into relation parts + `hasLabel`. `labelFieldName`/`CrossModelSupport.labelField` prefer `Name`, so every dropdown to a label entity is right automatically. Parser rejects a label next to an authored `name` field and any token referencing a `sensitive` field (the Name is visible on the personal surface). Staleness note: a referenced record's rename propagates on the referencing record's next write - bounded, documented. - **`identity` / `personal` / `sensitive` = the personal (my) surface.** `identity: ` on the entity representing the person (conventionally the unique e-mail) declares how the logged-in username maps to a record; `personal: true` on a record-owning to-one relation (at most one per entity; target must declare identity - same-model parse-checked, cross-model generation-checked via `TargetInfo.identityProperty`) makes the entity get an ADDITIONAL generated `MyController` (rest-java `EntityMyController.java.template`, `personalModels` collection): reads filtered to the mapped identity record (`Criteria.eq(identityProperty, User.getName())`), owner FK forced server-side on writes, foreign/missing records the same 404, `sensitive: true` fields (never the PK/identity/owner FK) stripped from responses AND ignored on writes - the allow-list is server-side, UI hiding alone would be cosmetic security. Composition children inherit the scope through their DIRECT parent (one hop - `requireMyParent` ancestor guard; deeper chains get no personal surface, documented). The power controller is untouched. Emitted as entity `identityProperty` + FK `relationshipPersonal`/`relationshipIdentityProperty` + field `sensitiveProperty`; parameterUtils derives `personalProperty`/`personalParent`/`sensitiveProperties`. Design/status: repo-root `PERSONALIZATION_PLAN.md` (phase A; personal UI, My Shell, per-user task assignee and collection-driven generation are the later phases). @@ -330,7 +330,7 @@ Three axes: ### Glue is generated **annotated client-Java**, and that *is* the artefact — not "code we avoid" -Decision (supersedes, **for the glue layer only**, the "prefer a model artifact, Java glue is the exception" wording elsewhere in this guide): every glue activity is generated as a **client-Java class against the SDK** (`org.eclipse.dirigible.sdk.*`) under `gen/events`, exactly as the trigger glue already is (a self-describing `MessageHandler` — `destination()`/`kind()` — that calls `Process.start(...)` and uses `Json`). The class **is** the model/artefact — `engine-java` synchronizes and runs it; it is deterministic, regenerated with the app, and replaceable via a `/custom/` override. We do **not** emit `.listener` / `.job` XML/JSON artefacts that point at a handler, and we do **not** target TypeScript. +Decision (supersedes, **for the glue layer only**, the "prefer a model artifact, Java glue is the exception" wording elsewhere in this guide): every glue activity is generated as a **client-Java class against the SDK** (`org.eclipse.dirigible.sdk.*`) under `gen/events/`, exactly as the trigger glue already is (a self-describing `MessageHandler` — `destination()`/`kind()` — that calls `Process.start(...)` and uses `Json`). The class **is** the model/artefact — `engine-java` synchronizes and runs it; it is deterministic, regenerated with the app, and replaceable via a `/custom/` override. We do **not** emit `.listener` / `.job` XML/JSON artefacts that point at a handler, and we do **not** target TypeScript. **Handler style (per [`../engine-java/CLAUDE.md`](../engine-java/CLAUDE.md)):** listener-style glue is generated as a **self-describing `MessageHandler`** (a `@Component` supplying `destination()`/`kind()`) — **not** a class-level `@Listener` (which is method-level only now, and mixing an interface with the annotation is rejected). Scheduled glue is a `@Component implements JobHandler` (self-describing `cron()`) or a `@Scheduled` method; websocket glue a `WebsocketHandler` or `@Websocket`+`@OnX`. One style per `@Component`. Older bullets below that say "`@Listener`/`@Scheduled` class" predate this and should be read as "the listener/scheduled glue", emitted in the current style. @@ -357,7 +357,7 @@ Every action below has a real SDK surface to generate against, so none of this n **Tier 1 — build first (reuses trigger + resolver almost entirely):** -1. **Generalized lifecycle reactions** — generalize the trigger from "onCreate→startProcess" to `{onCreate|onUpdate|onDelete} + when:` → any action. Prereq: the Java DAO must publish update/delete events (create is already published). → `gen/events/Reaction.java` (`@Listener`). +1. **Generalized lifecycle reactions** — generalize the trigger from "onCreate→startProcess" to `{onCreate|onUpdate|onDelete} + when:` → any action. Prereq: the Java DAO must publish update/delete events (create is already published). → `gen/events//Reaction.java` (`@Listener`). ```yaml reactions: - { event: { onUpdate: Loan, when: "status == 'APPROVED'" }, do: notify(loanApprovedEmail) } @@ -367,7 +367,7 @@ Every action below has a real SDK surface to generate against, so none of this n notifications: - { name: orderUpdated, event: { onUpdate: Order }, channel: email, to: ops@example.com, subject: "Order {id} updated, total {total}", body: "The order changed." } ``` - → `gen/events/Notification.java` (`@Listener`) using `sdk.mail.Mail`, bound to the entity's create / `-updated` / `-deleted` topic; sender from `DIRIGIBLE_MAIL_SENDER`. The author-facing fields are translated to Java by `NotificationSupport` (unit-tested) and emitted into `.glue` (`GlueIntentGenerator`), rendered by `Notification.java.template` via the `notifications` collection case in `generateUtils.js`. **Scope:** `to`/`{placeholder}` resolve a literal, a **direct field**, or a **one-hop `relation.field`** of a to-one relation — the listener loads the related entity once by FK id (same one-hop mechanism as the decision resolvers; `NotificationSupport.plan` emits the `relationLoads` the template renders). `when:` supports a single `field ==|!= literal` on a direct field. **Multi-hop** paths (`a.b.c`) and relation-path `when:` are the remaining gap; the parser rejects multi-hop `to` with a clear message. + → `gen/events//Notification.java` (`@Listener`) using `sdk.mail.Mail`, bound to the entity's create / `-updated` / `-deleted` topic; sender from `DIRIGIBLE_MAIL_SENDER`. The author-facing fields are translated to Java by `NotificationSupport` (unit-tested) and emitted into `.glue` (`GlueIntentGenerator`), rendered by `Notification.java.template` via the `notifications` collection case in `generateUtils.js`. **Scope:** `to`/`{placeholder}` resolve a literal, a **direct field**, or a **one-hop `relation.field`** of a to-one relation — the listener loads the related entity once by FK id (same one-hop mechanism as the decision resolvers; `NotificationSupport.plan` emits the `relationLoads` the template renders). `when:` supports a single `field ==|!= literal` on a direct field. **Multi-hop** paths (`a.b.c`) and relation-path `when:` are the remaining gap; the parser rejects multi-hop `to` with a clear message. 3. **Scheduled activities** — cron reminders / cleanups; query an entity and act per matching row. **(v1 implemented.)** ```yaml schedules: @@ -379,13 +379,13 @@ Every action below has a real SDK surface to generate against, so none of this n - { field: status, op: eq, value: "ACTIVE" } notify: { to: member.email, subject: "Overdue: {book.title}", body: "Your loan is overdue." } ``` - → `gen/events/Job.java`: a `@Scheduled(expression=cron)` `JobHandler` that runs `new Repository().findAll(Criteria...)` (the typed `Criteria` query API) and performs the per-row `notify` (reusing `NotificationSupport.plan` against the row entity - same relation-load + interpolation as notifications). `ScheduleSupport.criteriaExpression` builds the `Criteria` chain (`where` -> typed conditions; field names PascalCased to the entity properties). Honors the `.settings` override. **Gap:** the action is `notify` only (other actions + `between`/`in` operators are later); process `trigger: { onSchedule: }` (timer start event) is still open. + → `gen/events//Job.java`: a `@Scheduled(expression=cron)` `JobHandler` that runs `new Repository().findAll(Criteria...)` (the typed `Criteria` query API) and performs the per-row `notify` (reusing `NotificationSupport.plan` against the row entity - same relation-load + interpolation as notifications). `ScheduleSupport.criteriaExpression` builds the `Criteria` chain (`where` -> typed conditions; field names PascalCased to the entity properties). Honors the `.settings` override. **Gap:** the action is `notify` only (other actions + `between`/`in` operators are later); process `trigger: { onSchedule: }` (timer start event) is still open. 4. **Outbound integrations** — "tell another system" on an event. **(v1 implemented.)** ```yaml integrations: - { name: pushBookToCatalog, event: { onCreate: Book }, method: POST, url: "@config:CATALOG_URL" } ``` - → `gen/events/Integration.java`: an `@Listener` that forwards the entity-event JSON to the URL via `sdk.http.HttpClient` (`IntegrationSupport` maps the method + the `@config:KEY` URL sugar to `Configurations.get`). The event-binding (`EventBinding`) + topic are shared with notifications. **Gap:** body forwards the whole entity (custom body mapping + headers/auth are later). + → `gen/events//Integration.java`: an `@Listener` that forwards the entity-event JSON to the URL via `sdk.http.HttpClient` (`IntegrationSupport` maps the method + the `@config:KEY` URL sugar to `Configurations.get`). The event-binding (`EventBinding`) + topic are shared with notifications. **Gap:** body forwards the whole entity (custom body mapping + headers/auth are later). **Tier 2 — high value:** @@ -394,7 +394,7 @@ Every action below has a real SDK surface to generate against, so none of this n inbound: - { name: ingestOrder, path: /ingest, create: Order } ``` - → `gen/events/Webhook.java`: a `@Controller` with a `@Post("")` that deserializes the body (via the java.time-aware SDK `Json`) into the entity and `save`s it through the repository, returning the saved JSON. Served at `/services/java//gen/events/Webhook`. **Gap:** v1 action is `create` (ingest); upsert/match-set and start-process, plus queue/topic sources (`@Listener`/`Consumer`), are later. + → `gen/events//Webhook.java`: a `@Controller` with a `@Post("")` that deserializes the body (via the java.time-aware SDK `Json`) into the entity and `save`s it through the repository, returning the saved JSON. Served at `/services/java//gen/events//Webhook`. **Gap:** v1 action is `create` (ingest); upsert/match-set and start-process, plus queue/topic sources (`@Listener`/`Consumer`), are later. 6. **Status lifecycle (state machine) on an entity** — order/ticket/loan status; the highest "removes custom code" leverage of the set. ```yaml entities: @@ -421,7 +421,7 @@ Every action below has a real SDK surface to generate against, so none of this n rollups: - { name: memberLoanCount, entity: Loan, via: member, field: loanCount } # Member.loanCount = #Loans whose `member` FK = that Member ``` - → two `gen/events/RollupOn{Create,Delete}.java` `@Listener`s on the child's create/delete topics that recompute the affected parent's count via a typed `Criteria` (`findAll(Criteria.create().eq("", entity.)).size()`) and write it back. Recompute-on-event (self-healing); **eventually consistent, not transactionally exact** under heavy concurrency. **Gap:** no `where` filter (counts all children), and re-parenting on child update isn't tracked (only create/delete). **`op: sum`** keeps a decimal sum of the child `of` field (+ optional `capacity`/`balance`/`status` for payment-settlement); **`op: latest`** copies the `of` value of the child row with the greatest `by` date/timestamp onto the parent field (create/update/delete handlers; parent field must match `of`'s type; empty child set → null) — the "keep the parent's rate equal to the newest child rate" shape (currencies `Currency.rate` ← latest `CurrencyRate`). `renderRollupAggregate` in `generateUtils.js` tracks the max-`by` row type-agnostically (`var` + `Objects.equals`). + → two `gen/events//RollupOn{Create,Delete}.java` `@Listener`s on the child's create/delete topics that recompute the affected parent's count via a typed `Criteria` (`findAll(Criteria.create().eq("", entity.)).size()`) and write it back. Recompute-on-event (self-healing); **eventually consistent, not transactionally exact** under heavy concurrency. **Gap:** no `where` filter (counts all children), and re-parenting on child update isn't tracked (only create/delete). **`op: sum`** keeps a decimal sum of the child `of` field (+ optional `capacity`/`balance`/`status` for payment-settlement); **`op: latest`** copies the `of` value of the child row with the greatest `by` date/timestamp onto the parent field (create/update/delete handlers; parent field must match `of`'s type; empty child set → null) — the "keep the parent's rate equal to the newest child rate" shape (currencies `Currency.rate` ← latest `CurrencyRate`). `renderRollupAggregate` in `generateUtils.js` tracks the max-`by` row type-agnostically (`var` + `Objects.equals`). 10. **Dynamic user-task assignment** — `assignee: { fromPath: member.branch.manager }`, resolver-driven (extends the existing user-task glue). ### Guardrails (so this doesn't become the MDE expressiveness trap) @@ -453,6 +453,8 @@ Implemented and generating annotated client-Java off the shared `EventBinding` / **Done:** +- **Per-module namespacing of generated handlers + bean/entity names (cross-module collision fix).** A whole-suite deploy of two modules built from the same recipe (sales-invoices / purchase-invoices) surfaced two pre-existing collisions: (A) every event handler landed in the shared package `gen.events`, so same-named reactions collided by FQN and the registry-wide client-Java sync aborted ("already declared"); (B) the client bean container and the Hibernate entity name key on the SIMPLE class name, so a same-named entity (`Recurrence`) collided by bean name (`recurrenceController`) and entity name regardless of package. Fix A: handlers now land in **`gen.events.`** (`IntentNaming.javaModule` = the exact mirror of `sanitizeJavaIdentifier`; `IntentNaming.eventsPackage`) — the module segment goes UNDER `gen/events` (NOT `gen//events`) so the glue keeps surviving a model-only regeneration, which wipes `gen/` wholesale; producer (BPMN FQNs, transition/generate endpoint URLs) and consumer (the events template's `package` lines + renames, the Harmonia print-feeder paths) derive the same segment. An authored `delegate: gen.events.` (no further dot) is rewritten to THIS module's events package (`BpmnIntentGenerator.moduleScopedDelegate`), so existing intents binding generated delegates (SnapshotGenerator, AutoAllocateOnInvoice) keep working unchanged. Fix B: every generated bean carries a module-qualified `@Component("_")` (rest-java controllers, dao-java repository, all events-template `@Component`/`@Controller` handlers) and the generated `@Entity` declares `name = "_Entity"` (the shared-SessionFactory Hibernate entity-name key). Covered end-to-end by `IntentCrossModuleCollisionIT` (two modules, same-named entity + transition, both controllers + both transition endpoints live). + - **BPM event support wave 2: `abortOn` (#6340).** A process-level `abortOn: { status: [ids], then: }` cancels the in-flight instance when the trigger entity transitions into a listed status - an interrupting message event subprocess (terminate end) fired by a generated `Abort.java` `MessageHandler` on the `-transitioned` topic, correlating on the stamped ProcessId. New `ProcessAbortSupport`, `aborts` glue collection + `Abort.java.template` + `generateUtils.js` case, the event-subprocess emission + fixed-placement DI + abort-only-cleanup filtering in `BpmnIntentGenerator`, parser `validateAbortOn`, editor diagram (abort ellipse + dashed edges), assistant-guide + Allowed-values + mapping. Unit: `AbortOnIntentTest`, `GlueAbortsTest`. See the `abortOn` semantics bullet above. - **BPM event support wave 1: `wait` step + `timeout:`/`expire:` boundary timers (#6327/#6328).** A running process can now observe the outside world: `wait` parks on a message intermediate catch event resumed by a generated correlating `MessageHandler` (correlation by the stamped ProcessId through the `via:` back-reference), and user tasks carry non-cancelling `timeout:` (ISO-8601 `timeDuration`) / cancelling `expire:` (`timeDate` from a date field re-read at task entry by an auto-inserted `TimerLoader` delegate) boundary timers. New `ProcessWaitSupport`/`ProcessTimerSupport`, `waits`/`timerLoaders` glue collections + `Wait.java.template`/`TimerLoader.java.template` + `generateUtils.js` cases, ``/`intermediateCatchEvent`/`boundaryEvent` emission + boundary DI shapes in `BpmnIntentGenerator`, parser validation (`validateWaitSteps`/`validateUserTaskTimers`), editor diagram support (wait ellipse + dashed timer edges), and assistant-guide sections. See the two semantics bullets above. @@ -511,12 +513,12 @@ Implemented and generating annotated client-Java off the shared `EventBinding` / - CLOB on ALTER TABLE fix (`modules/database/database-sql` `DataTypeUtils`): a `text` field maps to a CLOB column, and while CREATE TABLE accepted the literal `CLOB`, the second sync's `TableAlterProcessor` failed with `Type [2005] not supported` - `getDatabaseTypeName` maps a JDBC type code back through `DATABASE_TYPE_TO_DATA_TYPE`, which was missing `Types.CLOB` (2005) and `Types.NCLOB` (2011) even though `STRING_TO_DATABASE_TYPE` parsed `"CLOB"` the other way. Both are now mapped to `DataType.CLOB`/`NCLOB`; covered by `DataTypeUtilsTest`. (Platform fix, not intent-specific - any CLOB column hit this on ALTER.) - Reports rewritten to the Dirigible `.report` shape with a materialised SQL `query` (was empty), `relation.field` -> `INNER JOIN`, `filter` -> qualified `WHERE`, and default-role `security`. Covered by `IntentEngineIT` (aggregate + join/filter reports). - Cross-artefact PascalCase: the `.form` control `model`/`id` bind to the PascalCase EDM property name; a bare to-one relation report dimension auto-joins and shows the target's `name`-like field instead of the raw FK id. -- Process triggers (`trigger: { onCreate: }`) fully wired in Java: validated by the parser; the new `template-application-events-java` ("Application - Glue Code - Java") template generates a `gen/events/Trigger.java` self-describing `MessageHandler` that starts the process on the entity's create event; the Java DAO template publishes that event. The EDM keeps only the persisted `ProcessId` column (`EdmIntentGenerator`). Covered by `IntentEngineIT` end-to-end (verified live: create → trigger → process start → ProcessId written back). +- Process triggers (`trigger: { onCreate: }`) fully wired in Java: validated by the parser; the new `template-application-events-java` ("Application - Glue Code - Java") template generates a `gen/events//Trigger.java` self-describing `MessageHandler` that starts the process on the entity's create event; the Java DAO template publishes that event. The EDM keeps only the persisted `ProcessId` column (`EdmIntentGenerator`). Covered by `IntentEngineIT` end-to-end (verified live: create → trigger → process start → ProcessId written back). - **`ProcessId` consumed by the generated entity-view UI** (in-context BPM task surfacing). The `ProcessId` the trigger writes back is read by the generated views: a shared `ProcessTasks` AngularJS module (`components/resources/resources-dashboard/.../dashboard/services/process-tasks.js` — service + `` directive) fetches the current user's Inbox tasks once, buckets them by `processInstanceId`, and a record shows its actionable tasks inline by matching `entity.ProcessId === task.processInstanceId`. Wired into every generated view (`list`, `manage`, `master-list`/`master-manage` `detail` and `main-details`) gated on a `hasProcess` flag that `parameterUtils.js` sets when an entity has a `ProcessId` property — so non-process entities generate unchanged. The generated task form (`FormIntentGenerator`) completes via the per-task **permission-checked** `/services/inbox/tasks/{id}` (not the role-guarded `/services/bpm/bpm-processes/tasks/{id}`, which blocks candidate-group users) and self-closes on completion via both `DialogHub.closeWindow()` (dialog/inbox) and `window.close()` (standalone window). (#6074 + refinements #6075.) - **Process glue externalized to `.glue`** (the precedent: `.report`/`.form` were lifted out of the EDM). The `triggers` + `resolvers` collections live in `.glue` (`GlueIntentGenerator`), NOT the `.model` - the EDM describes entities, the BPMN describes flow, neither owns "who starts a process / how its context is populated". The Glue-Code template binds to `extension: "glue"`; `generateUtils.js` has `triggers` + `resolvers` collection cases. (Supersedes the older "triggers in the .model" wiring.) -- **`setField` service tasks + `next` routing (declarative status/field set):** a `serviceTask` with `setField`/`value` sets a string/text field of the trigger entity via a generated `gen/events/.java` `JavaDelegate` (`SetFieldSupport` → the `setters` glue collection + `SetField.java.template` + the `setters` case in `generateUtils.js`), persisting with the targeted single-column `updateProperty`; a `next: ` arg overrides a step's linear successor so a decision's two branches converge instead of falling through. Replaces the `custom.` scaffold for the approve→ACTIVE / reject→REJECTED pattern. See the "Decision steps" / `setField` semantics bullet above. Covered by `IntentEngineIT.set_field_glue_sets_entity_status_on_approve_reject_branches`. +- **`setField` service tasks + `next` routing (declarative status/field set):** a `serviceTask` with `setField`/`value` sets a string/text field of the trigger entity via a generated `gen/events//.java` `JavaDelegate` (`SetFieldSupport` → the `setters` glue collection + `SetField.java.template` + the `setters` case in `generateUtils.js`), persisting with the targeted single-column `updateProperty`; a `next: ` arg overrides a step's linear successor so a decision's two branches converge instead of falling through. Replaces the `custom.` scaffold for the approve→ACTIVE / reject→REJECTED pattern. See the "Decision steps" / `setField` semantics bullet above. Covered by `IntentEngineIT.set_field_glue_sets_entity_status_on_approve_reject_branches`. - **`setRelationField` (set a relation-FK status):** the generic, relation-valued sibling of `setField` — `args: { setRelationField: , value: }` sets a to-one relation's FK to a seed id (unquoted), via the same `setters` glue + `SetField.java.template` (a `relation` flag branches the template). Allowed on a serviceTask (bound directly) and on a userTask (setter inserted after the task, like the Writer). See the `setRelationField` semantics bullet above. -- **Resolvers (`relation.field`) — for decisions AND user-task forms:** a `relation.field` referencing a one-hop to-one relation of the trigger entity, in either a **decision condition** (`book.price > 500`) **or a user-task form's `fields`** (`book.price` on `ApproveLoan`), gets a `${JavaTask}` resolver service task and a `gen/events/Resolve.java` `JavaDelegate` (generated from `.glue`) that loads the related entity by FK id and sets the `_` process variable (`book_price`); decision conditions are rewritten to the resolved variable and form controls bind to it. **The resolver is inserted before the EARLIEST step that needs it** (`ProcessResolverSupport.resolvers` scans steps in declaration order, deduping per process+handler so the first occurrence wins). This is the crux of the form fix: `book.price` used by both `librarianReview`'s form (step 1) and the later `rareBook` decision used to anchor the resolver at the *decision*, so the *form* (which runs first) showed the field empty; anchoring at the earliest step (the form's user task) fills the form, and the variable persists so the downstream decision still resolves. Decision conditions are rewritten against **all** process resolvers (variables are process-global), but each resolver task is inserted **once**. `ProcessResolverSupport` + `IntentEntities` (shared perspective/PK resolution). Insertion/rewrite happen on a copy of the step list so the glue generator still sees the original paths. A form-only `relation.field` (no decision references it) is a resolver trigger in its own right; one used on a form *outside* any process stays empty (no process → no resolver). The parser validates every form `relation.field` is a one-hop to-one of the form's `forEntity` with the field present on the target (multi-hop rejected). +- **Resolvers (`relation.field`) — for decisions AND user-task forms:** a `relation.field` referencing a one-hop to-one relation of the trigger entity, in either a **decision condition** (`book.price > 500`) **or a user-task form's `fields`** (`book.price` on `ApproveLoan`), gets a `${JavaTask}` resolver service task and a `gen/events//Resolve.java` `JavaDelegate` (generated from `.glue`) that loads the related entity by FK id and sets the `_` process variable (`book_price`); decision conditions are rewritten to the resolved variable and form controls bind to it. **The resolver is inserted before the EARLIEST step that needs it** (`ProcessResolverSupport.resolvers` scans steps in declaration order, deduping per process+handler so the first occurrence wins). This is the crux of the form fix: `book.price` used by both `librarianReview`'s form (step 1) and the later `rareBook` decision used to anchor the resolver at the *decision*, so the *form* (which runs first) showed the field empty; anchoring at the earliest step (the form's user task) fills the form, and the variable persists so the downstream decision still resolves. Decision conditions are rewritten against **all** process resolvers (variables are process-global), but each resolver task is inserted **once**. `ProcessResolverSupport` + `IntentEntities` (shared perspective/PK resolution). Insertion/rewrite happen on a copy of the step list so the glue generator still sees the original paths. A form-only `relation.field` (no decision references it) is a resolver trigger in its own right; one used on a form *outside* any process stays empty (no process → no resolver). The parser validates every form `relation.field` is a one-hop to-one of the form's `forEntity` with the field present on the target (multi-hop rejected). - **`.settings`** (`IntentSettings`, loaded/scaffolded by `IntentGenerationService.loadOrScaffoldSettings`): developer-owned, scaffolded once then preserved (not scrubbed). Holds the `generation` recipe (template id + parameters per model type), per-artefact `overrides` (`{triggers|resolvers|forms}..generate=false` -> skip and reuse a hand-written one), and `userTasks.candidateGroupsExtra` (defaults to `ADMINISTRATOR`, appended to every user-task `candidateGroups`). Loaded into `IntentGenerationContext` before generators run; honored by the Glue/Form/BPMN generators. The Generate endpoint returns a `codeGenerations` plan from the recipe + written files, and the **editor's Generate chains model->code** by replaying it through `generate.mjs`. Cross-module tenant-context fix: the Java `@Listener` dispatch (`ListenerClassConsumer`) now runs in the message's tenant context. - **Declarative-glue catalog (notifications, schedules, integrations, inbound webhooks, rollups)** generated as annotated client-Java off the shared `EventBinding`/`NotificationSupport`/`ScheduleSupport`/`Criteria` core, plus `.glue` collections + `template-application-events-java` templates + the `generateUtils.js` collection cases. The canonical showcase is `IntentEngineIT`'s `INTENT_YAML`; mirrored into `dirigiblelabs/sample-intent-model`. See "Status of the catalog". - **Configurable trigger business key + `timestamp` strategy.** `trigger: { businessKey: , businessKeyStrategy: timestamp }` — the started process's BPM business key is a chosen field (PK by default); the `timestamp` strategy mints a `yyyyMMddHHmmss` value when blank. `IntentEngineIT` covers the flagged key, the mint+persist, and the parse rejection. The strategy field is the extension point for future pluggable number generators (sequential / padded / config-prefixed). diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/IntentNaming.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/IntentNaming.java index f5817c86f07..9599ec100d9 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/IntentNaming.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/IntentNaming.java @@ -71,6 +71,50 @@ public static String tableName(IntentGenerationContext context, String entityNam return upperSnake(baseName(context)) + "_" + upperSnake(entityName); } + /** + * The intent's sanitized Java module segment - the exact mirror of the template engine's + * {@code sanitizeJavaIdentifier} ({@code parameterUtils.js}): lower-cased, every character outside + * {@code [a-z0-9_]} replaced by an underscore, a leading digit prefixed with an underscore + * ({@code sales-invoices} -> {@code sales_invoices}). Producer (this engine, which writes handler + * FQNs into the {@code .bpmn} and endpoint URLs into extensions) and consumer (the events template, + * which emits the {@code package} line) must derive the same segment or the generated references + * never match. + * + * @param context the generation context + * @return the sanitized module segment, never blank + */ + public static String javaModule(IntentGenerationContext context) { + String name = baseName(context).toLowerCase(Locale.ROOT); + StringBuilder out = new StringBuilder(name.length() + 1); + for (int i = 0; i < name.length(); i++) { + char c = name.charAt(i); + out.append((c >= 'a' && c <= 'z') || (c >= '0' && c <= '9') || c == '_' ? c : '_'); + } + if (out.length() == 0) { + return "_"; + } + if (Character.isDigit(out.charAt(0))) { + out.insert(0, '_'); + } + return out.toString(); + } + + /** + * The module-scoped Java package every generated event handler lands in: + * {@code gen.events.} (e.g. {@code gen.events.sales_invoices}). Namespacing per module + * keeps two modules that author a same-named reaction (both built from the same recipe) from + * colliding by FQN in the registry-wide client-Java compilation. The module segment goes UNDER + * {@code gen/events} - not {@code gen//events} - so the glue output stays a sibling of the + * model-to-code template's {@code gen/} folder and survives a model-only regeneration + * (which wipes {@code gen/} wholesale). + * + * @param context the generation context + * @return the events package for this intent module + */ + public static String eventsPackage(IntentGenerationContext context) { + return "gen.events." + javaModule(context); + } + /** * Capitalize the first letter to make an UpperCamelCase (PascalCase) name, preserving the rest - * the Dirigible EDM convention for property names ({@code id} -> {@code Id}, {@code loanedOn} -> diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/SnapshotSupport.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/SnapshotSupport.java index c36a4432ff8..f6d052a19af 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/SnapshotSupport.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/SnapshotSupport.java @@ -24,14 +24,15 @@ /** * Builds the {@code snapshots} glue collection: one descriptor per {@code function: Snapshot} child * whose master is a document (header-items) master, driving the generated - * {@code gen/events/SnapshotGenerator.java} delegate. Wired into a process as a - * {@code delegate:} service task ({@code delegate: gen.events.SnapshotGenerator}) so an - * immutable printed copy of the document is rendered and stored on issue - the number stays across - * amendments, only the snapshot {@code Version} increments. + * {@code gen/events//SnapshotGenerator.java} delegate. Wired into a process as a + * {@code delegate:} service task ({@code delegate: gen.events.SnapshotGenerator}, + * module-scoped by the BPMN generator) so an immutable printed copy of the document is rendered and + * stored on issue - the number stays across amendments, only the snapshot {@code Version} + * increments. * *

- * The delegate reuses the master's generated {@code PrintFeeder} (same {@code gen.events} package) - * to assemble the {@code {document, items}} payload, renders it server-side via + * The delegate reuses the master's generated {@code PrintFeeder} (same module-scoped events + * package) to assemble the {@code {document, items}} payload, renders it server-side via * {@code sdk.print.Print}, and stores the PDF via {@code sdk.cms.Attachments} - so only a document * master (which has a feeder) can carry a snapshot child. */ diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/bpmn/BpmnIntentGenerator.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/bpmn/BpmnIntentGenerator.java index a83b29fe499..495f8821cb2 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/bpmn/BpmnIntentGenerator.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/bpmn/BpmnIntentGenerator.java @@ -218,8 +218,9 @@ public void generate(IntentGenerationContext context) { Map writerByTask = stripProcessPrefix(writerByProcessTask, taskPrefix); Map setterByTask = stripProcessPrefix(setterByProcessTask, taskPrefix); context.writeModelFile(fileName, - render(process, rolesByLowerName, context.getProjectName(), processResolvers, processFieldLoads, processTimerLoads, - ownFieldPascalCase(process, byName), candidateGroupsExtra, writerByTask, setterByTask)); + render(process, rolesByLowerName, context.getProjectName(), IntentNaming.eventsPackage(context), processResolvers, + processFieldLoads, processTimerLoads, ownFieldPascalCase(process, byName), candidateGroupsExtra, writerByTask, + setterByTask)); } } @@ -285,9 +286,9 @@ private static String formPageUrl(String projectName, String form) { return "/services/web/" + projectName + "/gen/" + form + "/forms/" + form + "/index.html"; } - private static String render(ProcessIntent process, Map rolesByLowerName, String projectName, List resolvers, - List fieldLoads, List timerLoads, Map ownFieldPascalCase, String candidateGroupsExtra, - Map writerByTask, Map setterByTask) { + private static String render(ProcessIntent process, Map rolesByLowerName, String projectName, String eventsPackage, + List resolvers, List fieldLoads, List timerLoads, Map ownFieldPascalCase, + String candidateGroupsExtra, Map writerByTask, Map setterByTask) { // Insert each resolver service task before its anchor step (the earliest decision or user-task // form that needs it) and rewrite the decision conditions - on a COPY of the step list, never // mutating the shared model (the glue generator runs after this one and must still see the @@ -295,8 +296,8 @@ private static String render(ProcessIntent process, Map rolesByL // own-field decision, an expire-date-loader service task before a user task with an `expire:` // timer, and writer/setter service tasks after a user task with editable fields or a // setRelationField. - List steps = - augmentWithResolvers(process.getSteps(), resolvers, fieldLoads, timerLoads, ownFieldPascalCase, writerByTask, setterByTask); + List steps = augmentWithResolvers(process.getSteps(), eventsPackage, resolvers, fieldLoads, timerLoads, + ownFieldPascalCase, writerByTask, setterByTask); // abortOn: a -transitioned into a listed status cancels the in-flight instance via an // interrupting message event subprocess (below). Its optional `then` cleanup is an abort-only // serviceTask - pull it out of the main flow (it is emitted inside the event subprocess), and @@ -370,7 +371,7 @@ private static String render(ProcessIntent process, Map rolesByL .isBlank()) { continue; } - appendStepElement(sb, step, rolesByLowerName, projectName, processId, candidateGroupsExtra); + appendStepElement(sb, step, rolesByLowerName, projectName, processId, eventsPackage, candidateGroupsExtra); } for (BoundaryTimer timer : boundaryTimers) { appendBoundaryTimer(sb, timer); @@ -379,7 +380,7 @@ private static String render(ProcessIntent process, Map rolesByL .append(END_ID) .append("\">\n"); if (hasAbort) { - appendAbortHandler(sb, processId, abortThenStep, projectName); + appendAbortHandler(sb, processId, abortThenStep, projectName, eventsPackage); } writeSequenceFlows(sb, flows); sb.append(" \n"); @@ -462,7 +463,8 @@ private static String abortEndId(String processId) { * user tasks, parked waits and armed timers) when the correlating glue fires the message. Placing * the interruption in an event subprocess avoids wrapping the main flow in a subprocess. */ - private static void appendAbortHandler(StringBuilder sb, String processId, StepIntent cleanup, String projectName) { + private static void appendAbortHandler(StringBuilder sb, String processId, StepIntent cleanup, String projectName, + String eventsPackage) { String startId = abortStartId(processId); String endId = abortEndId(processId); sb.append(" \n"); String afterStart = endId; if (cleanup != null) { - appendServiceTask(sb, cleanup, projectName, processId); + appendServiceTask(sb, cleanup, projectName, processId, eventsPackage); afterStart = cleanup.getName(); } sb.append(" augmentWithResolvers(List steps, List resolvers, List fieldLoads, - List timerLoads, Map ownFieldPascalCase, Map writerByTask, - Map setterByTask) { + private static List augmentWithResolvers(List steps, String eventsPackage, List resolvers, + List fieldLoads, List timerLoads, Map ownFieldPascalCase, + Map writerByTask, Map setterByTask) { List result = new ArrayList<>(steps.size()); for (StepIntent step : steps) { for (Resolver resolver : resolvers) { @@ -522,21 +524,21 @@ private static List augmentWithResolvers(List steps, Lis .equals(resolver.beforeStep())) { // The task id is the lower-camel form of the handler so it matches the casing of the // authored step ids; the delegate still resolves the PascalCase handler class. - result.add(javaServiceTaskStep(IntentNaming.camelCase(resolver.handler()), "gen.events." + resolver.handler())); + result.add(javaServiceTaskStep(IntentNaming.camelCase(resolver.handler()), eventsPackage + "." + resolver.handler())); } } for (FieldLoad load : fieldLoads) { if (step.getName() != null && step.getName() .equals(load.beforeStep())) { // Load the trigger entity's own fields the decision tests, right before the gateway. - result.add(javaServiceTaskStep(IntentNaming.camelCase(load.handler()), "gen.events." + load.handler())); + result.add(javaServiceTaskStep(IntentNaming.camelCase(load.handler()), eventsPackage + "." + load.handler())); } } for (TimerLoad load : timerLoads) { if (step.getName() != null && step.getName() .equals(load.beforeStep())) { // Re-read the expire date field at task entry, so the boundary timer arms fresh. - result.add(javaServiceTaskStep(IntentNaming.camelCase(load.handler()), "gen.events." + load.handler())); + result.add(javaServiceTaskStep(IntentNaming.camelCase(load.handler()), eventsPackage + "." + load.handler())); } } boolean userTask = "userTask".equals(step.getKind()) && step.getName() != null; @@ -555,7 +557,7 @@ private static List augmentWithResolvers(List steps, Lis result.add(next == null ? step : withoutNext(step)); for (int i = 0; i < afterTask.size(); i++) { String handler = afterTask.get(i); - StepIntent delegateStep = javaServiceTaskStep(IntentNaming.camelCase(handler), "gen.events." + handler); + StepIntent delegateStep = javaServiceTaskStep(IntentNaming.camelCase(handler), eventsPackage + "." + handler); if (i == afterTask.size() - 1 && next != null && !next.isBlank()) { delegateStep.getArgs() .put("next", next); @@ -674,7 +676,7 @@ private static List dedupeConsecutive(List ids) { } private static void appendStepElement(StringBuilder sb, StepIntent step, Map rolesByLowerName, String projectName, - String processName, String candidateGroupsExtra) { + String processName, String eventsPackage, String candidateGroupsExtra) { String kind = step.getKind() == null ? "userTask" : step.getKind(); switch (kind) { case "userTask": @@ -682,7 +684,7 @@ private static void appendStepElement(StringBuilder sb, StepIntent step, Map\n"); } - private static void appendServiceTask(StringBuilder sb, StepIntent step, String projectName, String processName) { + private static void appendServiceTask(StringBuilder sb, StepIntent step, String projectName, String processName, String eventsPackage) { // Five service-task shapes: // - a generator-synthesized resolver carries a javaHandler (a client JavaDelegate FQN) -> JavaTask; - // - an author-declared serviceTask with a `setField` -> JavaTask bound to the gen.events. + // - an author-declared serviceTask with a `setField` -> JavaTask bound to the . // JavaDelegate the glue generator emits (sets a field of the trigger entity to a literal value); // - an author-declared serviceTask with a `setRelationField` -> JavaTask bound to the same - // gen.events. JavaDelegate (sets a to-one relation's FK to a seed id); + // . JavaDelegate (sets a to-one relation's FK to a seed id); // - an author-declared serviceTask with a `call` (a TS handler path) -> JSTask, the path qualified // with the project name (the JSTask delegate resolves relative to the registry root); // - an author-declared serviceTask with none of the above -> JavaTask bound to a custom. Java @@ -745,7 +748,7 @@ private static void appendServiceTask(StringBuilder sb, StepIntent step, String // this is how a reusable, parameterized delegate (e.g. a document number generator) is invoked. String delegate = stringArg(step, "delegate"); if (delegate != null && !delegate.isBlank()) { - appendDelegateServiceTask(sb, step, delegate.trim()); + appendDelegateServiceTask(sb, step, moduleScopedDelegate(delegate.trim(), eventsPackage)); return; } String javaHandler = stringArg(step, "javaHandler"); @@ -759,7 +762,7 @@ private static void appendServiceTask(StringBuilder sb, StepIntent step, String handlerValue = javaHandler; } else if (setField != null && !setField.isBlank() || setRelationField != null && !setRelationField.isBlank()) { java = true; - handlerValue = "gen.events." + SetFieldSupport.className(processName, step.getName()); + handlerValue = eventsPackage + "." + SetFieldSupport.className(processName, step.getName()); } else if (call != null && !call.isBlank()) { java = false; handlerValue = qualifyHandlerPath(projectName, call); @@ -786,6 +789,23 @@ private static void appendServiceTask(StringBuilder sb, StepIntent step, String sb.append(" \n"); } + /** + * Resolve an authored {@code delegate:} FQN against the module-scoped events package. A bare + * {@code gen.events.} reference (no further package segment - class names cannot contain + * dots) means "the generated events class of THIS module" and is rewritten to + * {@code .}, so intents that bind a generated delegate (e.g. + * {@code delegate: gen.events.SalesInvoiceSnapshotGenerator}) keep working unchanged and stay + * portable across module renames. Any other FQN - including an explicit + * {@code gen.events..} - is the author's own class and passes through verbatim. + */ + private static String moduleScopedDelegate(String delegateClass, String eventsPackage) { + String legacyPrefix = "gen.events."; + if (delegateClass.startsWith(legacyPrefix) && delegateClass.indexOf('.', legacyPrefix.length()) < 0) { + return eventsPackage + delegateClass.substring(legacyPrefix.length() - 1); + } + return delegateClass; + } + /** * Emit a service task bound to an author-named client * {@link org.flowable.engine.delegate.JavaDelegate} via {@code flowable:class} (resolved through diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/generates/GeneratesIntentGenerator.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/generates/GeneratesIntentGenerator.java index c70b22127b0..f90f729a5ae 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/generates/GeneratesIntentGenerator.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/generates/GeneratesIntentGenerator.java @@ -35,8 +35,8 @@ *

* The endpoint is the REST {@code @Controller} generated (server half) from the {@code .glue} * file's {@code generates} collection by the {@code template-application-events-java} template, - * served under {@code /services/java//gen/events/Generate/run} (mirroring the - * inbound webhook URL layout). + * served under {@code /services/java//gen/events//Generate/run} + * (mirroring the inbound webhook URL layout). * *

* Idempotent: identical input always produces byte-identical output. The {@code .extension} files @@ -72,7 +72,7 @@ public void generate(IntentGenerationContext context) { String fileBase = name + "-generate-action"; String modulePath = project + "/" + fileBase + ".js"; context.writeModelFile(fileBase + ".extension", buildExtensionJson(project, modulePath, g)); - context.writeModelFile(fileBase + ".js", buildDescriptorModule(project, g)); + context.writeModelFile(fileBase + ".js", buildDescriptorModule(project, IntentNaming.javaModule(context), g)); } } @@ -84,14 +84,15 @@ private static String buildExtensionJson(String project, String modulePath, Gene return JsonHelper.toJson(extension); } - private static String buildDescriptorModule(String project, GeneratesIntent g) { + private static String buildDescriptorModule(String project, String javaModule, GeneratesIntent g) { Map view = new LinkedHashMap<>(); view.put("id", project + "-" + g.getForEntity() + "-" + g.getName()); String label = g.getLabel() == null || g.getLabel() .isBlank() ? IntentNaming.humanize(g.getName()) : g.getLabel(); view.put("label", label); // The server controller (server half) is served under gen/events; NOT the entity api base. - view.put("endpoint", "/services/java/" + project + "/gen/events/" + IntentNaming.pascalIdentifier(g.getName()) + "Generate/run"); + view.put("endpoint", "/services/java/" + project + "/gen/events/" + javaModule + "/" + IntentNaming.pascalIdentifier(g.getName()) + + "Generate/run"); view.put("view", g.getForEntity()); view.put("type", g.getScope()); if (g.getIcon() != null && !g.getIcon() diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/transition/TransitionsIntentGenerator.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/transition/TransitionsIntentGenerator.java index 757eac1e78b..9e6afc71b77 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/transition/TransitionsIntentGenerator.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/transition/TransitionsIntentGenerator.java @@ -35,8 +35,8 @@ *

* The endpoint is the REST {@code @Controller} generated (server half) from the {@code .glue} * file's {@code transitions} collection by the {@code template-application-events-java} template, - * served under {@code /services/java//gen/events/Transition/run}. A transition - * is always per-record ({@code type: entity}) - a whole-view status flip has no meaning. + * served under {@code /services/java//gen/events//Transition/run}. A + * transition is always per-record ({@code type: entity}) - a whole-view status flip has no meaning. * *

* Idempotent: identical input always produces byte-identical output. The {@code .extension} files @@ -70,7 +70,7 @@ public void generate(IntentGenerationContext context) { String fileBase = name + "-transition-action"; String modulePath = project + "/" + fileBase + ".js"; context.writeModelFile(fileBase + ".extension", buildExtensionJson(project, modulePath, t)); - context.writeModelFile(fileBase + ".js", buildDescriptorModule(project, t)); + context.writeModelFile(fileBase + ".js", buildDescriptorModule(project, IntentNaming.javaModule(context), t)); } } @@ -82,14 +82,15 @@ private static String buildExtensionJson(String project, String modulePath, Tran return JsonHelper.toJson(extension); } - private static String buildDescriptorModule(String project, TransitionIntent t) { + private static String buildDescriptorModule(String project, String javaModule, TransitionIntent t) { Map view = new LinkedHashMap<>(); view.put("id", project + "-" + t.getForEntity() + "-" + t.getName()); String label = t.getLabel() == null || t.getLabel() .isBlank() ? IntentNaming.humanize(t.getName()) : t.getLabel(); view.put("label", label); // The server controller (server half) is served under gen/events; NOT the entity api base. - view.put("endpoint", "/services/java/" + project + "/gen/events/" + IntentNaming.pascalIdentifier(t.getName()) + "Transition/run"); + view.put("endpoint", "/services/java/" + project + "/gen/events/" + javaModule + "/" + IntentNaming.pascalIdentifier(t.getName()) + + "Transition/run"); view.put("view", t.getForEntity()); view.put("type", "entity"); if (t.getIcon() != null && !t.getIcon() diff --git a/components/engine/engine-intent/src/main/resources/intent-assistant-guide.md b/components/engine/engine-intent/src/main/resources/intent-assistant-guide.md index eb2da02bfee..3b86612621d 100644 --- a/components/engine/engine-intent/src/main/resources/intent-assistant-guide.md +++ b/components/engine/engine-intent/src/main/resources/intent-assistant-guide.md @@ -653,7 +653,7 @@ parameters with `fields: { : , ... }`: The delegate is bound via `flowable:class` (not the `${JavaTask}` dispatcher the `setField` / scaffolded-stub paths use), because only `flowable:class` lets Flowable inject the declared `fields` as delegate fields. Contrast the three "custom code" service-task shapes: `setField` / -`setRelationField` bind a **generated** `gen.events` delegate; a **bare** serviceTask (no +`setRelationField` bind a **generated** delegate in the module-scoped events package (`gen.events.`; the shorthand `gen.events.` in a `delegate:` always means THIS module's generated class); a **bare** serviceTask (no `delegate` / `call`) binds `custom.` and scaffolds a one-time stub under `custom/`; a `delegate` binds **your** named class and scaffolds nothing (you write it). **A delegate that touches an entity must live in that entity's project** and manage it through the generated @@ -838,7 +838,7 @@ Two halves are generated (the `generates` pattern): a client button (a `-transition-action.extension` + `.js` contribution to the app's `-custom-action` point, carrying an `endpoint`; always per-record) and a server-side Java `@Controller` (`Transition`, via the `.glue` file) served at -`/services/java//gen/events/Transition/run`. The controller re-loads the record, +`/services/java//gen/events//Transition/run` (the `` segment is the sanitized intent name, e.g. `sales-invoices` -> `sales_invoices`). The controller re-loads the record, validates the status + `when` guards (a failure returns **409** with the reason and leaves the record untouched), then flips ONLY the status column through the targeted `updateProperty` primitive - a workflow-style system write: no `-updated` re-fire (no onUpdate reactions), but the `-transitioned` @@ -893,7 +893,7 @@ observe it); it requires the `from` entity to declare a `function: EntityStatus` Two halves are generated: a client button (a `-generate-action.extension` + `.js` contribution to the app's `-custom-action` point, carrying an `endpoint`) and a server-side Java `@Controller` (`Generate`, via the `.glue` file) served at -`/services/java//gen/events/Generate/run`. The shared `customActions` store POSTs +`/services/java//gen/events//Generate/run`. The shared `customActions` store POSTs the selected id to that endpoint and toasts the created record (no page dialog). ### reports - read-only aggregations @@ -1290,7 +1290,8 @@ Generates two client-Java glue classes (bind them with a `rollups` sum entry tha across the payer's open invoices (oldest first), creating junction rows until the pot is used up. - **`OnInvoice`** - a `JavaDelegate` that pulls the customer's unallocated payment balance onto an invoice; wire it as a **`delegate:` service task** on the process step where the invoice becomes - payable (e.g. right after Issue), e.g. `args: { delegate: gen.events.AutoAllocateOnInvoice, next: … }`. + payable (e.g. right after Issue), e.g. `args: { delegate: gen.events.AutoAllocateOnInvoice, next: … }` + (the bare `gen.events.` shorthand resolves to this module's generated events package). **Rules:** `junction` / `invoice` / `payment` are declared entities; the junction must have a to-one relation to both the invoice and the payment; `amount` is a junction field; `total` / `paid` / `order` diff --git a/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/IntentNamingTest.java b/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/IntentNamingTest.java index 33934a4d520..b1143256564 100644 --- a/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/IntentNamingTest.java +++ b/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/IntentNamingTest.java @@ -11,10 +11,35 @@ import static org.junit.jupiter.api.Assertions.assertEquals; +import org.eclipse.dirigible.components.intent.model.IntentModel; import org.junit.jupiter.api.Test; class IntentNamingTest { + @Test + void javaModuleMirrorsTheTemplateEngineSanitizer() { + // Must produce exactly what parameterUtils.js sanitizeJavaIdentifier produces for the same + // name - the BPMN handler FQNs / endpoint URLs (producer) and the events template's package + // line (consumer) only match if both sides sanitize identically. + assertEquals("sales_invoices", IntentNaming.javaModule(named("sales-invoices"))); + assertEquals("numbers", IntentNaming.javaModule(named("numbers"))); + assertEquals("my_app_2", IntentNaming.javaModule(named("My App.2"))); + assertEquals("_7wonders", IntentNaming.javaModule(named("7wonders"))); + } + + @Test + void eventsPackageIsModuleScoped() { + // gen.events., NOT gen..events: the module segment goes UNDER gen/events so + // the glue output stays a sibling of gen/ and survives a model-only regeneration. + assertEquals("gen.events.sales_invoices", IntentNaming.eventsPackage(named("sales-invoices"))); + } + + private static IntentGenerationContext named(String name) { + IntentModel model = new IntentModel(); + model.setName(name); + return TestContexts.context(model); + } + @Test void upperSnakeKeepsCamelCaseBoundaries() { assertEquals("LOANED_ON", IntentNaming.upperSnake("loanedOn")); diff --git a/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Entity.java.template b/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Entity.java.template index 6d3df2e2859..cdb6cabeb28 100644 --- a/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Entity.java.template +++ b/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Entity.java.template @@ -12,7 +12,7 @@ import org.eclipse.dirigible.sdk.db.Table; import org.eclipse.dirigible.sdk.db.UpdatedAt; import org.eclipse.dirigible.sdk.db.UpdatedBy; -@Entity +@Entity(name = "${javaGenFolderName}_${name}Entity") @Table(name = "${tablePrefix}${dataName}") @Documentation("${name} entity mapping") public class ${name}Entity { diff --git a/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Repository.java.template b/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Repository.java.template index c1f660d89a8..bfffc1694e7 100644 --- a/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Repository.java.template +++ b/components/template/template-application-dao-java/src/main/resources/META-INF/dirigible/template-application-dao-java/data/Repository.java.template @@ -70,6 +70,7 @@ package gen.${javaGenFolderName}.data.${javaPerspectiveName}; #end #end import org.eclipse.dirigible.components.data.store.java.repository.JavaRepository; +import org.eclipse.dirigible.sdk.component.Component; import org.eclipse.dirigible.sdk.component.Repository; import org.eclipse.dirigible.sdk.messaging.Producer; import org.eclipse.dirigible.sdk.utils.Json; @@ -122,6 +123,7 @@ ${importsCode} #end @Repository +@Component("${javaGenFolderName}_${name}Repository") public class ${name}Repository extends JavaRepository<${name}Entity> { public ${name}Repository() { diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Abort.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Abort.java.template index cbfd21bb0ba..f81c2475f80 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Abort.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Abort.java.template @@ -1,4 +1,4 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import org.eclipse.dirigible.sdk.bpm.Process; import org.eclipse.dirigible.sdk.component.Component; @@ -22,7 +22,7 @@ import gen.${javaGenFolderName}.data.${javaPerspective}.${entity}Entity; * Self-describing MessageHandler (strong-interface style): the destination/kind come from the * interface, not an annotation; @Component makes it a managed bean. */ -@Component +@Component("${javaGenFolderName}_${process}Abort") public class ${process}Abort implements MessageHandler { @Override diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Expansion.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Expansion.java.template index b810ac7e4b6..b72d9f69124 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Expansion.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Expansion.java.template @@ -1,4 +1,4 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import org.eclipse.dirigible.components.data.store.java.repository.Criteria; import org.eclipse.dirigible.sdk.component.Component; @@ -22,7 +22,7 @@ import gen.${javaGenFolderName}.data.${javaChildPerspective}.${childEntity}Repos * and skipped, which both makes re-delivery idempotent and bounds the event cascade (a roll-up's * write-back to the master re-triggers this handler once; the skip terminates it). */ -@Component +@Component("${javaGenFolderName}_${className}") public class ${className} implements MessageHandler { @Override diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/FieldLoader.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/FieldLoader.java.template index be0032e75e0..0cf5503039f 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/FieldLoader.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/FieldLoader.java.template @@ -1,4 +1,4 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.delegate.JavaDelegate; diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Generate.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Generate.java.template index 8be69c54a6e..a92d02f9ae9 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Generate.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Generate.java.template @@ -1,7 +1,8 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import org.eclipse.dirigible.components.data.store.java.repository.Criteria; import org.eclipse.dirigible.sdk.http.Body; +import org.eclipse.dirigible.sdk.component.Component; import org.eclipse.dirigible.sdk.http.Controller; import org.eclipse.dirigible.sdk.http.Post; import org.eclipse.dirigible.sdk.utils.Json; @@ -14,9 +15,10 @@ import org.eclipse.dirigible.sdk.utils.Json; * * Generated from the intent generates block - do not edit; it is re-generated with the application. * Entity access goes ONLY through the generated repositories (validations / events / numbering). - * The endpoint is served under /services/java//gen/events/${className}Generate/run. + * The endpoint is served under /services/java//gen/events/${javaGenFolderName}/${className}Generate/run. */ @Controller +@Component("${javaGenFolderName}_${className}Generate") public class ${className}Generate { public static class Request { diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Integration.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Integration.java.template index 38880d61148..ee6abf3816f 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Integration.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Integration.java.template @@ -1,4 +1,4 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import java.util.HashMap; import java.util.Map; @@ -17,7 +17,7 @@ import org.eclipse.dirigible.sdk.utils.Json; * application. The event payload (the entity JSON) is sent as the request body. The target URL comes * from the intent (a literal or a configuration value). */ -@Component +@Component("${javaGenFolderName}_${className}Integration") public class ${className}Integration implements MessageHandler { @Override diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Job.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Job.java.template index 8e22feb976b..0b9f247027b 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Job.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Job.java.template @@ -1,4 +1,4 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import java.util.List; #if($action != "generate") @@ -30,7 +30,7 @@ import gen.${load.javaGenFolder}.data.${load.javaTargetPerspective}.${load.targe * Entity access goes ONLY through the generated repositories. The mail sender comes from the * DIRIGIBLE_MAIL_SENDER configuration. */ -@Component +@Component("${javaGenFolderName}_${className}Job") public class ${className}Job implements JobHandler { @Override diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Notification.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Notification.java.template index 7f96a5c074a..cd852daf84c 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Notification.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Notification.java.template @@ -1,4 +1,4 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import java.util.HashMap; import java.util.List; @@ -24,7 +24,7 @@ import gen.${load.javaGenFolder}.data.${load.javaTargetPerspective}.${load.targe * application. Binds to the topic the ${entity} repository publishes the event to. The sender * address comes from the DIRIGIBLE_MAIL_SENDER configuration. */ -@Component +@Component("${javaGenFolderName}_${className}Notification") public class ${className}Notification implements MessageHandler { @Override diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Numbering.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Numbering.java.template index 4266814ec8f..1fd6adea9ed 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Numbering.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Numbering.java.template @@ -1,4 +1,4 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import java.util.LinkedHashMap; import java.util.Map; diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Posting.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Posting.java.template index 6d1cd8d029a..86f4500ebb6 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Posting.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Posting.java.template @@ -1,4 +1,4 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import org.eclipse.dirigible.components.data.store.java.repository.Criteria; import org.eclipse.dirigible.sdk.component.Component; @@ -31,7 +31,7 @@ import org.eclipse.dirigible.sdk.utils.Json; * documents CARRYING the storno link, while the reversed sibling's guard counts only documents * WITHOUT it. */ -@Component +@Component("${javaGenFolderName}_${className}Posting") public class ${className}Posting implements MessageHandler { @Override diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/PrintFeeder.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/PrintFeeder.java.template index 48f614f6eb7..ac6590eabf3 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/PrintFeeder.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/PrintFeeder.java.template @@ -1,4 +1,4 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import java.util.ArrayList; import java.util.LinkedHashMap; @@ -6,6 +6,7 @@ import java.util.List; import java.util.Map; import org.eclipse.dirigible.components.data.store.java.repository.Criteria; +import org.eclipse.dirigible.sdk.component.Component; import org.eclipse.dirigible.sdk.http.Controller; import org.eclipse.dirigible.sdk.http.Get; import org.eclipse.dirigible.sdk.http.PathParam; @@ -21,9 +22,10 @@ import org.eclipse.dirigible.sdk.utils.Json; * what a print receives). Entity access goes ONLY through the generated repositories, so validations, * events and the multilingual translation overlay (print in the caller's language) all apply, and the * browser calls it as the logged-in user so authorization and tenant are the caller's own. - * Served at /services/java//gen/events/${className}PrintFeeder/{id}. + * Served at /services/java//gen/events/${javaGenFolderName}/${className}PrintFeeder/{id}. */ @Controller +@Component("${javaGenFolderName}_${className}PrintFeeder") public class ${className}PrintFeeder { @Get("/{id}") diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Resolver.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Resolver.java.template index 07c380d8c4f..2c4e668ef06 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Resolver.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Resolver.java.template @@ -1,4 +1,4 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.delegate.JavaDelegate; diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Rollup.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Rollup.java.template index 0ff79ccf831..060f7da26f8 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Rollup.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Rollup.java.template @@ -1,4 +1,4 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import org.eclipse.dirigible.components.data.store.java.repository.Criteria; import org.eclipse.dirigible.sdk.component.Component; @@ -21,7 +21,7 @@ import gen.${javaGenFolderName}.data.${javaParentPerspective}.${parentEntity}Rep * recomputed from the store on each event (self-healing); the write + the "-updated" event fire only when * a value actually changes, which bounds the transitive cascade so it terminates at rest. */ -@Component +@Component("${javaGenFolderName}_${className}") public class ${className} implements MessageHandler { @Override diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/SetField.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/SetField.java.template index fa2398443cb..005bf270ee0 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/SetField.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/SetField.java.template @@ -1,4 +1,4 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import org.eclipse.dirigible.sdk.bpm.Process; import org.eclipse.dirigible.sdk.messaging.Producer; diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/SettlementOnInvoice.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/SettlementOnInvoice.java.template index 25f75ef0935..c37ae2ea420 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/SettlementOnInvoice.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/SettlementOnInvoice.java.template @@ -1,4 +1,4 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import java.math.BigDecimal; diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/SettlementOnPayment.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/SettlementOnPayment.java.template index 3d9fd192e9a..297a9be7e17 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/SettlementOnPayment.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/SettlementOnPayment.java.template @@ -1,4 +1,4 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import java.math.BigDecimal; @@ -22,7 +22,7 @@ import gen.${paymentGenFolder}.data.${paymentJavaPerspective}.${paymentEntity}En * Generated from the intent settlements block - do not edit; it is re-generated with the application. * Entity access goes ONLY through the generated repositories (validations / events / i18n). */ -@Component +@Component("${javaGenFolderName}_${name}OnPayment") public class ${name}OnPayment implements MessageHandler { @Override diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Snapshot.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Snapshot.java.template index 5b27ea8285b..6abe1abbd73 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Snapshot.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Snapshot.java.template @@ -1,4 +1,4 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import org.eclipse.dirigible.components.data.store.java.repository.Criteria; import org.flowable.engine.delegate.DelegateExecution; @@ -13,8 +13,8 @@ import gen.${javaGenFolderName}.data.${snapshotJavaPerspective}.${snapshotEntity * stays across amendments - each generation only increments the copy's Version. * * Generated from the intent snapshots glue - do not edit; it is re-generated with the application. - * Wired as a `delegate:` service task (delegate: gen.events.${master}SnapshotGenerator). Data assembly - * reuses the generated ${master}PrintFeeder (same gen.events package); the PDF is rendered server-side + * Wired as a `delegate:` service task (delegate: gen.events.${master}SnapshotGenerator, module-scoped by the BPMN generator). Data assembly + * reuses the generated ${master}PrintFeeder (same module-scoped events package); the PDF is rendered server-side * via sdk.print.Print and stored in the tenant CMS via sdk.cms.Attachments; the ${snapshotEntity} row * is written through its generated repository (validations / events / audit). */ diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/TimerLoader.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/TimerLoader.java.template index ed74f3cb2b6..42c470b6e98 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/TimerLoader.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/TimerLoader.java.template @@ -1,4 +1,4 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import org.flowable.engine.delegate.DelegateExecution; import org.flowable.engine.delegate.JavaDelegate; diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Transition.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Transition.java.template index 87cd8d68f44..70190646f01 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Transition.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Transition.java.template @@ -1,6 +1,7 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import org.eclipse.dirigible.sdk.http.Body; +import org.eclipse.dirigible.sdk.component.Component; import org.eclipse.dirigible.sdk.http.Controller; import org.eclipse.dirigible.sdk.http.Post; import org.eclipse.dirigible.sdk.http.Response; @@ -17,9 +18,10 @@ import org.eclipse.dirigible.sdk.utils.Json; * * Generated from the intent transitions block - do not edit; it is re-generated with the application. * Entity access goes ONLY through the generated repositories. - * The endpoint is served under /services/java//gen/events/${className}Transition/run. + * The endpoint is served under /services/java//gen/events/${javaGenFolderName}/${className}Transition/run. */ @Controller +@Component("${javaGenFolderName}_${className}Transition") public class ${className}Transition { public static class Request { diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Trigger.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Trigger.java.template index 657f078e6d0..86a185d2be8 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Trigger.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Trigger.java.template @@ -1,4 +1,4 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import org.eclipse.dirigible.sdk.bpm.Process; import org.eclipse.dirigible.sdk.component.Component; @@ -19,7 +19,7 @@ import gen.${javaGenFolderName}.data.${javaPerspective}.${entity}Repository; * Self-describing MessageHandler (strong-interface style): the destination/kind come from the * interface, not an annotation; @Component makes it a managed bean with constructor injection. */ -@Component +@Component("${javaGenFolderName}_${process}Trigger") public class ${process}Trigger implements MessageHandler { private final ${entity}Repository repository; diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Wait.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Wait.java.template index 4a6509999c4..4d75674b307 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Wait.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Wait.java.template @@ -1,4 +1,4 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import org.eclipse.dirigible.sdk.bpm.Process; import org.eclipse.dirigible.sdk.component.Component; @@ -27,7 +27,7 @@ import gen.${javaGenFolderName}.data.${javaEventPerspective}.${eventEntity}Repos * Self-describing MessageHandler (strong-interface style): the destination/kind come from the * interface, not an annotation; @Component makes it a managed bean. */ -@Component +@Component("${javaGenFolderName}_${className}Wait") public class ${className}Wait implements MessageHandler { @Override diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Webhook.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Webhook.java.template index f1e842c598a..4f5fe080a05 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Webhook.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Webhook.java.template @@ -1,6 +1,7 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import org.eclipse.dirigible.sdk.http.Body; +import org.eclipse.dirigible.sdk.component.Component; import org.eclipse.dirigible.sdk.http.Controller; import org.eclipse.dirigible.sdk.http.Post; import org.eclipse.dirigible.sdk.utils.Json; @@ -12,9 +13,10 @@ import gen.${javaGenFolderName}.data.${javaPerspective}.${entity}Repository; * Inbound webhook ${name}: ingest a posted JSON payload as a ${entity}. * * Generated from the intent inbound block - do not edit; it is re-generated with the application. - * The endpoint is served under /services/java//gen/events/${className}Webhook${path}. + * The endpoint is served under /services/java//gen/events/${javaGenFolderName}/${className}Webhook${path}. */ @Controller +@Component("${javaGenFolderName}_${className}Webhook") public class ${className}Webhook { @Post("${path}") diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Writer.java.template b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Writer.java.template index cf28734cfc9..e23d5bf9da9 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Writer.java.template +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/events/Writer.java.template @@ -1,4 +1,4 @@ -package gen.events; +package gen.events.${javaGenFolderName}; import java.util.LinkedHashMap; import java.util.Map; diff --git a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/template/template.js b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/template/template.js index bf2af5031c2..f7216878ef9 100644 --- a/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/template/template.js +++ b/components/template/template-application-events-java/src/main/resources/META-INF/dirigible/template-application-events-java/template/template.js @@ -26,154 +26,154 @@ export function getTemplate(parameters) { { location: "/template-application-events-java/events/Trigger.java.template", action: "generate", - rename: "gen/events/{{process}}Trigger.java", + rename: "gen/events/{{javaGenFolderName}}/{{process}}Trigger.java", engine: "velocity", collection: "triggers" }, { location: "/template-application-events-java/events/Resolver.java.template", action: "generate", - rename: "gen/events/{{handler}}.java", + rename: "gen/events/{{javaGenFolderName}}/{{handler}}.java", engine: "velocity", collection: "resolvers" }, { location: "/template-application-events-java/events/SetField.java.template", action: "generate", - rename: "gen/events/{{className}}.java", + rename: "gen/events/{{javaGenFolderName}}/{{className}}.java", engine: "velocity", collection: "setters" }, { location: "/template-application-events-java/events/FieldLoader.java.template", action: "generate", - rename: "gen/events/{{handler}}.java", + rename: "gen/events/{{javaGenFolderName}}/{{handler}}.java", engine: "velocity", collection: "fieldLoaders" }, { location: "/template-application-events-java/events/TimerLoader.java.template", action: "generate", - rename: "gen/events/{{handler}}.java", + rename: "gen/events/{{javaGenFolderName}}/{{handler}}.java", engine: "velocity", collection: "timerLoaders" }, { location: "/template-application-events-java/events/Wait.java.template", action: "generate", - rename: "gen/events/{{className}}Wait.java", + rename: "gen/events/{{javaGenFolderName}}/{{className}}Wait.java", engine: "velocity", collection: "waits" }, { location: "/template-application-events-java/events/Abort.java.template", action: "generate", - rename: "gen/events/{{process}}Abort.java", + rename: "gen/events/{{javaGenFolderName}}/{{process}}Abort.java", engine: "velocity", collection: "aborts" }, { location: "/template-application-events-java/events/Writer.java.template", action: "generate", - rename: "gen/events/{{className}}.java", + rename: "gen/events/{{javaGenFolderName}}/{{className}}.java", engine: "velocity", collection: "writers" }, { location: "/template-application-events-java/events/Notification.java.template", action: "generate", - rename: "gen/events/{{className}}Notification.java", + rename: "gen/events/{{javaGenFolderName}}/{{className}}Notification.java", engine: "velocity", collection: "notifications" }, { location: "/template-application-events-java/events/Job.java.template", action: "generate", - rename: "gen/events/{{className}}Job.java", + rename: "gen/events/{{javaGenFolderName}}/{{className}}Job.java", engine: "velocity", collection: "schedules" }, { location: "/template-application-events-java/events/Integration.java.template", action: "generate", - rename: "gen/events/{{className}}Integration.java", + rename: "gen/events/{{javaGenFolderName}}/{{className}}Integration.java", engine: "velocity", collection: "integrations" }, { location: "/template-application-events-java/events/Webhook.java.template", action: "generate", - rename: "gen/events/{{className}}Webhook.java", + rename: "gen/events/{{javaGenFolderName}}/{{className}}Webhook.java", engine: "velocity", collection: "inbound" }, { location: "/template-application-events-java/events/Rollup.java.template", action: "generate", - rename: "gen/events/{{className}}.java", + rename: "gen/events/{{javaGenFolderName}}/{{className}}.java", engine: "velocity", collection: "rollups" }, { location: "/template-application-events-java/events/Expansion.java.template", action: "generate", - rename: "gen/events/{{className}}.java", + rename: "gen/events/{{javaGenFolderName}}/{{className}}.java", engine: "velocity", collection: "expansions" }, { location: "/template-application-events-java/events/PrintFeeder.java.template", action: "generate", - rename: "gen/events/{{className}}PrintFeeder.java", + rename: "gen/events/{{javaGenFolderName}}/{{className}}PrintFeeder.java", engine: "velocity", collection: "printFeeders" }, { location: "/template-application-events-java/events/Snapshot.java.template", action: "generate", - rename: "gen/events/{{master}}SnapshotGenerator.java", + rename: "gen/events/{{javaGenFolderName}}/{{master}}SnapshotGenerator.java", engine: "velocity", collection: "snapshots" }, { location: "/template-application-events-java/events/Numbering.java.template", action: "generate", - rename: "gen/events/{{entity}}NumberStamp.java", + rename: "gen/events/{{javaGenFolderName}}/{{entity}}NumberStamp.java", engine: "velocity", collection: "numbering" }, { location: "/template-application-events-java/events/SettlementOnPayment.java.template", action: "generate", - rename: "gen/events/{{name}}OnPayment.java", + rename: "gen/events/{{javaGenFolderName}}/{{name}}OnPayment.java", engine: "velocity", collection: "settlements" }, { location: "/template-application-events-java/events/SettlementOnInvoice.java.template", action: "generate", - rename: "gen/events/{{name}}OnInvoice.java", + rename: "gen/events/{{javaGenFolderName}}/{{name}}OnInvoice.java", engine: "velocity", collection: "settlements" }, { location: "/template-application-events-java/events/Generate.java.template", action: "generate", - rename: "gen/events/{{className}}Generate.java", + rename: "gen/events/{{javaGenFolderName}}/{{className}}Generate.java", engine: "velocity", collection: "generates" }, { location: "/template-application-events-java/events/Transition.java.template", action: "generate", - rename: "gen/events/{{className}}Transition.java", + rename: "gen/events/{{javaGenFolderName}}/{{className}}Transition.java", engine: "velocity", collection: "transitions" }, { location: "/template-application-events-java/events/Posting.java.template", action: "generate", - rename: "gen/events/{{className}}Posting.java", + rename: "gen/events/{{javaGenFolderName}}/{{className}}Posting.java", engine: "velocity", collection: "postings" }, diff --git a/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityController.java.template b/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityController.java.template index 34ad52fcb16..bdd72d606db 100644 --- a/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityController.java.template +++ b/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityController.java.template @@ -15,6 +15,7 @@ import gen.${javaGenFolderName}.data.${javaPerspectiveName}.${name}Repository; import org.eclipse.dirigible.components.api.security.UserFacade; import org.eclipse.dirigible.sdk.platform.Documentation; import org.eclipse.dirigible.sdk.http.Body; +import org.eclipse.dirigible.sdk.component.Component; import org.eclipse.dirigible.sdk.http.Controller; import org.eclipse.dirigible.sdk.http.Delete; import org.eclipse.dirigible.sdk.http.Get; @@ -33,6 +34,7 @@ import java.util.Map; import java.util.Set; @Controller +@Component("${javaGenFolderName}_${name}Controller") @Documentation("${projectName} - ${name} Controller") public class ${name}Controller { @@ -59,7 +61,7 @@ public class ${name}Controller { if (${masterEntityId} != null) { Map params = new LinkedHashMap<>(); params.put("${masterEntityId}", ${masterEntityId}); - result = repository.query("from ${name}Entity e where e.${masterEntityId} = :${masterEntityId}", params); + result = repository.query("from ${javaGenFolderName}_${name}Entity e where e.${masterEntityId} = :${masterEntityId}", params); } else { result = repository.findAll(actualLimit, actualOffset); } @@ -256,7 +258,7 @@ public class ${name}Controller { } private List<${name}Entity> runFilter(Map filter) { - StringBuilder hql = new StringBuilder("from ${name}Entity e"); + StringBuilder hql = new StringBuilder("from ${javaGenFolderName}_${name}Entity e"); Map params = new LinkedHashMap<>(); boolean first = true; if (filter != null && filter.get("equals") instanceof Map equals) { diff --git a/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityMyController.java.template b/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityMyController.java.template index 1a29d935b89..7f2117ca892 100644 --- a/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityMyController.java.template +++ b/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityMyController.java.template @@ -6,6 +6,7 @@ import gen.${javaGenFolderName}.data.${javaPerspectiveName}.${name}Repository; import org.eclipse.dirigible.components.data.store.java.repository.Criteria; import org.eclipse.dirigible.sdk.platform.Documentation; import org.eclipse.dirigible.sdk.http.Body; +import org.eclipse.dirigible.sdk.component.Component; import org.eclipse.dirigible.sdk.http.Controller; import org.eclipse.dirigible.sdk.http.Delete; import org.eclipse.dirigible.sdk.http.Get; @@ -31,6 +32,7 @@ import java.util.Map; * re-generated with the application. */ @Controller +@Component("${javaGenFolderName}_${name}MyController") @Documentation("${projectName} - ${name} personal (my) controller") public class ${name}MyController { @@ -59,7 +61,7 @@ public class ${name}MyController { requireMyParent(${personalParent.fkProperty}); Map params = new LinkedHashMap<>(); params.put("parent", ${personalParent.fkProperty}); - List<${name}Entity> result = repository.query("from ${name}Entity e where e.${personalParent.fkProperty} = :parent", params); + List<${name}Entity> result = repository.query("from ${javaGenFolderName}_${name}Entity e where e.${personalParent.fkProperty} = :parent", params); result.forEach(this::scrub); return result; } diff --git a/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityPartnerController.java.template b/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityPartnerController.java.template index a811255aa5b..91551fb40f8 100644 --- a/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityPartnerController.java.template +++ b/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/EntityPartnerController.java.template @@ -6,6 +6,7 @@ import gen.${javaGenFolderName}.data.${javaPerspectiveName}.${name}Repository; import org.eclipse.dirigible.components.data.store.java.repository.Criteria; import org.eclipse.dirigible.sdk.platform.Documentation; import org.eclipse.dirigible.sdk.http.Body; +import org.eclipse.dirigible.sdk.component.Component; import org.eclipse.dirigible.sdk.http.Controller; import org.eclipse.dirigible.sdk.http.Delete; import org.eclipse.dirigible.sdk.http.Get; @@ -32,6 +33,7 @@ import java.util.Map; * Generated from the intent partner attribute - do not edit; it is re-generated with the application. */ @Controller +@Component("${javaGenFolderName}_${name}PartnerController") @Documentation("${projectName} - ${name} partner controller") public class ${name}PartnerController { @@ -60,7 +62,7 @@ public class ${name}PartnerController { requireMyParent(${partnerParent.fkProperty}); Map params = new LinkedHashMap<>(); params.put("parent", ${partnerParent.fkProperty}); - List<${name}Entity> result = repository.query("from ${name}Entity e where e.${partnerParent.fkProperty} = :parent", params); + List<${name}Entity> result = repository.query("from ${javaGenFolderName}_${name}Entity e where e.${partnerParent.fkProperty} = :parent", params); result.forEach(this::scrub); return result; } diff --git a/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/reportFileEntity.java.template b/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/reportFileEntity.java.template index 76b2bf93c4f..7ec4aa28a3d 100644 --- a/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/reportFileEntity.java.template +++ b/components/template/template-application-rest-java/src/main/resources/META-INF/dirigible/template-application-rest-java/api/reportFileEntity.java.template @@ -11,6 +11,7 @@ import org.eclipse.dirigible.components.api.security.UserFacade; #end import org.eclipse.dirigible.sdk.platform.Documentation; import org.eclipse.dirigible.sdk.http.Body; +import org.eclipse.dirigible.sdk.component.Component; import org.eclipse.dirigible.sdk.http.Controller; import org.eclipse.dirigible.sdk.http.Get; import org.eclipse.dirigible.sdk.http.Post; @@ -23,6 +24,7 @@ import java.util.List; import java.util.Map; @Controller +@Component("${javaGenFolderName}_${name}Controller") @Documentation("${projectName} - ${name} Report Controller") public class ${name}Controller { diff --git a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/document/document-page.js.template b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/document/document-page.js.template index a5c5c95a02a..b234d45ef90 100644 --- a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/document/document-page.js.template +++ b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/document/document-page.js.template @@ -242,12 +242,12 @@ document.addEventListener('alpine:init', () => { this.printError = null; this.printBusy = true; try { - // The generated print feeder (gen/events/${name}PrintFeeder) loads the document + its related + // The generated print feeder (gen/events/${javaGenFolderName}/${name}PrintFeeder) loads the document + its related // graph through the repositories and returns the { document, items } payload the template binds // - so {{document..}} resolves. It is the auditable source of what a print // receives; the browser calls it as the logged-in user (auth + tenant + i18n are the caller's). const payload = await App.services.api.get( - '/services/java/${projectName}/gen/events/${name}PrintFeeder/' + encodeURIComponent(this.id), { baseUrl: '' }); + '/services/java/${projectName}/gen/events/${javaGenFolderName}/${name}PrintFeeder/' + encodeURIComponent(this.id), { baseUrl: '' }); const response = await fetch('/services/print/${name}?lang=' + encodeURIComponent(lang), { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/manage/form-page.js.template b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/manage/form-page.js.template index 98e8f75b402..d1d08f58524 100644 --- a/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/manage/form-page.js.template +++ b/components/template/template-application-ui-harmonia-java/src/main/resources/META-INF/dirigible/template-application-ui-harmonia-java/ui/perspective/manage/form-page.js.template @@ -542,12 +542,12 @@ document.addEventListener('alpine:init', () => { this.printError = null; this.printBusy = true; try { - // The generated print feeder (gen/events/${name}PrintFeeder) loads the document + its related + // The generated print feeder (gen/events/${javaGenFolderName}/${name}PrintFeeder) loads the document + its related // graph through the repositories and returns the { document, items } payload the template binds // - so {{document..}} resolves. It is called as the logged-in user (auth + // tenant + i18n are the caller's). const payload = await App.services.api.get( - '/services/java/${projectName}/gen/events/${name}PrintFeeder/' + encodeURIComponent(this.id), { baseUrl: '' }); + '/services/java/${projectName}/gen/events/${javaGenFolderName}/${name}PrintFeeder/' + encodeURIComponent(this.id), { baseUrl: '' }); const response = await fetch('/services/print/${name}?lang=' + encodeURIComponent(lang), { method: 'POST', headers: { 'Content-Type': 'application/json' }, diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentCrossModuleCollisionIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentCrossModuleCollisionIT.java new file mode 100644 index 00000000000..b31e0e56876 --- /dev/null +++ b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentCrossModuleCollisionIT.java @@ -0,0 +1,235 @@ +/* + * Copyright (c) 2010-2026 Eclipse Dirigible contributors + * + * All rights reserved. This program and the accompanying materials are made available under the + * terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at + * http://www.eclipse.org/legal/epl-v20.html + * + * SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0 + */ +package org.eclipse.dirigible.integration.tests.api; + +import static io.restassured.RestAssured.given; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; + +import com.google.gson.Gson; + +import org.eclipse.dirigible.components.initializers.synchronizer.SynchronizationProcessor; +import org.eclipse.dirigible.repository.api.IRepository; +import org.eclipse.dirigible.repository.api.IRepositoryStructure; +import org.eclipse.dirigible.repository.api.IResource; +import org.eclipse.dirigible.tests.base.IntegrationTest; +import org.eclipse.dirigible.tests.framework.restassured.RestAssuredExecutor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; + +/** + * Cross-module collision coverage: two intent modules that author a SAME-NAMED entity + * ({@code Recurrence}) and a SAME-NAMED reaction (the {@code cancelRecurrence} transition) must + * deploy together on one instance with both modules fully live. + * + *

+ * This is exactly the shape that used to fail with the shared {@code gen.events} package and + * simple-class-name keying: the two {@code CancelRecurrenceTransition} classes collided by FQN + * (registry-wide client-Java compilation refused the duplicate), and the two + * {@code RecurrenceController}/{@code RecurrenceRepository} beans collided by bean name (the + * container keeps the first and drops the second silently), as did the Hibernate entity name in the + * shared SessionFactory. The fix namespaces the events package per module + * ({@code gen.events.}) and module-qualifies the generated bean/entity names. + * + *

+ * Per the emission contract (kf-catalog §9a.7) the assertions target the OUTERMOST observable + * layer: both entity controllers answer over REST and both generated transition endpoints flip the + * status - not merely that the files were emitted. + */ +class IntentCrossModuleCollisionIT extends IntegrationTest { + + private static final String WORKSPACE = "workspace"; + private static final String PROJECT_A = "collide-a"; + private static final String PROJECT_B = "collide-b"; + + /** The same application shape authored twice under different module names. */ + private static String intentYaml(String moduleName) { + return """ + name: %s + description: cross-module collision fixture - same-named entity and reaction in two modules + + entities: + - name: RecurrenceStatus + kind: setting + fields: + - { name: id, type: integer, primaryKey: true, generated: true } + - { name: name, type: string, required: true, length: 100 } + + - name: Recurrence + fields: + - { name: id, type: integer, primaryKey: true, generated: true } + - { name: name, type: string, required: true, length: 100 } + relations: + - { name: Status, kind: manyToOne, to: RecurrenceStatus, function: EntityStatus, init: 1 } + + transitions: + - { name: cancelRecurrence, forEntity: Recurrence, from: [1], setStatus: 2 } + + seeds: + - name: statuses + entity: RecurrenceStatus + rows: + - { id: 1, name: Active } + - { id: 2, name: Cancelled } + """.formatted(moduleName); + } + + @Autowired + private IRepository repository; + @Autowired + private RestAssuredExecutor restAssuredExecutor; + @Autowired + private SynchronizationProcessor synchronizationProcessor; + + @Test + void two_modules_with_same_named_entity_and_reaction_deploy_together_without_collisions() { + generateProject(PROJECT_A); + generateProject(PROJECT_B); + + assertNamespacedEmission(PROJECT_A, "collide_a"); + assertNamespacedEmission(PROJECT_B, "collide_b"); + + publishProject(PROJECT_A); + publishProject(PROJECT_B); + synchronizationProcessor.forceProcessSynchronizers(); + + assertModuleLive(PROJECT_A, "collide_a"); + assertModuleLive(PROJECT_B, "collide_b"); + } + + /** Layer 1: the generated sources carry the module-scoped package and qualified names. */ + private void assertNamespacedEmission(String project, String module) { + String transition = contentOf(project, "gen/events/" + module + "/CancelRecurrenceTransition.java"); + assertTrue(transition.contains("package gen.events." + module + ";"), + "the transition handler must land in the module-scoped events package"); + assertTrue(transition.contains("@Component(\"" + module + "_CancelRecurrenceTransition\")"), + "the transition handler bean name must be module-qualified"); + + String entity = contentOf(project, "gen/" + module + "/data/recurrence/RecurrenceEntity.java"); + assertTrue(entity.contains("@Entity(name = \"" + module + "_RecurrenceEntity\")"), + "the Hibernate entity name must be module-qualified (one shared SessionFactory)"); + + String repositoryClass = contentOf(project, "gen/" + module + "/data/recurrence/RecurrenceRepository.java"); + assertTrue(repositoryClass.contains("@Component(\"" + module + "_RecurrenceRepository\")"), + "the repository bean name must be module-qualified"); + + String controller = contentOf(project, "gen/" + module + "/api/recurrence/RecurrenceController.java"); + assertTrue(controller.contains("@Component(\"" + module + "_RecurrenceController\")"), + "the controller bean name must be module-qualified"); + } + + /** + * Layer 2: the module actually WORKS on the running instance - the entity controller answers (bean + * + Hibernate registration survived the co-deploy) and the generated transition endpoint flips the + * status (the events handler class registered under its distinct FQN). + */ + private void assertModuleLive(String project, String module) { + String api = "/services/java/" + project + "/gen/" + module + "/api"; + AtomicInteger id = new AtomicInteger(); + restAssuredExecutor.execute(() -> id.set(given().contentType("application/json") + .body("{\"Name\":\"row of " + project + "\"}") + .when() + .post(api + "/recurrence/RecurrenceController") + .then() + .statusCode(200) + .extract() + .path("Id")), + 60); + restAssuredExecutor.execute(() -> given().when() + .get(api + "/recurrence/RecurrenceController/" + id.get()) + .then() + .statusCode(200) + .body("Name", equalTo("row of " + project)) + .body("Status", equalTo(1))); + restAssuredExecutor.execute(() -> given().contentType("application/json") + .body("{\"id\":" + id.get() + "}") + .when() + .post("/services/java/" + project + "/gen/events/" + module + + "/CancelRecurrenceTransition/run") + .then() + .statusCode(200) + .body("Status", equalTo(2))); + } + + /** Write the intent and drive model-to-code from the generate response's own plan. */ + private void generateProject(String project) { + writeIntent(project, intentYaml(project)); + AtomicReference>> plan = new AtomicReference<>(); + restAssuredExecutor.execute(() -> plan.set(given().when() + .post("/services/ide/intent/generate?workspace=" + WORKSPACE + "&project=" + + project + "&path=app.intent") + .then() + .statusCode(200) + .extract() + .jsonPath() + .getList("codeGenerations"))); + for (Map codeGeneration : plan.get()) { + String template = String.valueOf(codeGeneration.get("templateId")); + String modelPath = String.valueOf(codeGeneration.get("path")); + String parameters = new Gson().toJson(codeGeneration.get("parameters")); + String payload = "{\"template\":\"" + template + "\",\"parameters\":" + parameters + "}"; + restAssuredExecutor.execute(() -> given().contentType("application/json") + .body(payload) + .when() + .post("/services/js/service-generate/generate.mjs/model/" + WORKSPACE + "/" + project + + "?path=" + modelPath) + .then() + .statusCode(201)); + } + } + + private void publishProject(String project) { + restAssuredExecutor.execute(() -> given().when() + .post("/services/ide/publisher/" + WORKSPACE + "/" + project + "/") + .then() + .statusCode(200)); + } + + private void writeIntent(String project, String yaml) { + String path = projectPath(project) + "/app.intent"; + IResource existing = repository.getResource(path); + if (existing.exists()) { + existing.setContent(yaml.getBytes(StandardCharsets.UTF_8)); + } else { + repository.createResource(path, yaml.getBytes(StandardCharsets.UTF_8)); + } + } + + private String contentOf(String project, String fileName) { + return new String(repository.getResource(projectPath(project) + "/" + fileName) + .getContent(), + StandardCharsets.UTF_8); + } + + private static String projectPath(String project) { + return IRepositoryStructure.PATH_USERS + "/admin/" + WORKSPACE + "/" + project; + } + + @AfterEach + void cleanup() { + for (String project : List.of(PROJECT_A, PROJECT_B)) { + restAssuredExecutor.execute(() -> given().when() + .delete("/services/ide/publisher/" + WORKSPACE + "/" + project) + .then() + .statusCode(greaterThanOrEqualTo(200))); + if (repository.hasCollection(projectPath(project))) { + repository.removeCollection(projectPath(project)); + } + } + } +} diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java index 0a370ed53a5..64a2f20eef5 100644 --- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java +++ b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEmissionCoverageIT.java @@ -566,7 +566,7 @@ private void assertEmission() { String bpmn = contentOf("ClaimConfirm.bpmn"); assertTrue(bpmn.contains("flowable:assignee=\"${__personalUser}\""), "assignee: personal must emit a per-user flowable:assignee, not a candidate group"); - String claimTrigger = contentOf("gen/events/ClaimConfirmTrigger.java"); + String claimTrigger = contentOf("gen/events/emission/ClaimConfirmTrigger.java"); assertTrue(claimTrigger.contains("__personalUser"), "the trigger listener must seed the __personalUser variable from the identity mapping"); @@ -582,12 +582,12 @@ private void assertEmission() { rfqBpmn.contains("") && rfqBpmn.contains("${__reviewExpireDate}"), "expire must emit a cancelling boundary timer armed from the loader variable"); - String waitHandler = contentOf("gen/events/RfqFlowAwaitReplyWait.java"); + String waitHandler = contentOf("gen/events/emission/RfqFlowAwaitReplyWait.java"); assertTrue(waitHandler.contains("Process.correlateMessageEvent(carrier.ProcessId, \"RfqFlowAwaitReply\""), "the wait listener must correlate the message on the stamped ProcessId"); assertTrue(waitHandler.contains("new RfqRepository().findById(entity.Rfq)"), "the wait listener must resolve the parked record through the via back-reference"); - String timerLoader = contentOf("gen/events/LoadRfqFlowReviewExpire.java"); + String timerLoader = contentOf("gen/events/emission/LoadRfqFlowReviewExpire.java"); assertTrue(timerLoader.contains("execution.setVariable(\"__reviewExpireDate\", due)"), "the expire date loader must publish the variable the boundary timer arms from"); @@ -597,7 +597,7 @@ private void assertEmission() { approvalBpmn.contains(""), "abortOn must emit an interrupting, terminating event subprocess"); - String abortHandler = contentOf("gen/events/ApprovalFlowAbort.java"); + String abortHandler = contentOf("gen/events/emission/ApprovalFlowAbort.java"); assertTrue( abortHandler.contains("-transitioned") && abortHandler.contains("entity.Status == 3") && abortHandler.contains("Process.correlateMessageEvent(entity.ProcessId, \"ApprovalFlowAbort\""), @@ -662,7 +662,7 @@ private void assertEmission() { "the partner list toolbar must carry the Export and Print actions"); // collection-driven generation: the job creates the parent AND its per-working-day children. - String job = contentOf("gen/events/MonthlyClaimsJob.java"); + String job = contentOf("gen/events/emission/MonthlyClaimsJob.java"); assertTrue(job.contains("savedTarget"), "the scheduled generation must save the parent and keep its id for the children"); assertTrue(job.contains("getDayOfWeek"), "a days child must iterate the working days of the month"); assertTrue(job.contains("ClaimLineRepository"), "the child rows must be saved through the child's repository"); @@ -744,7 +744,7 @@ private void assertEmission() { // transitions: the server half is a controller that guards the source status + the when // guard (409) and flips ONLY the status column via the targeted updateProperty; the client // half is a custom-action contribution carrying the endpoint. - String transition = contentOf("gen/events/CancelEntryTransition.java"); + String transition = contentOf("gen/events/emission/CancelEntryTransition.java"); assertTrue(transition.contains("currentStatus == 1"), "transitions must emit the allowed-statuses guard"); assertTrue(transition.contains("Calc.eval(\"Paid\", source, 6)"), "the when guard must emit a Calc comparison"); assertTrue(transition.contains("Response.setStatus(409)"), "a failed guard must surface as 409"); @@ -757,12 +757,12 @@ private void assertEmission() { // postings reverses: the reversal handler negates the sibling's amount expressions on the // SAME side, locates the original through the empty storno link (fail-soft skip when none) // and stamps the link; the sibling's idempotency guard symmetrically excludes linked rows. - String stornoPosting = contentOf("gen/events/DocStornoPosting.java"); + String stornoPosting = contentOf("gen/events/emission/DocStornoPosting.java"); assertTrue(stornoPosting.contains("Calc.eval(\"-(Amount)\", source, 2)"), "the reversal must negate the sibling's amount expression on the same side"); assertTrue(stornoPosting.contains("nothing to reverse"), "the reversal must skip fail-soft when the source was never posted"); assertTrue(stornoPosting.contains("target.Storno = original.Id;"), "the reversal must stamp the storno link to the original"); - String basePosting = contentOf("gen/events/DocPostingPosting.java"); + String basePosting = contentOf("gen/events/emission/DocPostingPosting.java"); assertTrue(basePosting.contains("candidate.Storno == null"), "the reversed posting's idempotency guard must exclude reversal rows"); // label: the repository recomputes the stored display Name on every write path. @@ -894,7 +894,7 @@ private void assertRuntimeEnforcement() { .statusCode(409)); // transitions: a fresh DRAFT entry cancels (200, status CANCELLED)... - String transitionRun = "/services/java/" + PROJECT + "/gen/events/CancelEntryTransition/run"; + String transitionRun = "/services/java/" + PROJECT + "/gen/events/emission/CancelEntryTransition/run"; AtomicInteger cancellable = new AtomicInteger(); restAssuredExecutor.execute(() -> cancellable.set(given().contentType("application/json") .body("{\"Date\":\"2026-01-16\",\"Account\":2}") @@ -953,7 +953,7 @@ private void assertRuntimeEnforcement() { restAssuredExecutor.execute(() -> given().contentType("application/json") .body("{\"id\":" + doc.get() + "}") .when() - .post("/services/java/" + PROJECT + "/gen/events/PostDocTransition/run") + .post("/services/java/" + PROJECT + "/gen/events/emission/PostDocTransition/run") .then() .statusCode(200)); AtomicInteger originalEntry = new AtomicInteger(); @@ -971,7 +971,7 @@ private void assertRuntimeEnforcement() { restAssuredExecutor.execute(() -> given().contentType("application/json") .body("{\"id\":" + doc.get() + "}") .when() - .post("/services/java/" + PROJECT + "/gen/events/VoidDocTransition/run") + .post("/services/java/" + PROJECT + "/gen/events/emission/VoidDocTransition/run") .then() .statusCode(200)); AtomicInteger reversalEntry = new AtomicInteger(); @@ -1286,7 +1286,7 @@ private void assertAbortOnRuntime() { restAssuredExecutor.execute(() -> given().contentType("application/json") .body("{\"id\":" + approval.get() + "}") .when() - .post("/services/java/" + PROJECT + "/gen/events/CancelApprovalTransition/run") + .post("/services/java/" + PROJECT + "/gen/events/emission/CancelApprovalTransition/run") .then() .statusCode(200)); restAssuredExecutor.execute(() -> given().when() diff --git a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEngineIT.java b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEngineIT.java index 9e3ce14e1a3..e9ea95ccc79 100644 --- a/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEngineIT.java +++ b/tests/tests-integrations/src/main/java/org/eclipse/dirigible/integration/tests/api/IntentEngineIT.java @@ -416,7 +416,7 @@ void glue_template_generates_the_trigger_and_resolver_handlers() { // this exercises the generateUtils.js "triggers" + "resolvers" collection cases end to end. generateFromModel("template-application-events-java/template/template.js", "orders.glue"); - String handler = contentOf("gen/events/OrderApprovalTrigger.java"); + String handler = contentOf("gen/events/orders/OrderApprovalTrigger.java"); assertTrue(handler.contains("class OrderApprovalTrigger"), "the glue template should generate a handler class named after the process"); assertTrue(handler.contains("implements MessageHandler"), "the trigger should be a self-describing MessageHandler"); @@ -432,7 +432,7 @@ void glue_template_generates_the_trigger_and_resolver_handlers() { // The decision resolver (customer.creditLimit) is a JavaDelegate that loads Customer and sets // the variable the rewritten condition tests. - String resolver = contentOf("gen/events/ResolveCustomerCreditLimit.java"); + String resolver = contentOf("gen/events/orders/ResolveCustomerCreditLimit.java"); assertTrue(resolver.contains("class ResolveCustomerCreditLimit implements JavaDelegate"), "the resolver should be a Flowable JavaDelegate"); assertTrue(resolver.contains("import gen.orders.data.customer.CustomerRepository"), @@ -449,7 +449,7 @@ void glue_template_generates_the_trigger_and_resolver_handlers() { // The form-only relation.field (customer.name on the ApproveOrder form) produces its own resolver // even though no decision references it - the user-task form is a resolver trigger in its own // right. - String formResolver = contentOf("gen/events/ResolveCustomerName.java"); + String formResolver = contentOf("gen/events/orders/ResolveCustomerName.java"); assertTrue(formResolver.contains("class ResolveCustomerName implements JavaDelegate"), "a relation.field referenced only by a user-task form should still generate a resolver"); assertTrue(formResolver.contains("execution.setVariable(\"customer_name\"") && formResolver.contains("entity.Name"), @@ -458,7 +458,7 @@ void glue_template_generates_the_trigger_and_resolver_handlers() { // The notification (onUpdate: Order) is a self-describing @Component MessageHandler that sends mail // when an Order is updated - // exercises the generateUtils.js "notifications" collection case end to end. - String notification = contentOf("gen/events/OrderUpdatedNotification.java"); + String notification = contentOf("gen/events/orders/OrderUpdatedNotification.java"); assertTrue(notification.contains("class OrderUpdatedNotification implements MessageHandler"), "the notification should be a message-handling listener (PascalCased class name)"); assertTrue(notification.contains("@Component") && notification.contains("return \"intent-test-Order-Order-updated\""), @@ -483,7 +483,7 @@ void glue_template_generates_the_trigger_and_resolver_handlers() { // The schedule is a self-describing @Component JobHandler (cron()) that queries via a typed // Criteria and notifies per row. - String job = contentOf("gen/events/StaleOrdersJob.java"); + String job = contentOf("gen/events/orders/StaleOrdersJob.java"); assertTrue( job.contains("@Component") && job.contains("class StaleOrdersJob implements JobHandler") && job.contains("return \"0 0 9 * * ?\""), @@ -499,7 +499,7 @@ void glue_template_generates_the_trigger_and_resolver_handlers() { // The integration is a self-describing @Component MessageHandler that forwards the entity JSON to // an external endpoint. - String integration = contentOf("gen/events/PushOrderToWarehouseIntegration.java"); + String integration = contentOf("gen/events/orders/PushOrderToWarehouseIntegration.java"); assertTrue(integration.contains("class PushOrderToWarehouseIntegration implements MessageHandler"), "the integration should be a message-handling listener"); assertTrue(integration.contains("@Component") && integration.contains("return \"intent-test-Order-Order\""), @@ -512,7 +512,7 @@ void glue_template_generates_the_trigger_and_resolver_handlers() { "a POST integration should forward the entity JSON as the request body"); // The inbound webhook is a @Controller that ingests a posted JSON payload as the entity. - String webhook = contentOf("gen/events/IngestOrderWebhook.java"); + String webhook = contentOf("gen/events/orders/IngestOrderWebhook.java"); assertTrue(webhook.contains("@Controller") && webhook.contains("class IngestOrderWebhook"), "the inbound webhook should be a @Controller"); assertTrue(webhook.contains("@Post(\"/ingest\")"), "the webhook should expose the declared path"); @@ -526,13 +526,13 @@ void glue_template_generates_the_trigger_and_resolver_handlers() { // Together with the assertions above, this proves the full declarative-glue catalog - triggers, // resolvers, notifications, schedules, integrations, inbound webhooks and rollups - is generated // from a single app.intent. - String rollupCreate = contentOf("gen/events/OrderCustomerRollupOnCreate.java"); + String rollupCreate = contentOf("gen/events/orders/OrderCustomerRollupOnCreate.java"); assertTrue( rollupCreate.contains("@Component") && rollupCreate.contains("return \"intent-test-Order-Order\"") && rollupCreate.contains("new OrderRepository().findAll(Criteria.create().eq(\"Customer\", entity.Customer))") && rollupCreate.contains("int count = rows.size();") && rollupCreate.contains("parent.OrderCount = count"), "the rollup create-listener should recompute the parent count via Criteria"); - assertTrue(contentOf("gen/events/OrderCustomerRollupOnDelete.java").contains("intent-test-Order-Order-deleted"), + assertTrue(contentOf("gen/events/orders/OrderCustomerRollupOnDelete.java").contains("intent-test-Order-Order-deleted"), "the rollup delete-listener should bind the child's -deleted topic"); // The print feeder (Order is a document master via the OrderItem composition child): a @Controller @@ -540,7 +540,7 @@ void glue_template_generates_the_trigger_and_resolver_handlers() { // { document, items } payload the .print template binds - exercises the generateUtils.js // "printFeeders" collection case end to end. This class IS the audit of what a print receives. assertTrue(contentOf("orders.glue").contains("\"printFeeders\""), "the glue should carry a printFeeders collection"); - String feeder = contentOf("gen/events/OrderPrintFeeder.java"); + String feeder = contentOf("gen/events/orders/OrderPrintFeeder.java"); assertTrue(feeder.contains("@Controller") && feeder.contains("class OrderPrintFeeder") && feeder.contains("@Get(\"/{id}\")"), "the feeder should be a @Controller exposing GET /{id}"); assertTrue(feeder.contains("new gen.orders.data.order.OrderRepository().findById(id)"), @@ -597,9 +597,9 @@ void set_field_glue_sets_entity_status_on_approve_reject_branches() { // BPMN: each setField serviceTask binds the generated JavaDelegate (NOT a custom/ stub), the // decision routes to both branches, and `next: done` makes activate skip past reject to the end. String bpmn = contentOf("MemberApproval.bpmn"); - assertTrue(bpmn.contains("