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 @@ -80,6 +80,20 @@ public T save(T entity, String eventTopic) {
return store().save(entity, eventTopic);
}

/**
* Insert a new entity instance, publishing it on the given topic plus any further events the write
* emits about other rows — e.g. a create-from announcing its source's completed transition only
* once the document that transition was about exists. All of them share the insert's transaction.
*
* @param entity the entity to insert
* @param eventTopic the topic to publish the saved entity on; {@code null} publishes nothing
* @param additionalEvents further events to record with the same write
* @return the saved entity (with any generated identifier populated)
*/
public T save(T entity, String eventTopic, List<DomainEvent> additionalEvents) {
return store().save(entity, eventTopic, additionalEvents);
}

/**
* Update an existing entity instance.
*
Expand Down Expand Up @@ -174,6 +188,23 @@ public int updateProperties(Object id, Map<String, Object> values, String eventT
return store().updateProperties(entityClass, id, values, eventTopic);
}

/**
* Targeted multi-column write publishing the resulting row on the given topic plus any further
* events the write emits about other rows — an aggregate's {@code "-rekeyed"} notice about the
* tuple the row just left. All of them share the mutation's transaction and are recorded only when
* the row actually existed to be written.
*
* @param id the primary-key value
* @param values the properties to set (plain identifiers) with their new values
* @param eventTopic the topic to publish the resulting row on; {@code null} publishes nothing
* @param additionalEvents further events to record with the same write
* @return the number of updated rows ({@code 0} when the id does not exist or {@code values} is
* empty)
*/
public int updateProperties(Object id, Map<String, Object> values, String eventTopic, List<DomainEvent> additionalEvents) {
return store().updateProperties(entityClass, id, values, eventTopic, additionalEvents);
}

/**
* Look up an entity by primary key. An absent id is an ordinary outcome — a dangling foreign key an
* event handler should skip, a path parameter a controller should answer {@code 404} for — so it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,10 +91,25 @@ public <T> T save(T entity) {
* @return the same entity (with any generated identifier populated)
*/
public <T> T save(T entity, String eventTopic) {
return save(entity, eventTopic, List.of());
}

