Skip to content

feat(intent): a declarative fileName pattern names both server-side renders (#6899) - #6904

Merged
delchev merged 1 commit into
masterfrom
feat/6899-filename-pattern
Aug 22, 2026
Merged

feat(intent): a declarative fileName pattern names both server-side renders (#6899)#6904
delchev merged 1 commit into
masterfrom
feat/6899-filename-pattern

Conversation

@delchev

@delchev delchev commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

Fixes #6899.

The problem

The two server-side PDF renders named their files with hardcoded, mutually inconsistent expressions:

  • snapshot mint"<Entity> <id> v<n>.pdf", i.e. the numeric primary key, even though the mint runs after the number: stamp, so the document number was right there;
  • mail attachment (attach: print) — the first number: field with an id fallback.

So one document reached the archive and the customer's inbox under two different names, and neither was configurable. A real archive wants a self-describing name: ORD0000042_20260822_MyCompany_AcmeLtd.pdf.

The change

A fileName: pattern on the function: Snapshot child and inside a notify block (with attach: only — a plain-text message has no file to name). Literal text plus {token} interpolations; no expression language.

- name: SalesInvoiceCopy
  function: Snapshot
  fileName: "{number}_{date:yyyyMMdd}_{company.shortName|company.name}"
Token Renders
{field} a field of the rendered record
{relation.field} a field of a one-hop to-one relation
{field:pattern} a date/timestamp through a DateTimeFormatter pattern
{A|B} the first non-blank operand, left to right
{Version} the copy's version — a snapshot only

Tokens use the authored field/relation names, exactly as a notify subject does (the issue's PascalCase sketch is illustrative; the contract it names — "the same as notify interpolation" — is what shipped, so an author does not have to learn a second casing rule).

New FileNameSupport translates a pattern once into a Java expression plus the one-hop relation loads it reads. The new SDK helper sdk.print.FileNames sanitizes every interpolated value at run time: trim, whitespace → one _, path/control characters (/ \ : * ? " < > |) stripped, repeats collapsed. Non-ASCII is deliberately kept — a local-language document legitimately carries a non-Latin name, and keeping names Latin is an application data convention, not the platform's guess. The literal separators between tokens are the author's and are emitted verbatim.

Three things worth a reviewer's attention:

  1. The pattern's relation loads are merged into the notify block's own, deduped by local. Both name the local after the relation, so declaring it twice would not compile — and this is what lets a name read a relation the message text never mentions (SendBill in the IT does exactly that: a literal recipient, {Person.name} in the file name).
  2. attach: recordPrint refuses a relation hop. That document is rendered once, in renderDocument(source) before the per-row loop, where the block's relation locals do not exist. Same one-field rule the record. scope already has, and rejected at parse rather than emitted as a read of an undeclared local.
  3. The Snapshot delegate now always loads its master (the name is a property of the document, not of the copy) and returns early when it is gone — previously it only loaded it under languageFrom.

Called-out behaviour change: absent a pattern, the snapshot default changes from the primary-key form to the same number-or-id expression the mail uses, plus the version. That is the point — the two finally agree. Already-stored copies are untouched.

Rejected at parse/generation, never rendered empty: an unknown field/relation, a multi-hop path, unbalanced or nested braces, a format on a non-date field, a format DateTimeFormatter won't accept, a pattern that interpolates nothing, and {Version} on a mailed copy. A silently-dropped token would produce archive names nobody can tell apart, which is the failure being replaced.

Tests

  • FileNamesTest (12) — the run-time sanitizer: whitespace, path/control characters, a stripped character not leaving a separator run, non-ASCII kept, both date shapes, a non-date value degrading, and first(...).
  • FileNameIntentTest (16) — every rejection above plus the accepted full pattern on both sides.
  • GlueFileNameTest (9) — the emitted expressions, the version-suffix rules, the merged/deduped loads, and both defaults asserted as exact strings, agreeing with each other.
  • IntentEngineIT (58 green) — OrderCopy gains fileName: "{orderDate:yyyyMMdd}_{customer.name}"; asserts the load off the document, the SDK date call, the hop read, the version suffix, and that the old primary-key name is gone.
  • IntentEmissionCoverageIT (green) — SendBill gains fileName: "BILL_{note|Person.name}". That project's client-Java compile is the compile proof for the emitted expression (an undeclared local fails the whole batch and takes every REST assertion with it).

Verified: formatter:validate clean, mvn -P release javadoc clean on api-modules-java + engine-intent, 852 + 28 + 77 unit tests green, both ITs green headless.

Docs

🤖 Generated with Claude Code

…enders (#6899)

The two PDF renders named their files with hardcoded, mutually inconsistent
expressions. The snapshot mint used "<Entity> <id> v<n>.pdf" - the numeric
PRIMARY KEY, even though the mint runs after the `number:` stamp, so the
document number was available - while `attach: print` already used the number.
One document reached the archive and the customer's inbox under two different
names, and neither was configurable. A real archive wants a self-describing
name: ORD0000042_20260822_MyCompany_AcmeLtd.pdf.

A `fileName:` pattern is now authorable on the `function: Snapshot` child and
inside a `notify` block (with `attach:` only - a plain-text message has no file
to name): literal text plus {token} interpolations, no expression language.

  fileName: "{number}_{date:yyyyMMdd}_{company.shortName|company.name}"

- {field} / one-hop {relation.field} - the SAME path vocabulary and authored
  names a notify subject resolves, against the same record.
- {field:pattern} - a date/timestamp through a DateTimeFormatter pattern.
- {A|B} - alternative operands, first non-blank wins (an optional short name
  beside the legal name is filled for some records and not others).
- {Version} - a snapshot only; a pattern without it gets _v<n> appended, so two
  versions of a copy never share a name.

New FileNameSupport translates a pattern once into a Java expression plus the
one-hop relation loads it reads; the new SDK helper sdk.print.FileNames
sanitizes every interpolated VALUE at run time (trim, whitespace -> one `_`,
path/control characters stripped, non-ASCII deliberately KEPT - a local-language
document legitimately carries a non-Latin name, and keeping names Latin is an
application data convention, not the platform's guess). The literal separators
between tokens are the author's and are emitted verbatim.

Three things worth knowing:

- The pattern's relation loads are MERGED into the notify block's own, deduped
  by local: both name the local after the relation, so declaring it twice would
  not compile - and a pattern may read a relation the message text never
  mentions.
- `attach: recordPrint` renders the anchor once, before the per-row loop, where
  those locals do not exist, so a relation hop is refused there - the same
  one-field rule the `record.` scope already has.
- The Snapshot delegate now always loads its master (the name is a property of
  the DOCUMENT, not of the copy) and returns early when it is gone.

Absent a pattern the snapshot default CHANGES from the primary-key form to the
same number-or-id expression the mail uses, plus the version - a deliberate,
called-out behaviour change that finally makes the two agree. Already-stored
copies are untouched.

An unknown field/relation, a multi-hop path, an unbalanced or nested brace, a
format on a non-date field, an invalid DateTimeFormatter pattern, a pattern that
interpolates nothing and {Version} on a mailed copy are all validation errors: a
token that silently rendered empty would produce archive names nobody can tell
apart, which is the failure this replaces.

Covered by FileNamesTest (the run-time sanitizer), FileNameIntentTest (every
rejection), GlueFileNameTest (the emitted expressions, the merged loads, both
defaults agreeing), IntentEngineIT (OrderCopy's date-modifier + relation-hop
pattern in the generated mint) and IntentEmissionCoverageIT (SendBill's {A|B}
pattern, whose client-Java compile also proves the emitted expression compiles).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
*/
private static PrintAttachment languageFromAttachment(String path, EntityIntent entity, Map<String, EntityIntent> byName,
Map<String, String> compositionParents, NotificationSupport.CrossModelLookup crossModel, boolean anchorScoped, String local) {
Map<String, String> compositionParents, NotificationSupport.CrossModelLookup crossModel, boolean anchorScoped, String local,
@delchev
delchev merged commit d235976 into master Aug 22, 2026
10 checks passed
@delchev
delchev deleted the feat/6899-filename-pattern branch August 22, 2026 07:22
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.

intent: declarative fileName pattern for snapshot mints and attach: print - today two hardcoded, inconsistent names

2 participants