Skip to content

Commit a03b590

Browse files
dionesiusapclaude
andcommitted
feat: add catalog show command and use full UUIDs in search
Implements 'mxcli catalog show <uuid>' for detailed endpoint inspection and changes search output to show full UUIDs instead of shortened. Changes: - Show full UUID (36 chars) in search table output - Users can copy UUID directly for use with show command - Add catalog show command with human-readable and JSON output - Display entities, actions, security scheme, environment New types: - EndpointDetails, ServiceVersion, Contract, Document - Entity, Attribute, Association - Action, Parameter, ReturnType - SecurityScheme, SecurityType, Role - EnvironmentWithApp (nested Application in endpoint response) New client method: - GetEndpoint(ctx, uuid) - Calls GET /endpoints/{uuid} Updated documentation: - Skills file reflects full UUID in table - Proposal updated with full UUID rationale - Table width now ~155 chars with full UUIDs Tested against real Catalog API. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent 46c07bf commit a03b590

5 files changed

Lines changed: 303 additions & 16 deletions

File tree

.claude/skills/mendix/catalog-search.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,10 +37,10 @@ mxcli catalog search "data" --json | jq '.[] | {name, uuid, type}'
3737

3838
**Table (default):**
3939
```
40-
NAME TYPE VERSION APPLICATION ENVIRONMENT PROD UUID
41-
CustomerService OData 1.2.0 CRM Application Production Yes a7f3c2d1
42-
OrderAPI REST 2.0.1 E-commerce Platform Acceptance No b8e4d3e2
43-
InventorySync SOAP 1.0.0 Warehouse System Test No c9f5e4f3
40+
NAME TYPE VERSION APPLICATION ENVIRONMENT PROD UUID
41+
CustomerService OData 1.2.0 CRM Application Production Yes a7f3c2d1-4b5e-6c7f-8d9e-0a1b2c3d4e5f
42+
OrderAPI REST 2.0.1 E-commerce Platform Acceptance No b8e4d3e2-1a2b-3c4d-5e6f-7a8b9c0d1e2f
43+
InventorySync SOAP 1.0.0 Warehouse System Test No c9f5e4f3-2b3c-4d5e-6f7a-8b9c0d1e2f3a
4444
4545
Total: 42 results (showing 1-3)
4646
```
@@ -51,7 +51,7 @@ Total: 42 results (showing 1-3)
5151
- **APPLICATION**: Hosting application name
5252
- **ENVIRONMENT**: Production, Acceptance, Test
5353
- **PROD**: "Yes" if production, blank otherwise
54-
- **UUID**: First 8 characters (full UUID in JSON mode)
54+
- **UUID**: Full UUID (36 chars) - copy this for use with `mxcli catalog show <uuid>`
5555

