From 07c4104c86ec676f4088c422b89f518164a1c7d3 Mon Sep 17 00:00:00 2001 From: Andrej Koelewijn Date: Sat, 23 May 2026 16:52:22 +0000 Subject: [PATCH] fix #595: key PublishedODataService entity-type ID map by qualified name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `serializePublishedODataService` keyed `entityTypeIDMap` by `et.ExposedName` (e.g. "Customers") but looked it up by `es.EntityTypeName` (e.g. "OdTest.Customer"). The two values are different, so the lookup always returned an empty string and the `if entityTypeID != ""` guard in `serializePublishedEntitySet` meant `EntityTypePointer` was never written. Without that pointer, Studio Pro's `EntitySet.Check` can't navigate from a set to its type, NREs on the missing reference, and aborts the entire background project checker — hiding all other problems in the project, not just the published OData service. Key the map by `et.Entity` (the qualified entity name), matching how `EntityTypeName` is populated on `PublishedEntitySet` in `astEntityDefToModel`. Extend `TestSerializePublishedODataService` to assert `EntityTypePointer` matches the corresponding EntityType's `$ID`. The prior test covered other fields but not this pointer, which is why the bug landed undetected — the existing assertions only checked the EntitySet's own fields, never the cross-reference. Add `mdl-examples/bug-tests/595-published-odata-entitytypepointer.mdl` as the end-to-end fixture: applying it to a project must yield a project that opens in Studio Pro without the `EntitySet.Check` NRE. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../595-published-odata-entitytypepointer.mdl | 38 +++++++++++++++++++ sdk/mpr/writer_odata.go | 11 ++++-- sdk/mpr/writer_odata_test.go | 17 ++++++++- 3 files changed, 62 insertions(+), 4 deletions(-) create mode 100644 mdl-examples/bug-tests/595-published-odata-entitytypepointer.mdl diff --git a/mdl-examples/bug-tests/595-published-odata-entitytypepointer.mdl b/mdl-examples/bug-tests/595-published-odata-entitytypepointer.mdl new file mode 100644 index 000000000..0d83e71f2 --- /dev/null +++ b/mdl-examples/bug-tests/595-published-odata-entitytypepointer.mdl @@ -0,0 +1,38 @@ +-- Bug test: published OData service EntitySet must carry EntityTypePointer. +-- Issue #595: serializePublishedODataService keyed its entityTypeIDMap by +-- ExposedName but looked it up by qualified entity name, so the resolved +-- ID was always empty and ODataPublish$EntitySet.EntityTypePointer was +-- never written. Studio Pro's EntitySet.Check then dereferenced null and +-- aborted the entire project checker with a NullReferenceException. +-- +-- The Go unit test in sdk/mpr/writer_odata_test.go (TestSerializePublishedODataService) +-- asserts EntityTypePointer matches the EntityType's $ID. This MDL script +-- is the end-to-end fixture: applying it to a project must yield a project +-- that opens in Studio Pro without the EntitySet.Check NRE. +-- +-- Run with: +-- mxcli exec mdl-examples/bug-tests/595-published-odata-entitytypepointer.mdl -p .mpr +-- then open .mpr in Studio Pro and confirm no NRE in the project checker. +create module bug595; + +create persistent entity bug595.Customer ( + Name: string(200), + Email: string(200) +); + +create odata service bug595.CustomerAPI ( + path: '/odata/customers', + version: '1.0.0', + ODataVersion: OData4, + namespace: 'bug595.Customers' +) +authentication basic +{ + publish entity bug595.Customer as 'Customers' ( + ReadMode: source, + InsertMode: source, + UpdateMode: source, + DeleteMode: not_supported + ) + expose (*); +}; diff --git a/sdk/mpr/writer_odata.go b/sdk/mpr/writer_odata.go index 8fc8b8aa0..2435ef8f5 100644 --- a/sdk/mpr/writer_odata.go +++ b/sdk/mpr/writer_odata.go @@ -209,8 +209,13 @@ func (w *Writer) serializePublishedODataService(svc *model.PublishedODataService authTypes = append(authTypes, at) } - // Serialize entity types and build ID map for entity set pointers - entityTypeIDMap := make(map[string]string) // ExposedName -> entity type ID + // Serialize entity types and build ID map for entity set pointers. + // Issue #595: key by qualified entity name (et.Entity), not ExposedName. + // PublishedEntitySet.EntityTypeName holds the qualified name, so keying + // by ExposedName made the lookup return "" and EntityTypePointer was + // never written. Studio Pro's EntitySet.Check then NREs dereferencing + // the missing pointer and aborts the whole project checker. + entityTypeIDMap := make(map[string]string) // qualified entity name -> entity type ID entityTypes := bson.A{} for _, et := range svc.EntityTypes { etID := string(et.ID) @@ -218,7 +223,7 @@ func (w *Writer) serializePublishedODataService(svc *model.PublishedODataService etID = generateUUID() et.ID = model.ID(etID) } - entityTypeIDMap[et.ExposedName] = etID + entityTypeIDMap[et.Entity] = etID entityTypes = append(entityTypes, serializePublishedEntityType(et)) } diff --git a/sdk/mpr/writer_odata_test.go b/sdk/mpr/writer_odata_test.go index db4d94f78..3e0d088c2 100644 --- a/sdk/mpr/writer_odata_test.go +++ b/sdk/mpr/writer_odata_test.go @@ -7,6 +7,7 @@ import ( "github.com/mendixlabs/mxcli/model" "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" ) func TestSerializeConsumedODataService(t *testing.T) { @@ -158,7 +159,7 @@ func TestSerializePublishedODataService(t *testing.T) { AuthenticationTypes: []string{"Basic", "Session"}, EntityTypes: []*model.PublishedEntityType{ { - BaseElement: model.BaseElement{ID: "et-1"}, + BaseElement: model.BaseElement{ID: "11111111-1111-1111-1111-111111111111"}, Entity: "MyModule.Customer", ExposedName: "Customers", Members: []*model.PublishedMember{ @@ -282,6 +283,20 @@ func TestSerializePublishedODataService(t *testing.T) { assertField(t, esMap, "$Type", "ODataPublish$EntitySet") assertField(t, esMap, "ExposedName", "Customers") + // Issue #595: EntityTypePointer must reference the owning EntityType. + // Without it, Studio Pro's EntitySet.Check NREs (it can't navigate from + // the set to its type). The map lookup in serializePublishedODataService + // was previously keyed by ExposedName instead of the qualified entity + // name, so the resolved ID was always empty and the pointer was omitted. + etID := etMap["$ID"].(primitive.Binary) + esPointer, ok := esMap["EntityTypePointer"].(primitive.Binary) + if !ok { + t.Fatalf("EntityTypePointer: expected primitive.Binary, got %T (%v)", esMap["EntityTypePointer"], esMap["EntityTypePointer"]) + } + if string(esPointer.Data) != string(etID.Data) { + t.Errorf("EntityTypePointer = %x, want %x (entity type $ID)", esPointer.Data, etID.Data) + } + if v, ok := esMap["UsePaging"].(bool); !ok || !v { t.Errorf("UsePaging: expected true, got %v", esMap["UsePaging"]) }