Skip to content

Commit 5d64e28

Browse files
akoclaude
andcommitted
feat: CREATE/DROP PUBLISHED REST SERVICE with bug fixes
Implements issue #135: full write support for published REST services. New features: - CREATE PUBLISHED REST SERVICE with resources, operations, path params - CREATE OR REPLACE (deletes existing before creating) - DROP PUBLISHED REST SERVICE - Folder property support (CREATE and DESCRIBE) - SEARCH indexes REST service paths, names, resource names Bug fixes from testing: - BUG 1: DESCRIBE outputs uppercase HTTP methods and quoted paths for roundtrip compatibility - BUG 2: CREATE OR REPLACE now replaces instead of duplicating - BUG 3: Folder property stored and rendered in DESCRIBE - BUG 4: SEARCH indexes published REST service strings Future work tracked in issues: - #161: ALTER PUBLISHED REST SERVICE - #162: GRANT/REVOKE ACCESS on PUBLISHED REST SERVICE Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 1bc2946 commit 5d64e28

18 files changed

+14369
-12568
lines changed

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.

mdl/ast/ast_rest.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,44 @@ type DropRestClientStmt struct {
6262
}
6363

6464
func (s *DropRestClientStmt) isStatement() {}
65+
66+
// ============================================================================
67+
// Published REST Service Statements
68+
// ============================================================================
69+
70+
// CreatePublishedRestServiceStmt represents:
71+
//
72+
// CREATE PUBLISHED REST SERVICE Module.Name (Path: '...', Version: '...') { RESOURCE ... };
73+
type CreatePublishedRestServiceStmt struct {
74+
Name QualifiedName
75+
Path string
76+
Version string
77+
ServiceName string
78+
Folder string
79+
Resources []*PublishedRestResourceDef
80+
CreateOrReplace bool
81+
}
82+
83+
func (s *CreatePublishedRestServiceStmt) isStatement() {}
84+
85+
type PublishedRestResourceDef struct {
86+
Name string
87+
Operations []*PublishedRestOperationDef
88+
}
89+
90+
type PublishedRestOperationDef struct {
91+
HTTPMethod string // GET, POST, PUT, DELETE, PATCH
92+
Path string // endpoint path (e.g. "/{id}")
93+
Microflow QualifiedName // backing microflow
94+
Deprecated bool
95+
ImportMapping string // optional qualified name
96+
ExportMapping string // optional qualified name
97+
Commit string // optional: "Yes", "No"
98+
}
99+
100+
// DropPublishedRestServiceStmt represents: DROP PUBLISHED REST SERVICE Module.Name
101+
type DropPublishedRestServiceStmt struct {
102+
Name QualifiedName
103+
}
104+
105+
func (s *DropPublishedRestServiceStmt) isStatement() {}

mdl/catalog/builder_strings.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,33 @@ func (b *Builder) buildStrings() error {
124124
}
125125
}
126126

127+
// Extract from published REST services
128+
prsServices, err := b.reader.ListPublishedRestServices()
129+
if err == nil {
130+
for _, svc := range prsServices {
131+
moduleID := b.hierarchy.findModuleID(svc.ContainerID)
132+
moduleName := b.hierarchy.getModuleName(moduleID)
133+
qn := moduleName + "." + svc.Name
134+
svcID := string(svc.ID)
135+
136+
insert(qn, "PUBLISHED_REST_SERVICE", svc.Path, "rest_path", "", svcID, moduleName)
137+
insert(qn, "PUBLISHED_REST_SERVICE", svc.ServiceName, "service_name", "", svcID, moduleName)
138+
insert(qn, "PUBLISHED_REST_SERVICE", svc.Version, "version", "", svcID, moduleName)
139+
140+
for _, res := range svc.Resources {
141+
insert(qn, "PUBLISHED_REST_SERVICE", res.Name, "resource_name", "", svcID, moduleName)
142+
for _, op := range res.Operations {
143+
if op.Path != "" {
144+
insert(qn, "PUBLISHED_REST_SERVICE", op.Path, "operation_path", "", svcID, moduleName)
145+
}
146+
if op.Summary != "" {
147+
insert(qn, "PUBLISHED_REST_SERVICE", op.Summary, "operation_summary", "", svcID, moduleName)
148+
}
149+
}
150+
}
151+
}
152+
}
153+
127154
b.report("strings", count)
128155
return nil
129156
}

mdl/executor/cmd_published_rest.go

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"strings"
99

1010
"github.com/mendixlabs/mxcli/mdl/ast"
11+
"github.com/mendixlabs/mxcli/model"
1112
)
1213

