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 @@ -773,6 +773,53 @@ static List<Map<String, Object>> buildTransitionsForTest(IntentModel model) {
return buildTransitions(model, IntentEntities.byName(model), IntentEntities.compositionParents(model), IntentSettings.parse("{}"));
}

/**
* Build the {@code posts} glue collection: one descriptor per {@code posts:} rule
* ({@link org.eclipse.dirigible.components.intent.model.PostIntent}). Each descriptor drives a
* generated event handler that, on the source document's {@code event:} event, emits mapped rows
* into the {@code into:} target (per {@code forEach:} line item), idempotently by
* {@code idempotentBy}. See kf-catalog PROPOSAL_EVENT_POSTING.md. This is the structural glue; the
* handler template + BPMN/listener wiring is the next stage.
*/
private static List<Map<String, Object>> buildPosts(IntentModel model, Map<String, EntityIntent> byName) {
List<Map<String, Object>> out = new ArrayList<>();
for (org.eclipse.dirigible.components.intent.model.PostIntent p : model.getPosts()) {
if (p.getName() == null || p.getName()
.isBlank()
|| p.getForEntity() == null || p.getInto() == null || p.getEvent() == null) {
continue; // malformed: name/forEntity/into/on are required
}
if (byName.get(p.getForEntity()) == null) {
continue; // bad source reference
}
Map<String, Object> e = new LinkedHashMap<>();
e.put("name", p.getName());
e.put("className", IntentNaming.pascalIdentifier(p.getName()));
e.put("entity", p.getForEntity());
e.put("event", p.getEvent());
e.put("forEach", p.getForEach() == null ? "" : p.getForEach());
e.put("into", p.getInto());
e.put("idempotentBy", p.getIdempotentBy() == null ? "" : p.getIdempotentBy());
e.put("guard", p.getGuard() == null ? "" : p.getGuard());
List<Map<String, String>> set = new ArrayList<>();
for (Map.Entry<String, String> f : p.getSet()
.entrySet()) {
Map<String, String> pair = new LinkedHashMap<>();
pair.put("field", IntentNaming.pascalCase(f.getKey()));
pair.put("value", f.getValue());
set.add(pair);
}
e.put("set", set);
out.add(e);
}
return out;
}

/** Test hook: build the {@code posts} glue collection without a repository. */
static List<Map<String, Object>> buildPostsForTest(IntentModel model) {
return buildPosts(model, IntentEntities.byName(model));
}

/**
* Test hook: build the {@code generates} glue collection without a repository. With a null context
* a cross-model target falls back to {@link CrossModelSupport}'s naming-convention defaults
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,23 @@ public class IntentModel {
private List<PostingIntent> postings = new ArrayList<>();
/** Declarative on-demand status transitions - guarded per-record buttons (void/cancel/close). */
private List<TransitionIntent> transitions = new ArrayList<>();
/**
* Declarative event-driven row posts - emit mapped rows into a target entity on a document event.
*/
private List<PostIntent> posts = new ArrayList<>();

public List<ActionIntent> getActions() {
return actions;
}

public List<PostIntent> getPosts() {
return posts;
}

public void setPosts(List<PostIntent> posts) {
this.posts = posts == null ? new ArrayList<>() : posts;
}

