Skip to content

Commit cdebe62

Browse files
akoclaude
andcommitted
fix(odata): auto-fetch $metadata on CREATE ODATA CLIENT and store all required BSON fields
- CREATE ODATA CLIENT now fetches $metadata from MetadataUrl and caches it in the Metadata field (with SHA-256 hash in MetadataHash) - Writer now serializes all required BSON fields: Metadata, MetadataHash, MetadataReferences, ValidatedEntities, LastUpdated, UseQuerySegment, MinimumMxVersion, RecommendedMxVersion, ExportLevel - Fetch failure is a non-fatal warning (service still created without metadata) - On success, prints summary of cached entity types and actions - Fixes NullReferenceException in mx check when ConsumedODataService was missing expected fields - Remove unreachable contract browsing examples from doctype test Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent fa3fdb4 commit cdebe62

File tree

3 files changed

+97
-44
lines changed

3 files changed

+97
-44
lines changed

mdl-examples/doctype-tests/10-odata-examples.mdl

Lines changed: 5 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -366,39 +366,13 @@ SHOW EXTERNAL ACTIONS IN OdTest;
366366
/
367367

368368
-- ############################################################################
369-
-- LEVEL 10: CONTRACT BROWSING (cached $metadata)
369+
-- NOTE: CREATE ODATA CLIENT now auto-fetches and caches $metadata from the
370+
-- MetadataUrl. CONTRACT BROWSING commands (SHOW CONTRACT ENTITIES/ACTIONS)
371+
-- work on services with cached metadata. The test URLs above are not reachable
372+
-- from CI, so contract browsing examples are not included here.
373+
-- See cmd/mxcli/help_topics/odata.txt for syntax reference.
370374
-- ############################################################################
371375

372-
/**
373-
* Level 10.1: Browse entity types from a consumed OData service's cached $metadata
374-
*/
375-
SHOW CONTRACT ENTITIES FROM OdTest.SalesforceAPI;
376-
/
377-
378-
/**
379-
* Level 10.2: Browse actions/functions from the contract
380-
*/
381-
SHOW CONTRACT ACTIONS FROM OdTest.SalesforceAPI;
382-
/
383-
384-
/**
385-
* Level 10.3: Describe a specific entity type from the contract
386-
*/
387-
DESCRIBE CONTRACT ENTITY OdTest.SalesforceAPI.Account;
388-
/
389-
390-
/**
391-
* Level 10.4: Generate CREATE EXTERNAL ENTITY from the contract
392-
*/
393-
DESCRIBE CONTRACT ENTITY OdTest.SalesforceAPI.Account FORMAT mdl;
394-
/
395-
396-
/**
397-
* Level 10.5: Describe a specific action from the contract
398-
*/
399-
DESCRIBE CONTRACT ACTION OdTest.SalesforceAPI.CreateOrder;
400-
/
401-
402376
-- ############################################################################
403377
-- LEVEL 8.8: DROP (cleanup)
404378
-- ############################################################################

mdl/executor/cmd_odata.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
package executor
44

55
import (
6+
"crypto/sha256"
67
"fmt"
78
"io"
9+
"net/http"
810
"sort"
911
"strings"
12+
"time"
1013

1114
"github.com/mendixlabs/mxcli/mdl/ast"
1215
"github.com/mendixlabs/mxcli/model"
@@ -1096,11 +1099,35 @@ func (e *Executor) createODataClient(stmt *ast.CreateODataClientStmt) error {
10961099
newSvc.HttpConfiguration = cfg
10971100
}
10981101

1102+
// Fetch and cache $metadata from the service URL
1103+
if newSvc.MetadataUrl != "" {
1104+
metadata, hash, err := fetchODataMetadata(newSvc.MetadataUrl)
1105+
if err != nil {
1106+
fmt.Fprintf(e.output, "Warning: could not fetch $metadata: %v\n", err)
1107+
} else if metadata != "" {
1108+
newSvc.Metadata = metadata
1109+
newSvc.MetadataHash = hash
1110+
newSvc.Validated = true
1111+
}
1112+
}
1113+
10991114
if err := e.writer.CreateConsumedODataService(newSvc); err != nil {
11001115
return fmt.Errorf("failed to create OData client: %w", err)
11011116
}
11021117
e.invalidateHierarchy()
11031118
fmt.Fprintf(e.output, "Created OData client: %s.%s\n", stmt.Name.Module, stmt.Name.Name)
1119+
if newSvc.Metadata != "" {
1120+
// Parse to show summary
1121+
if doc, err := mpr.ParseEdmx(newSvc.Metadata); err == nil {
1122+
entityCount := 0
1123+
actionCount := 0
1124+
for _, s := range doc.Schemas {
1125+
entityCount += len(s.EntityTypes)
1126+
}
1127+
actionCount = len(doc.Actions)
1128+
fmt.Fprintf(e.output, " Cached $metadata: %d entity types, %d actions\n", entityCount, actionCount)
1129+
}
1130+
}
11041131
return nil
11051132
}
11061133