1314
// showPublishedRestServices handles SHOW PUBLISHED REST SERVICES [IN module] command.
@@ -102,6 +103,10 @@ func (e *Executor) describePublishedRestService(name ast.QualifiedName) error {
102103
if svc.ServiceName != "" {
103104
fmt.Fprintf(e.output, ",\n ServiceName: '%s'", svc.ServiceName)
104105
}
106+
folderPath := h.BuildFolderPath(svc.ContainerID)
107+
if folderPath != "" {
108+
fmt.Fprintf(e.output, ",\n Folder: '%s'", folderPath)
109+
}
105110
fmt.Fprintln(e.output, "\n)")
106111

107112
if len(svc.Resources) > 0 {
@@ -121,8 +126,12 @@ func (e *Executor) describePublishedRestService(name ast.QualifiedName) error {
121126
if op.Summary != "" {
122127
summary = fmt.Sprintf(" -- %s", op.Summary)
123128
}
124-
fmt.Fprintf(e.output, " %s %s%s%s;%s\n",
125-
op.HTTPMethod, op.Path, mf, deprecated, summary)
129+
opPath := ""
130+
if op.Path != "" {
131+
opPath = fmt.Sprintf(" '%s'", op.Path)
132+
}
133+
fmt.Fprintf(e.output, " %s%s%s%s;%s\n",
134+
strings.ToUpper(op.HTTPMethod), opPath, mf, deprecated, summary)
126135
}
127136
fmt.Fprintln(e.output, " }")
128137
}
@@ -137,3 +146,120 @@ func (e *Executor) describePublishedRestService(name ast.QualifiedName) error {
137146

138147
return fmt.Errorf("published REST service not found: %s", name)
139148
}
149+
150+
// findPublishedRestService looks up a published REST service by module and name.
151+
func (e *Executor) findPublishedRestService(moduleName, name string) (*model.PublishedRestService, error) {
152+
services, err := e.reader.ListPublishedRestServices()
153+
if err != nil {
154+
return nil, err
155+
}
156+
h, err := e.getHierarchy()
157+
if err != nil {
158+
return nil, err
159+
}
160+
for _, svc := range services {
161+
modID := h.FindModuleID(svc.ContainerID)
162+
modName := h.GetModuleName(modID)
163+
if modName == moduleName && svc.Name == name {
164+
return svc, nil
165+
}
166+
}
167+
return nil, fmt.Errorf("not found")
168+
}
169+
170+
// execCreatePublishedRestService creates a new published REST service.
171+
func (e *Executor) execCreatePublishedRestService(s *ast.CreatePublishedRestServiceStmt) error {
172+
if e.writer == nil {
173+
return fmt.Errorf("not connected to a project in write mode")
174+
}
175+
176+
// Handle CREATE OR REPLACE — delete existing if found
177+
if s.CreateOrReplace {
178+
if existing, _ := e.findPublishedRestService(s.Name.Module, s.Name.Name); existing != nil {
179+
if err := e.writer.DeletePublishedRestService(existing.ID); err != nil {
180+
return fmt.Errorf("failed to replace existing service: %w", err)
181+
}
182+
}
183+
}
184+
185+
module, err := e.findModule(s.Name.Module)
186+
if err != nil {
187+
return fmt.Errorf("module %s not found", s.Name.Module)
188+
}
189+
190+
containerID := module.ID
191+
if s.Folder != "" {
192+
folderID, err := e.resolveFolder(module.ID, s.Folder)
193+
if err != nil {
194+
return fmt.Errorf("failed to resolve folder '%s': %w", s.Folder, err)
195+
}
196+
containerID = folderID
197+
}
198+
199+
svc := &model.PublishedRestService{
200+
ContainerID: containerID,
201+
Name: s.Name.Name,
202+
Path: s.Path,
203+
Version: s.Version,
204+
ServiceName: s.ServiceName,
205+
}
206+
207+
for _, resDef := range s.Resources {
208+
resource := &model.PublishedRestResource{
209+
Name: resDef.Name,
210+
}
211+
for _, opDef := range resDef.Operations {
212+
op := &model.PublishedRestOperation{
213+
HTTPMethod: opDef.HTTPMethod,
214+
Path: opDef.Path,
215+
Microflow: opDef.Microflow.String(),
216+
Summary: "",
217+
Deprecated: opDef.Deprecated,
218+
}
219+
resource.Operations = append(resource.Operations, op)
220+
}
221+
svc.Resources = append(svc.Resources, resource)
222+
}
223+
224+
if err := e.writer.CreatePublishedRestService(svc); err != nil {
225+
return fmt.Errorf("failed to create published REST service: %w", err)
226+
}
227+
228+
if !e.quiet {
229+
fmt.Fprintf(e.output, "Created published REST service %s.%s\n", s.Name.Module, s.Name.Name)
230+
}
231+
return nil
232+
}
233+
234+
// execDropPublishedRestService deletes a published REST service.
235+
func (e *Executor) execDropPublishedRestService(s *ast.DropPublishedRestServiceStmt) error {
236+
if e.writer == nil {
237+
return fmt.Errorf("not connected to a project in write mode")
238+
}
239+
240+
services, err := e.reader.ListPublishedRestServices()
241+
if err != nil {
242+
return fmt.Errorf("failed to list published REST services: %w", err)
243+
}
244+
245+
h, err := e.getHierarchy()
246+
if err != nil {
247+
return err
248+
}
249+
250+
for _, svc := range services {
251+
modID := h.FindModuleID(svc.ContainerID)
252+
modName := h.GetModuleName(modID)
253+
if modName == s.Name.Module && svc.Name == s.Name.Name {
254+
if err := e.writer.DeletePublishedRestService(svc.ID); err != nil {
255+
return fmt.Errorf("failed to drop published REST service: %w", err)
256+
}
257+
if !e.quiet {
258+
fmt.Fprintf(e.output, "Dropped published REST service %s.%s\n", s.Name.Module, s.Name.Name)
259+
}
260+
return nil
261+
}
262+
}
263+
264+
return fmt.Errorf("published REST service %s.%s not found", s.Name.Module, s.Name.Name)
265+
}

mdl/executor/executor_dispatch.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -194,6 +194,12 @@ func (e *Executor) executeInner(stmt ast.Statement) error {
194194
return e.createRestClient(s)
195195
case *ast.DropRestClientStmt:
196196
return e.dropRestClient(s)
197+
198+
// Published REST service statements
199+
case *ast.CreatePublishedRestServiceStmt:
200+
return e.execCreatePublishedRestService(s)
201+
case *ast.DropPublishedRestServiceStmt:
202+
return e.execDropPublishedRestService(s)
197203
case *ast.CreateExternalEntityStmt:
198204
return e.execCreateExternalEntity(s)
199205
case *ast.CreateExternalEntitiesStmt:

mdl/grammar/MDLLexer.g4

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,6 +449,8 @@ BODY: B O D Y;
449449
RESPONSE: R E S P O N S E;
450450
REQUEST: R E Q U E S T;
451451
SEND: S E N D;
452+
DEPRECATED: D E P R E C A T E D;
453+
RESOURCE: R E S O U R C E;
452454
JSON: J S O N;
453455
XML: X M L;
454456
STATUS: S T A T U S;

mdl/grammar/MDLParser.g4

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ createStatement
103103
| createImportMappingStatement
104104
| createExportMappingStatement
105105
| createConfigurationStatement
106+
| createPublishedRestServiceStatement
106107
)
107108
;
108109

@@ -272,6 +273,7 @@ dropStatement
272273
| DROP IMPORT MAPPING qualifiedName
273274
| DROP EXPORT MAPPING qualifiedName
274275
| DROP REST CLIENT qualifiedName
276+
| DROP PUBLISHED REST SERVICE qualifiedName
275277
| DROP CONFIGURATION STRING_LITERAL
276278
| DROP FOLDER STRING_LITERAL IN (qualifiedName | IDENTIFIER)
277279
;
@@ -2358,6 +2360,52 @@ restResponseSpec
23582360
| NONE // no response
23592361
;
23602362

2363+
// =============================================================================
2364+
// PUBLISHED REST SERVICE CREATION
2365+
// =============================================================================
2366+
2367+
/**
2368+
* CREATE PUBLISHED REST SERVICE Module.Name (
2369+
* Path: 'api/v1',
2370+
* Version: '1.0.0',
2371+
* ServiceName: 'My API'
2372+
* )
2373+
* {
2374+
* RESOURCE 'orders' {
2375+
* Get / MICROFLOW Module.GetOrders;
2376+
* Post / MICROFLOW Module.CreateOrder;
2377+
* }
2378+
* };
2379+
*/
2380+
createPublishedRestServiceStatement
2381+
: PUBLISHED REST SERVICE qualifiedName
2382+
LPAREN publishedRestProperty (COMMA publishedRestProperty)* RPAREN
2383+
LBRACE publishedRestResource* RBRACE
2384+
;
2385+
2386+
publishedRestProperty
2387+
: identifierOrKeyword COLON STRING_LITERAL
2388+
;
2389+
2390+
publishedRestResource
2391+
: RESOURCE STRING_LITERAL LBRACE publishedRestOperation* RBRACE
2392+
;
2393+
2394+
publishedRestOperation
2395+
: restHttpMethod publishedRestOpPath?
2396+
MICROFLOW qualifiedName
2397+
(DEPRECATED)?
2398+
(IMPORT MAPPING qualifiedName)?
2399+
(EXPORT MAPPING qualifiedName)?
2400+
(COMMIT identifierOrKeyword)?
2401+
SEMICOLON?
2402+
;
2403+
2404+
publishedRestOpPath
2405+
: STRING_LITERAL
2406+
| SLASH
2407+
;
2408+
23612409
// =============================================================================
23622410
// INDEX CREATION (standalone)
23632411
// =============================================================================
@@ -3552,6 +3600,6 @@ keyword
35523600
| COLLECTION // Image collection keyword
35533601
| STRUCTURES | MAPPINGS | VIA | KEY | SCHEMA // JSON Structure / Import Mapping keywords
35543602
| FILE_KW // REST client file keyword
3555-
| SEND | REQUEST // REST operation call keywords
3603+
| SEND | REQUEST | DEPRECATED | RESOURCE // REST operation call keywords
35563604
| STRUCTURES // JSON structure keywords
35573605
;

mdl/grammar/parser/MDLLexer.interp

Lines changed: 7 additions & 1 deletion
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)