Skip to content

Intent declarative glue: notifications (event → email @Listener) - #6052

Merged
delchev merged 3 commits into
masterfrom
intent-glue-notifications
Jun 19, 2026
Merged

Intent declarative glue: notifications (event → email @Listener)#6052
delchev merged 3 commits into
masterfrom
intent-glue-notifications

Conversation

@delchev

@delchev delchev commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

What

First item of the declarative-glue catalog (see engine-intent/CLAUDE.md"Planned: declarative glue"): a notifications: block in the intent that sends mail on an entity lifecycle event — generated as an annotated client-Java @Listener, no hand-written code.

notifications:
  - name: orderUpdated
    event: { onUpdate: Order }          # onCreate | onUpdate | onDelete (+ optional when:)
    to: ops@example.com                 # literal address or a direct field of the entity
    subject: "Order {id} updated, total {total}"
    body: "The order changed."

→ generates gen/events/OrderUpdatedNotification.java: an @Listener bound to the entity's …-updated topic that sends via sdk.mail.Mail (sender from DIRIGIBLE_MAIL_SENDER).

How

  • Parse + validate (NotificationIntent + IntentParser): unique name; exactly one of onCreate/onUpdate/onDelete on a declared entity; supported channel; recipient present.
  • NotificationSupport (pure, unit-tested) translates the author fields into the Java the listener pastes in — recipient (literal or entity.<Field>), {field} interpolation, and a single-comparison when guard.
  • GlueIntentGenerator emits a notifications collection into <intent>.glue; Notification.java.template renders the listener; generateUtils.js gets the notifications collection case. Honors the .settings overrides.notifications.<name>.generate=false switch.

Notes / decisions

  • The event key is event:, not on: — YAML 1.1 resolves a bare on (also off/yes/no) to boolean true, which silently emptied the binding. IntentEngineIT caught it.
  • v1 scope: to/placeholders resolve direct fields or literals; when is a single field ==|!= literal. Relation-path recipients (member.email) are rejected by the parser with a clear message and are the next increment (they reuse the decision-resolver machinery).
  • Also restores the "Planned: declarative glue" plan section to CLAUDE.md (it was lost in the Intent Editor: AI assistant (Claude chat + patch preview) #6040 squash merge).

Testing

  • NotificationSupportTest — 4 unit tests for the expression translation (green locally).
  • IntentEngineIT.glue_template_generates_… extended to assert the generated OrderUpdatedNotification.java (topic, Mail.send, recipient, subject interpolation, lowercased-package import) — green locally (Tests run: 1, Failures: 0). CI runs the full suite on H2/PostgreSQL/MSSQL.

Depends on / relates to

🤖 Generated with Claude Code

delchev and others added 3 commits June 19, 2026 15:29
…lue)

First increment of the declarative-glue catalog (see engine-intent CLAUDE.md
"Planned: declarative glue"). Adds the NotificationIntent model and parses a
top-level `notifications:` block; validates, in one pass with the other blocks:
unique name, exactly one entity lifecycle event (onCreate/onUpdate/onDelete)
referencing a declared entity, a supported channel (email), and a recipient.

Parse-only for now (mirrors how triggers/resolvers landed validation-first);
the generator that emits the annotated client-Java @Listener + Mail glue is the
next increment.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lue)

Second increment of the notifications pattern: turn a parsed `notifications:`
block into an annotated client-Java @Listener that sends mail on an entity
event - no hand-written code.

- NotificationSupport (pure, unit-tested) translates the author-facing fields
  into the Java expressions the listener pastes in: recipient (literal address
  or direct field), {field} subject/body interpolation, and a single-comparison
  `when` guard. v1 supports direct fields + literals; relation-path recipients
  are rejected by the parser with a clear message (next increment).
- GlueIntentGenerator emits a `notifications` collection into <intent>.glue;
  Notification.java.template renders gen/events/<Name>Notification.java
  (@Listener bound to the entity's create / -updated / -deleted topic, sending
  via sdk.mail.Mail, sender from DIRIGIBLE_MAIL_SENDER); generateUtils.js gets
  the `notifications` collection case. Honors the .settings override switch.
- The event-binding key is `event:`, not `on:` - YAML 1.1 resolves a bare `on`
  to boolean true, which silently emptied the binding (caught by IntentEngineIT).

Restores the "Planned: declarative glue" section to engine-intent/CLAUDE.md
(lost in the #6040 squash merge) and marks notifications v1 done.

Covered by NotificationSupportTest (expression logic) and IntentEngineIT
(the notification renders to @Listener Java end to end).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Completes the notifications recipient/interpolation support: `to`, `subject`
and `body` can now reference a one-hop `relation.field` of a to-one relation of
the event entity (e.g. on an Order event, `{customer.name}` or
`to: customer.email`), not just direct fields and literals.

NotificationSupport.plan() resolves each referenced relation to a RelationLoad
(reusing the decision-resolver one-hop convention: FK property = PascalCase
relation, target entity/perspective); the listener loads the related entity
once by FK id (`new <Target>Repository().findById(entity.<Fk>)`) and the
expressions read its field. An unresolvable recipient relation skips the
notification (logged); the parser now only rejects multi-hop `to`.

Covered by NotificationSupportTest (direct/literal, one-hop relation, and the
unresolvable-recipient cases) and IntentEngineIT (the generated listener loads
the related Customer and the subject reads customer.Name) - green locally.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@delchev

delchev commented Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Update: added one-hop relation.field resolution (commit b7e0480). The "v1 scope" in the description is now broader — to/subject/body resolve a literal, a direct field, or a one-hop relation.field of a to-one relation (the listener loads the related entity once by FK id, reusing the decision-resolver convention). E.g. on an Order event: to: customer.email, subject: "Order for {customer.name}".

Remaining gap: multi-hop paths (a.b.c) and relation-path when: guards — the parser rejects multi-hop to with a clear message. Verified by NotificationSupportTest (5) + IntentEngineIT (the generated listener loads the related Customer and reads customer.Name).

@delchev
delchev merged commit 819e07d into master Jun 19, 2026
9 of 12 checks passed
@delchev
delchev deleted the intent-glue-notifications branch June 19, 2026 16:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant