feat(intent): manyToMany materialises the intermediate entity (#6718) - #6741
Merged
Conversation
`kind: manyToMany` was accepted by the parser and materialised by nothing - no
link table, no UI, no error. An author (or the AI agent) got a clean Generate and
an application with the relationship simply missing: the
authored-but-silently-unconsumed class of failure.
An n:m has always been an intermediate (link) entity here, so the kind that names
one now writes it. ManyToManyExpander runs first thing in IntentParser.validate
and rewrites
- { name: products, kind: manyToMany, to: Product }
into the link entity OrderProduct - a generated integer key, a composition to the
declaring side, a manyToOne to the target (cross-model when the relation carried
model:) - and turns the authored relation into the navigation-only oneToMany to
it. Everything downstream (every validator, every generator, the editor diagram,
the /parse response) sees an ordinary composition + association pair, so the link
gets its table, its FK columns, its detail grid under the declaring entity's page,
and can be seeded, reported on and referenced like any other entity.
It is an expansion rather than a generator on purpose: one representation of an
n:m in the model means no generator, present or future, can forget the case -
which is exactly how the keyword came to parse cleanly and generate nothing.
The link carries only its key and the two foreign keys. Bridge data (a quantity, a
partial amount, a valid-from) means the link is a domain entity, authored
explicitly with its own fields and no manyToMany - unchanged, and now the
documented boundary rather than the only option.
Nothing else is silently dropped either: the target-picker attributes (where /
show / major / size / leafOnly) travel onto the link's target relation, while the
attributes that only describe a hand-authored to-one (composition, function, init,
dependsOn, calculated actions, personal, partner) are rejected naming what was
written. So are the same pair declared from both sides (it is one link table), a
link name colliding with a declared entity (drop the manyToMany or name it with
`through:`), and `through:` on any other kind. A self-referencing n:m is
legitimate and names its two ends apart.
Tests: ManyToManyIntentTest (the shape, through:, cross-model, self-reference and
every refusal), EdmManyToManyTest (the .model: a DEPENDENT link table with both
FKs, no column on the declaring entity, no perspective of its own), and
IntentEmissionCoverageIT - the layer that matters - asserting the link table in
the generated schema and a link row round-tripping through the master's detail
query on the published app.
Docs: module README + CLAUDE.md, the assistant guide (so the agent proposes the
kind), root CLAUDE.md.
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 #6718.
The trap
kind: manyToManywas accepted by the parser and materialised by nothing — no join table, no UI, no error. An author (or the AI agent) who wrote it got a clean Generate and an application with the relationship simply missing: the authored-but-silently-unconsumed class of failure (same family as #6543).The issue offered two ways out; this is the second — materialisation, using the intermediate entity that was already the supported shape.
What it does
ManyToManyExpanderruns first thing inIntentParser.validateand rewritesinto the link entity, before any validator or generator sees the model:
with the authored relation becoming the navigation-only
oneToManyto that link. Everything downstream — every validator, every generator, the editor's diagram, the/parseresponse — sees an ordinary composition + association pair, so the link gets its table, its FK columns, its detail grid with a dropdown under the declaring entity's page, and it can be seeded, reported on and referenced like any other entity.Why an expansion and not a generator: one representation of an n:m in the model means no generator, present or future, can forget the case — which is exactly how the keyword came to parse cleanly and generate nothing.
Where the boundary now sits
The generated link carries only its key and the two FKs. Bridge data (a quantity, a partial
amount, a valid-from date) means the link is a domain entity — authored explicitly with its own fields and nomanyToMany, exactly as today, now as the documented boundary rather than the only option.Nothing else is silently dropped
where/show/major/size/leafOnlytravel onto the link's target relation.composition,function,init,dependsOn, calculated actions,personal,partner— are rejected, naming what was written.manyToMany, or name it withthrough:), andthrough:on any other kind.model:) is supported — the link owns the cross-model association.Tests
ManyToManyIntentTest— the materialised shape,through:, cross-model, the picker attributes travelling, self-reference, and every refusal.EdmManyToManyTest— the.model: aDEPENDENTlink table (ORDERS_ORDER_PRODUCT,MANAGE_DETAILS) with a COMPOSITION and an ASSOCIATION FK, no column on the declaring entity, no perspective of its own.IntentEmissionCoverageIT— the layer that matters, per the module's emission + runtime contract: the link table in the generated schema, the link controller, and a link row created and read back through the master's detail query on the published app.Verified locally: full
-P unit-testssweep,IntentEmissionCoverageIT(83s, green),formatter:validate, and the release-profile javadoc build onengine-intent.Docs
In-repo: module
README.md+CLAUDE.md, the assistant guide (so the agent proposes the kind), rootCLAUDE.md. Doc sites: dirigible.io PR #181 (merged) and the vendor-neutral spec IntentFile/intentfile.github.io#19.