Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude/docs/client-java.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Client `.java` under `/registry/public/<project>/...` is synchronized by `JavaSy
- All client annotations/facades live in `org.eclipse.dirigible.sdk.*` (`api-modules-java`), not the old `engine.java.annotations.*`. Compile **and** bean-wiring errors surface in the IDE Problems view.
- **Manage entities ONLY through their generated `<Entity>Repository` — never the generic `Store`/`Database` for entity CRUD.** The generated `@Repository extends JavaRepository<T>` is the sole sanctioned load/save/update/delete path; it carries validations, **event publishing** (create/`-updated`/`-deleted` topics that intent triggers/reactions/rollups/notifications consume — recorded in the tenant's `DIRIGIBLE_EVENT_OUTBOX` inside the write's own transaction, so the row and its event commit together and a broker outage neither loses the event nor fails the write; `EventOutboxRelayJob` drains what the in-process publish could not deliver), and — for `multilingual: true` entities — the **read-time translation overlay** (every find translates string properties from the sibling `<TABLE>_LANG` table for the caller's `Accept-Language`, via the SDK `org.eclipse.dirigible.sdk.db.Translator`). The name-keyed `org.eclipse.dirigible.sdk.db.Store` and raw `Database` SQL bypass all of that silently and must not touch a managed entity. (`updateWithoutEvent` is fine — a deliberate repository method that keeps validations/i18n and only omits the event, for workflow-driven system writes.) So a reusable delegate/service that must touch a *specific* entity lives **in that entity's project** (importing its repository); only entity-agnostic helpers belong in a shared project. See the engine-java guide.

- **Several writes that only make sense together are ONE transaction.** Every store call is otherwise its own transaction, so a multi-write operation that fails halfway leaves the earlier writes behind - an intent create-from committed the invoice header and the source's INVOICED flip and then failed on a line (#7069). `org.eclipse.dirigible.components.data.store.java.repository.UnitOfWork.call(() -> { ... })` runs the block on one session and one transaction (thread-bound, so every repository joins it; nested blocks defer to the outermost), reads see the block's own writes, and the outbox events dispatch only once the whole unit committed - which is why an announcement about the unit's outcome belongs INSIDE the block, recorded through the write it is about (the create-from's `-transitioned` rides its source's status flip): the unit's commit is what makes it true, and the outbox only hands out what committed, so a crash after the commit no longer loses the event (#7160). The `History` trail and document numbering deliberately stay outside. Alongside it, a generated repository now refuses a write that leaves a defaultless NOT NULL column empty with a `ValidationException` naming the property (a 400), instead of letting the statement come back as a driver-specific constraint violation; a column carrying a DEFAULT is exempt, the database supplying its value.
- **Several writes that only make sense together are ONE transaction.** Every store call is otherwise its own transaction, so a multi-write operation that fails halfway leaves the earlier writes behind - an intent create-from committed the invoice header and the source's INVOICED flip and then failed on a line (#7069). `org.eclipse.dirigible.components.data.store.java.repository.UnitOfWork.call(() -> { ... })` runs the block on one session and one transaction (thread-bound, so every repository joins it; nested blocks defer to the outermost), reads see the block's own writes, and the outbox events dispatch only once the whole unit committed - which is why an announcement about the unit's outcome belongs INSIDE the block, recorded through the write it is about (the create-from's `-transitioned` rides its source's status flip): the unit's commit is what makes it true, and the outbox only hands out what committed, so a crash after the commit no longer loses the event (#7160). The `History` trail and document numbering deliberately stay outside - which is why anything that can refuse a create (a required input, a from-status guard, a create-from's source-row rule) is decided BEFORE the target's header is saved: a refusal after the save takes the row back but not the number it spent or the trail row it wrote (#7224). Alongside it, a generated repository now refuses a write that leaves a defaultless NOT NULL column empty with a `ValidationException` naming the property (a 400), instead of letting the statement come back as a driver-specific constraint violation; a column carrying a DEFAULT is exempt, the database supplying its value.

**Detailed guide:** [`components/engine/engine-java/CLAUDE.md`](components/engine/engine-java/CLAUDE.md). Read it before changing anything under `engine-java`, `data-store-java`, the `sdk.*` annotations, or the `*-java` templates — it covers the container, the consumers, the two handler styles + no-mixing rule, the `JavaHandler`-as-bean path, controller routing / OpenAPI / `@Roles`, `data-store-java` dynamic-map persistence, error surfacing, the **removed** internals (`RepositoryRegistry` / `RepositoryClassConsumer` / `DependencyResolver` / reflective fallback / `@Extension`), and the three-repo (platform + `dirigiblelabs/sample-java-*` + docs) sequencing.

2 changes: 1 addition & 1 deletion .claude/docs/intent-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ A single `app.intent` YAML file at a project root is the source of truth one alt

**The enrichment channel (`phases:` + `onPhase`, [#6929](https://github.com/eclipse-dirigible/dirigible/issues/6929)):** a value a listener computes AFTER the insert — a moving-average cost, a snapshot column, an external lookup — must be written back **event-silently** or it re-fires every onUpdate consumer of a change the user never made; so it published nothing at all, and a declarative consumer of that value had no moment to bind. Bound to `onCreate` it RACED the enrichment (two listeners on one topic have no order — each `MessageHandler` is its own durable subscriber, and there is no priority anywhere), and posted a balanced-looking journal entry for a null amount with parse, generation, compile and publish all green. The fix is a CHANNEL, not an ordering contract the broker cannot keep: an entity declares the moments it announces (`phases: [costed]`), the Java DAO template emits one **`announce<Phase>(id, values)`** per phase — `updateProperties` with the phase's own topic, so the enrichment and its notice ride ONE write into the outbox and commit together — and any glue consumer binds `event: { onPhase: <Entity>, phase: <name> }`. The generated method is the point: a hand-typed topic string reproduces exactly the silence being removed, a mistyped `announceCosted` is a compile error. Accepted by `postings:` (the driver), `notifications:`, `integrations:`, `outbound:` and an event-driven `generates:`, with the `when:` guard optional there (the phase already IS one moment); deliberately not by a process `trigger:`, a `wait` or `resolves:`. Refused at parse, each because it is otherwise silent: a phase that is not a lower-camel identifier, one named after a platform channel (`updated`/`deleted`/`transitioned`/`rekeyed`), a duplicate, a `phase:` key on another axis, and a binding naming a phase the entity does not declare. Details in the engine-intent guide's phases bullet.

**Which source rows become lines (`items: where:` + `refuse:`, [#7091](https://github.com/eclipse-dirigible/dirigible/issues/7091)):** a create-from's mirror `items:` block cloned EVERY row of the source document into a target line and the DSL could not say which rows qualified, so base-timesheets billed every member timesheet of the project-month - a DRAFT / REJECTED one at the same footing as an APPROVED one, and an EMPTY one (whose mapped quantity the target refuses) stopped the whole Generate until someone deleted the row by hand. "Invoice the approved month" is the one flow a billing clerk runs, and the module could either bill unapproved hours or not bill at all; the gap is fleet-wide (proforma -> invoice, quotation -> order, order -> invoice). `where:` is the rule - the same `{ field, op, value }` triples a `schedules[].where` carries, incl. a moment value resolved against the clock of the run - pushed into the very `Criteria` that already selects the source's rows by their master foreign key, so an unqualified row is never loaded; a condition naming the source ITEM's own `function: EntityStatus` relation may use the seeded status name (on the item's nomenclature, never the header's) - as, since [#7251](https://github.com/eclipse-dirigible/dirigible/issues/7251), does the `schedules[].where` row query this shape was modelled on, where a name generated `.eq("Status", "OVERDUE")` into the job and matched nothing forever. `refuse:` declares the other reading: an unqualified row stops the whole run with the authored message plus the KEYS of the offending rows, instead of being left out - dropping a rejected line silently and billing it silently are both wrong for different months, so skipping is the default and `refuse:` without a `where:` is refused at parse. **A rule that qualifies no row refuses too**, rather than committing a header with no lines - the harder failure to notice, the document existing and counting as the period's billing. Scoped to a `where`-declaring block, so a rule-less items block is byte-identical; the rule's `field` is checked against the item source's own properties at parse, unlike a schedule's query, whose source may be a cross-model row.
**Which source rows become lines (`items: where:` + `refuse:`, [#7091](https://github.com/eclipse-dirigible/dirigible/issues/7091)):** a create-from's mirror `items:` block cloned EVERY row of the source document into a target line and the DSL could not say which rows qualified, so base-timesheets billed every member timesheet of the project-month - a DRAFT / REJECTED one at the same footing as an APPROVED one, and an EMPTY one (whose mapped quantity the target refuses) stopped the whole Generate until someone deleted the row by hand. "Invoice the approved month" is the one flow a billing clerk runs, and the module could either bill unapproved hours or not bill at all; the gap is fleet-wide (proforma -> invoice, quotation -> order, order -> invoice). `where:` is the rule - the same `{ field, op, value }` triples a `schedules[].where` carries, incl. a moment value resolved against the clock of the run - pushed into the very `Criteria` that already selects the source's rows by their master foreign key, so an unqualified row is never loaded; a condition naming the source ITEM's own `function: EntityStatus` relation may use the seeded status name (on the item's nomenclature, never the header's) - as, since [#7251](https://github.com/eclipse-dirigible/dirigible/issues/7251), does the `schedules[].where` row query this shape was modelled on, where a name generated `.eq("Status", "OVERDUE")` into the job and matched nothing forever. `refuse:` declares the other reading: an unqualified row stops the whole run with the authored message plus the KEYS of the offending rows, instead of being left out - dropping a rejected line silently and billing it silently are both wrong for different months, so skipping is the default and `refuse:` without a `where:` is refused at parse. **A rule that qualifies no row refuses too**, rather than committing a header with no lines - the harder failure to notice, the document existing and counting as the period's billing. Scoped to a `where`-declaring block, so a rule-less items block is byte-identical; the rule's `field` is checked against the item source's own properties at parse, unlike a schedule's query, whose source may be a cross-model row. Both refusals are decided BEFORE the target header is saved ([#7224](https://github.com/eclipse-dirigible/dirigible/issues/7224)): the generated repository allocates the document number and writes the `History` create entry outside the unit of work, so a refusal fired after the save spent a number of a gap-free series and left a trail row for a document that never existed - once per click.

**A create-from is not offered twice (`fromStatus:`, [#7068](https://github.com/eclipse-dirigible/dirigible/issues/7068)):** a `generates:` with a `sourceStatus:` completion hook flipped its source once the target existed and then went on offering the same button on the flipped record - and answering the same endpoint 200 - so a second click minted a **second document**: a proforma already INVOICED produced a second invoice, in the customer's hands. The hook declared what "already done" looks like; nothing consulted it. A create-from now carries a from-status guard resolved ONCE and fed to both halves of the action: the generated `run()` refuses with **409** before anything is created, and the contributed action descriptor carries the same guard so the shared `customActions` store stops OFFERING the click on a record it would refuse (`getActions(view, type, record)` takes the record the view already has). Two shapes: `fromStatus: [...]` is the explicit allow-list - the `from:` of a `transitions:` entry, spelled differently only because `from:` on a create-from already names the source ENTITY - and absent it a declared `sourceStatus` IMPLIES the deny-list of exactly that status, so a model that already carries the defect is fixed with no authoring change. The guard is on the CLICK: an event-driven create-from keeps its own at-most-once back-reference guard and qualifies its moment with `event.when`, so `fromStatus` on an event-only rule is refused at parse rather than silently ignored - as are a `page` scope, a source with no `function: EntityStatus` relation, and an allow-list containing the `sourceStatus` the action itself writes.

Expand Down
Loading
Loading