feat(intent): a notify body carries links, and the intent never spells a route (#6553) - #6744
Merged
Merged
Conversation
…s a route (#6553) A notification that cannot be acted on gets ignored, and until now "you have an approval waiting" could only carry a link by hand-typing one: body: "Review it here: {appUrl}/services/web/orders/gen/orders/index.html#/Order/{id}/edit" That is the generated application's URL layout, typed into the one artifact that is forbidden to know it - correct until a template changes the layout, and silently wrong afterwards. {appUrl} (#6642) supplies only the origin, so it was the author's job to append the rest. Two reserved tokens finish the job the issue asked for - a link to the record or to the task: body: "Approve it here: {recordUrl}\nEverything waiting on you: {inboxUrl}" The split is the design. NotificationSupport resolves each token to a BARE JAVA IDENTIFIER and records the use; NotifySupport.deepLinkFields carries the only facts the intent layer owns - the entity and its key property, model facts and not paths - plus a uses* flag per link; and the events TEMPLATE declares the local, composing <appUrl>/services/web/<project>/gen/<genFolder>/index.html #/<Entity>/<id>/edit from parameters it already has. So the route lives in the layer that knows it (the same reason Trigger.java.template, not the generator, assembles __entityUrl), the path-agnostic rule holds, and a route change stays a template change. All four notify call sites picked it up from the two shared seams (buildNotifications, buildSchedules, and notifyFields -> transitions + sends), so none can be forgotten. A link is declared only where the message names it - no dead local in generated code - and inside a fan-out {recordUrl} links the ROW, like every other bare path, while {record.<field>} keeps reading the anchor. Covered by NotificationSupportTest (both tokens, and the unused one not reported), IntentEngineIT (the composed locals at the notification + schedule sites, the unused link NOT declared, and recordUrl never falling through to a non-existent entity property) and IntentEmissionCoverageIT - where it is also the compile proof, since an undeclared local fails the whole client-Java batch and every REST assertion in that gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #6553.
Problem
A notification that cannot be acted on is a notification that gets ignored — "you have an approval waiting" without a link is noise.
{appUrl}(#6642) gave the notify block the application's origin, which left the author to type the rest:That is the generated application's URL layout, hand-written into the one artifact that is forbidden to know it. It is correct until a template changes the layout, and silently wrong afterwards — and the issue explicitly asks for the opposite: "the concrete route should be assembled by the template layer (which knows the generated paths), keeping the intent layer path-agnostic."
Fix
Two reserved link tokens, usable in
subject:/body:at every notify call site — the record link and the task link the issue names:{recordUrl}{inboxUrl}{appUrl}The split is the whole design.
NotificationSupport.Resolverresolves each token to a bare Java identifier and records the use.NotifySupport.deepLinkFieldscarries the only facts the intent layer owns —recordUrlEntity/recordUrlKeyProperty(model facts, not paths) — plus auses*flag per link. The events template declares the local:So the route is composed in the layer that knows it — the same reason
Trigger.java.template, not the generator, assembles__entityUrlfor the task form — the path-agnostic rule holds, and a change to the generated app's URL layout stays a template change alone.Notes
buildNotifications,buildSchedules, andnotifyFields→ transitions + sends), so no call site can be forgotten.{recordUrl}links the ROW, like every other bare path in the block;{record.<field>}keeps reading the anchor for values.DIRIGIBLE_APP_BASE_URL, tenant-overridable, read per dispatch in the sending tenant's configuration scope (#6649).Tests
NotificationSupportTest— both tokens resolve to the bare locals and are reported; an unreferenced link is not reported.IntentEngineIT— the composed locals at the notification and schedule call sites, the unused link not declared, andrecordUrlnever falling through to a non-existententity.RecordUrl.IntentEmissionCoverageIT— the transition and the fan-out send, including that a fan-out links the ROW (#/BillRecipient/) and not the anchor. This gate is also the compile proof: an undeclared local fails the whole client-Java batch and takes every REST assertion with it.Both ITs green locally (
IntentEngineIT46/46,IntentEmissionCoverageIT1/1), plus the 543 unit tests of the two changed modules.Docs
dirigible-io.github.io— PR #185 (mine to merge).IntentFile/intent-specification+IntentFile/intentfile.github.io— PRs opened for the maintainer; the spec did not document the link placeholders at all, so both introduce the whole family.🤖 Generated with Claude Code