diff --git a/components/engine/engine-intent/CLAUDE.md b/components/engine/engine-intent/CLAUDE.md index 66034c63ebe..13ee6a2cac2 100644 --- a/components/engine/engine-intent/CLAUDE.md +++ b/components/engine/engine-intent/CLAUDE.md @@ -764,7 +764,7 @@ Implemented and generating annotated client-Java off the shared `EventBinding` / - **Parser hardening.** A wrong-typed scalar (e.g. an unquoted brace recipient `to: {member.email}`, which YAML parses as an object) now surfaces as a clean `IntentValidationException` issue with a helpful message instead of a raw 500 Gson error that wedged the editor. `IntentParserTest` covers it. - **Externalized AI system prompt.** Moved from an inline string to `intent-assistant-guide.md` (classpath resource, fail-fast load), corrected to the full current schema incl. the glue catalog + `businessKey`/`businessKeyStrategy`, and restored the propose-the-whole-file tool contract the draft had dropped. - **Settlements re-allocate a corrected payment (#6818).** The payment spread handler used to bind the payment's bare create topic only, so a payment booked for the wrong amount and corrected afterwards - or created incomplete and completed later - was never re-allocated and the invoice kept the original settled figure. It is now emitted once per bound payment event (create + `-updated`) from its own glue collection, **`settlementListeners`** - the `settlements` collection still drives the one-per-settlement `OnInvoice` delegate, and a second collection is what lets the two templates fan out differently while sharing one descriptor (`rollupEntry` copies it per class name + topic suffix, exactly as roll-ups and expansions do). Note the two traps a new glue collection carries: it needs a `case` in `GlueGenerator` (Java - the only generator since #6707; the old `generateUtils.js` twin is gone) or it renders whole-model with raw `${...}` placeholders, and `GlueGenerator.copy` is a per-key allow-list, so a new descriptor key that is not listed there never reaches the template. The handler itself was already a recompute of the payment's *unallocated* balance (re-delivery is a no-op by construction); it now also **releases** the excess - newest allocation first, through the junction repository - when the payment is corrected below what it already covers, so the recompute converges in both directions. Two boundaries of that release: the CREATE handler never releases - a create event is the first word about a payment, so a negative pot there can only be a DELAYED create racing a correction the updated handler already allocated, and releasing on the stale payload would undo it (#6865). And a corrected MATCH column (the payment re-filed under another Customer) is invisible to the amount-based recompute (`pot - allocated == 0`), so the payment's match columns are grouping keys and a third listener on the payment's `-rekeyed` topic releases everything and re-allocates from the STORE - both re-key notices run the same store-driven recompute, so delivery order cannot matter; emitted only for a LOCAL payment, since a cross-model payment's DAO belongs to the owner model and a projection has no repository to re-read (#6864). -- **What a Duplicate does NOT copy (`duplicable` object form, #7358).** `duplicable: true` cloned every ordinary user field of the header, so a copied invoice kept the source's `date`, `due` and `taxEventDate` - "same invoice as last month" opened dated last month, and the module could not fix it on its own side: a `calculatedActionOnCreate` fills an EMPTY value and respects a present one by contract, which is exactly what makes the copied value stick. The key now also takes `{ defaults: {...}, reset: [...] }` - `reset` drops a field from the clone so the create path fills it as it would on a hand-made document, `defaults` writes a constant, with the same `now` token and the same field-shape rendering `generates.defaults` has (`date` -> `YYYY-MM-DD`, `month` -> `YYYY-MM`, `week` -> `YYYY-Www`). The shorthand is normalized to the empty object on the RAW tree (`IntentParser.normalizeDuplicable`, the `expandUniqueShorthand` precedent) so one typed class carries both forms and the unknown-key walk sees the two keys; `EntityIntent.duplicable` is therefore a `DuplicateIntent`, not a `Boolean`. Both halves reach the document template as **structured** `.edm` attributes (`duplicateReset`, `duplicateDefaults` in `STRUCTURED_ATTRIBUTES` and in `transform-edm.js`'s `ENTITY_STRUCTURED`) - entity metadata a flat attribute cannot carry is lost on the next modeler save (#6826), which here would silently put the copied dates back. Refused at parse, each because accepting it would be silent: a name that is neither a field nor a to-one of the entity, one of the built-in drops (identity, `number:`, `function: EntityStatus`, `readOnly`, `aggregate`), the same name in both lists, `now` on a property that is not a date/month/week, and a `reset` of a **required** field with no `defaultValue` and no create-time rule - that one would make every duplicate fail on the server's own "field is required". The `now` value renders from the LOCAL calendar fields in `todayAs`, never `toISOString()`: east of Greenwich that is yesterday after the evening cut-over. Out of scope and still true: the copy is client-side and not atomic - a failed line POST leaves a half-copied draft. +- **What a Duplicate does NOT copy (`duplicable` object form, #7358).** `duplicable: true` cloned every ordinary user field of the header, so a copied invoice kept the source's `date`, `due` and `taxEventDate` - "same invoice as last month" opened dated last month, and the module could not fix it on its own side: a `calculatedActionOnCreate` fills an EMPTY value and respects a present one by contract, which is exactly what makes the copied value stick. The key now also takes `{ defaults: {...}, reset: [...] }` - `reset` drops a field from the clone so the create path fills it as it would on a hand-made document, `defaults` writes a constant, with the same `now` token and the same field-shape rendering `generates.defaults` has (`date` -> `YYYY-MM-DD`, `timestamp` -> the ISO instant, `month` -> `YYYY-MM`, `week` -> `YYYY-Www`). The shorthand is normalized to the empty object on the RAW tree (`IntentParser.normalizeDuplicable`, the `expandUniqueShorthand` precedent) so one typed class carries both forms and the unknown-key walk sees the two keys; `EntityIntent.duplicable` is therefore a `DuplicateIntent`, not a `Boolean`. Both halves reach the document template as **structured** `.edm` attributes (`duplicateReset`, `duplicateDefaults` in `STRUCTURED_ATTRIBUTES` and in `transform-edm.js`'s `ENTITY_STRUCTURED`) - entity metadata a flat attribute cannot carry is lost on the next modeler save (#6826), which here would silently put the copied dates back. Refused at parse, each because accepting it would be silent: a name that is neither a field nor a to-one of the entity, one of the built-in drops (identity, `number:`, `function: EntityStatus`, `readOnly`, `aggregate`), the same name in both lists, `now` on a property that does not hold a moment (not a date/timestamp/month/week - #7396 added the timestamp shape, which the check listed out of the three it had a renderer for although it is the one value an author would write on such a field), and a `reset` of a **required** field with no `defaultValue` and no create-time rule - that one would make every duplicate fail on the server's own "field is required". The `now` value renders from the LOCAL calendar fields in `todayAs`, never `toISOString()`: east of Greenwich that is yesterday after the evening cut-over - except the `timestamp` shape, which IS an instant and not a wall clock, and carries the ISO instant the form's own `toPayload()` sends for a datetime input, since a `java.time.Instant` property does not bind the local calendar shapes at all. Out of scope and still true: the copy is client-side and not atomic - a failed line POST leaves a half-copied draft. - **CI runs on Corretto 24** (compile target stays 21); the integration-test fork gets `-Xmx6g`. (Root-level change; recorded here because it landed alongside the intent work.) **Cross-artefact field naming:** the `.form` control `model` (and control `id`) bind to the entity property, so they use `IntentNaming.pascalCase` to match the EDM property names (`loanedOn` -> `LoanedOn`). The `.report` references physical UPPER_SNAKE columns and humanized display aliases (no camelCase property identifiers), so it needs no PascalCasing. diff --git a/components/engine/engine-intent/README.md b/components/engine/engine-intent/README.md index 817aca55f19..b1197b52c8c 100644 --- a/components/engine/engine-intent/README.md +++ b/components/engine/engine-intent/README.md @@ -97,7 +97,7 @@ Entity-level extras: `order: [Id, Product, Quantity, ...]` sequences form contro `duplicable: true` adds a Duplicate button on a document (clones header + items through the normal create path), and its object form says what the copy must NOT carry over - `duplicable: { defaults: { date: now }, reset: [due, taxEventDate] }`, where `reset` hands a field back to the entity's own -create-time rule and `defaults` writes a constant (`now` is today in the field's own shape); `imports: |` injects Java import lines into the generated repository (pairs with +create-time rule and `defaults` writes a constant (`now` is the current moment in the field's own shape); `imports: |` injects Java import lines into the generated repository (pairs with calculated actions); `aggregate: true` on a document master's numeric field keeps it equal to the sum of the items' same-named field (the totals footer). diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGenerator.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGenerator.java index 956d4b72710..d04d58f0fbc 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGenerator.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGenerator.java @@ -3613,12 +3613,18 @@ private static List duplicateResets(EntityIntent entity) { return resets; } + /** The authored field types a {@code now} default renders in their own shape. */ + private static final Set NOW_SHAPES = Set.of("date", "timestamp", "month", "week"); + /** * The constants a Duplicate writes into the cloned header, as {@code {name, shape, js}} entries in - * authored order. {@code shape} is {@code date} / {@code month} / {@code week} for the {@code now} - * token - today in the field's own shape, rendered by the document page's {@code todayAs} helper - * against the LOCAL clock - and {@code literal} otherwise, where {@code js} carries the value - * already coerced to the property's type as a JavaScript literal. + * authored order. {@code shape} is {@code date} / {@code timestamp} / {@code month} / {@code week} + * for the {@code now} token - the current moment in the field's own shape, rendered by the document + * page's {@code todayAs} helper against the LOCAL clock - and {@code literal} otherwise, where + * {@code js} carries the value already coerced to the property's type as a JavaScript literal. The + * shape is the AUTHORED type, not a narrowing of it: a {@code timestamp} property binds a + * {@code java.time.Instant} on the server, which the {@code YYYY-MM-DD} a {@code date} shape + * produces does not fill (#7396). * * @param entity the duplicable document master * @return the entries, never null @@ -3637,7 +3643,7 @@ private static List> duplicateDefaults(EntityIntent entity) entry.put("name", IntentNaming.pascalCase(name.trim())); String type = duplicateDefaultType(entity, name.trim()); if ("now".equals(value.trim())) { - entry.put("shape", "month".equals(type) || "week".equals(type) ? type : "date"); + entry.put("shape", NOW_SHAPES.contains(type) ? type : "date"); entry.put("js", ""); } else { entry.put("shape", "literal"); diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/model/DuplicateIntent.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/model/DuplicateIntent.java index fdeb560e289..6bf69448114 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/model/DuplicateIntent.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/model/DuplicateIntent.java @@ -34,16 +34,17 @@ * The two keys compose and never overlap: {@code reset} is for a field that HAS a create-time rule * (a {@code calculatedActionOnCreate}, a {@code defaultValue}) and must be handed back to it - a * copied value would be respected by that rule and stick; {@code defaults} is for a field that has - * none, where the copy needs a value stated here. {@code now} renders today in the field's own - * shape, the same token and the same rendering {@code generates.defaults} uses. + * none, where the copy needs a value stated here. {@code now} renders the current moment in the + * field's own shape, the same token and the same rendering {@code generates.defaults} uses. */ public class DuplicateIntent { /** * Constants written into the cloned header after the resets, by the entity's own field / to-one - * relation name. {@code now} is today in the field's shape ({@code date} -> {@code YYYY-MM-DD}, a - * {@code month} field -> {@code YYYY-MM}, a {@code week} field -> {@code YYYY-Www}); any other - * value is a literal coerced to the property's type. + * relation name. {@code now} is the current moment in the field's shape ({@code date} -> + * {@code YYYY-MM-DD}, a {@code timestamp} field -> the ISO instant, a {@code month} field -> + * {@code YYYY-MM}, a {@code week} field -> {@code YYYY-Www}); any other value is a literal coerced + * to the property's type. */ private Map defaults = new LinkedHashMap<>(); diff --git a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/parser/IntentParser.java b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/parser/IntentParser.java index 28965cd188e..44f41240906 100644 --- a/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/parser/IntentParser.java +++ b/components/engine/engine-intent/src/main/java/org/eclipse/dirigible/components/intent/parser/IntentParser.java @@ -718,9 +718,17 @@ private static void validateDuplicableReset(String subject, String name, Relatio } /** - * A {@code defaults} value: {@code now} is today in the field's own shape, so it is only meaningful - * on a field that HOLDS a date - the same rule and the same wording {@code generates.defaults} - * uses. Anything else is a literal, coerced to the property's type at generation. + * The field types a {@code now} default is meaningful on: the ones that HOLD the current moment, + * each rendered in its own shape. A {@code timestamp} is one of them - it differs from a + * {@code date} only in precision, and the copy of a document is made now in both cases (#7396). + */ + private static final Set NOW_FIELD_TYPES = Set.of("date", "timestamp", "month", "week"); + + /** + * A {@code defaults} value: {@code now} is the current moment in the field's own shape, so it is + * only meaningful on a field that HOLDS one - the same rule and the same wording + * {@code generates.defaults} uses. Anything else is a literal, coerced to the property's type at + * generation. */ private static void validateDuplicableDefault(String subject, String name, String type, String value, List issues) { if (value == null || value.isBlank()) { @@ -731,9 +739,9 @@ private static void validateDuplicableDefault(String subject, String name, Strin return; } String kind = type == null ? "" : type.toLowerCase(Locale.ROOT); - if (!"date".equals(kind) && !"month".equals(kind) && !"week".equals(kind)) { - issues.add(subject + " assigns [" + name + "] the value now, but that property is not a date - now is today in the field's own" - + " shape, so it is only a value for a date / month / week field"); + if (!NOW_FIELD_TYPES.contains(kind)) { + issues.add(subject + " assigns [" + name + "] the value now, but that property does not hold a moment - now is the current" + + " moment in the field's own shape, so it is only a value for a date / timestamp / month / week field"); } } 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 6e9e3b8fbc5..11ee5b81729 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 @@ -806,18 +806,18 @@ value and respect a present one. Say so with the object form: `reset:` is for a field that HAS a create-time rule (a `calculatedActionOnCreate`, a `defaultValue`) and must be handed back to it; `defaults:` is for a field that has none, where the copy needs a value -stated here. `now` is today in the field's own shape (a `date` field -> `YYYY-MM-DD`, a `month` field --> `YYYY-MM`, a `week` field -> `YYYY-Www`), the same token `generates.defaults` takes; any other -value is a literal coerced to the property's type. Both keys name the entity's own fields and to-one -relations - no `relation.field` paths. +stated here. `now` is the current moment in the field's own shape (a `date` field -> `YYYY-MM-DD`, a +`timestamp` field -> the ISO instant, a `month` field -> `YYYY-MM`, a `week` field -> `YYYY-Www`), the +same token `generates.defaults` takes; any other value is a literal coerced to the property's type. +Both keys name the entity's own fields and to-one relations - no `relation.field` paths. Refused at parse: a name that is neither a field nor a to-one relation of the entity; one that is already dropped anyway (the primary key, the `number:` field, the `function: EntityStatus` relation, a `readOnly` or an `aggregate` field) - naming it would let you believe you control something the Duplicate decided long before reading the block; the same name in both lists; `now` on a property -that is not a date / month / week; and a `reset` on a **required** field with neither a -`defaultValue` nor a create-time rule, which would make every duplicate fail on the server's own -"field is required". +that does not hold a moment (not a date / timestamp / month / week); and a `reset` on a **required** +field with neither a `defaultValue` nor a create-time rule, which would make every duplicate fail on +the server's own "field is required". **Control order (`order:`):** by default the generated UI controls (form inputs, list columns, detail rows) follow the declaration order - all fields first, then the to-one relations, so relations end up @@ -3111,8 +3111,8 @@ EmployeeTimesheet for each active employee". Per matching row, a new target reco saved through the target's generated repository, so its create-time logic (document numbering, status init, calculated fields) fires. The **row is the source**, so `from` is implicit (the schedule's `entity`); `map` copies a field or to-one relation of the row onto a target property, `defaults` sets -`now` (rendered in the target field's own shape - date / `YYYY-MM` month / `YYYY-Www` week) or a -literal. The target may live in another model via `uses:` (same as `generates`). +`now` (rendered in the target field's own shape - date / instant / `YYYY-MM` month / `YYYY-Www` week) +or a literal. The target may live in another model via `uses:` (same as `generates`). ```yaml schedules: diff --git a/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGeneratorTest.java b/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGeneratorTest.java index ecc3d160755..376c3675e3e 100644 --- a/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGeneratorTest.java +++ b/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/generator/edm/EdmIntentGeneratorTest.java @@ -306,7 +306,7 @@ void duplicableObjectFormEmitsTheResetsAndTheDefaults() { entities: - name: SalesInvoice duplicable: - defaults: { date: now, note: "Copy", period: now } + defaults: { date: now, note: "Copy", period: now, recordedAt: now } reset: [due] fields: - { name: id, type: integer, primaryKey: true, generated: true } @@ -314,6 +314,7 @@ void duplicableObjectFormEmitsTheResetsAndTheDefaults() { - { name: due, type: date, calculatedActionOnCreate: custom.DueDate } - { name: note, type: string } - { name: period, type: month } + - { name: recordedAt, type: timestamp } - name: SalesInvoiceItem fields: - { name: id, type: integer, primaryKey: true, generated: true } @@ -329,7 +330,7 @@ void duplicableObjectFormEmitsTheResetsAndTheDefaults() { assertEquals(List.of("Due"), invoice.get("duplicateReset"), "a reset is carried as the GENERATED property name"); List> defaults = (List>) invoice.get("duplicateDefaults"); - assertEquals(3, defaults.size(), "every default reaches the template, in authored order"); + assertEquals(4, defaults.size(), "every default reaches the template, in authored order"); assertEquals("Date", defaults.get(0) .get("name")); assertEquals("date", defaults.get(0) @@ -345,6 +346,13 @@ void duplicableObjectFormEmitsTheResetsAndTheDefaults() { assertEquals("month", defaults.get(2) .get("shape"), "a month field gets the YYYY-MM shape, not a full date"); + // #7396: the shape is the AUTHORED type, not a narrowing of it - a timestamp property binds a + // java.time.Instant, which the YYYY-MM-DD of a `date` shape does not fill. + assertEquals("RecordedAt", defaults.get(3) + .get("name")); + assertEquals("timestamp", defaults.get(3) + .get("shape"), + "a timestamp field gets the instant shape, not a date"); // Both keys are structured, so they must be written as JSON attributes rather than dropped - // what the .edm cannot say is lost on the next modeler save (#6826). diff --git a/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/parser/DuplicableIntentTest.java b/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/parser/DuplicableIntentTest.java index 020c340634d..32bba1c1f33 100644 --- a/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/parser/DuplicableIntentTest.java +++ b/components/engine/engine-intent/src/test/java/org/eclipse/dirigible/components/intent/parser/DuplicableIntentTest.java @@ -52,6 +52,7 @@ class DuplicableIntentTest { - { name: total, type: decimal, aggregate: true } - { name: printedAt, type: date, readOnly: true } - { name: period, type: month } + - { name: recordedAt, type: timestamp } relations: - { name: customer, kind: manyToOne, to: Customer, required: true } - { name: status, kind: manyToOne, to: SalesInvoiceStatus, function: EntityStatus } @@ -139,7 +140,7 @@ void aNameInBothResetAndDefaultsIsRejectedAsAContradiction() { @Test void nowOnANonDateFieldIsRejected() { - assertIssue(objectForm("defaults: { note: now }"), "now is today in the field's own shape"); + assertIssue(objectForm("defaults: { note: now }"), "now is the current moment in the field's own shape"); } @Test @@ -150,6 +151,26 @@ void nowOnAMonthFieldIsAccepted() { .getDefaults()); } + /** + * #7396: a {@code timestamp} property holds exactly the kind of value {@code now} means - it + * differs from a {@code date} only in precision, and the copy is made now in both cases - but the + * check listed the three shapes it had a renderer for and refused the one value an author would + * write there, leaving a {@code calculatedActionOnCreate} class as the only way to say it. + */ + @Test + void nowOnATimestampFieldIsAccepted() { + EntityIntent invoice = entity(IntentParser.parse(objectForm("defaults: { recordedAt: now }")), "SalesInvoice"); + + assertEquals(Map.of("recordedAt", "now"), invoice.getDuplicable() + .getDefaults()); + } + + /** A default on a to-one relation assigns its raw foreign key, so `now` is still not a value. */ + @Test + void nowOnARelationIsStillRejected() { + assertIssue(objectForm("defaults: { customer: now }"), "now is the current moment in the field's own shape"); + } + @Test void resettingARequiredFieldWithNoCreateTimeRuleIsRejected() { assertIssue(objectForm("reset: [date]"), "would make every duplicate fail"); 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 c9cc9395c6d..237d0b93365 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 @@ -761,12 +761,16 @@ document.addEventListener('alpine:init', () => { } }, - // Today in a date field's own shape, for a `duplicable.defaults` value of `now`. Built from the - // LOCAL calendar fields, never from toISOString(): that is UTC, so east of Greenwich every copy - // made after the evening cut-over would be dated yesterday. + // The current moment in the target field's own shape, for a `duplicable.defaults` value of `now`. + // The calendar shapes are built from the LOCAL calendar fields, never from toISOString(): that is + // UTC, so east of Greenwich every copy made after the evening cut-over would be dated yesterday. + // A `timestamp` field is the exception and the reason: it binds a java.time.Instant on the server, + // which is an instant and not a wall clock, so it carries the full ISO instant the form's own + // toPayload() sends for a datetime input - the local calendar shapes would not bind at all. todayAs(shape) { const now = new Date(); const pad = (n) => String(n).padStart(2, '0'); + if (shape === 'timestamp') return now.toISOString(); if (shape === 'month') return now.getFullYear() + '-' + pad(now.getMonth() + 1); if (shape === 'week') { // ISO-8601 week: Thursday of the current week decides both the week number and its year. 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 744b2fdebf3..02878c750ac 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 @@ -736,15 +736,16 @@ class IntentEmissionCoverageIT extends IntegrationTest { - name: Reorder function: Document duplicable: - defaults: { orderedOn: now, period: now, comment: "Copy" } + defaults: { orderedOn: now, period: now, recordedAt: now, comment: "Copy" } reset: [note] fields: - - { name: id, type: integer, primaryKey: true, generated: true } - - { name: reference, type: string, length: 40, function: DocumentTitle } - - { name: orderedOn, type: date, required: true } - - { name: period, type: month } - - { name: note, type: string, length: 100 } - - { name: comment, type: string, length: 100 } + - { name: id, type: integer, primaryKey: true, generated: true } + - { name: reference, type: string, length: 40, function: DocumentTitle } + - { name: orderedOn, type: date, required: true } + - { name: period, type: month } + - { name: recordedAt, type: timestamp } + - { name: note, type: string, length: 100 } + - { name: comment, type: string, length: 100 } - name: ReorderItem function: DocumentItem fields: @@ -2789,6 +2790,12 @@ private void assertEmission() { "now on a date field must be written as today in that field's shape"); assertTrue(reorderDoc.contains("header['Period'] = this.todayAs('month');"), "now on a month field must be the YYYY-MM shape, not a full date"); + // #7396: the same rule one shape further - a timestamp property binds a java.time.Instant, so + // the copy carries the full ISO instant, not the local YYYY-MM-DD a `date` shape produces. + assertTrue(reorderDoc.contains("header['RecordedAt'] = this.todayAs('timestamp');"), + "now on a timestamp field must be written in the timestamp shape, not narrowed to a date"); + assertTrue(reorderDoc.contains("if (shape === 'timestamp') return now.toISOString();"), + "todayAs must render a timestamp as the ISO instant the backend binds for a java.time.Instant"); assertTrue(reorderDoc.contains("header['Comment'] = \"Copy\";"), "a literal default must reach the page quoted"); assertTrue(reorderDoc.contains("todayAs(shape)") && reorderDoc.contains("now.getFullYear() + '-' + pad(now.getMonth() + 1)"), "todayAs must build from the LOCAL calendar fields - toISOString is UTC, so a copy made in the evening"