Skip to content

Commit 6b59e19

Browse files
committed
Merge branch 'odata'
2 parents f68236b + cdebe62 commit 6b59e19

File tree

24 files changed

+7061
-6190
lines changed

24 files changed

+7061
-6190
lines changed

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,7 @@ Full syntax tables for all MDL statements (microflows, pages, security, navigati
406406
- CALCULATED BY microflow syntax for calculated attributes
407407
- Image collections (SHOW/DESCRIBE/CREATE/DROP)
408408
- OData contract browsing (SHOW/DESCRIBE CONTRACT ENTITIES/ACTIONS FROM cached $metadata)
409+
- AsyncAPI contract browsing (SHOW/DESCRIBE CONTRACT CHANNELS/MESSAGES FROM cached AsyncAPI)
409410
- SHOW EXTERNAL ACTIONS, SHOW PUBLISHED REST SERVICES
410411
- Integration catalog tables (rest_clients, rest_operations, published_rest_services, external_entities, external_actions, business_events)
411412

cmd/mxcli/help_topics/odata.txt

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,16 +13,21 @@ SHOW COMMANDS
1313
SHOW EXTERNAL ACTIONS; -- List external actions used in microflows
1414
SHOW EXTERNAL ACTIONS IN MyModule; -- Filter by module
1515

16-
CONTRACT COMMANDS (browse cached $metadata)
17-
--------------------------------------------
16+
CONTRACT COMMANDS (browse cached contracts)
17+
-------------------------------------------
1818

19+
OData ($metadata):
1920
SHOW CONTRACT ENTITIES FROM MyModule.MyClient; -- List entity types from $metadata
2021
SHOW CONTRACT ACTIONS FROM MyModule.MyClient; -- List actions/functions from $metadata
21-
2222
DESCRIBE CONTRACT ENTITY MyModule.MyClient.Product; -- Properties and nav props
2323
DESCRIBE CONTRACT ENTITY MyModule.MyClient.Product FORMAT mdl; -- Generate CREATE EXTERNAL ENTITY
2424
DESCRIBE CONTRACT ACTION MyModule.MyClient.CreateOrder; -- Parameters and return type
2525

26+
AsyncAPI (business events):
27+
SHOW CONTRACT CHANNELS FROM MyModule.MyEventClient; -- List channels
28+
SHOW CONTRACT MESSAGES FROM MyModule.MyEventClient; -- List messages with schemas
29+
DESCRIBE CONTRACT MESSAGE MyModule.MyEventClient.OrderChanged; -- Message properties
30+
2631
DESCRIBE COMMANDS
2732
-----------------
2833

cmd/mxcli/lsp_completions_gen.go

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

docs-site/src/appendixes/quick-reference.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,9 @@ CREATE CONSTANT MyModule.EnableLogging TYPE Boolean DEFAULT true;
9999
| Show contract actions | `SHOW CONTRACT ACTIONS FROM Module.Client;` | Browse cached $metadata |
100100
| Describe contract entity | `DESCRIBE CONTRACT ENTITY Module.Client.Entity [FORMAT mdl];` | Properties, types, keys |
101101
| Describe contract action | `DESCRIBE CONTRACT ACTION Module.Client.Action [FORMAT mdl];` | Parameters, return type |
102+
| Show contract channels | `SHOW CONTRACT CHANNELS FROM Module.Service;` | Browse cached AsyncAPI |
103+
| Show contract messages | `SHOW CONTRACT MESSAGES FROM Module.Service;` | Browse cached AsyncAPI |
104+
| Describe contract message | `DESCRIBE CONTRACT MESSAGE Module.Service.Message;` | Message payload properties |
102105

103106
**OData Client Example:**
104107
```sql

docs/01-project/MDL_QUICK_REFERENCE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ CREATE CONSTANT MyModule.EnableLogging TYPE Boolean DEFAULT true;
102102
| Show contract actions | `SHOW CONTRACT ACTIONS FROM Module.Client;` | Browse cached $metadata |
103103
| Describe contract entity | `DESCRIBE CONTRACT ENTITY Module.Client.Entity [FORMAT mdl];` | Properties, types, keys |
104104
| Describe contract action | `DESCRIBE CONTRACT ACTION Module.Client.Action [FORMAT mdl];` | Parameters, return type |
105+
| Show contract channels | `SHOW CONTRACT CHANNELS FROM Module.Service;` | Browse cached AsyncAPI |
106+
| Show contract messages | `SHOW CONTRACT MESSAGES FROM Module.Service;` | Browse cached AsyncAPI |
107+
| Describe contract message | `DESCRIBE CONTRACT MESSAGE Module.Service.Message;` | Message payload properties |
105108

106109
**OData Client Example:**
107110
```sql

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/ast/ast_query.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ const (
8282
ShowConstantValues // SHOW CONSTANT VALUES [IN module]
8383
ShowContractEntities // SHOW CONTRACT ENTITIES FROM Module.Service
8484
ShowContractActions // SHOW CONTRACT ACTIONS FROM Module.Service
85+
ShowContractChannels // SHOW CONTRACT CHANNELS FROM Module.Service (AsyncAPI)
86+
ShowContractMessages // SHOW CONTRACT MESSAGES FROM Module.Service (AsyncAPI)
8587
)
8688

8789
// String returns the human-readable name of the show object type.
@@ -193,6 +195,10 @@ func (t ShowObjectType) String() string {
193195
return "CONTRACT ENTITIES"
194196
case ShowContractActions:
195197
return "CONTRACT ACTIONS"
198+
case ShowContractChannels:
199+
return "CONTRACT CHANNELS"
200+
case ShowContractMessages:
201+
return "CONTRACT MESSAGES"
196202
default:
197203
return "UNKNOWN"
198204
}
@@ -247,6 +253,7 @@ const (
247253
DescribePublishedRestService // DESCRIBE PUBLISHED REST SERVICE Module.Name
248254
DescribeContractEntity // DESCRIBE CONTRACT ENTITY Service.EntityName [FORMAT mdl]
249255
DescribeContractAction // DESCRIBE CONTRACT ACTION Service.ActionName [FORMAT mdl]
256+
DescribeContractMessage // DESCRIBE CONTRACT MESSAGE Service.MessageName
250257
)
251258

252259
// String returns the human-readable name of the describe object type.
@@ -308,6 +315,8 @@ func (t DescribeObjectType) String() string {
308315
return "CONTRACT ENTITY"
309316
case DescribeContractAction:
310317
return "CONTRACT ACTION"
318+
case DescribeContractMessage:
319+
return "CONTRACT MESSAGE"
311320
default:
312321
return "UNKNOWN"
313322
}

mdl/executor/cmd_contract.go

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -467,3 +467,224 @@ func edmToMendixType(p *mpr.EdmProperty) string {
467467
return "String(200)"
468468
}
469469
}
470+
471+
// ============================================================================
472+
// AsyncAPI Contract Commands
473+
// ============================================================================
474+
475+
// showContractChannels handles SHOW CONTRACT CHANNELS FROM Module.Service.
476+
func (e *Executor) showContractChannels(name *ast.QualifiedName) error {
477+
if name == nil {
478+
return fmt.Errorf("service name required: SHOW CONTRACT CHANNELS FROM Module.Service")
479+
}
480+
481+
doc, svcQN, err := e.parseAsyncAPIContract(*name)
482+
if err != nil {
483+
return err
484+
}
485+
486+
if len(doc.Channels) == 0 {
487+
fmt.Fprintf(e.output, "No channels found in contract for %s.\n", svcQN)
488+
return nil
489+
}
490+
491+
type row struct {
492+
channel string
493+
operation string
494+
opID string
495+
message string
496+
}
497+
498+
var rows []row
499+
chWidth := len("Channel")
500+
opWidth := len("Operation")
501+
opIDWidth := len("OperationID")
502+
msgWidth := len("Message")
503+
504+
for _, ch := range doc.Channels {
505+
rows = append(rows, row{ch.Name, ch.OperationType, ch.OperationID, ch.MessageRef})
506+
if len(ch.Name) > chWidth {
507+
chWidth = len(ch.Name)
508+
}
509+
if len(ch.OperationType) > opWidth {
510+
opWidth = len(ch.OperationType)
511+
}
512+
if len(ch.OperationID) > opIDWidth {
513+
opIDWidth = len(ch.OperationID)
514+
}
515+
if len(ch.MessageRef) > msgWidth {
516+
msgWidth = len(ch.MessageRef)
517+
}
518+
}
519+
520+
fmt.Fprintf(e.output, "| %-*s | %-*s | %-*s | %-*s |\n",
521+
chWidth, "Channel", opWidth, "Operation", opIDWidth, "OperationID", msgWidth, "Message")
522+
fmt.Fprintf(e.output, "|-%s-|-%s-|-%s-|-%s-|\n",
523+
strings.Repeat("-", chWidth), strings.Repeat("-", opWidth),
524+
strings.Repeat("-", opIDWidth), strings.Repeat("-", msgWidth))
525+
for _, r := range rows {
526+
fmt.Fprintf(e.output, "| %-*s | %-*s | %-*s | %-*s |\n",
527+
chWidth, r.channel, opWidth, r.operation, opIDWidth, r.opID, msgWidth, r.message)
528+
}
529+
fmt.Fprintf(e.output, "\n(%d channels in %s contract)\n", len(rows), svcQN)
530+
531+
return nil
532+
}
533+
534+
// showContractMessages handles SHOW CONTRACT MESSAGES FROM Module.Service.
535+
func (e *Executor) showContractMessages(name *ast.QualifiedName) error {
536+
if name == nil {
537+
return fmt.Errorf("service name required: SHOW CONTRACT MESSAGES FROM Module.Service")
538+
}
539+
540+
doc, svcQN, err := e.parseAsyncAPIContract(*name)
541+
if err != nil {
542+
return err
543+
}
544+
545+
if len(doc.Messages) == 0 {
546+
fmt.Fprintf(e.output, "No messages found in contract for %s.\n", svcQN)
547+
return nil
548+
}
549+
550+
type row struct {
551+
name string
552+
title string
553+
contentType string
554+
props int
555+
}
556+
557+
var rows []row
558+
nameWidth := len("Message")
559+
titleWidth := len("Title")
560+
ctWidth := len("ContentType")
561+
562+
for _, msg := range doc.Messages {
563+
rows = append(rows, row{msg.Name, msg.Title, msg.ContentType, len(msg.Properties)})
564+
if len(msg.Name) > nameWidth {
565+
nameWidth = len(msg.Name)
566+
}
567+
if len(msg.Title) > titleWidth {
568+
titleWidth = len(msg.Title)
569+
}
570+
if len(msg.ContentType) > ctWidth {
571+
ctWidth = len(msg.ContentType)
572+
}
573+
}
574+
575+
sort.Slice(rows, func(i, j int) bool {
576+
return strings.ToLower(rows[i].name) < strings.ToLower(rows[j].name)
577+
})
578+
579+
propsWidth := len("Props")
580+
581+
fmt.Fprintf(e.output, "| %-*s | %-*s | %-*s | %-*s |\n",
582+
nameWidth, "Message", titleWidth, "Title", ctWidth, "ContentType", propsWidth, "Props")
583+
fmt.Fprintf(e.output, "|-%s-|-%s-|-%s-|-%s-|\n",
584+
strings.Repeat("-", nameWidth), strings.Repeat("-", titleWidth),
585+
strings.Repeat("-", ctWidth), strings.Repeat("-", propsWidth))
586+
for _, r := range rows {
587+
fmt.Fprintf(e.output, "| %-*s | %-*s | %-*s | %-*d |\n",
588+
nameWidth, r.name, titleWidth, r.title, ctWidth, r.contentType, propsWidth, r.props)
589+
}
590+
fmt.Fprintf(e.output, "\n(%d messages in %s contract)\n", len(rows), svcQN)
591+
592+
return nil
593+
}
594+
595+
// describeContractMessage handles DESCRIBE CONTRACT MESSAGE Module.Service.MessageName.
596+
func (e *Executor) describeContractMessage(name ast.QualifiedName) error {
597+
svcName, msgName, err := splitContractRef(name)
598+
if err != nil {
599+
return err
600+
}
601+
602+
doc, svcQN, err := e.parseAsyncAPIContract(svcName)
603+
if err != nil {
604+
return err
605+
}
606+
607+
msg := doc.FindMessage(msgName)
608+
if msg == nil {
609+
return fmt.Errorf("message %q not found in contract for %s", msgName, svcQN)
610+
}
611+
612+
fmt.Fprintf(e.output, "%s\n", msg.Name)
613+
if msg.Title != "" {
614+
fmt.Fprintf(e.output, " Title: %s\n", msg.Title)
615+
}
616+
if msg.Description != "" {
617+
fmt.Fprintf(e.output, " Description: %s\n", msg.Description)
618+
}
619+
if msg.ContentType != "" {
620+
fmt.Fprintf(e.output, " ContentType: %s\n", msg.ContentType)
621+
}
622+
623+
if len(msg.Properties) > 0 {
624+
fmt.Fprintln(e.output)
625+
nameWidth := len("Property")
626+
typeWidth := len("Type")
627+
for _, p := range msg.Properties {
628+
if len(p.Name) > nameWidth {
629+
nameWidth = len(p.Name)
630+
}
631+
t := asyncTypeString(p)
632+
if len(t) > typeWidth {
633+
typeWidth = len(t)
634+
}
635+
}
636+
637+
fmt.Fprintf(e.output, " %-*s %-*s\n", nameWidth, "Property", typeWidth, "Type")
638+
fmt.Fprintf(e.output, " %s %s\n", strings.Repeat("-", nameWidth), strings.Repeat("-", typeWidth))
639+
for _, p := range msg.Properties {
640+
fmt.Fprintf(e.output, " %-*s %-*s\n", nameWidth, p.Name, typeWidth, asyncTypeString(p))
641+
}
642+
}
643+
644+
return nil
645+
}
646+
647+
// parseAsyncAPIContract finds a business event service by name and parses its cached AsyncAPI document.
648+
func (e *Executor) parseAsyncAPIContract(name ast.QualifiedName) (*mpr.AsyncAPIDocument, string, error) {
649+
services, err := e.reader.ListBusinessEventServices()
650+
if err != nil {
651+
return nil, "", fmt.Errorf("failed to list business event services: %w", err)
652+
}
653+
654+
h, err := e.getHierarchy()
655+
if err != nil {
656+
return nil, "", fmt.Errorf("failed to build hierarchy: %w", err)
657+
}
658+
659+
for _, svc := range services {
660+
modID := h.FindModuleID(svc.ContainerID)
661+
modName := h.GetModuleName(modID)
662+
663+
if !strings.EqualFold(modName, name.Module) || !strings.EqualFold(svc.Name, name.Name) {
664+
continue
665+
}
666+
667+
svcQN := modName + "." + svc.Name
668+
669+
if svc.Document == "" {
670+
return nil, svcQN, fmt.Errorf("no cached AsyncAPI contract for %s. This service has no Document field (it may be a publisher, not a consumer)", svcQN)
671+
}
672+
673+
doc, err := mpr.ParseAsyncAPI(svc.Document)
674+
if err != nil {
675+
return nil, svcQN, fmt.Errorf("failed to parse AsyncAPI contract for %s: %w", svcQN, err)
676+
}
677+
678+
return doc, svcQN, nil
679+
}
680+
681+
return nil, "", fmt.Errorf("business event service not found: %s.%s", name.Module, name.Name)
682+
}
683+
684+
// asyncTypeString formats an AsyncAPI property type for display.
685+
func asyncTypeString(p *mpr.AsyncAPIProperty) string {
686+
if p.Format != "" {
687+
return p.Type + " (" + p.Format + ")"
688+
}
689+
return p.Type
690+
}

0 commit comments

Comments
 (0)