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: 2 additions & 0 deletions .claude/docs/intent-layer.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ A single `app.intent` YAML file at a project root is the source of truth one alt

**...and a field against a LITERAL (`checks: compare` with `value:`, [#7338](https://github.com/eclipse-dirigible/dirigible/issues/7338)):** all five check kinds related two things the model already NAMED - two fields of a row, two item sums, an item count - so the commonest validation in a business model had no declaration at all: `VacationDay.Days > 0` (a negative row silently inflates the parent entitlement, because the roll-up sums the column verbatim), a quantity `>= 0`, a percentage `<= 100`. The three workarounds in the fleet were each worse than the gap: a hand-edit of the generated controller's `validate()` (dropped by the next regeneration, silently), a `calculatedActionOnCreate` that throws (a calculation, not a refusal, firing only on the field that declares it and reaching the caller as whatever the action's exception carries), or not enforcing it at all. `- { kind: compare, field: days, op: gt, value: 0, message: ... }` reuses `compare` and its whole implementation: `value:` and `than:` are mutually exclusive and exactly one is required, since a comparison has one right-hand side. The literal is TYPED by the field it is compared with, by `CheckSupport.compareLiteral` - the one rule the parser refuses on and the generator renders with, so nothing is refused that would have generated and nothing generates that was not refused. A numeric field takes a number (compared by value through `BigDecimal`, exact across the widths); a temporal one takes a moment (`CURRENT_DATE` / `CURRENT_TIMESTAMP` / `NOW` with at most one signed ISO-8601 offset - the vocabulary `items: where:` already carries, resolved against the clock of the WRITE) or a quoted ISO-8601 date/instant, rendered in the shape the generated column actually carries (`LocalDate` for a `date`, `Instant` for a `timestamp`; a comparison across the two does not compile). An absent operand is not a violation, exactly as with `than:`. **The second half is the gate.** `compare` used to refuse a `status:`; it now takes the optional one `requiredWhen` has, and that is the routing: without a gate the rule holds on every user write (the three generated controllers, 400 with the authored message), with one it is the repository's and holds when the record is persisted CARRYING that status. "days > 0 before SUBMITTED" is the rule base-vacations actually needed and mis-authored as an `itemsMin`, which counted a child the approval delegate had not created yet and refused every submission in the field for three weeks.

**What a row IS when no single field says it (entity-level `unique:`, [#6793](https://github.com/eclipse-dirigible/dirigible/issues/6793)/[#6796](https://github.com/eclipse-dirigible/dirigible/issues/6796), cross-model since [#7116](https://github.com/eclipse-dirigible/dirigible/pull/7116)):** an entity declares its **composite business keys** - `unique: [{ fields: [ProjectTimesheet, Employee], message: "..." }]` - beside the field attribute `unique: true`, which can only ever say that ONE column is a key. Each member is an own field or an own **to-one** relation, which contributes its foreign-key column; a to-many and a `subset` are refused (neither has a column on this side to constrain - a subset column holds a normalized SET of the target's keys, not an identity), a single-name key is refused naming the field attribute it duplicates, and so are a repeated member and a repeated key. `EdmIntentGenerator.uniqueConstraint` emits one `.edm`/`.model` constraint per key: the name is `<Entity>_<Member>...` in PascalCase and each column is `upperSnake(entity)_upperSnake(member)` - **derived from the names, with no lookup of the member's target**, which is exactly why a **cross-model** to-one qualifies like a same-model one (#7092/#7116): the consumer stores the target's id in its own FK column and the projection entity is only the read-side copy that feeds the dropdown. That is the shape most transactional keys in a modular fleet have - `(projectMonth, Employee)`, `(payrollRun, Employee)`, `(Customer, period)` - where the master data is owned by another module by design. The constraint carries `properties` (the authored property names, for the `.edm` twin the modeler rebuilds the `.model` from, so a later `dataName` change follows the key) alongside the `columns` the schema template emits, and a `message` - the authored one, else a generated "A <entity> with the same <a>, <b> and <c> already exists". The key is created in the database AND recognised by the generated controller, so a colliding write is answered **409 with that message** instead of a driver-specific 500 (anchored to the innermost `SQLException`, [#7138](https://github.com/eclipse-dirigible/dirigible/issues/7138)). A partitioned document number (`number: { per: ... }`) synthesizes the `(partition, number)` key it means, unless the author already spelled it out in either column order.

**A form may show a field of its COUNTERPARTY ([#7093](https://github.com/eclipse-dirigible/dirigible/issues/7093)):** a task form's `fields` take one-hop `relation.field` paths, but the validator resolved the hop against LOCAL entities only - so the one relation a billing document's form most needs to read a field of, its counterparty, was the one it refused (`form [SendSalesInvoice] field [Customer.email] references unknown field [email] on [Customer]`), while the same path already resolved cross-model as a `notify` recipient and a `languageFrom`. A cross-model to-one is now resolved where every other cross-model reference is: at GENERATION, against the owner model's `.model`, which supplies the perspective the generated resolver's imports name and the key type behind its `Number` accessor - the delegate loads the OWNER's `gen.<model>.data.<perspective>` Entity/Repository, the registry-wide-compile mechanism a notify relation load already uses, and the control renders read-only like a local hop. A field the owner model does not declare is a **422** rather than a skipped resolver: skipping it would leave the BPMN with a service task pointing at a handler nothing generated and the control bound to a variable nothing ever sets. The same path in a `decision` condition comes with it, being one resolver.

**Deleting a header deletes the lines it owns (`whenMasterDeleted:`, [#7100](https://github.com/eclipse-dirigible/dirigible/issues/7100)):** a deleted master left its composition children behind - rows pointing at an id that no longer exists, invisible in the UI (no parent page renders them) and still counted by every report and roll-up over the child, so a deleted vacation request's five days kept the entitlement EXHAUSTED. The cascade is now emitted for EVERY composition master, because it is what composition MEANS: the master's generated repository deletes the children at the head of `delete`/`deleteById`, in the same transaction and through each child's OWN repository, so the child's `-deleted` event (hence the roll-up relinquishing), its history trail and its own cascade all run - a deep chain unwinds level by level. The reverse index this needs (`CompositionChildren` in `ide-template`) is DERIVED from the child's `masterEntity`/`masterEntityId`, so a hand-authored `.edm` gets it too. The author's alternative is `whenMasterDeleted: refuse` on the child's composition relation - the same method rejects the master's delete while any child exists, naming both entities - which is refused at parse on a non-composition and on a SECOND composition (the EDM emits that one as a plain association, so the key would ask for a cascade nothing would run). `cascade` is the default and emits no `.edm` attribute, so an untouched model is byte-identical. This is the data-side half of the process-side `whenDeleted: abort | refuse` (#7074).
Expand Down
Loading
Loading