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.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,8 @@ A single `app.intent` YAML file at a project root is the source of truth one alt

**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<E,T>`, invoked as `Beans.get(<class>.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 `<TABLE>_LANG` table (`GUID, Id, <PascalCase translatable columns>, 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).

**First-class document numbering (`number:` + the `.numbers` artefact, `engine-numbering`):** a string field may declare `number: { series: Sales Invoice, per: Company, stampOn: create|issue }` — the intent references a **series by name only**; the number's shape (literal prefix + sequence zero-padded to a total width, no token grammar) lives OUTSIDE the model: declared per module in an authored **`.numbers`** artefact (`{"series": [{"name": "Sales Invoice", "prefix": "SI", "size": 10}]}`, a requirement declaration like `.roles`) and configured per tenant in the application shell's Document Numbering settings (`/services/core/numbering`). One per-tenant table `DIRIGIBLE_DOCUMENT_NUMBERS` holds shape AND counter, one row per (series, partition); the synchronizer only INSERTs missing rows, Settings writes prefix/size/next, the allocator (`sdk.numbering.DocumentNumbers.next`) increments the counter — sequences are continuous, never auto-reset, and allocating an undeclared series fails loudly. `per:` partitions a series by a to-one relation's value (per company — two legal entities never share a counter). A differing cross-module re-declaration fails that artefact naming both modules; the removed `format`/`scope`/`resetOn` keys are rejected at parse. Details in the engine-intent guide's numbering bullet.

**The general platform line this enshrines:** authoring artifacts (`.edm`, `.model`, `.form`, `.report`, `.intent`) get **workspace editors + an explicit Generate**; only runtime artifacts (`.roles`, `.bpmn`, `.csvim`, `.table`, jobs, listeners, …) get **synchronizers**. Applying the synchronizer hammer to an authoring artifact generates into the registry where no modeler, Projects view, or template can use it — that mistake was made once and reverted; the inventory of synchronizers (grep `extends BaseSynchronizer`) deliberately contains no authoring formats.

## Harmonia runtime UI (`template-application-ui-harmonia-java` + `template-form-builder-harmonia`)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,52 +10,55 @@
package org.eclipse.dirigible.sdk.numbering;

import java.sql.SQLException;
import java.util.Map;

import org.eclipse.dirigible.components.engine.numbering.DocumentNumberService;
import org.eclipse.dirigible.sdk.component.Beans;

/**
* Client SDK for first-class document numbering: allocate the next gap-free number for a series and
* render it through the series' format. Backed by the platform's per-tenant counter store (the same
* store the application shell's Document Numbering settings manage), so hand-written
* {@code custom/} code and the generated stamping share one engine and one sequence.
* Allocates gap-free document numbers from a named series.
*
* <p>
* Example: {@code DocumentNumbers.next("SalesInvoice", "SI-{seq:07}", Map.of("year", "2026"))} →
* {@code SI-0000001} (then {@code SI-0000002}, …). The scope map both partitions the counter and
* feeds the format's {@code {year}} / {@code {<Field>}} tokens.
* The number's SHAPE is not passed here and is deliberately not knowable from application code: a
* series' prefix and total width are declared once in a module's {@code .numbers} artefact and are
* configurable per tenant afterwards, so one application serves jurisdictions with different
* numbering conventions without being forked or regenerated.
*
* <p>
* A series may be PARTITIONED - typically per company, because two legal entities in one tenant
* each owe their own sequential range. Pass the partition value (the {@code per} relation's id) and
* that partition's own sequence is used.
*
* <p>
* Example: {@code DocumentNumbers.next("Sales Invoice", String.valueOf(entity.Company))}.
*/
public final class DocumentNumbers {

private DocumentNumbers() {}

/**
* Allocate and format the next number for a series.
* Allocate the next number of an unpartitioned series.
*
* @param series the series identity (documents sharing a sequence pass the same series)
* @param format the format template ({@code {seq}} / {@code {seq:0N}} / {@code {series}} / scope
* tokens), or {@code null}/blank for the default {@code {series}-{seq:06}}
* @param scope the resolved scope values partitioning the counter (empty for an unscoped series)
* @return the formatted document number
* @param series the series identity
* @return the allocated number
*/
public static String next(String series, String format, Map<String, String> scope) {
try {
return Beans.get(DocumentNumberService.class)
.next(series, format, scope);
} catch (SQLException e) {
throw new IllegalStateException("Failed to allocate a document number for series [" + series + "]", e);
}
public static String next(String series) {
return next(series, null);
}

/**
* Allocate and format the next number for an unscoped series.
* Allocate the next number of a series within a partition.
*
* @param series the series identity
* @param format the format template (see {@link #next(String, String, Map)})
* @return the formatted document number
* @param partition the partition value (the {@code per} relation's id), or null when the series is
* not partitioned
* @return the allocated number
*/
public static String next(String series, String format) {
return next(series, format, Map.of());
public static String next(String series, String partition) {
try {
return Beans.get(DocumentNumberService.class)
.next(series, partition);
} catch (SQLException e) {
throw new IllegalStateException("Failed to allocate a document number for series [" + series + "]", e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ public interface SynchronizersOrder {
/** The access. */
int ACCESS = 40;

/**
* The number-series declaration ({@code .numbers}). Deliberately before every artefact type that
* could allocate a document number during synchronization (client Java components, BPMN, CSVIM), so
* a declared series is provisioned before the first allocation can ask for it.
*/
int NUMBER_SERIES = 45;

/** The job. */
int JOB = 50;

Expand Down
1 change: 1 addition & 0 deletions components/engine/engine-intent/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,7 @@ Semantics worth knowing:

- **`composition: true` on a to-one relation makes it a composition.** The owning entity becomes DEPENDENT (managed as details under its parent's perspective) and the FK is NOT NULL. `required: true` *alone* only makes the FK NOT NULL - the entity stays a top-level PRIMARY association (plain dropdown, its own perspective). Composition is **opt-in**, matching the Dirigible convention (where it is an explicit `relationshipType="COMPOSITION"` and most required FKs are plain associations); `composition` already implies NOT NULL, so `required` need not also be set. Only a `manyToOne`/`oneToOne` can be a composition; an entity's *first* `composition` to-one is its composition parent. Declare the inverse `oneToMany` on the master (`Member` with `loans: oneToMany to Loan` + `Loan.member` `composition: true`) so `Loan` is managed as a detail of `Member`; the `oneToMany` itself is navigation-only (the EDM generator ignores `oneToMany`/`manyToMany` since the FK lives on the child). (This replaced the earlier "first required to-one is automatically a composition" heuristic, which made entities like a `Loan` with a required `member` FK silently nest under `Member` instead of staying top-level.) **Every to-one FK property** (composition or association) carries `relationshipType` / `relationshipCardinality` (`1_n` / `n_1` / `1_1`) / `relationshipName` (`<owner>_<target>`) / `relationshipEntityName` / `relationshipEntityPerspectiveName` - the last two drive the generated dropdown's data URL, so they are not optional.
- **`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/<Entity>` rather than a missing per-entity perspective. Settings are still real entities (own table, CSVIM seeds, FK columns) - only their UI placement differs.
- **First-class document numbering (`number:` on a string field) — the intent references a SERIES, the shape lives outside the model.** `number: { series: Sales Invoice, per: Company, stampOn: issue }` on a non-key string field gives it a platform-allocated, gap-free document number. A number series is a **tenant-level business object**: the intent (and the generated code) reference it only by name; its shape — a literal prefix + the sequence zero-padded to a total width, no token grammar — is declared once per module in a **`.numbers` artefact** at the project root (`{"series": [{"name": "Sales Invoice", "prefix": "SI", "size": 10}]}`, AUTHORED like `.roles`, never generated) and configured per tenant in the application shell's **Document Numbering** settings (`/services/core/numbering`). The `.numbers` synchronizer (`engine-numbering`, `NumberSeriesSynchronizer`, multitenant, `SynchronizersOrder.NUMBER_SERIES` = before anything allocating) INSERTs a missing series row per tenant and never updates one — the counter is live and the shape may be tenant-configured; an identical cross-module re-declaration is a skip (a shared legal range), a DIFFERING one fails that artefact loudly naming both locations; artefact DELETE never touches the series row. Sequences are CONTINUOUS and never auto-reset (BG law; an annual restart is an admin setting prefix + next in January). `per:` names a to-one relation (never an EntityStatus) whose value PARTITIONS the series — one row per (series, partition) in the per-tenant `DIRIGIBLE_DOCUMENT_NUMBERS` table, each partition its own sequence/prefix/width, materialized on first allocation from the series' base row (two legal entities in one tenant must not share a counter; identical numbers across partitions are correct). `stampOn: create` = the generated DAO allocates at insert via `sdk.numbering.DocumentNumbers.next(series[, partition])`; `stampOn: issue` = the field is created with a UUID placeholder (the `generatedUuid` auto-fill) and the generated `gen/events/<module>/<Entity>NumberStamp.java` delegate replaces it at the issue step, idempotently. Allocating an UNDECLARED series fails loudly — never invent a shape. The REMOVED keys `format`/`scope`/`resetOn` are rejected on the raw YAML tree (`IntentParser.rejectRemovedNumberKeys`) because the typed Gson mapping would silently drop them — an intent still carrying `format:` must fail, not quietly lose its shape. `NumberingSupport` builds the `numbering` glue collection; `NumberingSdkIT` covers the SDK + synchronizer end-to-end.
- **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<E, T>` (`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.<Field> = Beans.get(<class>.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: <field>, value: <literal> }` sets a `string`/`text` field of the process's **trigger entity** to a literal value, generated as a `gen/events/<module>/<Process><Step>.java` `JavaDelegate` (`SetFieldSupport` → the `setters` glue collection → `SetField.java.template`) instead of scaffolding a hand-written `custom.<Step>` stub - it persists the set column via the targeted single-column `updateProperty(id, "<Field>", 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: <step|end> }`** 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.
Expand Down
Loading
Loading