From 102345e51b68131850e26c1be7b710f86fb517fe Mon Sep 17 00:00:00 2001 From: delchev Date: Wed, 22 Jul 2026 18:18:52 +0300 Subject: [PATCH 1/2] feat(intent): auto-generate type: uuid fields on create (no hand-written action) A `type: uuid` field is now platform-generated: the generated repository assigns a random UUID on create when the value is empty, so a document's system/business-key uuid no longer needs a hand-written calculatedActionOnCreate. The author can still seed/import an explicit value - it is only filled when blank. - EdmIntentGenerator marks a uuid property generatedUuid="true" (alongside the existing read-only flag). - The DAO Repository.java.template save() (create path) assigns java.util.UUID.randomUUID() to each generatedUuid property that is null/blank, next to the calculated-on-create assignments. Verified: the EDM marks Customer.Uuid generatedUuid; a generated CompanyRepository.save() contains the UUID auto-fill and compiles clean (544 beans, no javac errors). This is the reusable UUID primitive the first-class numbering placeholder (stampOn: issue, N3b) will reuse - together they retire the hand-written per-document UUID/number placeholder actions. Co-Authored-By: Claude Opus 4.8 --- .../intent/generator/edm/EdmIntentGenerator.java | 6 ++++++ .../intent/generator/edm/EdmIntentGeneratorTest.java | 4 +++- .../data/Repository.java.template | 8 ++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) 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 ada15a6e64..aacbdc8002 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 @@ -918,6 +918,12 @@ private static Map propertyMap(String entityName, FieldIntent fi if (field.isReadOnly() || "uuid".equalsIgnoreCase(field.getType())) { p.put("isReadOnlyProperty", "true"); } + // A uuid field is platform-generated: the generated repository assigns a random UUID on create + // when the value is empty (no hand-written calculatedActionOnCreate needed). The author can + // still seed/import an explicit value - it is only filled when blank. + if ("uuid".equalsIgnoreCase(field.getType())) { + p.put("generatedUuid", "true"); + } if (field.isSensitive()) { // Hidden from the personal (my) surface: absent from its pages and stripped from the // personal REST controller's responses. The power surface ignores this attribute. 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 ea5ee86164..d9562c2c82 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 @@ -66,8 +66,10 @@ void customerEmitsCrossModelProjectionsAndForeignKeys() { .noneMatch(p -> "Country".equals(p.get("name"))), "projection targets must not create perspectives"); - // uuid carries the unique constraint; the four audit columns are present. + // uuid carries the unique constraint and is platform-generated on create (no custom action); + // the four audit columns are present. assertEquals("true", propertyByName(customer, "Uuid").get("dataUnique")); + assertEquals("true", propertyByName(customer, "Uuid").get("generatedUuid")); assertEquals("CREATED_AT", propertyByName(customer, "CreatedAt").get("auditType")); assertEquals("UPDATED_BY", propertyByName(customer, "UpdatedBy").get("auditType")); 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 b78ed136d8..abffae0ab6 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 @@ -143,6 +143,14 @@ public class ${name}Repository extends JavaRepository<${name}Entity> { #end #end #end +## A uuid field (intent type: uuid) is platform-generated: assign a random UUID on create when empty. +#foreach ($property in $properties) +#if($property.generatedUuid) + if (entity.${property.name} == null || entity.${property.name}.isBlank()) { + entity.${property.name} = java.util.UUID.randomUUID().toString(); + } +#end +#end #if($documentMaster) recalculate(entity); #end From 6cbbaa1e3c8f40eedb2b1039159f791aae95f422 Mon Sep 17 00:00:00 2001 From: delchev Date: Wed, 22 Jul 2026 18:33:22 +0300 Subject: [PATCH 2/2] feat(intent): generate create-time numbering from number: {} (N3b-i) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit N3b-i of first-class document numbering: turn a `number: {}` field (N1) into its create-time behavior against the runtime (N2) via the SDK (N3a). No hand-written placeholder action. - EdmIntentGenerator emits per-field number markers (numberSeries / numberFormat / numberScope [PascalCased] / numberStampOn) and, by mode: stampOn:create -> numberStampOnCreate="true"; stampOn:issue -> generatedUuid="true" (a UUID placeholder on create, reusing the uuid auto-fill), pending the issue stamp step (N3b-ii). The number field is read-only. - Repository.java.template save() (create path): for a numberStampOnCreate field, build the scope map (year = current year; any other name reads the entity's field) and stamp the real number via sdk.numbering.DocumentNumbers.next(series, format, scope) when the field is blank. Verified live: a Company with `number: { series: CompanyDoc, format: "CO-{seq:05}", stampOn: create }` stamps CO-00001 / CO-00002 / CO-00003 on successive REST creates (gap-free), the counter shows in the shell's Document Numbering settings, and it compiles clean. EdmIntentGeneratorTest#numberFieldEmitsStampMarkers covers both modes. Stacked on #6386 (uuid auto-fill — the issue placeholder). N3b-ii: the generated issue stamp delegate (gen.events.NumberStamp, idempotent) the process wires via delegate:, so stampOn:issue documents (invoices) drop SalesInvoiceNumberAction + the generateNumber delegate. Co-Authored-By: Claude Opus 4.8 --- .../generator/edm/EdmIntentGenerator.java | 27 +++++++++++++++ .../generator/edm/EdmIntentGeneratorTest.java | 33 +++++++++++++++++++ .../data/Repository.java.template | 20 +++++++++++ 3 files changed, 80 insertions(+) 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 aacbdc8002..5abd425193 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 @@ -34,6 +34,7 @@ import org.eclipse.dirigible.components.intent.model.EntityIntent; import org.eclipse.dirigible.components.intent.model.LabelExpression; import org.eclipse.dirigible.components.intent.model.FieldIntent; +import org.eclipse.dirigible.components.intent.model.NumberIntent; import org.eclipse.dirigible.components.intent.model.IntentModel; import org.eclipse.dirigible.components.intent.model.RelationIntent; import org.eclipse.dirigible.components.intent.model.RollupIntent; @@ -924,6 +925,32 @@ private static Map propertyMap(String entityName, FieldIntent fi if ("uuid".equalsIgnoreCase(field.getType())) { p.put("generatedUuid", "true"); } + // First-class document numbering (intent `number: {}`): the platform maintains a per-series + // counter and stamps the formatted number. stampOn:create stamps the real number on insert + // (the generated repository calls sdk.numbering.DocumentNumbers); stampOn:issue holds a UUID + // placeholder on create (reusing the uuid auto-fill above) until the generated stamp step runs. + if (field.getNumber() != null) { + NumberIntent number = field.getNumber(); + List numberScope = new ArrayList<>(); + if (number.getScope() != null) { + for (String scopeName : number.getScope()) { + // Scope names index the counter AND read the entity's field on create; PascalCase them + // to match the generated entity property (year stays the literal token). + numberScope.add("year".equalsIgnoreCase(scopeName) ? "year" : IntentNaming.pascalCase(scopeName)); + } + } + p.put("numberSeries", number.getSeries() == null ? entityName : number.getSeries()); + p.put("numberFormat", number.getFormat() == null ? "" : number.getFormat()); + p.put("numberScope", numberScope); + p.put("isReadOnlyProperty", "true"); + if ("issue".equalsIgnoreCase(number.getStampOn())) { + p.put("numberStampOn", "issue"); + p.put("generatedUuid", "true"); // UUID placeholder on create; stamped at the issue step + } else { + p.put("numberStampOn", "create"); + p.put("numberStampOnCreate", "true"); // real number allocated + formatted on insert + } + } if (field.isSensitive()) { // Hidden from the personal (my) surface: absent from its pages and stripped from the // personal REST controller's responses. The power surface ignores this attribute. 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 d9562c2c82..62b3a2c7a4 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 @@ -77,6 +77,39 @@ void customerEmitsCrossModelProjectionsAndForeignKeys() { assertEquals("master-data", customer.get("perspectiveNavId")); } + @Test + void numberFieldEmitsStampMarkers() { + String yaml = + """ + name: billing + entities: + - name: SalesInvoice + fields: + - { name: id, type: integer, primaryKey: true, generated: true } + - { name: number, type: string, number: { series: SalesInvoice, format: "SI-{seq:07}", scope: [year], stampOn: issue } } + - name: Proforma + fields: + - { name: id, type: integer, primaryKey: true, generated: true } + - { name: number, type: string, number: { series: Proforma, format: "PF-{seq:05}", stampOn: create } } + """; + Map model = EdmIntentGenerator.buildModelJsonForTest(IntentParser.parse(yaml), "billing"); + List> entities = entities(model); + + // stampOn: issue -> a UUID placeholder on create (reusing the uuid auto-fill) + the series markers. + Map siNumber = propertyByName(entityByName(entities, "SalesInvoice"), "Number"); + assertEquals("issue", siNumber.get("numberStampOn")); + assertEquals("SalesInvoice", siNumber.get("numberSeries")); + assertEquals("true", siNumber.get("generatedUuid")); + assertNull(siNumber.get("numberStampOnCreate")); + + // stampOn: create -> the real number is stamped on insert (numberStampOnCreate), no placeholder. + Map pfNumber = propertyByName(entityByName(entities, "Proforma"), "Number"); + assertEquals("create", pfNumber.get("numberStampOn")); + assertEquals("true", pfNumber.get("numberStampOnCreate")); + assertEquals("PF-{seq:05}", pfNumber.get("numberFormat")); + assertNull(pfNumber.get("generatedUuid")); + } + @Test void crossModelProjectionCellIsMarkedProjectionInTheEdmDiagram() { IntentModel parsed = IntentParser.parse(readResource("/billing/customers.intent")); 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 abffae0ab6..c1f660d89a 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 @@ -144,6 +144,8 @@ public class ${name}Repository extends JavaRepository<${name}Entity> { #end #end ## A uuid field (intent type: uuid) is platform-generated: assign a random UUID on create when empty. +## A number: {} field with stampOn:issue also carries generatedUuid, so its create-time placeholder is +## a UUID (the real number is stamped at the issue step); stampOn:create fields are handled just below. #foreach ($property in $properties) #if($property.generatedUuid) if (entity.${property.name} == null || entity.${property.name}.isBlank()) { @@ -151,6 +153,24 @@ public class ${name}Repository extends JavaRepository<${name}Entity> { } #end #end +## First-class numbering, stampOn:create: allocate + format the real document number on insert (when +## empty), via the shared per-tenant counter. The scope both partitions the counter and feeds the +## format's tokens; `year` is the current year, any other name reads the entity's own field. +#foreach ($property in $properties) +#if($property.numberStampOnCreate) + if (entity.${property.name} == null || entity.${property.name}.isBlank()) { + java.util.Map ${property.name}Scope = new java.util.LinkedHashMap<>(); +#foreach ($scopeName in $property.numberScope) +#if($scopeName == "year") + ${property.name}Scope.put("year", String.valueOf(java.time.Year.now().getValue())); +#else + ${property.name}Scope.put("${scopeName}", entity.${scopeName} == null ? "" : String.valueOf(entity.${scopeName})); +#end +#end + entity.${property.name} = org.eclipse.dirigible.sdk.numbering.DocumentNumbers.next("${property.numberSeries}", "${property.numberFormat}", ${property.name}Scope); + } +#end +#end #if($documentMaster) recalculate(entity); #end