/**
* Insert a new entity, publishing it on the given topic plus any further events the write emits
* about other rows — e.g. a create-from announcing its source's completed transition only once the
* document that transition was about exists. All of them share the insert's transaction.
*
* @param <T> the entity type
* @param entity the entity to insert
* @param eventTopic the topic to publish the saved entity on; {@code null} publishes nothing
* @param additionalEvents further events to record with the same write
* @return the same entity (with any generated identifier populated)
*/
public <T> T save(T entity, String eventTopic, List<DomainEvent> additionalEvents) {
RegisteredEntity meta = resolve(entity.getClass());
applyCreateAudit(entity, meta);
Map<String, Object> data = EntityBeanMapper.toMap(entity, meta);
prepareOutbox(eventTopic != null);
prepareOutbox(eventTopic != null || !additionalEvents.isEmpty());

try (Session session = entityManager.getSessionFactory()
.openSession()) {
Expand All @@ -112,7 +127,7 @@ public <T> T save(T entity, String eventTopic) {
// the row was actually inserted with.
writeId(entity, meta, generatedId);
}
events = outbox.record(session, eventsOf(eventTopic, entity, List.of()));
events = outbox.record(session, eventsOf(eventTopic, entity, additionalEvents));
tx.commit();
} catch (RuntimeException ex) {
rollback(tx, ex);
Expand Down Expand Up @@ -258,6 +273,26 @@ public <T> int updateProperties(Class<T> type, Object id, Map<String, Object> va
* empty)
*/
public <T> int updateProperties(Class<T> type, Object id, Map<String, Object> values, String eventTopic) {
return updateProperties(type, id, values, eventTopic, List.of());
}

/**
* Targeted multi-column write publishing the resulting row on the given topic plus any further
* events the write emits about other rows — an aggregate's {@code "-rekeyed"} notice about the
* tuple the row just left. All of them share the mutation's transaction and are recorded only when
* the row actually existed to be written.
*
* @param <T> the entity type
* @param type the entity class
* @param id the primary-key value
* @param values the properties to set (plain identifiers) with their new values
* @param eventTopic the topic to publish the resulting row on; {@code null} publishes nothing
* @param additionalEvents further events to record with the same write
* @return the number of updated rows ({@code 0} when the id does not exist or {@code values} is
* empty)
*/
public <T> int updateProperties(Class<T> type, Object id, Map<String, Object> values, String eventTopic,
List<DomainEvent> additionalEvents) {
if (values == null || values.isEmpty()) {
return 0;
}
Expand All @@ -278,7 +313,7 @@ public <T> int updateProperties(Class<T> type, Object id, Map<String, Object> va
RegisteredEntity meta = resolve(type);
String idProperty = meta.idField()
.getName();
prepareOutbox(eventTopic != null);
prepareOutbox(eventTopic != null || !additionalEvents.isEmpty());
try (Session session = entityManager.getSessionFactory()
.openSession()) {
Transaction tx = session.beginTransaction();
Expand All @@ -293,8 +328,8 @@ public <T> int updateProperties(Class<T> type, Object id, Map<String, Object> va
}
updated = query.setParameter("id", id)
.executeUpdate();
events = outbox.record(session, eventTopic == null || updated == 0 ? List.of()
: eventsOf(eventTopic, readInTransaction(session, type, meta, id), List.of()));
events = outbox.record(session, updated == 0 ? List.of()
: eventsOf(eventTopic, eventTopic == null ? null : readInTransaction(session, type, meta, id), additionalEvents));
tx.commit();
} catch (RuntimeException ex) {
rollback(tx, ex);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,12 +134,9 @@ import org.eclipse.dirigible.components.data.store.java.repository.JavaRepositor
import org.eclipse.dirigible.sdk.component.Component;
import org.eclipse.dirigible.sdk.component.Repository;
#if($groupingKeys && $groupingKeys.size() > 0)
## The rekey notices are the ONLY events this repository still names: the update event itself is
## recorded in the outbox by the base write. The update path records its notice there too (a
## DomainEvent riding the write's transaction); the targeted path publishes directly, which is why
## Producer is still needed here.
## The rekey notices are the ONLY events this repository still names; both paths record them in the
## outbox as DomainEvents riding the write's own transaction, so no bare publish remains here.
import org.eclipse.dirigible.components.data.store.java.repository.DomainEvent;
import org.eclipse.dirigible.sdk.messaging.Producer;
import org.eclipse.dirigible.sdk.utils.Json;
#end
#if(($documentChecks && $documentChecks.size() > 0) || $rollupGuard || ($guardChecks && $guardChecks.size() > 0) || $lifecycleEdges)
Expand Down Expand Up @@ -442,9 +439,9 @@ public class ${name}Repository extends JavaRepository<${name}Entity> {
* <p>
* Still event-free for the reactions: the only event it can publish is {@code "-rekeyed"}, and only
* when the write MOVED the row between groups - a topic nothing but the generated aggregate /
* roll-up handlers subscribes to. Those two notices are published directly rather than recorded in
* the outbox: the targeted write carries at most one outbox topic, and this path needs two bodies
* (the row as it stood, and the row as written).
* roll-up handlers subscribes to. Both notices are recorded in the outbox with the write itself,
* so the mutation and the recompute signals commit together - a broker outage delays the
* recompute instead of losing it.
#end
*/
@Override
Expand Down Expand Up @@ -531,18 +528,39 @@ public class ${name}Repository extends JavaRepository<${name}Entity> {
#end
}
#end
#if($groupingKeys && $groupingKeys.size() > 0)
// BOTH sides of the move, on the one topic only the aggregate / roll-up handlers listen to -
// so no other reaction sees a second event, which is why this path can signal them without
// re-publishing "-updated". The previous row names the group the row LEFT (recomputed without
// it, its total drops instead of staying stale); the written row names the one it moved INTO,
// which on the targeted path gets no event of its own at all. Handed to the base write so both
// notices commit with the mutation itself - a broker outage delays the recompute instead of
// losing it - and recorded only when the row actually existed to be written.
java.util.List<DomainEvent> rekeyEvents = groupingMoved
? java.util.List.of(new DomainEvent("${projectName}-${perspectiveName}-${name}-rekeyed", groupingPrevious),
new DomainEvent("${projectName}-${perspectiveName}-${name}-rekeyed", Json.stringify(entity)))
: java.util.List.of();
#end
#if($history == "true")
// Change history for the targeted write: it is how the system writes (a roll-up total, a
// workflow write-back, a stamped number), so the trail attributes it to SYSTEM - a person
// reading the record must be able to tell those apart from an edit somebody made.
${name}Entity historyBefore = super.findById(id);
#if($groupingKeys && $groupingKeys.size() > 0)
int updatedCount = super.updateProperties(id, values, eventTopic, rekeyEvents);
#else
int updatedCount = super.updateProperties(id, values, eventTopic);
#end
if (updatedCount > 0) {
History.recordUpdate(HISTORY_TABLE, id, History.SYSTEM, historyBefore, super.findById(id), HISTORY_PROPERTIES);
}
#else
#if($groupingKeys && $groupingKeys.size() > 0)
int updatedCount = super.updateProperties(id, values, eventTopic, rekeyEvents);
#else
int updatedCount = super.updateProperties(id, values, eventTopic);
#end
#end
#if($documentItem)
if (updatedCount > 0 && (values.containsKey("${documentItem.fkProperty}")#foreach($f in $documentItem.fields) || values.containsKey("${f.field}")#end)) {
// A line written by a TARGETED primitive - a workflow setField, a glue updateProperty /
Expand All @@ -563,17 +581,6 @@ public class ${name}Repository extends JavaRepository<${name}Entity> {
new ${documentItem.parentEntity}Repository().recalculate(documentBefore);
}
}
#end
#if($groupingKeys && $groupingKeys.size() > 0)
if (updatedCount > 0 && groupingMoved) {
// BOTH sides of the move, on the one topic only the aggregate / roll-up handlers listen to -
// so no other reaction sees a second event, which is why this path can signal them without
// re-publishing "-updated". The previous row names the group the row LEFT (recomputed without
// it, its total drops instead of staying stale); the written row names the one it moved INTO,
// which on the targeted path gets no event of its own at all.
Producer.sendToTopic("${projectName}-${perspectiveName}-${name}-rekeyed", groupingPrevious);
Producer.sendToTopic("${projectName}-${perspectiveName}-${name}-rekeyed", Json.stringify(entity));
}
#end
return updatedCount;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,8 +126,9 @@ public class ${className}Resolve implements MessageHandler {
* longer destroy the identification, and the audit trace is the LAST thing a failure can take.
*
* Nothing else of the record is touched (both writes are targeted), so no "-updated" re-fires and a
* concurrent write to any other column cannot be reverted. A targeted write also publishes NO event,
* so a status that IS written announces itself - see the "-transitioned" publish below.
* concurrent write to any other column cannot be reverted. A plain targeted write publishes NO
* event, so the routing write carries the "-transitioned" topic itself - the flip and its
* announcement commit together through the outbox.
*/
private static void stamp(Object id, String outcome, Integer resolved#if($writesStatus == "true"), Integer status#end) {
${entity}Repository repository = new ${entity}Repository();
Expand All @@ -144,21 +145,16 @@ public class ${className}Resolve implements MessageHandler {
#if($writesStatus == "true")
if (status != null) {
try {
repository.updateProperty(id, "${statusProperty}", status);
// The status this lookup just routed the record to IS a transition, and the constructs
// that react to one - `generates:` and `postings:` bound to `event: { onTransition: ... }`
// - listen on "-transitioned". The targeted write above publishes nothing at all, so
// without this the AUTOMATIC path silently did nothing while the manual one (a
// `transitions:` button, which does publish) worked: exactly the wrong way round, and with
// no log line to show it. Inside the try on purpose - a status the record could not take
// is not a transition, so a rejected move must announce nothing. Reload so the payload
// carries the committed row rather than the pre-write snapshot, the same shape the
// transition controller and the create-from completion hook publish.
${entity}Entity transitioned = repository.findById(id);
if (transitioned != null) {
org.eclipse.dirigible.sdk.messaging.Producer.sendToTopic("${projectName}-${perspective}-${entity}-transitioned",
Json.stringify(transitioned));
}
// - listen on "-transitioned". A plain targeted write publishes nothing at all, so the
// topic rides the routing write into the outbox: the flip and its announcement commit
// together, the payload is the row exactly as the statement left it, and a broker outage
// delays the announcement instead of losing it. Inside the try on purpose - a status the
// record could not take is not a transition, so a rejected move must announce nothing,
// and a rejected write records no event either.
repository.updateProperties(id, java.util.Map.of("${statusProperty}", status),
"${projectName}-${perspective}-${entity}-transitioned");
} catch (org.eclipse.dirigible.sdk.db.ValidationException rejected) {
// The record cannot take this status where it currently stands - an unmodeled lifecycle
// move, or a gate the status would trip. Retrying cannot help (nothing about the record
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,11 @@ import org.eclipse.dirigible.sdk.utils.Json;
* Transition ${name}: the guarded on-demand status flip on a ${entity} (loaded by the id in the
* posted body). The record must currently be in one of the allowed statuses [${fromStatuses}]#if($guardExpr != "")
* and satisfy the guard [${guardText}]#end; otherwise 409 and the record stays untouched. On success
* ONLY the status column is written - through the targeted updateProperty primitive, a
* ONLY the status column is written - through the targeted updateProperties primitive, a
* workflow-style system write (no "-updated" re-fire; a full-row update would merge the stale
* pre-check snapshot and revert concurrent writes) - and the "-transitioned" topic is published so
* posting glue / integrations observe the transition.
* pre-check snapshot and revert concurrent writes) - and the "-transitioned" notice rides that write
* into the outbox, so the flip and its announcement commit together and posting glue / integrations
* can never miss a transition whose row is durable.
*
* Generated from the intent transitions block - do not edit; it is re-generated with the application.
* Entity access goes ONLY through the generated repositories.
Expand Down Expand Up @@ -69,11 +70,14 @@ public class ${className}Transition {
return "{\"error\": \"${name} requires ${guardText}\"}";
}
#end
repository.updateProperty(req.id, "${statusProperty}", ${setStatus});
// Reload so the "-transitioned" payload carries the committed row, not the pre-check snapshot.
// The status write and its "-transitioned" notice commit together: the topic rides the
// targeted write into the outbox, so a broker outage delays the announcement instead of losing
// it, and the event payload is the row exactly as the statement left it - never a re-read a
// concurrent write could have moved on.
repository.updateProperties(req.id, java.util.Map.of("${statusProperty}", ${setStatus}),
"${projectName}-${perspective}-${entity}-transitioned");
// Reload so the response (and the notify block) carries the committed row, not the pre-check snapshot.
source = repository.findById(req.id);
org.eclipse.dirigible.sdk.messaging.Producer.sendToTopic(
"${projectName}-${perspective}-${entity}-transitioned", Json.stringify(source));
#if($notify == "true")
sendNotification(source);
#end
Expand Down
Loading
Loading