Lumnik Hooks Cookbook¶
10 recipes covering every hook pattern.
Illustrative entities.
BusinessPartner/Repairused throughout are example class names — no such module ships with lumnik. Substitute the entity of your own module; the hook mechanics (manifest, bindings, context API) are what each recipe demonstrates.
Recipe 1: BEFORE_CREATE lifecycle hook — block invalid data¶
Pattern: Cancel a create operation when a business rule is violated.
Use case: Block BusinessPartner creation if creditLimit > 100000 and the user lacks lm_credit_approver role.
hooks.yaml:
- id: credit-limit-check
target: io.lumnik.businesspartner.partner.BusinessPartner
events: [BEFORE_CREATE, BEFORE_UPDATE]
handler:
type: java
class: io.lumnik.custom.partner.CreditLimitHook
Handler:
@ApplicationScoped
public class CreditLimitHook implements LifecycleHook<BusinessPartner> {
private static final BigDecimal THRESHOLD = new BigDecimal("100000");
@Override public void onBeforeCreate(BusinessPartner p, HookContext ctx) { check(p, ctx); }
@Override public void onBeforeUpdate(BusinessPartner p, HookContext ctx) { check(p, ctx); }
private void check(BusinessPartner p, HookContext ctx) {
if (p.getCreditLimit() != null
&& p.getCreditLimit().compareTo(THRESHOLD) > 0
&& !ctx.user().hasRole("lm_credit_approver")) {
ctx.fail("credit_limit_exceeded",
"Credit limit > " + THRESHOLD + " requires lm_credit_approver role");
}
}
}
Expected: POST /api/tenants/{tid}/partners with creditLimit: 200000 → 409 credit_limit_exceeded.
Recipe 2: BEFORE_CREATE — happy path¶
Pattern: Same hook, values within the allowed range pass through.
Expected: POST /api/tenants/{tid}/partners with creditLimit: 5000 → 201.
Recipe 3: BEFORE action hook — audit on status change¶
Pattern: Fire logic before an @ActionHook-annotated method executes.
Use case: Audit-log every repair status change.
hooks.yaml:
- id: repair-status-change-audit
action: repair.status-change
when: BEFORE
handler:
type: java
class: io.lumnik.custom.repair.RepairStatusChangeAuditHook
Handler:
@ApplicationScoped
public class RepairStatusChangeAuditHook implements ActionHandler {
@Override
public void handle(Object payload, HookContext ctx) {
ctx.log("Action repair.status-change BEFORE invoked",
Map.of("payload", payload == null ? "null" : payload.toString()));
}
}
Expected: POST /api/tenants/{tid}/repairs/{id}/status → 200 (hook logs and does not block).
Recipe 4: AFTER_COMPLETE lifecycle hook — post-completion side effect¶
Pattern: Run code after a document completes (e.g., notify, publish event).
Use case: Log and publish repair.completed event when a repair moves to TERMINE.
hooks.yaml:
- id: repair-complete-notify
target: io.lumnik.repair.Repair
events: [AFTER_COMPLETE]
handler:
type: java
class: io.lumnik.custom.repair.RepairCompleteHook
Handler:
@ApplicationScoped
public class RepairCompleteHook implements LifecycleHook<Repair> {
@Override
public void onAfterComplete(Repair r, HookContext ctx) {
ctx.log("Repair " + (r.getRef() != null ? r.getRef() : "?")
+ " completed for " + (r.getClientName() != null ? r.getClientName() : "?"));
ctx.event("repair.completed", Map.of(
"repairId", r.getId(),
"ref", r.getRef() != null ? r.getRef() : "",
"clientName", r.getClientName() != null ? r.getClientName() : "",
"tenantId", ctx.tenantId()));
}
}
Expected: POST /api/tenants/{tid}/repairs/{id}/status with status: TERMINE → 200, hook fires.
Recipe 5: Callout — suggest priority for CRITICAL equipment¶
Pattern: Field-level reactive logic triggered by the frontend.
Use case: When the equipment field of a repair changes, suggest URGENT priority if it contains "CRITICAL".
hooks.yaml:
callouts:
- id: repair-equipment-changed
entity: io.lumnik.repair.Repair
field: equipment
handler:
type: java
class: io.lumnik.custom.repair.RepairEquipmentCallout
Handler:
@ApplicationScoped
public class RepairEquipmentCallout implements Callout<Repair> {
@Override
public CalloutResult onChange(Repair r, HookContext ctx) {
if (r.getEquipment() != null && r.getEquipment().toUpperCase().contains("CRITICAL"))
return CalloutResult.patch(Map.of("priority", "URGENT"));
return CalloutResult.patch(Map.of("priority", "NORMAL"));
}
}
Expected: POST /api/callouts/repair/Repair/equipment with {"equipment":"Server CRITICAL unit"} → 200, patch.priority = URGENT.
Recipe 6: Callout — default path returns NORMAL¶
Pattern: Same callout, non-critical equipment value.
Expected: POST /api/callouts/repair/Repair/equipment with standard equipment → 200, patch.priority = NORMAL.
Recipe 7: Admin endpoint lists all registered hooks¶
Pattern: Introspect the live hook registry.
Expected: GET /api/platform/hooks → 200, response contains credit-limit-check, repair-complete-notify, etc.
curl -H "X-Tenant-ID: 1" http://localhost:8080/api/platform/hooks | jq '.hooks[].id'
Recipe 8: Transactional rollback — blocked create leaves no trace¶
Pattern: Verify that ctx.fail(...) causes a full transaction rollback. The entity must not appear in subsequent queries.
Expected:
1. POST /partners with creditLimit: 999999 → 409
2. GET /partners → list does NOT contain the blocked partner code
Recipe 9: Kotlin script hook — VIP discount log¶
Pattern: Delegate business logic to a hot-reloadable Kotlin script.
hooks.yaml:
- id: vip-discount-rule
target: io.lumnik.businesspartner.partner.BusinessPartner
events: [BEFORE_UPDATE]
handler:
type: kotlin
script: scripts/io/lumnik/custom/VipDiscount.kts
priority: 200
Script (VipDiscount.kts):
import io.lumnik.businesspartner.partner.BusinessPartner
import io.lumnik.hub.hook.HookContext
val ent = bindings["entity"] as BusinessPartner
val ctx = bindings["ctx"] as HookContext
if (ent.name?.contains("VIP", ignoreCase = true) == true) {
ctx.log("VIP discount eligible for ${ent.code}")
}
Expected: PUT /partners/{id} where partner name contains "VIP" → 200, no error (script only logs).
Recipe 10: Admin endpoint — hook detail by ID¶
Pattern: Retrieve a specific hook by its ID.
Expected: GET /api/platform/hooks/credit-limit-check → 200, body contains id, module, enabled: true.
{
"id": "credit-limit-check",
"module": "my-customization",
"enabled": true,
"events": ["BEFORE_CREATE", "BEFORE_UPDATE"],
...
}