Skip to content

Commit 32802d8

Browse files
akoclaude
andcommitted
feat(catalog): add contract_entities, contract_actions, contract_messages tables
Parse cached $metadata and AsyncAPI contracts during REFRESH CATALOG and populate queryable catalog tables for contract assets. New catalog tables: - contract_entities — entity types from cached OData $metadata - contract_actions — actions/functions from cached OData $metadata - contract_messages — messages from cached AsyncAPI documents Also: - Register all integration tables in CATALOG.xxx query name mapping (rest_clients, rest_operations, published_rest_*, external_*, business_events, contract_*) - Register all integration tables in SHOW CATALOG TABLES listing - Update help topics, quick reference, docs-site with catalog query examples Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent e424eaa commit 32802d8

File tree

9 files changed

+330
-1
lines changed

9 files changed

+330
-1
lines changed

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,7 @@ Full syntax tables for all MDL statements (microflows, pages, security, navigati
409409
- AsyncAPI contract browsing (SHOW/DESCRIBE CONTRACT CHANNELS/MESSAGES FROM cached AsyncAPI)
410410
- SHOW EXTERNAL ACTIONS, SHOW PUBLISHED REST SERVICES
411411
- Integration catalog tables (rest_clients, rest_operations, published_rest_services, external_entities, external_actions, business_events)
412+
- Contract catalog tables (contract_entities, contract_actions, contract_messages — parsed from cached $metadata and AsyncAPI)
412413

413414
**Not Yet Implemented:**
414415
- 47 of 52 metamodel domains (REST, etc.)

cmd/mxcli/help_topics/odata.txt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,5 +128,17 @@ CATALOG QUERIES
128128
SELECT * FROM CATALOG.ODATA_CLIENTS;
129129
SELECT * FROM CATALOG.ODATA_SERVICES;
130130
SELECT * FROM CATALOG.ENTITIES WHERE IsExternal = 1;
131+
SELECT * FROM CATALOG.EXTERNAL_ENTITIES;
132+
SELECT * FROM CATALOG.EXTERNAL_ACTIONS;
133+
134+
-- Contract entities/actions from cached $metadata (requires REFRESH CATALOG)
135+
SELECT * FROM CATALOG.CONTRACT_ENTITIES;
136+
SELECT * FROM CATALOG.CONTRACT_ACTIONS;
137+
SELECT * FROM CATALOG.CONTRACT_MESSAGES;
138+
139+
-- Find all available entities from a specific service
140+
SELECT EntityName, EntitySetName, KeyProperties, PropertyCount
141+
FROM CATALOG.CONTRACT_ENTITIES
142+
WHERE ServiceQualifiedName = 'MyModule.SalesforceAPI';
131143

132144
CREATE OR MODIFY — all three types support idempotent upsert.

docs-site/src/appendixes/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 channels | `SHOW CONTRACT CHANNELS FROM Module.Service;` | Browse cached AsyncAPI |
103103
| Show contract messages | `SHOW CONTRACT MESSAGES FROM Module.Service;` | Browse cached AsyncAPI |
104104
| Describe contract message | `DESCRIBE CONTRACT MESSAGE Module.Service.Message;` | Message payload properties |
105+
| Query contract entities | `SELECT * FROM CATALOG.CONTRACT_ENTITIES;` | Requires REFRESH CATALOG |
106+
| Query contract actions | `SELECT * FROM CATALOG.CONTRACT_ACTIONS;` | Requires REFRESH CATALOG |
107+
| Query contract messages | `SELECT * FROM CATALOG.CONTRACT_MESSAGES;` | Requires REFRESH CATALOG |
105108

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

docs/01-project/MDL_QUICK_REFERENCE.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ CREATE CONSTANT MyModule.EnableLogging TYPE Boolean DEFAULT true;
105105
| Show contract channels | `SHOW CONTRACT CHANNELS FROM Module.Service;` | Browse cached AsyncAPI |
106106
| Show contract messages | `SHOW CONTRACT MESSAGES FROM Module.Service;` | Browse cached AsyncAPI |
107107
| Describe contract message | `DESCRIBE CONTRACT MESSAGE Module.Service.Message;` | Message payload properties |
108+
| Query contract entities | `SELECT * FROM CATALOG.CONTRACT_ENTITIES;` | Requires REFRESH CATALOG |
109+
| Query contract actions | `SELECT * FROM CATALOG.CONTRACT_ACTIONS;` | Requires REFRESH CATALOG |
110+
| Query contract messages | `SELECT * FROM CATALOG.CONTRACT_MESSAGES;` | Requires REFRESH CATALOG |
108111

109112
**OData Client Example:**
110113
```sql

mdl/catalog/builder.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -356,6 +356,14 @@ func (b *Builder) Build(progress ProgressFunc) error {
356356
return fmt.Errorf("failed to build business events: %w", err)
357357
}
358358

359+
if err := b.buildContractEntities(); err != nil {
360+
return fmt.Errorf("failed to build contract entities: %w", err)
361+
}
362+
363+
if err := b.buildContractMessages(); err != nil {
364+
return fmt.Errorf("failed to build contract messages: %w", err)
365+
}
366+
359367
if err := b.buildNavigation(); err != nil {
360368
return fmt.Errorf("failed to build navigation: %w", err)
361369
}

mdl/catalog/builder_contract.go

Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package catalog
4+
5+
import (
6+
"crypto/sha256"
7+
"fmt"
8+
"strings"
9+
10+
"github.com/mendixlabs/mxcli/sdk/mpr"
11+
)
12+
13+
// buildContractEntities parses cached $metadata from consumed OData services
14+
// and populates the contract_entities and contract_actions catalog tables.
15+
func (b *Builder) buildContractEntities() error {
16+
services, err := b.reader.ListConsumedODataServices()
17+
if err != nil {
18+
return err
19+
}
20+
21+
entityStmt, err := b.tx.Prepare(`
22+
INSERT INTO contract_entities (Id, ServiceId, ServiceQualifiedName,
23+
EntityName, EntitySetName, KeyProperties, PropertyCount, NavigationCount,
24+
Summary, Description, ModuleName,
25+
ProjectId, SnapshotId, SnapshotDate, SnapshotSource)
26+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
27+
`)
28+
if err != nil {
29+
return err
30+
}
31+
defer entityStmt.Close()
32+
33+
actionStmt, err := b.tx.Prepare(`
34+
INSERT INTO contract_actions (Id, ServiceId, ServiceQualifiedName,
35+
ActionName, IsBound, ParameterCount, ReturnType, ModuleName,
36+
ProjectId, SnapshotId, SnapshotDate, SnapshotSource)
37+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
38+
`)
39+
if err != nil {
40+
return err
41+
}
42+
defer actionStmt.Close()
43+
44+
projectID, _, snapshotID, snapshotDate, snapshotSource, _, _, _ := b.snapshotMeta()
45+
46+
entityCount := 0
47+
actionCount := 0
48+
49+
for _, svc := range services {
50+
if svc.Metadata == "" {
51+
continue
52+
}
53+
54+
moduleID := b.hierarchy.findModuleID(svc.ContainerID)
55+
moduleName := b.hierarchy.getModuleName(moduleID)
56+
svcQN := moduleName + "." + svc.Name
57+
58+
doc, err := mpr.ParseEdmx(svc.Metadata)
59+
if err != nil {
60+
continue // skip services with unparseable metadata
61+
}
62+
63+
// Build entity set lookup
64+
esMap := make(map[string]string)
65+
for _, es := range doc.EntitySets {
66+
esMap[es.EntityType] = es.Name
67+
}
68+
69+
for _, s := range doc.Schemas {
70+
for _, et := range s.EntityTypes {
71+
syntheticID := fmt.Sprintf("%x", sha256.Sum256([]byte(svcQN+"|entity|"+et.Name)))[:32]
72+
entitySetName := esMap[s.Namespace+"."+et.Name]
73+
keyProps := strings.Join(et.KeyProperties, ", ")
74+
75+
_, err := entityStmt.Exec(
76+
syntheticID,
77+
string(svc.ID),
78+
svcQN,
79+
et.Name,
80+
entitySetName,
81+
keyProps,
82+
len(et.Properties),
83+
len(et.NavigationProperties),
84+
et.Summary,
85+
et.Description,
86+
moduleName,
87+
projectID, snapshotID, snapshotDate, snapshotSource,
88+
)
89+
if err != nil {
90+
return err
91+
}
92+
entityCount++
93+
}
94+
}
95+
96+
for _, a := range doc.Actions {
97+
syntheticID := fmt.Sprintf("%x", sha256.Sum256([]byte(svcQN+"|action|"+a.Name)))[:32]
98+
isBound := 0
99+
if a.IsBound {
100+
isBound = 1
101+
}
102+
retType := a.ReturnType
103+
if retType == "" {
104+
retType = "(void)"
105+
}
106+
107+
_, err := actionStmt.Exec(
108+
syntheticID,
109+
string(svc.ID),
110+
svcQN,
111+
a.Name,
112+
isBound,
113+
len(a.Parameters),
114+
retType,
115+
moduleName,
116+
projectID, snapshotID, snapshotDate, snapshotSource,
117+
)
118+
if err != nil {
119+
return err
120+
}
121+
actionCount++
122+
}
123+
}
124+
125+
b.report("Contract Entities", entityCount)
126+
if actionCount > 0 {
127+
b.report("Contract Actions", actionCount)
128+
}
129+
return nil
130+
}
131+
132+
// buildContractMessages parses cached AsyncAPI documents from business event client
133+
// services and populates the contract_messages catalog table.
134+
func (b *Builder) buildContractMessages() error {
135+
services, err := b.cachedBusinessEventServices()
136+
if err != nil {
137+
return err
138+
}
139+
140+
stmt, err := b.tx.Prepare(`
141+
INSERT INTO contract_messages (Id, ServiceId, ServiceQualifiedName,
142+
ChannelName, OperationType, MessageName, Title, ContentType, PropertyCount,
143+
ModuleName, ProjectId, SnapshotId, SnapshotDate, SnapshotSource)
144+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
145+
`)
146+
if err != nil {
147+
return err
148+
}
149+
defer stmt.Close()
150+
151+
projectID, _, snapshotID, snapshotDate, snapshotSource, _, _, _ := b.snapshotMeta()
152+
153+
count := 0
154+
155+
for _, svc := range services {
156+
if svc.Document == "" {
157+
continue
158+
}
159+
160+
moduleID := b.hierarchy.findModuleID(svc.ContainerID)
161+
moduleName := b.hierarchy.getModuleName(moduleID)
162+
svcQN := moduleName + "." + svc.Name
163+
164+
doc, err := mpr.ParseAsyncAPI(svc.Document)
165+
if err != nil {
166+
continue
167+
}
168+
169+
// Build message lookup for property counts
170+
msgProps := make(map[string]int)
171+
for _, msg := range doc.Messages {
172+
msgProps[msg.Name] = len(msg.Properties)
173+
}
174+
175+
for _, ch := range doc.Channels {
176+
syntheticID := fmt.Sprintf("%x", sha256.Sum256([]byte(svcQN+"|channel|"+ch.Name+"|"+ch.MessageRef)))[:32]
177+
178+
propCount := msgProps[ch.MessageRef]
179+
180+
// Find message details
181+
title := ""
182+
contentType := ""
183+
if msg := doc.FindMessage(ch.MessageRef); msg != nil {
184+
title = msg.Title
185+
contentType = msg.ContentType
186+
}
187+
188+
_, err := stmt.Exec(
189+
syntheticID,
190+
string(svc.ID),
191+
svcQN,
192+
ch.Name,
193+
ch.OperationType,
194+
ch.MessageRef,
195+
title,
196+
contentType,
197+
propCount,
198+
moduleName,
199+
projectID, snapshotID, snapshotDate, snapshotSource,
200+
)
201+
if err != nil {
202+
return err
203+
}
204+
count++
205+
}
206+
}
207+
208+
b.report("Contract Messages", count)
209+
return nil
210+
}

mdl/catalog/catalog.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,16 @@ func (c *Catalog) Tables() []string {
130130
"CATALOG.DATABASE_CONNECTIONS",
131131
"CATALOG.CONSTANTS",
132132
"CATALOG.CONSTANT_VALUES",
133+
"CATALOG.REST_CLIENTS",
134+
"CATALOG.REST_OPERATIONS",
135+
"CATALOG.PUBLISHED_REST_SERVICES",
136+
"CATALOG.PUBLISHED_REST_OPERATIONS",
137+
"CATALOG.EXTERNAL_ENTITIES",
138+
"CATALOG.EXTERNAL_ACTIONS",
139+
"CATALOG.BUSINESS_EVENTS",
140+
"CATALOG.CONTRACT_ENTITIES",
141+
"CATALOG.CONTRACT_ACTIONS",
142+
"CATALOG.CONTRACT_MESSAGES",
133143
"CATALOG.STRINGS",
134144
"CATALOG.SOURCE",
135145
}

mdl/catalog/tables.go

Lines changed: 72 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,59 @@ func (c *Catalog) createTables() error {
588588
SnapshotSource TEXT
589589
)`,
590590

591+
// Contract entities — entity types parsed from cached $metadata on consumed OData services
592+
`CREATE TABLE IF NOT EXISTS contract_entities (
593+
Id TEXT PRIMARY KEY,
594+
ServiceId TEXT,
595+
ServiceQualifiedName TEXT,
596+
EntityName TEXT,
597+
EntitySetName TEXT,
598+
KeyProperties TEXT,
599+
PropertyCount INTEGER DEFAULT 0,
600+
NavigationCount INTEGER DEFAULT 0,
601+
Summary TEXT,
602+
Description TEXT,
603+
ModuleName TEXT,
604+
ProjectId TEXT,
605+
SnapshotId TEXT,
606+
SnapshotDate TEXT,
607+
SnapshotSource TEXT
608+
)`,
609+
610+
// Contract actions — actions/functions parsed from cached $metadata on consumed OData services
611+
`CREATE TABLE IF NOT EXISTS contract_actions (
612+
Id TEXT PRIMARY KEY,
613+
ServiceId TEXT,
614+
ServiceQualifiedName TEXT,
615+
ActionName TEXT,
616+
IsBound INTEGER DEFAULT 0,
617+
ParameterCount INTEGER DEFAULT 0,
618+
ReturnType TEXT,
619+
ModuleName TEXT,
620+
ProjectId TEXT,
621+
SnapshotId TEXT,
622+
SnapshotDate TEXT,
623+
SnapshotSource TEXT
624+
)`,
625+
626+
// Contract messages — messages parsed from cached AsyncAPI on business event client services
627+
`CREATE TABLE IF NOT EXISTS contract_messages (
628+
Id TEXT PRIMARY KEY,
629+
ServiceId TEXT,
630+
ServiceQualifiedName TEXT,
631+
ChannelName TEXT,
632+
OperationType TEXT,
633+
MessageName TEXT,
634+
Title TEXT,
635+
ContentType TEXT,
636+
PropertyCount INTEGER DEFAULT 0,
637+
ModuleName TEXT,
638+
ProjectId TEXT,
639+
SnapshotId TEXT,
640+
SnapshotDate TEXT,
641+
SnapshotSource TEXT
642+
)`,
643+
591644
`CREATE TABLE IF NOT EXISTS database_connections (
592645
Id TEXT PRIMARY KEY,
593646
Name TEXT,
@@ -754,7 +807,19 @@ func (c *Catalog) createTables() error {
754807
UNION ALL
755808
SELECT Id, 'BUSINESS_EVENT' as ObjectType, MessageName as Name, ServiceQualifiedName || '.' || MessageName as QualifiedName, ModuleName, '' as Folder, '' as Description,
756809
ProjectId, SnapshotId || '' as ProjectName, SnapshotId, SnapshotDate, SnapshotSource
757-
FROM business_events`,
810+
FROM business_events
811+
UNION ALL
812+
SELECT Id, 'CONTRACT_ENTITY' as ObjectType, EntityName as Name, ServiceQualifiedName || '.' || EntityName as QualifiedName, ModuleName, '' as Folder, Summary as Description,
813+
ProjectId, '' as ProjectName, SnapshotId, SnapshotDate, SnapshotSource
814+
FROM contract_entities
815+
UNION ALL
816+
SELECT Id, 'CONTRACT_ACTION' as ObjectType, ActionName as Name, ServiceQualifiedName || '.' || ActionName as QualifiedName, ModuleName, '' as Folder, '' as Description,
817+
ProjectId, '' as ProjectName, SnapshotId, SnapshotDate, SnapshotSource
818+
FROM contract_actions
819+
UNION ALL
820+
SELECT Id, 'CONTRACT_MESSAGE' as ObjectType, MessageName as Name, ServiceQualifiedName || '.' || MessageName as QualifiedName, ModuleName, '' as Folder, '' as Description,
821+
ProjectId, '' as ProjectName, SnapshotId, SnapshotDate, SnapshotSource
822+
FROM contract_messages`,
758823

759824
// FTS5 virtual tables for full-text search
760825
`CREATE VIRTUAL TABLE IF NOT EXISTS strings USING fts5(
@@ -827,6 +892,12 @@ func (c *Catalog) createTables() error {
827892
`CREATE INDEX IF NOT EXISTS idx_external_actions_module ON external_actions(ModuleName)`,
828893
`CREATE INDEX IF NOT EXISTS idx_business_events_service ON business_events(ServiceId)`,
829894
`CREATE INDEX IF NOT EXISTS idx_business_events_module ON business_events(ModuleName)`,
895+
`CREATE INDEX IF NOT EXISTS idx_contract_entities_service ON contract_entities(ServiceId)`,
896+
`CREATE INDEX IF NOT EXISTS idx_contract_entities_module ON contract_entities(ModuleName)`,
897+
`CREATE INDEX IF NOT EXISTS idx_contract_actions_service ON contract_actions(ServiceId)`,
898+
`CREATE INDEX IF NOT EXISTS idx_contract_actions_module ON contract_actions(ModuleName)`,
899+
`CREATE INDEX IF NOT EXISTS idx_contract_messages_service ON contract_messages(ServiceId)`,
900+
`CREATE INDEX IF NOT EXISTS idx_contract_messages_module ON contract_messages(ModuleName)`,
830901
`CREATE INDEX IF NOT EXISTS idx_constants_name ON constants(Name)`,
831902
`CREATE INDEX IF NOT EXISTS idx_constants_module ON constants(ModuleName)`,
832903
`CREATE INDEX IF NOT EXISTS idx_constant_values_constant ON constant_values(ConstantName)`,

0 commit comments

Comments
 (0)