Skip to content

Commit 193d305

Browse files
akoclaude
andcommitted
fix: serialize ByAssociation DataSource for DataGrid 2 (mendixlabs#198 follow-up)
Two related issues, fixed together: 1. Grammar rejected qualified association names. The previous \`attributePathV3\` rule didn't allow dots, so \`ASSOCIATION Module.Order_OrderLine\` and \`\$currentObject/Module.Order_OrderLine\` both failed to parse. Added a new \`associationPathV3\` rule that accepts qualified names separated by /, supporting both single-step (\`Module.Assoc\`) and multi-step (\`Module.Assoc/Module.DestEntity\`) paths. 2. The writer never emitted Forms\$AssociationSource BSON. The executor built a pages.AssociationSource model object but SerializeCustomWidgetDataSource (used by pluggable widgets like DataGrid 2) had no case for it, falling through to nil. Added the case with a serializeAssociationSource helper that emits the proper DomainModels\$IndirectEntityRef + Steps + optional SourceVariable structure matching what Studio Pro produces. Verified end-to-end against test3 (Mendix 11.9): a DataGrid 2 with \`DataSource: \$currentObject/AssocGrid.OrderLine_Order/AssocGrid.OrderLine\` now generates valid BSON. \`mx check\` passes (the remaining CE7007 is on the parent DataView, a separate pre-existing issue). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 9c1d144 commit 193d305

10 files changed

Lines changed: 9498 additions & 9225 deletions

File tree

mdl/ast/ast_page_v3.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,11 +60,12 @@ type WidgetV3 struct {
6060

6161
// DataSourceV3 represents a V3 datasource expression.
6262
type DataSourceV3 struct {
63-
Type string // "parameter", "database", "microflow", "nanoflow", "association", "selection"
64-
Reference string // Entity name, flow name, widget name, or parameter name
65-
Args []FlowArgV3 // Arguments for microflow/nanoflow calls
66-
Where string // XPath constraint (for database source)
67-
OrderBy []OrderByItemV3 // Sort order (for database source)
63+
Type string // "parameter", "database", "microflow", "nanoflow", "association", "selection"
64+
Reference string // Entity name, flow name, widget name, or parameter name
65+
ContextVariable string // Context variable name (for association source: $currentObject → "currentObject")
66+
Args []FlowArgV3 // Arguments for microflow/nanoflow calls
67+
Where string // XPath constraint (for database source)
68+
OrderBy []OrderByItemV3 // Sort order (for database source)
6869
}
6970

7071
// FlowArgV3 represents an argument for microflow/nanoflow/page calls.

mdl/executor/cmd_pages_builder_v3.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -586,13 +586,20 @@ func (pb *pageBuilder) buildDataSourceV3(ds *ast.DataSourceV3) (pages.DataSource
586586
}, entityName, nil
587587

588588
case "association":
589-
// Association path source
589+
// Association path source — emits Forms$AssociationSource BSON.
590+
// EntityPath is the qualified association name (and optional /DestinationEntity).
591+
// ContextVariable is the page parameter name (empty when source is $currentObject of parent dataview).
592+
ctxVar := ds.ContextVariable
593+
if ctxVar == "currentObject" {
594+
ctxVar = "" // implicit context — no SourceVariable in BSON
595+
}
590596
return &pages.AssociationSource{
591597
BaseElement: model.BaseElement{
592598
ID: model.ID(mpr.GenerateID()),
593599
TypeName: "Forms$AssociationSource",
594600
},
595-
EntityPath: ds.Reference,
601+
EntityPath: ds.Reference,
602+
ContextVariable: ctxVar,
596603
}, "", nil
597604

598605
case "selection":

mdl/grammar/MDLParser.g4

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2168,17 +2168,22 @@ attributeListV3
21682168

21692169
// V3 DataSource expressions
21702170
dataSourceExprV3
2171-
: VARIABLE SLASH attributePathV3 // $currentObject/Module.Assoc (ByAssociation — sugar for ASSOCIATION)
2171+
: VARIABLE SLASH associationPathV3 // $currentObject/Module.Assoc (ByAssociation — sugar for ASSOCIATION)
21722172
| VARIABLE // $ParamName
21732173
| DATABASE FROM? qualifiedName // DATABASE [FROM] Entity [WHERE ...] [SORT BY ...]
21742174
(WHERE (xpathConstraint (andOrXpath? xpathConstraint)* | expression))?
21752175
(SORT_BY sortColumn (COMMA sortColumn)*)?
21762176
| MICROFLOW qualifiedName microflowArgsV3? // MICROFLOW Module.Flow
21772177
| NANOFLOW qualifiedName microflowArgsV3? // NANOFLOW Module.Flow
2178-
| ASSOCIATION attributePathV3 // ASSOCIATION Path (explicit form)
2178+
| ASSOCIATION associationPathV3 // ASSOCIATION Module.Assoc (explicit form)
21792179
| SELECTION IDENTIFIER // SELECTION widgetName
21802180
;
21812181

2182+
// Association path: Module.Assoc or Module.Assoc/Module.Entity or multi-step
2183+
associationPathV3
2184+
: qualifiedName (SLASH qualifiedName)*
2185+
;
2186+
21822187
// V3 Action expressions
21832188
actionExprV3
21842189
: SAVE_CHANGES (CLOSE_PAGE)? // SAVE_CHANGES or SAVE_CHANGES CLOSE_PAGE

mdl/grammar/parser/MDLParser.interp

Lines changed: 2 additions & 1 deletion
Large diffs are not rendered by default.

mdl/grammar/parser/mdl_parser.go

Lines changed: 9388 additions & 9210 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mdl/grammar/parser/mdlparser_base_listener.go

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mdl/grammar/parser/mdlparser_listener.go

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

mdl/visitor/visitor_page_v3.go

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -618,8 +618,9 @@ func buildDataSourceV3(ctx parser.IDataSourceExprV3Context) *ast.DataSourceV3 {
618618
if v := dsCtx.VARIABLE(); v != nil && dsCtx.SLASH() != nil {
619619
// $currentObject/Module.Assoc — ByAssociation data source (sugar for ASSOCIATION Path)
620620
ds.Type = "association"
621-
if pathCtx := dsCtx.AttributePathV3(); pathCtx != nil {
622-
ds.Reference = buildAttributePathV3(pathCtx)
621+
ds.ContextVariable = strings.TrimPrefix(v.GetText(), "$")
622+
if pathCtx := dsCtx.AssociationPathV3(); pathCtx != nil {
623+
ds.Reference = buildAssociationPathV3(pathCtx)
623624
}
624625
} else if v := dsCtx.VARIABLE(); v != nil {
625626
// $ParamName
@@ -669,8 +670,8 @@ func buildDataSourceV3(ctx parser.IDataSourceExprV3Context) *ast.DataSourceV3 {
669670
} else if dsCtx.ASSOCIATION() != nil {
670671
// ASSOCIATION Path
671672
ds.Type = "association"
672-
if pathCtx := dsCtx.AttributePathV3(); pathCtx != nil {
673-
ds.Reference = buildAttributePathV3(pathCtx)
673+
if pathCtx := dsCtx.AssociationPathV3(); pathCtx != nil {
674+
ds.Reference = buildAssociationPathV3(pathCtx)
674675
}
675676
} else if dsCtx.SELECTION() != nil {
676677
// SELECTION widgetName
@@ -804,6 +805,20 @@ func buildAttributeListV3(ctx parser.IAttributeListV3Context) []string {
804805
return attrs
805806
}
806807

808+
// buildAssociationPathV3 builds an association path string from a parser context.
809+
// Format: "Module.Assoc" or "Module.Assoc/Module.Entity" — qualified names separated by /.
810+
func buildAssociationPathV3(ctx parser.IAssociationPathV3Context) string {
811+
if ctx == nil {
812+
return ""
813+
}
814+
apc := ctx.(*parser.AssociationPathV3Context)
815+
var parts []string
816+
for _, qn := range apc.AllQualifiedName() {
817+
parts = append(parts, getQualifiedNameText(qn))
818+
}
819+
return strings.Join(parts, "/")
820+
}
821+
807822
// buildAttributePathV3 builds an attribute path string.
808823
// Handles quoted identifiers (e.g., "Order") by stripping quotes.
809824
func buildAttributePathV3(ctx parser.IAttributePathV3Context) string {

sdk/mpr/writer_widgets.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,11 +295,64 @@ func SerializeCustomWidgetDataSource(ds pages.DataSource) bson.D {
295295
{Key: "ParameterMappings", Value: bson.A{int32(3)}},
296296
}},
297297
}
298+
case *pages.AssociationSource:
299+
return serializeAssociationSource(d)
298300
default:
299301
return nil
300302
}
301303
}
302304

305+
// serializeAssociationSource builds a Forms$AssociationSource BSON document.
306+
// EntityPath is "Module.Assoc" or "Module.Assoc/Module.DestEntity".
307+
// When DestinationEntity is omitted, it's left empty — Studio Pro will resolve it.
308+
func serializeAssociationSource(d *pages.AssociationSource) bson.D {
309+
parts := strings.Split(d.EntityPath, "/")
310+
association := parts[0]
311+
destEntity := ""
312+
if len(parts) >= 2 {
313+
destEntity = parts[1]
314+
}
315+
316+
step := bson.D{
317+
{Key: "$ID", Value: idToBsonBinary(generateUUID())},
318+
{Key: "$Type", Value: "DomainModels$EntityRefStep"},
319+
{Key: "Association", Value: association},
320+
{Key: "DestinationEntity", Value: destEntity},
321+
}
322+
323+
entityRef := bson.D{
324+
{Key: "$ID", Value: idToBsonBinary(generateUUID())},
325+
{Key: "$Type", Value: "DomainModels$IndirectEntityRef"},
326+
{Key: "Steps", Value: bson.A{int32(2), step}},
327+
}
328+
329+
var sourceVar any
330+
if d.ContextVariable != "" {
331+
sourceVar = bson.D{
332+
{Key: "$ID", Value: idToBsonBinary(generateUUID())},
333+
{Key: "$Type", Value: "Forms$PageVariable"},
334+
{Key: "LocalVariable", Value: ""},
335+
{Key: "PageParameter", Value: d.ContextVariable},
336+
{Key: "SnippetParameter", Value: ""},
337+
{Key: "SubKey", Value: ""},
338+
{Key: "UseAllPages", Value: false},
339+
{Key: "Widget", Value: ""},
340+
}
341+
}
342+
343+
id := string(d.ID)
344+
if id == "" {
345+
id = generateUUID()
346+
}
347+
return bson.D{
348+
{Key: "$ID", Value: idToBsonBinary(id)},
349+
{Key: "$Type", Value: "Forms$AssociationSource"},
350+
{Key: "EntityRef", Value: entityRef},
351+
{Key: "ForceFullObjects", Value: false},
352+
{Key: "SourceVariable", Value: sourceVar},
353+
}
354+
}
355+
303356
// ============================================================================
304357
// Reference Serialization
305358
// ============================================================================

sdk/pages/pages_datasources.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ func (ListenToWidgetSource) isDataSource() {}
7878
// AssociationSource retrieves data via association.
7979
type AssociationSource struct {
8080
model.BaseElement
81-
EntityPath string `json:"entityPath"`
81+
EntityPath string `json:"entityPath"` // "Module.Assoc" or "Module.Assoc/Module.DestEntity"
82+
ContextVariable string `json:"contextVariable,omitempty"` // page parameter name (without $) — empty for $currentObject
8283
}
8384

8485
func (AssociationSource) isDataSource() {}

0 commit comments

Comments
 (0)