@@ -1475,3 +1502,32 @@ func astEntityDefToModel(def *ast.PublishedEntityDef) (*model.PublishedEntityTyp
14751502

14761503
return entityType, entitySet
14771504
}
1505+
1506+
// fetchODataMetadata downloads the $metadata document from the service URL.
1507+
// Returns the metadata XML and its SHA-256 hash, or empty strings if the fetch fails.
1508+
func fetchODataMetadata(metadataUrl string) (metadata string, hash string, err error) {
1509+
if metadataUrl == "" {
1510+
return "", "", nil
1511+
}
1512+
1513+
client := &http.Client{Timeout: 30 * time.Second}
1514+
resp, err := client.Get(metadataUrl)
1515+
if err != nil {
1516+
return "", "", fmt.Errorf("failed to fetch $metadata from %s: %w", metadataUrl, err)
1517+
}
1518+
defer resp.Body.Close()
1519+
1520+
if resp.StatusCode != http.StatusOK {
1521+
return "", "", fmt.Errorf("$metadata fetch returned HTTP %d from %s", resp.StatusCode, metadataUrl)
1522+
}
1523+
1524+
body, err := io.ReadAll(resp.Body)
1525+
if err != nil {
1526+
return "", "", fmt.Errorf("failed to read $metadata response: %w", err)
1527+
}
1528+
1529+
metadata = string(body)
1530+
h := sha256.Sum256(body)
1531+
hash = fmt.Sprintf("%x", h)
1532+
return metadata, hash, nil
1533+
}

sdk/mpr/writer_odata.go

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,28 @@ func (w *Writer) DeleteConsumedODataService(id model.ID) error {
4848
// serializeConsumedODataService converts a ConsumedODataService to BSON bytes.
4949
func (w *Writer) serializeConsumedODataService(svc *model.ConsumedODataService) ([]byte, error) {
5050
doc := bson.M{
51-
"$ID": idToBsonBinary(string(svc.ID)),
52-
"$Type": "Rest$ConsumedODataService",
53-
"Name": svc.Name,
54-
"Documentation": svc.Documentation,
55-
"Version": svc.Version,
56-
"ServiceName": svc.ServiceName,
57-
"ODataVersion": svc.ODataVersion,
58-
"MetadataUrl": svc.MetadataUrl,
59-
"TimeoutExpression": svc.TimeoutExpression,
60-
"ProxyType": svc.ProxyType,
61-
"Description": svc.Description,
62-
"Validated": svc.Validated,
63-
"Excluded": svc.Excluded,
51+
"$ID": idToBsonBinary(string(svc.ID)),
52+
"$Type": "Rest$ConsumedODataService",
53+
"Name": svc.Name,
54+
"Documentation": svc.Documentation,
55+
"Version": svc.Version,
56+
"ServiceName": svc.ServiceName,
57+
"ODataVersion": svc.ODataVersion,
58+
"MetadataUrl": svc.MetadataUrl,
59+
"TimeoutExpression": svc.TimeoutExpression,
60+
"ProxyType": svc.ProxyType,
61+
"Description": svc.Description,
62+
"Validated": svc.Validated,
63+
"Excluded": svc.Excluded,
64+
"ExportLevel": "Hidden",
65+
"Metadata": svc.Metadata,
66+
"MetadataHash": svc.MetadataHash,
67+
"MetadataReferences": bson.A{int32(0)}, // empty BSON array marker
68+
"ValidatedEntities": bson.A{int32(0)}, // empty BSON array marker
69+
"LastUpdated": "",
70+
"UseQuerySegment": false,
71+
"MinimumMxVersion": "",
72+
"RecommendedMxVersion": "",
6473
}
6574

6675
// Microflow references (BY_NAME)
@@ -85,6 +94,20 @@ func (w *Writer) serializeConsumedODataService(svc *model.ConsumedODataService)
8594
doc["ProxyPassword"] = svc.ProxyPassword
8695
}
8796

97+
// Mendix Catalog integration (optional)
98+
if svc.ApplicationId != "" {
99+
doc["ApplicationId"] = svc.ApplicationId
100+
}
101+
if svc.EndpointId != "" {
102+
doc["EndpointId"] = svc.EndpointId
103+
}
104+
if svc.CatalogUrl != "" {
105+
doc["CatalogUrl"] = svc.CatalogUrl
106+
}
107+
if svc.EnvironmentType != "" {
108+
doc["EnvironmentType"] = svc.EnvironmentType
109+
}
110+
88111
// HTTP configuration (required nested part)
89112
doc["HttpConfiguration"] = serializeHttpConfiguration(svc.HttpConfiguration)
90113

0 commit comments

Comments
 (0)