5656
**JSON mode:**
5757
```bash

cmd/mxcli/cmd_catalog.go

Lines changed: 121 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package main
55
import (
66
"encoding/json"
77
"fmt"
8+
"strings"
89
"text/tabwriter"
910

1011
"github.com/mendixlabs/mxcli/internal/auth"
@@ -38,6 +39,18 @@ Examples:
3839
RunE: runCatalogSearch,
3940
}
4041

42+
var catalogShowCmd = &cobra.Command{
43+
Use: "show <uuid>",
44+
Short: "Show detailed endpoint metadata",
45+
Long: `Display detailed metadata for a Catalog endpoint including entities, actions, and contract.
46+
47+
Examples:
48+
mxcli catalog show a7f3c2d1-4b5e-6c7f-8d9e-0a1b2c3d4e5f
49+
mxcli catalog show a7f3c2d1 --json`,
50+
Args: cobra.ExactArgs(1),
51+
RunE: runCatalogShow,
52+
}
53+
4154
func init() {
4255
catalogSearchCmd.Flags().String("profile", auth.ProfileDefault, "credential profile name")
4356
catalogSearchCmd.Flags().String("service-type", "", "filter by service type (OData, REST, SOAP)")
@@ -47,7 +60,11 @@ func init() {
4760
catalogSearchCmd.Flags().Int("offset", 0, "pagination offset")
4861
catalogSearchCmd.Flags().Bool("json", false, "output as JSON array")
4962

63+
catalogShowCmd.Flags().String("profile", auth.ProfileDefault, "credential profile name")
64+
catalogShowCmd.Flags().Bool("json", false, "output full JSON response")
65+
5066
catalogCmd.AddCommand(catalogSearchCmd)
67+
catalogCmd.AddCommand(catalogShowCmd)
5168
rootCmd.AddCommand(catalogCmd)
5269
}
5370

@@ -113,10 +130,7 @@ func outputTable(cmd *cobra.Command, resp *catalog.SearchResponse) error {
113130
if item.Environment.Type == "Production" {
114131
prod = "Yes"
115132
}
116-
uuid := item.UUID
117-
if len(uuid) >= 8 {
118-
uuid = uuid[:8] // Short UUID
119-
}
133+
uuid := item.UUID // Full UUID (36 chars) so users can use it with `show`
120134

121135
fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
122136
name, typ, version, app, env, prod, uuid)
@@ -134,6 +148,109 @@ func outputJSON(cmd *cobra.Command, data []catalog.SearchResult) error {
134148
return enc.Encode(data)
135149
}
136150

151+
func runCatalogShow(cmd *cobra.Command, args []string) error {
152+
uuid := args[0]
153+
profile, _ := cmd.Flags().GetString("profile")
154+
asJSON, _ := cmd.Flags().GetBool("json")
155+
156+
// Create client
157+
client, err := catalog.NewClient(cmd.Context(), profile)
158+
if err != nil {
159+
if _, ok := err.(*auth.ErrNoCredential); ok {
160+
return fmt.Errorf("no credential found. Run: mxcli auth login")
161+
}
162+
return err
163+
}
164+
165+
// Get endpoint details
166+
endpoint, err := client.GetEndpoint(cmd.Context(), uuid)
167+
if err != nil {
168+
if _, ok := err.(*auth.ErrUnauthenticated); ok {
169+
return fmt.Errorf("authentication failed. Run: mxcli auth login")
170+
}
171+
return err
172+
}
173+
174+
// Output
175+
if asJSON {
176+
enc := json.NewEncoder(cmd.OutOrStdout())
177+
enc.SetIndent("", " ")
178+
return enc.Encode(endpoint)
179+
}
180+
return outputEndpointDetails(cmd, endpoint)
181+
}
182+
183+
func outputEndpointDetails(cmd *cobra.Command, ep *catalog.EndpointDetails) error {
184+
w := cmd.OutOrStdout()
185+
sv := ep.ServiceVersion
186+
187+
// Basic info
188+
fmt.Fprintf(w, "Name: %s\n", sv.Description)
189+
fmt.Fprintf(w, "Type: %s\n", sv.Type)
190+
fmt.Fprintf(w, "Version: %s\n", sv.Version)
191+
fmt.Fprintf(w, "Application: %s\n", ep.Environment.Application.Name)
192+
fmt.Fprintf(w, "Environment: %s (%s)\n", ep.Environment.Type, ep.Environment.Location)
193+
if ep.Location != "" {
194+
fmt.Fprintf(w, "Location: %s\n", ep.Location)
195+
}
196+
fmt.Fprintf(w, "\n")
197+
198+
// Security
199+
if sv.SecurityScheme != nil && len(sv.SecurityScheme.SecurityTypes) > 0 {
200+
var types []string
201+
for _, st := range sv.SecurityScheme.SecurityTypes {
202+
types = append(types, st.Name)
203+
}
204+
fmt.Fprintf(w, "Security: %s\n", strings.Join(types, ", "))
205+
}
206+
fmt.Fprintf(w, "Validated: %v\n", ep.Validated)
207+
fmt.Fprintf(w, "Last Updated: %s\n", ep.LastUpdated)
208+
fmt.Fprintf(w, "\n")
209+
210+
// Entities (OData only)
211+
if sv.TotalEntities > 0 {
212+
fmt.Fprintf(w, "Entities (%d):\n", sv.TotalEntities)
213+
for _, ent := range sv.Entities {
214+
fmt.Fprintf(w, " - %s (%d attributes", ent.Name, ent.TotalAttributes)
215+
if ent.TotalAssociations > 0 {
216+
fmt.Fprintf(w, ", %d associations", ent.TotalAssociations)
217+
}
218+
fmt.Fprintf(w, ")\n")
219+
220+
// Show first 3 attributes
221+
if len(ent.Attributes) > 0 {
222+
var attrNames []string
223+
for i, attr := range ent.Attributes {
224+
if i >= 3 {
225+
break
226+
}
227+
attrNames = append(attrNames, attr.Name)
228+
}
229+
fmt.Fprintf(w, " Attributes: %s", strings.Join(attrNames, ", "))
230+
if len(ent.Attributes) > 3 {
231+
fmt.Fprintf(w, ", ...")
232+
}
233+
fmt.Fprintf(w, "\n")
234+
}
235+
}
236+
fmt.Fprintf(w, "\n")
237+
}
238+
239+
// Actions (OData only)
240+
if sv.TotalActions > 0 {
241+
fmt.Fprintf(w, "Actions (%d):\n", sv.TotalActions)
242+
for _, action := range sv.Actions {
243+
fmt.Fprintf(w, " - %s", action.Name)
244+
if action.TotalParameters > 0 {
245+
fmt.Fprintf(w, " (%d parameters)", action.TotalParameters)
246+
}
247+
fmt.Fprintf(w, "\n")
248+
}
249+
}
250+
251+
return nil
252+
}
253+
137254
func truncate(s string, max int) string {
138255
if len(s) <= max {
139256
return s

docs/11-proposals/PROPOSAL_catalog_integration.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -186,13 +186,13 @@ mxcli catalog search "sales" --owned-only
186186

187187
### Table Output Format
188188

189-
**Design Decision:** 7 columns, ~120 chars wide (fits standard terminal width).
189+
**Design Decision:** 7 columns, ~155 chars wide with full UUIDs.
190190

191191
```
192-
NAME TYPE VERSION APPLICATION ENVIRONMENT PROD UUID
193-
CustomerService OData 1.2.0 CRM Application Production Yes a7f3c2d1
194-
OrderAPI REST 2.0.1 E-commerce Platform Acceptance No b8e4d3e2
195-
InventorySync SOAP 1.0.0 Warehouse System Test No c9f5e4f3
192+
NAME TYPE VERSION APPLICATION ENVIRONMENT PROD UUID
193+
CustomerService OData 1.2.0 CRM Application Production Yes a7f3c2d1-4b5e-6c7f-8d9e-0a1b2c3d4e5f
194+
OrderAPI REST 2.0.1 E-commerce Platform Acceptance No b8e4d3e2-1a2b-3c4d-5e6f-7a8b9c0d1e2f
195+
InventorySync SOAP 1.0.0 Warehouse System Test No c9f5e4f3-2b3c-4d5e-6f7a-8b9c0d1e2f3a
196196
```
197197

198198
**Column Widths:**
@@ -202,10 +202,10 @@ InventorySync SOAP 1.0.0 Warehouse System Test No
202202
- APPLICATION (20 chars) — Truncate with "..." if longer
203203
- ENVIRONMENT (12 chars) — Type field (Production, Acceptance, Test)
204204
- PROD (4 chars) — "Yes" if environment.Type == "Production", blank otherwise
205-
- UUID (8 chars) — First 8 chars only (full UUID available in --json mode)
205+
- UUID (36 chars) — Full UUID for use with `mxcli catalog show <uuid>`
206206

207207
**Rationale:**
208-
- Short UUIDs reduce cognitive load while remaining unique enough for manual lookup
208+
- Full UUIDs required for `catalog show` command (API requires full UUID)
209209
- PROD column provides at-a-glance production status without reading ENVIRONMENT
210210
- APPLICATION provides context without requiring a separate lookup
211211
- Full details available via `--json` for scripting use cases

internal/catalog/client.go

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,35 @@ func (c *Client) Search(ctx context.Context, opts SearchOptions) (*SearchRespons
8484

8585
return &result, nil
8686
}
87+
88+
// GetEndpoint retrieves detailed endpoint metadata by UUID.
89+
// Calls GET /endpoints/{uuid} and returns parsed result including embedded contract.
90+
func (c *Client) GetEndpoint(ctx context.Context, uuid string) (*EndpointDetails, error) {
91+
reqURL := c.baseURL + "/endpoints/" + uuid
92+
req, err := http.NewRequestWithContext(ctx, "GET", reqURL, nil)
93+
if err != nil {
94+
return nil, err
95+
}
96+
req.Header.Set("Accept", "application/json")
97+
98+
resp, err := c.httpClient.Do(req)
99+
if err != nil {
100+
// auth.authTransport wraps 401/403 as auth.ErrUnauthenticated
101+
return nil, err
102+
}
103+
defer resp.Body.Close()
104+
105+
if resp.StatusCode == http.StatusNotFound {
106+
return nil, fmt.Errorf("endpoint %s not found", uuid)
107+
}
108+
if resp.StatusCode != http.StatusOK {
109+
return nil, fmt.Errorf("catalog API returned status %d", resp.StatusCode)
110+
}
111+
112+
var result EndpointDetails
113+
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
114+
return nil, fmt.Errorf("decode response: %w", err)
115+
}
116+
117+
return &result, nil
118+
}

internal/catalog/types.go

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,141 @@ type Owner struct {
5757
Email string `json:"email"`
5858
UUID string `json:"uuid"`
5959
}
60+
61+
// EndpointDetails represents the full response from GET /endpoints/{uuid}.
62+
type EndpointDetails struct {
63+
UUID string `json:"uuid"`
64+
Path string `json:"path"`
65+
Location string `json:"location"`
66+
Discoverable bool `json:"discoverable"`
67+
Validated bool `json:"validated"`
68+
SecurityClassification string `json:"securityClassification"`
69+
Connections int `json:"connections"`
70+
LastUpdated string `json:"lastUpdated"`
71+
ServiceVersion ServiceVersion `json:"serviceVersion"`
72+
Environment EnvironmentWithApp `json:"environment"`
73+
}
74+
75+
// EnvironmentWithApp extends Environment with nested Application (used in endpoint details).
76+
type EnvironmentWithApp struct {
77+
Name string `json:"name"`
78+
Location string `json:"location"`
79+
Type string `json:"type"`
80+
UUID string `json:"uuid"`
81+
Application Application `json:"application"`
82+
}
83+
84+
// ServiceVersion contains version-specific metadata and contract.
85+
type ServiceVersion struct {
86+
Version string `json:"version"`
87+
Description string `json:"description"`
88+
UUID string `json:"uuid"`
89+
PublishDate string `json:"publishDate"`
90+
Type string `json:"type"` // "OData", "REST", "SOAP"
91+
Contracts []Contract `json:"contracts"`
92+
SecurityScheme *SecurityScheme `json:"securityScheme,omitempty"`
93+
TotalEntities int `json:"totalEntities"`
94+
Entities []Entity `json:"entities"`
95+
TotalActions int `json:"totalActions"`
96+
Actions []Action `json:"actions"`
97+
}
98+
99+
// Contract represents an API contract (metadata, OpenAPI, WSDL, etc).
100+
type Contract struct {
101+
Type string `json:"type"` // "CSDL", "OpenAPI", "WSDL"
102+
SpecificationVersion string `json:"specificationVersion"`
103+
DocumentBaseURL string `json:"documentBaseURL"`
104+
Documents []Document `json:"documents"`
105+
}
106+
107+
// Document contains the actual contract content.
108+
type Document struct {
109+
IsPrimary bool `json:"isPrimary"`
110+
URI string `json:"uri"`
111+
Contents string `json:"contents"` // Embedded XML/JSON contract
112+
}
113+
114+
// SecurityScheme describes authentication requirements.
115+
type SecurityScheme struct {
116+
SecurityTypes []SecurityType `json:"securityTypes"`
117+
MxAllowedRoles []Role `json:"mxAllowedRoles,omitempty"`
118+
}
119+
120+
// SecurityType represents an authentication method.
121+
type SecurityType struct {
122+
Name string `json:"name"` // "Basic", "MxID", etc.
123+
MarketplaceModuleID string `json:"marketplaceModuleID,omitempty"`
124+
}
125+
126+
// Role represents a Mendix module role.
127+
type Role struct {
128+
Name string `json:"name"`
129+
UUID string `json:"uuid"`
130+
}
131+
132+
// Entity represents an OData entity set.
133+
type Entity struct {
134+
Name string `json:"name"`
135+
EntitySetName string `json:"entitySetName"`
136+
EntityTypeName string `json:"entityTypeName"`
137+
Namespace string `json:"namespace"`
138+
Validated bool `json:"validated"`
139+
Updatable bool `json:"updatable"`
140+
Insertable bool `json:"insertable"`
141+
Deletable bool `json:"deletable"`
142+
TotalAttributes int `json:"totalAttributes"`
143+
Attributes []Attribute `json:"attributes"`
144+
TotalAssociations int `json:"totalAssociations"`
145+
Associations []Association `json:"associations"`
146+
}
147+
148+
// Attribute represents an entity attribute.
149+
type Attribute struct {
150+
Name string `json:"name"`
151+
TypeName string `json:"typeName"`
152+
TypeKind string `json:"typeKind"`
153+
Updatable bool `json:"updatable"`
154+
Insertable bool `json:"insertable"`
155+
Filterable bool `json:"filterable"`
156+
Sortable bool `json:"sortable"`
157+
}
158+
159+
// Association represents an entity association.
160+
type Association struct {
161+
Name string `json:"name"`
162+
ReferencedDataset string `json:"referencedDataset"`
163+
Multiplicity string `json:"multiplicity"`
164+
EntitySetName string `json:"entitySetName"`
165+
EntityTypeName string `json:"entityTypeName"`
166+
Namespace string `json:"namespace"`
167+
}
168+
169+
// Action represents an OData action or function.
170+
type Action struct {
171+
Name string `json:"name"`
172+
FullyQualifiedName string `json:"fullyQualifiedName"`
173+
Summary string `json:"summary"`
174+
Description string `json:"description"`
175+
TotalParameters int `json:"totalParameters"`
176+
Parameters []Parameter `json:"parameters"`
177+
ReturnType *ReturnType `json:"returnType,omitempty"`
178+
}
179+
180+
// Parameter represents an action/function parameter.
181+
type Parameter struct {
182+
Name string `json:"name"`
183+
TypeKind string `json:"typekind"`
184+
TypeName string `json:"typeName"`
185+
IsCollection bool `json:"isCollection"`
186+
Nullable bool `json:"nullable"`
187+
Summary string `json:"summary"`
188+
Description string `json:"description"`
189+
}
190+
191+
// ReturnType represents an action/function return type.
192+
type ReturnType struct {
193+
TypeKind string `json:"typekind"`
194+
TypeName string `json:"typeName"`
195+
IsCollection bool `json:"isCollection"`
196+
Nullable bool `json:"nullable"`
197+
}

0 commit comments

Comments
 (0)