public List<TransitionIntent> getTransitions() {
return transitions;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* 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.model;

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

/**
* A declarative event-driven row post: on a document event, emit mapped rows into a target entity
* (per line item), idempotently by a back-reference. Replaces the hand-written ledger-posting glue
* (goods docs -> StockMovement, document -> journal line). See kf-catalog
* PROPOSAL_EVENT_POSTING.md.
*
* <p>
* Example (a goods receipt posting one signed movement per line on the POSTED transition):
*
* <pre>
* posts:
* - name: goodsReceiptLedger
* forEntity: GoodsReceipt
* event: POSTED # a document status value, or `create`
* forEach: items # the composition-child collection (omit = one row per master)
* into: StockMovement # the target entity (local or cross-model)
* idempotentBy: GoodsReceipt # the back-reference FK on the target; skip if rows already exist
* guard: negativeStock # optional precondition; a violation aborts the event
* set:
* Product: item.Product # item.&lt;field&gt;
* Store: Receipt.Store # &lt;Master&gt;.&lt;field&gt;
* Quantity: item.Quantity # or a Calc expression (e.g. -item.Quantity)
* GoodsReceipt: Receipt.Id # the back-reference (matches idempotentBy)
* </pre>
*/
public class PostIntent {

/** Stable name of this post rule (also the generated handler class stem). */
private String name;

/** The source document entity this post fires for. */
private String forEntity;

/**
* The trigger EVENT: a document status value (e.g. {@code POSTED}) after that transition, or
* {@code create}. (Named `event` not `on` - `on` is a YAML 1.1 boolean keyword.)
*/
private String event;

/**
* The composition-child collection to iterate; {@code null} = emit one row per the source record.
*/
private String forEach;

/** The target entity rows are emitted into (local or cross-model). */
private String into;

/**
* The back-reference FK on the target: the generated handler writes it and skips when target rows
* already reference this source.
*/
private String idempotentBy;

/**
* Optional named precondition evaluated before any row is written; a violation aborts the event.
*/
private String guard;

/**
* Field map for each emitted row: target field -> value (a constant, {@code Master.field},
* {@code item.field}, or a Calc expression).
*/
private Map<String, String> set = new LinkedHashMap<>();

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public String getForEntity() {
return forEntity;
}

public void setForEntity(String forEntity) {
this.forEntity = forEntity;
}

public String getEvent() {
return event;
}

public void setEvent(String event) {
this.event = event;
}

public String getForEach() {
return forEach;
}

public void setForEach(String forEach) {
this.forEach = forEach;
}

public String getInto() {
return into;
}

public void setInto(String into) {
this.into = into;
}

public String getIdempotentBy() {
return idempotentBy;
}

public void setIdempotentBy(String idempotentBy) {
this.idempotentBy = idempotentBy;
}

public String getGuard() {
return guard;
}

public void setGuard(String guard) {
this.guard = guard;
}

public Map<String, String> getSet() {
return set;
}

public void setSet(Map<String, String> set) {
this.set = set == null ? new LinkedHashMap<>() : set;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* 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 static org.junit.jupiter.api.Assertions.assertEquals;

import java.util.List;
import java.util.Map;

import org.eclipse.dirigible.components.intent.model.IntentModel;
import org.eclipse.dirigible.components.intent.parser.IntentParser;
import org.junit.jupiter.api.Test;

/**
* Verifies the {@code posts} glue the {@link GlueIntentGenerator} emits from a {@code posts:} rule:
* the source entity + event + per-item collection + target + idempotency back-reference, and the
* field map (including a sign-flip Calc expression). Structural glue - the handler template + BPMN
* wiring is a later stage (kf-catalog PROPOSAL_EVENT_POSTING.md).
*/
class GluePostsTest {

private static final String YAML = """
name: inventory
entities:
- name: StockMovement
fields:
- { name: id, type: integer, primaryKey: true, generated: true }
- { name: quantity, type: decimal }
relations:
- { name: Product, kind: manyToOne, to: Product }
- { name: GoodsIssue, kind: manyToOne, to: GoodsIssue }
- name: Product
fields:
- { name: id, type: integer, primaryKey: true, generated: true }
- name: GoodsIssue
fields:
- { name: id, type: integer, primaryKey: true, generated: true }
relations:
- { name: Store, kind: manyToOne, to: Product }
- name: GoodsIssueItem
fields:
- { name: id, type: integer, primaryKey: true, generated: true }
- { name: quantity, type: decimal }
relations:
- { name: GoodsIssue, kind: manyToOne, to: GoodsIssue, composition: true }
- { name: Product, kind: manyToOne, to: Product }
posts:
- name: goodsIssueLedger
forEntity: GoodsIssue
event: POSTED
forEach: items
into: StockMovement
idempotentBy: GoodsIssue
guard: negativeStock
set:
Product: item.Product
Quantity: "-item.Quantity"
GoodsIssue: Issue.Id
""";

@SuppressWarnings("unchecked")
@Test
void emitsThePostGlueDescriptor() {
IntentModel model = IntentParser.parse(YAML);
List<Map<String, Object>> posts = GlueIntentGenerator.buildPostsForTest(model);
assertEquals(1, posts.size());
Map<String, Object> p = posts.get(0);

assertEquals("goodsIssueLedger", p.get("name"));
assertEquals("GoodsIssueLedger", p.get("className"));
assertEquals("GoodsIssue", p.get("entity"));
assertEquals("POSTED", p.get("event"));
assertEquals("items", p.get("forEach"));
assertEquals("StockMovement", p.get("into"));
assertEquals("GoodsIssue", p.get("idempotentBy"));
assertEquals("negativeStock", p.get("guard"));

List<Map<String, String>> set = (List<Map<String, String>>) p.get("set");
assertEquals(3, set.size());
// field names are pascal-cased; values (incl. the sign-flip expression) pass through verbatim.
assertEquals(Map.of("field", "Product", "value", "item.Product"), set.get(0));
assertEquals(Map.of("field", "Quantity", "value", "-item.Quantity"), set.get(1));
assertEquals(Map.of("field", "GoodsIssue", "value", "Issue.Id"), set.get(2));
}
}
Loading