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
Original file line number Diff line number Diff line change
Expand Up @@ -101,11 +101,13 @@ public void generate(IntentGenerationContext context) {
List<Map<String, Object>> postings = buildPostings(model, byName, compositionParents, settings, context);
List<Map<String, Object>> printFeeders = PrintFeederSupport.buildPrintFeeders(model, byName, compositionParents, context);
List<Map<String, Object>> snapshots = SnapshotSupport.buildSnapshots(model, byName, compositionParents);
List<Map<String, Object>> numbering = NumberingSupport.buildNumbering(model, compositionParents);

if (triggers.isEmpty() && resolvers.isEmpty() && fieldLoaders.isEmpty() && timerLoaders.isEmpty() && waits.isEmpty()
&& aborts.isEmpty() && writers.isEmpty() && setters.isEmpty() && notifications.isEmpty() && schedules.isEmpty()
&& integrations.isEmpty() && inbound.isEmpty() && rollups.isEmpty() && expansions.isEmpty() && settlements.isEmpty()
&& generates.isEmpty() && transitions.isEmpty() && printFeeders.isEmpty() && postings.isEmpty() && snapshots.isEmpty()) {
&& generates.isEmpty() && transitions.isEmpty() && printFeeders.isEmpty() && postings.isEmpty() && snapshots.isEmpty()
&& numbering.isEmpty()) {
// No process glue for this intent - any stale .glue is removed by the post-pass scrub.
return;
}
Expand All @@ -131,6 +133,7 @@ public void generate(IntentGenerationContext context) {
glue.put("postings", postings);
glue.put("printFeeders", printFeeders);
glue.put("snapshots", snapshots);
glue.put("numbering", numbering);
context.writeModelFile(IntentNaming.baseName(context) + ".glue", JsonHelper.toJson(glue));
LOGGER.debug(
"Wrote glue with [{}] trigger(s), [{}] resolver(s), [{}] writer(s), [{}] setter(s),"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (c) 2010-2026 Eclipse Dirigible contributors
*
* All rights reserved. This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v2.0 which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v20.html
*
* SPDX-FileCopyrightText: Eclipse Dirigible contributors SPDX-License-Identifier: EPL-2.0
*/
package org.eclipse.dirigible.components.intent.generator;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.LinkedHashMap;

import org.eclipse.dirigible.components.intent.model.EntityIntent;
import org.eclipse.dirigible.components.intent.model.FieldIntent;
import org.eclipse.dirigible.components.intent.model.IntentModel;
import org.eclipse.dirigible.components.intent.model.NumberIntent;

/**
* Builds the {@code numbering} glue collection: one descriptor per {@code number: { stampOn: issue
* }} field, driving the generated {@code gen/events/<Entity>NumberStamp.java} delegate. The
* document is created with a UUID placeholder (the uuid auto-fill); this delegate, wired as a
* {@code delegate:} service task at the issue step, replaces it with the real formatted number -
* idempotently, so a re-issue after an amend keeps the number.
*
* <p>
* The number is allocated + formatted via {@code sdk.numbering.DocumentNumbers} (the shared
* per-tenant counter) and written with the targeted {@code updateProperty} (a workflow system
* write).
*/
final class NumberingSupport {

private NumberingSupport() {}

/**
* One numbering descriptor per {@code stampOn: issue} number field in the model.
*
* @param model the parsed intent model
* @param compositionParents each entity's transitive composition parent (perspective resolution)
* @return the {@code numbering} collection (possibly empty)
*/
static List<Map<String, Object>> buildNumbering(IntentModel model, Map<String, String> compositionParents) {
List<Map<String, Object>> numbering = new ArrayList<>();
for (EntityIntent entity : model.getEntities()) {
for (FieldIntent field : entity.getFields()) {
NumberIntent number = field.getNumber();
if (number == null || !"issue".equalsIgnoreCase(number.getStampOn())) {
continue; // stampOn:create is handled at insert by the DAO; only issue needs a step
}
List<String> scope = new ArrayList<>();
if (number.getScope() != null) {
for (String scopeName : number.getScope()) {
scope.add("year".equalsIgnoreCase(scopeName) ? "year" : IntentNaming.pascalCase(scopeName));
}
}
Map<String, Object> descriptor = new LinkedHashMap<>();
descriptor.put("entity", entity.getName());
descriptor.put("perspective", IntentEntities.resolvePerspective(entity.getName(), compositionParents));
descriptor.put("masterPk", IntentEntities.keyFieldName(entity));
descriptor.put("field", IntentNaming.pascalCase(field.getName()));
descriptor.put("series", number.getSeries() == null ? entity.getName() : number.getSeries());
descriptor.put("format", number.getFormat() == null ? "" : number.getFormat());
descriptor.put("scope", scope);
numbering.add(descriptor);
}
}
return numbering;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -918,6 +919,38 @@ private static Map<String, Object> 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");
}
// 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<String> 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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,15 +66,50 @@ 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"));

// The entity's navigation group flows to perspectiveNavId (the shared-shell groupId).
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<String, Object> model = EdmIntentGenerator.buildModelJsonForTest(IntentParser.parse(yaml), "billing");
List<Map<String, Object>> entities = entities(model);

// stampOn: issue -> a UUID placeholder on create (reusing the uuid auto-fill) + the series markers.
Map<String, Object> 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<String, Object> 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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,34 @@ 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.
## 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()) {
entity.${property.name} = java.util.UUID.randomUUID().toString();
}
#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<String, String> ${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
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package gen.events;

import java.util.LinkedHashMap;
import java.util.Map;

import org.flowable.engine.delegate.DelegateExecution;
import org.flowable.engine.delegate.JavaDelegate;

import gen.${javaGenFolderName}.data.${javaPerspective}.${entity}Entity;
import gen.${javaGenFolderName}.data.${javaPerspective}.${entity}Repository;

/**
* Document-number stamp for ${entity} (field ${field}, series ${series}, stampOn: issue). Wired as a
* `delegate:` service task at the issue step: it replaces the create-time UUID placeholder with the
* real formatted number, idempotently - a re-issue after an amend keeps the number.
*
* Generated from the intent numbering glue - do not edit; it is re-generated with the application. The
* number is allocated + formatted via sdk.numbering.DocumentNumbers (the shared per-tenant counter,
* also managed from the application shell's Document Numbering settings) and written with the targeted
* updateProperty - a workflow system write (no "-updated" event re-fire, no full-row merge).
*/
public class ${entity}NumberStamp implements JavaDelegate {

@Override
public void execute(DelegateExecution execution) {
Object key = execution.getVariable("${masterPk}");
if (!(key instanceof Number)) {
return;
}
int id = ((Number) key).intValue();
${entity}Repository repository = new ${entity}Repository();
${entity}Entity entity = repository.findById(id);
if (entity == null) {
return;
}
// Idempotent: only replace the create-time UUID placeholder; skip an already-stamped number.
if (entity.${field} != null && !isPlaceholder(entity.${field})) {
return;
}
Map<String, String> scope = new LinkedHashMap<>();
#foreach ($scopeName in $scope)
#if($scopeName == "year")
scope.put("year", String.valueOf(java.time.Year.now().getValue()));
#else
scope.put("${scopeName}", entity.${scopeName} == null ? "" : String.valueOf(entity.${scopeName}));
#end
#end
String number = org.eclipse.dirigible.sdk.numbering.DocumentNumbers.next("${series}", "${format}", scope);
repository.updateProperty(id, "${field}", number);
}

/** Whether the value is still the create-time UUID placeholder (vs an already-stamped number). */
private static boolean isPlaceholder(String value) {
try {
java.util.UUID.fromString(value);
return true;
} catch (IllegalArgumentException notAUuid) {
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,13 @@ export function getTemplate(parameters) {
engine: "velocity",
collection: "snapshots"
},
{
location: "/template-application-events-java/events/Numbering.java.template",
action: "generate",
rename: "gen/events/{{entity}}NumberStamp.java",
engine: "velocity",
collection: "numbering"
},
{
location: "/template-application-events-java/events/SettlementOnPayment.java.template",
action: "generate",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,34 @@ export function generateFiles(model, parameters, templateSources) {
}
}
break;
case "numbering":
// Document-number stamp delegates (intent layer): one JavaDelegate per number: field
// with stampOn:issue, wired as a delegate: service task at the issue step. It stamps
// the real number via sdk.numbering.DocumentNumbers. Own loop; the entity repo lives
// in this project (javaGenFolderName), its Java package segment is the sanitized
// perspective.
if (model.numbering) {
for (let n = 0; n < model.numbering.length; n++) {
const num = model.numbering[n];
const numberingParameters = {
...parameters,
entity: num.entity,
javaPerspective: sanitizeJavaIdentifier(num.perspective),
masterPk: num.masterPk,
field: num.field,
series: num.series,
format: num.format,
scope: num.scope
};
const cleanNumberingParameters = cleanData(numberingParameters);
generatedFiles.push({
location: location,
content: getGenerationEngine(template).generate(location, content, cleanNumberingParameters),
path: templateEngines.getMustacheEngine().generate(location, template.rename, cleanNumberingParameters)
});
}
}
break;
default:
// No collection
parameters.models = model.entities;
Expand Down
Loading