diff --git a/cmd/mxcli/docker/detect.go b/cmd/mxcli/docker/detect.go index a31428c9..9c606f96 100644 --- a/cmd/mxcli/docker/detect.go +++ b/cmd/mxcli/docker/detect.go @@ -13,10 +13,10 @@ // CDN downloads are the primary source for mxbuild and runtime. // // Resolution priority (all platforms): -// 1. Explicit path (--mxbuild-path) -// 2. PATH lookup -// 3. OS-specific known locations (Studio Pro on Windows) -// 4. Cached CDN downloads (~/.mxcli/mxbuild/) +// 1. Explicit path (--mxbuild-path) +// 2. PATH lookup +// 3. OS-specific known locations (Studio Pro on Windows) +// 4. Cached CDN downloads (~/.mxcli/mxbuild/) // // Path discovery on Windows must NOT hardcode drive letters. Use environment // variables (PROGRAMFILES, PROGRAMW6432, SystemDrive) to locate install dirs. diff --git a/mdl/ast/ast_entity.go b/mdl/ast/ast_entity.go index 9e76ce0c..1246d611 100644 --- a/mdl/ast/ast_entity.go +++ b/mdl/ast/ast_entity.go @@ -33,15 +33,15 @@ func (k EntityKind) String() string { // CreateEntityStmt represents: CREATE [OR MODIFY] PERSISTENT|NON-PERSISTENT ENTITY Module.Name [EXTENDS Parent] (attributes) ... type CreateEntityStmt struct { - Name QualifiedName - Kind EntityKind - Generalization *QualifiedName // Parent entity for inheritance (e.g., System.Image) - Attributes []Attribute - Indexes []Index - EventHandlers []EventHandlerDef // ON BEFORE/AFTER CREATE/COMMIT/DELETE/ROLLBACK CALL ... - Position *Position - Documentation string - Comment string + Name QualifiedName + Kind EntityKind + Generalization *QualifiedName // Parent entity for inheritance (e.g., System.Image) + Attributes []Attribute + Indexes []Index + EventHandlers []EventHandlerDef // ON BEFORE/AFTER CREATE/COMMIT/DELETE/ROLLBACK CALL ... + Position *Position + Documentation string + Comment string CreateOrModify bool // true for CREATE OR MODIFY } @@ -58,17 +58,17 @@ func (s *DropEntityStmt) isStatement() {} type AlterEntityOp int const ( - AlterEntityAddAttribute AlterEntityOp = iota // ADD ATTRIBUTE / ADD COLUMN - AlterEntityRenameAttribute // RENAME ATTRIBUTE / RENAME COLUMN - AlterEntityModifyAttribute // MODIFY ATTRIBUTE / MODIFY COLUMN - AlterEntityDropAttribute // DROP ATTRIBUTE / DROP COLUMN - AlterEntitySetDocumentation // SET DOCUMENTATION - AlterEntitySetComment // SET COMMENT - AlterEntityAddIndex // ADD INDEX - AlterEntityDropIndex // DROP INDEX - AlterEntitySetPosition // SET POSITION (x, y) - AlterEntityAddEventHandler // ADD EVENT HANDLER ON BEFORE/AFTER CREATE/COMMIT/DELETE/ROLLBACK CALL Mod.MF - AlterEntityDropEventHandler // DROP EVENT HANDLER ON BEFORE/AFTER CREATE/COMMIT/DELETE/ROLLBACK + AlterEntityAddAttribute AlterEntityOp = iota // ADD ATTRIBUTE / ADD COLUMN + AlterEntityRenameAttribute // RENAME ATTRIBUTE / RENAME COLUMN + AlterEntityModifyAttribute // MODIFY ATTRIBUTE / MODIFY COLUMN + AlterEntityDropAttribute // DROP ATTRIBUTE / DROP COLUMN + AlterEntitySetDocumentation // SET DOCUMENTATION + AlterEntitySetComment // SET COMMENT + AlterEntityAddIndex // ADD INDEX + AlterEntityDropIndex // DROP INDEX + AlterEntitySetPosition // SET POSITION (x, y) + AlterEntityAddEventHandler // ADD EVENT HANDLER ON BEFORE/AFTER CREATE/COMMIT/DELETE/ROLLBACK CALL Mod.MF + AlterEntityDropEventHandler // DROP EVENT HANDLER ON BEFORE/AFTER CREATE/COMMIT/DELETE/ROLLBACK ) // EventHandlerDef represents an event handler in CREATE/ALTER ENTITY syntax. @@ -84,17 +84,17 @@ type EventHandlerDef struct { type AlterEntityStmt struct { Name QualifiedName Operation AlterEntityOp - Attribute *Attribute // For ADD ATTRIBUTE - AttributeName string // For RENAME/MODIFY/DROP ATTRIBUTE - NewName string // For RENAME ATTRIBUTE - DataType DataType // For MODIFY ATTRIBUTE - Calculated bool // For MODIFY ATTRIBUTE with CALCULATED - CalculatedMicroflow *QualifiedName // For MODIFY ATTRIBUTE with CALCULATED microflow - Documentation string // For SET DOCUMENTATION - Comment string // For SET COMMENT - Index *Index // For ADD INDEX - IndexName string // For DROP INDEX - Position *Position // For SET POSITION + Attribute *Attribute // For ADD ATTRIBUTE + AttributeName string // For RENAME/MODIFY/DROP ATTRIBUTE + NewName string // For RENAME ATTRIBUTE + DataType DataType // For MODIFY ATTRIBUTE + Calculated bool // For MODIFY ATTRIBUTE with CALCULATED + CalculatedMicroflow *QualifiedName // For MODIFY ATTRIBUTE with CALCULATED microflow + Documentation string // For SET DOCUMENTATION + Comment string // For SET COMMENT + Index *Index // For ADD INDEX + IndexName string // For DROP INDEX + Position *Position // For SET POSITION EventHandler *EventHandlerDef // For ADD/DROP EVENT HANDLER } diff --git a/mdl/ast/ast_microflow.go b/mdl/ast/ast_microflow.go index 4753f1cd..76dddc18 100644 --- a/mdl/ast/ast_microflow.go +++ b/mdl/ast/ast_microflow.go @@ -657,11 +657,11 @@ func (s *ExportToMappingStmt) isMicroflowStatement() {} // TransformJsonStmt represents: $Result = TRANSFORM $Input WITH Module.Transformer type TransformJsonStmt struct { - OutputVariable string // Result string variable (without $) - InputVariable string // Source JSON string variable (without $) - Transformation QualifiedName // Data transformer qualified name - ErrorHandling *ErrorHandlingClause // Optional ON ERROR clause - Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation + OutputVariable string // Result string variable (without $) + InputVariable string // Source JSON string variable (without $) + Transformation QualifiedName // Data transformer qualified name + ErrorHandling *ErrorHandlingClause // Optional ON ERROR clause + Annotations *ActivityAnnotations // Optional @position, @caption, @color, @annotation } func (s *TransformJsonStmt) isMicroflowStatement() {} diff --git a/mdl/ast/ast_query.go b/mdl/ast/ast_query.go index 4fec48c0..5c8ae402 100644 --- a/mdl/ast/ast_query.go +++ b/mdl/ast/ast_query.go @@ -79,7 +79,7 @@ const ( ShowImageCollections // SHOW IMAGE COLLECTIONS [IN module] ShowRestClients // SHOW REST CLIENTS [IN module] ShowPublishedRestServices // SHOW PUBLISHED REST SERVICES [IN module] - ShowDataTransformers // LIST DATA TRANSFORMERS [IN module] + ShowDataTransformers // LIST DATA TRANSFORMERS [IN module] ShowConstantValues // SHOW CONSTANT VALUES [IN module] ShowContractEntities // SHOW CONTRACT ENTITIES FROM Module.Service ShowContractActions // SHOW CONTRACT ACTIONS FROM Module.Service diff --git a/mdl/ast/ast_rest.go b/mdl/ast/ast_rest.go index 7a6992ed..03593831 100644 --- a/mdl/ast/ast_rest.go +++ b/mdl/ast/ast_rest.go @@ -32,15 +32,15 @@ type RestOperationDef struct { Documentation string Method string // "GET", "POST", "PUT", "PATCH", "DELETE" Path string - Parameters []RestParamDef // path parameters - QueryParameters []RestParamDef // query parameters - Headers []RestHeaderDef // HTTP headers - BodyType string // "JSON", "FILE", "TEMPLATE", "MAPPING", "" (none) - BodyVariable string // e.g. "$ItemData" or template string - BodyMapping *RestMappingDef // for MAPPING body - ResponseType string // "JSON", "STRING", "FILE", "STATUS", "NONE", "MAPPING" - ResponseVariable string // e.g. "$CreatedItem" - ResponseMapping *RestMappingDef // for MAPPING response + Parameters []RestParamDef // path parameters + QueryParameters []RestParamDef // query parameters + Headers []RestHeaderDef // HTTP headers + BodyType string // "JSON", "FILE", "TEMPLATE", "MAPPING", "" (none) + BodyVariable string // e.g. "$ItemData" or template string + BodyMapping *RestMappingDef // for MAPPING body + ResponseType string // "JSON", "STRING", "FILE", "STATUS", "NONE", "MAPPING" + ResponseVariable string // e.g. "$CreatedItem" + ResponseMapping *RestMappingDef // for MAPPING response Timeout int } diff --git a/mdl/errors/errors.go b/mdl/errors/errors.go new file mode 100644 index 00000000..b077a6d1 --- /dev/null +++ b/mdl/errors/errors.go @@ -0,0 +1,144 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Package mdlerrors provides structured error types for the MDL executor. +// +// Typed errors support errors.As for programmatic classification. +// Sentinel or wrapped errors may also support errors.Is where applicable +// (for example, ErrExit and BackendError via Unwrap). +// Every error preserves the original message via Error() for backward-compatible +// string output — callers that only use %v or .Error() see no change. +// +// Only BackendError supports Unwrap — it wraps an underlying storage/IO error. +// All other error types are leaf errors with no wrapped cause. +package mdlerrors + +import ( + "errors" + "fmt" +) + +// ErrExit is a sentinel error indicating clean script/session termination. +// Use errors.Is(err, ErrExit) to detect exit requests. +var ErrExit = errors.New("exit") + +// NotConnectedError indicates an operation was attempted without an active project connection. +type NotConnectedError struct { + // WriteMode is true when write access was required but not available. + WriteMode bool + msg string +} + +// NewNotConnected creates a NotConnectedError for read access. +func NewNotConnected() *NotConnectedError { + return &NotConnectedError{msg: "not connected to a project"} +} + +// NewNotConnectedWrite creates a NotConnectedError for write access. +func NewNotConnectedWrite() *NotConnectedError { + return &NotConnectedError{WriteMode: true, msg: "not connected to a project in write mode"} +} + +func (e *NotConnectedError) Error() string { return e.msg } + +// NotFoundError indicates a named element was not found. +type NotFoundError struct { + // Kind is the element type (e.g. "entity", "module", "microflow"). + Kind string + // Name is the qualified or simple name of the element. + Name string + msg string +} + +// NewNotFound creates a NotFoundError. +func NewNotFound(kind, name string) *NotFoundError { + return &NotFoundError{ + Kind: kind, + Name: name, + msg: fmt.Sprintf("%s not found: %s", kind, name), + } +} + +// NewNotFoundMsg creates a NotFoundError with a custom message. +func NewNotFoundMsg(kind, name, msg string) *NotFoundError { + return &NotFoundError{Kind: kind, Name: name, msg: msg} +} + +func (e *NotFoundError) Error() string { return e.msg } + +// AlreadyExistsError indicates an element already exists when creating. +type AlreadyExistsError struct { + // Kind is the element type. + Kind string + // Name is the qualified or simple name. + Name string + msg string +} + +// NewAlreadyExists creates an AlreadyExistsError. +func NewAlreadyExists(kind, name string) *AlreadyExistsError { + return &AlreadyExistsError{ + Kind: kind, + Name: name, + msg: fmt.Sprintf("%s already exists: %s", kind, name), + } +} + +// NewAlreadyExistsMsg creates an AlreadyExistsError with a custom message. +func NewAlreadyExistsMsg(kind, name, msg string) *AlreadyExistsError { + return &AlreadyExistsError{Kind: kind, Name: name, msg: msg} +} + +func (e *AlreadyExistsError) Error() string { return e.msg } + +// UnsupportedError indicates an unsupported operation, feature, or property. +type UnsupportedError struct { + // What holds the full error message describing what is unsupported + // (e.g. "unsupported attribute type: Binary"). + What string + msg string +} + +// NewUnsupported creates an UnsupportedError. +func NewUnsupported(msg string) *UnsupportedError { + return &UnsupportedError{What: msg, msg: msg} +} + +func (e *UnsupportedError) Error() string { return e.msg } + +// ValidationError indicates invalid input or configuration. +type ValidationError struct { + msg string +} + +// NewValidation creates a ValidationError. +func NewValidation(msg string) *ValidationError { + return &ValidationError{msg: msg} +} + +// NewValidationf creates a ValidationError with formatted message. +func NewValidationf(format string, args ...any) *ValidationError { + return &ValidationError{msg: fmt.Sprintf(format, args...)} +} + +func (e *ValidationError) Error() string { return e.msg } + +// BackendError wraps an error from the underlying storage layer (mpr/SDK). +type BackendError struct { + // Op describes the operation that failed (e.g. "get domain model", "write entity"). + Op string + Err error +} + +// NewBackend creates a BackendError wrapping a cause. +func NewBackend(op string, err error) *BackendError { + return &BackendError{Op: op, Err: err} +} + +func (e *BackendError) Error() string { + if e.Err == nil { + return fmt.Sprintf("failed to %s", e.Op) + } + return fmt.Sprintf("failed to %s: %v", e.Op, e.Err) +} + +func (e *BackendError) Unwrap() error { return e.Err } diff --git a/mdl/errors/errors_test.go b/mdl/errors/errors_test.go new file mode 100644 index 00000000..36c7e0a3 --- /dev/null +++ b/mdl/errors/errors_test.go @@ -0,0 +1,159 @@ +// SPDX-License-Identifier: Apache-2.0 + +package mdlerrors + +import ( + "errors" + "fmt" + "testing" +) + +func TestErrExit(t *testing.T) { + err := ErrExit + if !errors.Is(err, ErrExit) { + t.Fatal("errors.Is(ErrExit, ErrExit) should be true") + } + wrapped := fmt.Errorf("wrapper: %w", ErrExit) + if !errors.Is(wrapped, ErrExit) { + t.Fatal("errors.Is(wrapped, ErrExit) should be true") + } +} + +func TestNotConnectedError(t *testing.T) { + t.Run("read mode", func(t *testing.T) { + err := NewNotConnected() + if err.Error() != "not connected to a project" { + t.Fatalf("unexpected message: %s", err.Error()) + } + if err.WriteMode { + t.Fatal("WriteMode should be false") + } + var target *NotConnectedError + if !errors.As(err, &target) { + t.Fatal("errors.As should match *NotConnectedError") + } + }) + + t.Run("write mode", func(t *testing.T) { + err := NewNotConnectedWrite() + if err.Error() != "not connected to a project in write mode" { + t.Fatalf("unexpected message: %s", err.Error()) + } + if !err.WriteMode { + t.Fatal("WriteMode should be true") + } + var target *NotConnectedError + if !errors.As(err, &target) { + t.Fatal("errors.As should match *NotConnectedError") + } + }) + + t.Run("wrapped", func(t *testing.T) { + inner := NewNotConnected() + wrapped := fmt.Errorf("context: %w", inner) + var target *NotConnectedError + if !errors.As(wrapped, &target) { + t.Fatal("errors.As should match through wrapping") + } + }) +} + +func TestNotFoundError(t *testing.T) { + err := NewNotFound("entity", "MyModule.MyEntity") + if err.Error() != "entity not found: MyModule.MyEntity" { + t.Fatalf("unexpected message: %s", err.Error()) + } + if err.Kind != "entity" || err.Name != "MyModule.MyEntity" { + t.Fatalf("unexpected fields: Kind=%s Name=%s", err.Kind, err.Name) + } + var target *NotFoundError + if !errors.As(err, &target) { + t.Fatal("errors.As should match *NotFoundError") + } + + custom := NewNotFoundMsg("microflow", "MyModule.DoSomething", "microflow MyModule.DoSomething does not exist") + if custom.Error() != "microflow MyModule.DoSomething does not exist" { + t.Fatalf("unexpected message: %s", custom.Error()) + } +} + +func TestAlreadyExistsError(t *testing.T) { + err := NewAlreadyExists("entity", "MyModule.MyEntity") + if err.Error() != "entity already exists: MyModule.MyEntity" { + t.Fatalf("unexpected message: %s", err.Error()) + } + var target *AlreadyExistsError + if !errors.As(err, &target) { + t.Fatal("errors.As should match *AlreadyExistsError") + } + + custom := NewAlreadyExistsMsg("entity", "MyModule.MyEntity", "entity already exists: MyModule.MyEntity (use CREATE OR MODIFY to update)") + if custom.Kind != "entity" { + t.Fatalf("unexpected Kind: %s", custom.Kind) + } +} + +func TestUnsupportedError(t *testing.T) { + err := NewUnsupported("unsupported attribute type: Binary") + if err.Error() != "unsupported attribute type: Binary" { + t.Fatalf("unexpected message: %s", err.Error()) + } + var target *UnsupportedError + if !errors.As(err, &target) { + t.Fatal("errors.As should match *UnsupportedError") + } +} + +func TestValidationError(t *testing.T) { + err := NewValidation("invalid entity name") + if err.Error() != "invalid entity name" { + t.Fatalf("unexpected message: %s", err.Error()) + } + var target *ValidationError + if !errors.As(err, &target) { + t.Fatal("errors.As should match *ValidationError") + } + + errf := NewValidationf("invalid %s: %s", "entity name", "123Bad") + if errf.Error() != "invalid entity name: 123Bad" { + t.Fatalf("unexpected message: %s", errf.Error()) + } +} + +func TestBackendError(t *testing.T) { + cause := fmt.Errorf("disk full") + err := NewBackend("write entity", cause) + if err.Error() != "failed to write entity: disk full" { + t.Fatalf("unexpected message: %s", err.Error()) + } + var target *BackendError + if !errors.As(err, &target) { + t.Fatal("errors.As should match *BackendError") + } + if target.Op != "write entity" { + t.Fatalf("unexpected Op: %s", target.Op) + } + + // Unwrap + if !errors.Is(err, cause) { + t.Fatal("errors.Is should find the cause through Unwrap") + } + + // Double-wrapped + wrapped := fmt.Errorf("outer: %w", err) + if !errors.As(wrapped, &target) { + t.Fatal("errors.As should match through double wrapping") + } + if !errors.Is(wrapped, cause) { + t.Fatal("errors.Is should find cause through double wrapping") + } + + // Nil cause + nilErr := NewBackend("test op", nil) + if nilErr.Error() != "failed to test op" { + t.Fatalf("unexpected nil-cause message: %s", nilErr.Error()) + } + if nilErr.Unwrap() != nil { + t.Fatal("Unwrap should return nil when cause is nil") + } +} diff --git a/mdl/executor/cmd_agenteditor_agents.go b/mdl/executor/cmd_agenteditor_agents.go index ffd42837..2b7bd47c 100644 --- a/mdl/executor/cmd_agenteditor_agents.go +++ b/mdl/executor/cmd_agenteditor_agents.go @@ -12,18 +12,19 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/sdk/agenteditor" ) // showAgentEditorAgents handles SHOW AGENTS [IN module]. func (e *Executor) showAgentEditorAgents(moduleName string) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } agents, err := e.reader.ListAgentEditorAgents() if err != nil { - return fmt.Errorf("failed to list agents: %w", err) + return mdlerrors.NewBackend("list agents", err) } h, err := e.getHierarchy() @@ -64,12 +65,12 @@ func (e *Executor) showAgentEditorAgents(moduleName string) error { // round-trippable CREATE AGENT statement reflecting the Contents JSON. func (e *Executor) describeAgentEditorAgent(name ast.QualifiedName) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } a := e.findAgentEditorAgent(name.Module, name.Name) if a == nil { - return fmt.Errorf("agent not found: %s", name) + return mdlerrors.NewNotFound("agent", name.String()) } h, err := e.getHierarchy() diff --git a/mdl/executor/cmd_agenteditor_kbs.go b/mdl/executor/cmd_agenteditor_kbs.go index e7b94ec4..16a82597 100644 --- a/mdl/executor/cmd_agenteditor_kbs.go +++ b/mdl/executor/cmd_agenteditor_kbs.go @@ -12,18 +12,19 @@ import ( "fmt" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/sdk/agenteditor" ) // showAgentEditorKnowledgeBases handles SHOW KNOWLEDGE BASES [IN module]. func (e *Executor) showAgentEditorKnowledgeBases(moduleName string) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } kbs, err := e.reader.ListAgentEditorKnowledgeBases() if err != nil { - return fmt.Errorf("failed to list knowledge bases: %w", err) + return mdlerrors.NewBackend("list knowledge bases", err) } h, err := e.getHierarchy() @@ -62,12 +63,12 @@ func (e *Executor) showAgentEditorKnowledgeBases(moduleName string) error { // describeAgentEditorKnowledgeBase handles DESCRIBE KNOWLEDGE BASE Module.Name. func (e *Executor) describeAgentEditorKnowledgeBase(name ast.QualifiedName) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } k := e.findAgentEditorKnowledgeBase(name.Module, name.Name) if k == nil { - return fmt.Errorf("knowledge base not found: %s", name) + return mdlerrors.NewNotFound("knowledge base", name.String()) } h, err := e.getHierarchy() diff --git a/mdl/executor/cmd_agenteditor_mcpservices.go b/mdl/executor/cmd_agenteditor_mcpservices.go index 8ee6409e..966fd94e 100644 --- a/mdl/executor/cmd_agenteditor_mcpservices.go +++ b/mdl/executor/cmd_agenteditor_mcpservices.go @@ -12,18 +12,19 @@ import ( "fmt" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/sdk/agenteditor" ) // showAgentEditorConsumedMCPServices handles SHOW CONSUMED MCP SERVICES [IN module]. func (e *Executor) showAgentEditorConsumedMCPServices(moduleName string) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } svcs, err := e.reader.ListAgentEditorConsumedMCPServices() if err != nil { - return fmt.Errorf("failed to list consumed MCP services: %w", err) + return mdlerrors.NewBackend("list consumed MCP services", err) } h, err := e.getHierarchy() @@ -58,12 +59,12 @@ func (e *Executor) showAgentEditorConsumedMCPServices(moduleName string) error { // describeAgentEditorConsumedMCPService handles DESCRIBE CONSUMED MCP SERVICE Module.Name. func (e *Executor) describeAgentEditorConsumedMCPService(name ast.QualifiedName) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } c := e.findAgentEditorConsumedMCPService(name.Module, name.Name) if c == nil { - return fmt.Errorf("consumed MCP service not found: %s", name) + return mdlerrors.NewNotFound("consumed MCP service", name.String()) } h, err := e.getHierarchy() diff --git a/mdl/executor/cmd_agenteditor_models.go b/mdl/executor/cmd_agenteditor_models.go index db3663c1..b7e877e8 100644 --- a/mdl/executor/cmd_agenteditor_models.go +++ b/mdl/executor/cmd_agenteditor_models.go @@ -12,18 +12,19 @@ import ( "fmt" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/sdk/agenteditor" ) // showAgentEditorModels handles SHOW MODELS [IN module]. func (e *Executor) showAgentEditorModels(moduleName string) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } models, err := e.reader.ListAgentEditorModels() if err != nil { - return fmt.Errorf("failed to list models: %w", err) + return mdlerrors.NewBackend("list models", err) } h, err := e.getHierarchy() @@ -65,12 +66,12 @@ func (e *Executor) showAgentEditorModels(moduleName string) error { // Emits a round-trippable CREATE MODEL statement. func (e *Executor) describeAgentEditorModel(name ast.QualifiedName) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } m := e.findAgentEditorModel(name.Module, name.Name) if m == nil { - return fmt.Errorf("model not found: %s", name) + return mdlerrors.NewNotFound("model", name.String()) } h, err := e.getHierarchy() diff --git a/mdl/executor/cmd_alter_page.go b/mdl/executor/cmd_alter_page.go index 9267c3f7..9a247af3 100644 --- a/mdl/executor/cmd_alter_page.go +++ b/mdl/executor/cmd_alter_page.go @@ -10,6 +10,7 @@ import ( "go.mongodb.org/mongo-driver/bson/primitive" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/mpr" ) @@ -17,15 +18,15 @@ import ( // execAlterPage handles ALTER PAGE/SNIPPET Module.Name { operations }. func (e *Executor) execAlterPage(s *ast.AlterPageStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } if e.writer == nil { - return fmt.Errorf("project not opened for writing") + return mdlerrors.NewNotConnectedWrite() } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } var unitID model.ID @@ -55,11 +56,11 @@ func (e *Executor) execAlterPage(s *ast.AlterPageStmt) error { // which is required by Mendix Studio Pro). rawBytes, err := e.reader.GetRawUnitBytes(unitID) if err != nil { - return fmt.Errorf("failed to load raw %s data: %w", strings.ToLower(containerType), err) + return mdlerrors.NewBackend("load raw "+strings.ToLower(containerType)+" data", err) } var rawData bson.D if err := bson.Unmarshal(rawBytes, &rawData); err != nil { - return fmt.Errorf("failed to unmarshal %s BSON: %w", strings.ToLower(containerType), err) + return mdlerrors.NewBackend("unmarshal "+strings.ToLower(containerType)+" BSON", err) } // Resolve module name for building new widgets @@ -75,49 +76,49 @@ func (e *Executor) execAlterPage(s *ast.AlterPageStmt) error { switch o := op.(type) { case *ast.SetPropertyOp: if err := applySetPropertyWith(rawData, o, findWidget); err != nil { - return fmt.Errorf("SET failed: %w", err) + return mdlerrors.NewBackend("SET", err) } case *ast.InsertWidgetOp: if err := e.applyInsertWidgetWith(rawData, o, modName, containerID, findWidget); err != nil { - return fmt.Errorf("INSERT failed: %w", err) + return mdlerrors.NewBackend("INSERT", err) } case *ast.DropWidgetOp: if err := applyDropWidgetWith(rawData, o, findWidget); err != nil { - return fmt.Errorf("DROP failed: %w", err) + return mdlerrors.NewBackend("DROP", err) } case *ast.ReplaceWidgetOp: if err := e.applyReplaceWidgetWith(rawData, o, modName, containerID, findWidget); err != nil { - return fmt.Errorf("REPLACE failed: %w", err) + return mdlerrors.NewBackend("REPLACE", err) } case *ast.AddVariableOp: if err := applyAddVariable(&rawData, o); err != nil { - return fmt.Errorf("ADD VARIABLE failed: %w", err) + return mdlerrors.NewBackend("ADD VARIABLE", err) } case *ast.DropVariableOp: if err := applyDropVariable(rawData, o); err != nil { - return fmt.Errorf("DROP VARIABLE failed: %w", err) + return mdlerrors.NewBackend("DROP VARIABLE", err) } case *ast.SetLayoutOp: if containerType == "SNIPPET" { - return fmt.Errorf("SET Layout is not supported for snippets") + return mdlerrors.NewUnsupported("SET Layout is not supported for snippets") } if err := applySetLayout(rawData, o); err != nil { - return fmt.Errorf("SET Layout failed: %w", err) + return mdlerrors.NewBackend("SET Layout", err) } default: - return fmt.Errorf("unknown ALTER %s operation type: %T", containerType, op) + return mdlerrors.NewUnsupported(fmt.Sprintf("unknown ALTER %s operation type: %T", containerType, op)) } } // Marshal back to BSON bytes (bson.D preserves field ordering) outBytes, err := bson.Marshal(rawData) if err != nil { - return fmt.Errorf("failed to marshal modified %s: %w", strings.ToLower(containerType), err) + return mdlerrors.NewBackend("marshal modified "+strings.ToLower(containerType), err) } // Save if err := e.writer.UpdateRawUnit(string(unitID), outBytes); err != nil { - return fmt.Errorf("failed to save modified %s: %w", strings.ToLower(containerType), err) + return mdlerrors.NewBackend("save modified "+strings.ToLower(containerType), err) } fmt.Fprintf(e.output, "Altered %s %s\n", strings.ToLower(containerType), s.PageName.String()) @@ -140,7 +141,7 @@ func applySetLayout(rawData bson.D, op *ast.SetLayoutOp) error { } } if formCall == nil { - return fmt.Errorf("page has no FormCall (layout reference)") + return mdlerrors.NewValidation("page has no FormCall (layout reference)") } // Detect the old layout name from existing Parameter values @@ -172,7 +173,7 @@ func applySetLayout(rawData bson.D, op *ast.SetLayoutOp) error { } if oldLayoutQN == "" { - return fmt.Errorf("cannot determine current layout from FormCall") + return mdlerrors.NewValidation("cannot determine current layout from FormCall") } if oldLayoutQN == newLayoutQN { @@ -781,9 +782,9 @@ func setColumnProperty(colDoc bson.D, propKeyMap map[string]string, propName str dSet(valDoc, "PrimitiveValue", strVal) return nil } - return fmt.Errorf("column property %q has no Value", propName) + return mdlerrors.NewValidation(fmt.Sprintf("column property %q has no Value", propName)) } - return fmt.Errorf("column property %q not found", propName) + return mdlerrors.NewNotFound("column property", propName) } // ============================================================================ @@ -810,18 +811,18 @@ func applySetPropertyWith(rawData bson.D, op *ast.SetPropertyOp, find widgetFind result = find(rawData, op.Target.Widget) } if result == nil { - return fmt.Errorf("widget %q not found", op.Target.Name()) + return mdlerrors.NewNotFound("widget", op.Target.Name()) } // Apply each property for propName, value := range op.Properties { if op.Target.IsColumn() { if err := setColumnProperty(result.widget, result.colPropKeys, propName, value); err != nil { - return fmt.Errorf("failed to set %s on %s: %w", propName, op.Target.Name(), err) + return mdlerrors.NewBackend("set "+propName+" on "+op.Target.Name(), err) } } else { if err := setRawWidgetProperty(result.widget, propName, value); err != nil { - return fmt.Errorf("failed to set %s on %s: %w", propName, op.Target.Name(), err) + return mdlerrors.NewBackend("set "+propName+" on "+op.Target.Name(), err) } } } @@ -844,7 +845,7 @@ func applyPageLevelSet(rawData bson.D, properties map[string]interface{}) error strVal, _ := value.(string) dSet(rawData, "Url", strVal) default: - return fmt.Errorf("unsupported page-level property: %s", propName) + return mdlerrors.NewUnsupported("unsupported page-level property: " + propName) } } return nil @@ -928,7 +929,7 @@ func setWidgetCaption(widget bson.D, value interface{}) error { func setWidgetAttributeRef(widget bson.D, value interface{}) error { attrPath, ok := value.(string) if !ok { - return fmt.Errorf("Attribute value must be a string") + return mdlerrors.NewValidation("Attribute value must be a string") } // Build the new AttributeRef value @@ -954,14 +955,14 @@ func setWidgetAttributeRef(widget bson.D, value interface{}) error { } // No existing AttributeRef field — this widget may not support it - return fmt.Errorf("widget does not have an AttributeRef property; Attribute can only be SET on input widgets (TextBox, TextArea, DatePicker, etc.)") + return mdlerrors.NewValidation("widget does not have an AttributeRef property; Attribute can only be SET on input widgets (TextBox, TextArea, DatePicker, etc.)") } // setWidgetDataSource sets the DataSource on a DataView or list widget. func setWidgetDataSource(widget bson.D, value interface{}) error { ds, ok := value.(*ast.DataSourceV3) if !ok { - return fmt.Errorf("DataSource value must be a datasource expression") + return mdlerrors.NewValidation("DataSource value must be a datasource expression") } var serialized interface{} @@ -1019,7 +1020,7 @@ func setWidgetDataSource(widget bson.D, value interface{}) error { }}, } default: - return fmt.Errorf("unsupported DataSource type for ALTER PAGE SET: %s", ds.Type) + return mdlerrors.NewUnsupported("unsupported DataSource type for ALTER PAGE SET: " + ds.Type) } dSet(widget, "DataSource", serialized) @@ -1042,15 +1043,15 @@ func setWidgetLabel(widget bson.D, value interface{}) error { func setWidgetContent(widget bson.D, value interface{}) error { strVal, ok := value.(string) if !ok { - return fmt.Errorf("Content value must be a string") + return mdlerrors.NewValidation("Content value must be a string") } content := dGetDoc(widget, "Content") if content == nil { - return fmt.Errorf("widget has no Content property") + return mdlerrors.NewValidation("widget has no Content property") } template := dGetDoc(content, "Template") if template == nil { - return fmt.Errorf("Content has no Template") + return mdlerrors.NewValidation("Content has no Template") } items := dGetArrayElements(dGet(template, "Items")) if len(items) > 0 { @@ -1059,7 +1060,7 @@ func setWidgetContent(widget bson.D, value interface{}) error { return nil } } - return fmt.Errorf("Content.Template has no Items with Text") + return mdlerrors.NewValidation("Content.Template has no Items with Text") } // setTranslatableText sets a translatable text value in BSON. @@ -1100,7 +1101,7 @@ func setTranslatableText(parent bson.D, key string, value interface{}) { func setPluggableWidgetProperty(widget bson.D, propName string, value interface{}) error { obj := dGetDoc(widget, "Object") if obj == nil { - return fmt.Errorf("property %q not found (widget has no pluggable Object)", propName) + return mdlerrors.NewNotFoundMsg("property", propName, fmt.Sprintf("property %q not found (widget has no pluggable Object)", propName)) } // Build TypePointer ID -> PropertyKey map from Type.ObjectType.PropertyTypes @@ -1157,9 +1158,9 @@ func setPluggableWidgetProperty(widget bson.D, propName string, value interface{ } return nil } - return fmt.Errorf("property %q has no Value map", propName) + return mdlerrors.NewValidation(fmt.Sprintf("property %q has no Value map", propName)) } - return fmt.Errorf("pluggable property %q not found in widget Object", propName) + return mdlerrors.NewNotFound("pluggable property", propName) } // ============================================================================ @@ -1180,13 +1181,13 @@ func (e *Executor) applyInsertWidgetWith(rawData bson.D, op *ast.InsertWidgetOp, result = find(rawData, op.Target.Widget) } if result == nil { - return fmt.Errorf("widget %q not found", op.Target.Name()) + return mdlerrors.NewNotFound("widget", op.Target.Name()) } // Check for duplicate widget names before building for _, w := range op.Widgets { if w.Name != "" && find(rawData, w.Name) != nil { - return fmt.Errorf("duplicate widget name '%s': a widget with this name already exists on the page", w.Name) + return mdlerrors.NewAlreadyExistsMsg("widget", w.Name, fmt.Sprintf("duplicate widget name '%s': a widget with this name already exists on the page", w.Name)) } } @@ -1196,7 +1197,7 @@ func (e *Executor) applyInsertWidgetWith(rawData bson.D, op *ast.InsertWidgetOp, // Build new widget BSON from AST (pass rawData for page param + widget scope resolution) newBsonWidgets, err := e.buildWidgetsBson(op.Widgets, moduleName, moduleID, entityCtx, rawData) if err != nil { - return fmt.Errorf("failed to build widgets: %w", err) + return mdlerrors.NewBackend("build widgets", err) } // Calculate insertion index @@ -1236,7 +1237,7 @@ func applyDropWidgetWith(rawData bson.D, op *ast.DropWidgetOp, find widgetFinder result = find(rawData, target.Widget) } if result == nil { - return fmt.Errorf("widget %q not found", target.Name()) + return mdlerrors.NewNotFound("widget", target.Name()) } // Remove from parent array @@ -1268,13 +1269,13 @@ func (e *Executor) applyReplaceWidgetWith(rawData bson.D, op *ast.ReplaceWidgetO result = find(rawData, op.Target.Widget) } if result == nil { - return fmt.Errorf("widget %q not found", op.Target.Name()) + return mdlerrors.NewNotFound("widget", op.Target.Name()) } // Check for duplicate widget names (skip the widget being replaced) for _, w := range op.NewWidgets { if w.Name != "" && w.Name != op.Target.Widget && find(rawData, w.Name) != nil { - return fmt.Errorf("duplicate widget name '%s': a widget with this name already exists on the page", w.Name) + return mdlerrors.NewAlreadyExistsMsg("widget", w.Name, fmt.Sprintf("duplicate widget name '%s': a widget with this name already exists on the page", w.Name)) } } @@ -1284,7 +1285,7 @@ func (e *Executor) applyReplaceWidgetWith(rawData bson.D, op *ast.ReplaceWidgetO // Build new widget BSON from AST (pass rawData for page param + widget scope resolution) newBsonWidgets, err := e.buildWidgetsBson(op.NewWidgets, moduleName, moduleID, entityCtx, rawData) if err != nil { - return fmt.Errorf("failed to build replacement widgets: %w", err) + return mdlerrors.NewBackend("build replacement widgets", err) } // Replace: remove old widget, insert new ones at same position @@ -1455,7 +1456,7 @@ func applyAddVariable(rawData *bson.D, op *ast.AddVariableOp) error { for _, ev := range existingVars { if evDoc, ok := ev.(bson.D); ok { if dGetString(evDoc, "Name") == op.Variable.Name { - return fmt.Errorf("variable $%s already exists", op.Variable.Name) + return mdlerrors.NewAlreadyExists("variable", "$"+op.Variable.Name) } } } @@ -1499,7 +1500,7 @@ func applyAddVariable(rawData *bson.D, op *ast.AddVariableOp) error { func applyDropVariable(rawData bson.D, op *ast.DropVariableOp) error { elements := dGetArrayElements(dGet(rawData, "Variables")) if elements == nil { - return fmt.Errorf("variable $%s not found", op.VariableName) + return mdlerrors.NewNotFound("variable", "$"+op.VariableName) } // Find and remove the variable @@ -1516,7 +1517,7 @@ func applyDropVariable(rawData bson.D, op *ast.DropVariableOp) error { } if !found { - return fmt.Errorf("variable $%s not found", op.VariableName) + return mdlerrors.NewNotFound("variable", "$"+op.VariableName) } dSetArray(rawData, "Variables", kept) @@ -1553,7 +1554,7 @@ func (e *Executor) buildWidgetsBson(widgets []*ast.WidgetV3, moduleName string, for _, w := range widgets { bsonD, err := pb.buildWidgetV3ToBSON(w) if err != nil { - return nil, fmt.Errorf("failed to build widget %s: %w", w.Name, err) + return nil, mdlerrors.NewBackend("build widget "+w.Name, err) } if bsonD == nil { continue diff --git a/mdl/executor/cmd_alter_workflow.go b/mdl/executor/cmd_alter_workflow.go index da9c9364..2c4ddd95 100644 --- a/mdl/executor/cmd_alter_workflow.go +++ b/mdl/executor/cmd_alter_workflow.go @@ -9,6 +9,7 @@ import ( "go.mongodb.org/mongo-driver/bson" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/workflows" @@ -21,10 +22,10 @@ const bsonArrayMarker = int32(3) // execAlterWorkflow handles ALTER WORKFLOW Module.Name { operations }. func (e *Executor) execAlterWorkflow(s *ast.AlterWorkflowStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } if e.writer == nil { - return fmt.Errorf("project not opened for writing") + return mdlerrors.NewNotConnectedWrite() } // Version pre-check: workflows require Mendix 9.12+ @@ -36,13 +37,13 @@ func (e *Executor) execAlterWorkflow(s *ast.AlterWorkflowStmt) error { h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Find workflow by qualified name allWorkflows, err := e.reader.ListWorkflows() if err != nil { - return fmt.Errorf("failed to list workflows: %w", err) + return mdlerrors.NewBackend("list workflows", err) } var wfID model.ID @@ -55,17 +56,17 @@ func (e *Executor) execAlterWorkflow(s *ast.AlterWorkflowStmt) error { } } if wfID == "" { - return fmt.Errorf("workflow not found: %s.%s", s.Name.Module, s.Name.Name) + return mdlerrors.NewNotFound("workflow", s.Name.Module+"."+s.Name.Name) } // Load raw BSON as ordered document rawBytes, err := e.reader.GetRawUnitBytes(wfID) if err != nil { - return fmt.Errorf("failed to load raw workflow data: %w", err) + return mdlerrors.NewBackend("load raw workflow data", err) } var rawData bson.D if err := bson.Unmarshal(rawBytes, &rawData); err != nil { - return fmt.Errorf("failed to unmarshal workflow BSON: %w", err) + return mdlerrors.NewBackend("unmarshal workflow BSON", err) } // Apply operations sequentially @@ -73,70 +74,70 @@ func (e *Executor) execAlterWorkflow(s *ast.AlterWorkflowStmt) error { switch o := op.(type) { case *ast.SetWorkflowPropertyOp: if err := applySetWorkflowProperty(&rawData, o); err != nil { - return fmt.Errorf("SET %s failed: %w", o.Property, err) + return mdlerrors.NewBackend("SET "+o.Property, err) } case *ast.SetActivityPropertyOp: if err := applySetActivityProperty(rawData, o); err != nil { - return fmt.Errorf("SET ACTIVITY failed: %w", err) + return mdlerrors.NewBackend("SET ACTIVITY", err) } case *ast.InsertAfterOp: if err := e.applyInsertAfterActivity(rawData, o); err != nil { - return fmt.Errorf("INSERT AFTER failed: %w", err) + return mdlerrors.NewBackend("INSERT AFTER", err) } case *ast.DropActivityOp: if err := applyDropActivity(rawData, o); err != nil { - return fmt.Errorf("DROP ACTIVITY failed: %w", err) + return mdlerrors.NewBackend("DROP ACTIVITY", err) } case *ast.ReplaceActivityOp: if err := e.applyReplaceActivity(rawData, o); err != nil { - return fmt.Errorf("REPLACE ACTIVITY failed: %w", err) + return mdlerrors.NewBackend("REPLACE ACTIVITY", err) } case *ast.InsertOutcomeOp: if err := e.applyInsertOutcome(rawData, o); err != nil { - return fmt.Errorf("INSERT OUTCOME failed: %w", err) + return mdlerrors.NewBackend("INSERT OUTCOME", err) } case *ast.DropOutcomeOp: if err := applyDropOutcome(rawData, o); err != nil { - return fmt.Errorf("DROP OUTCOME failed: %w", err) + return mdlerrors.NewBackend("DROP OUTCOME", err) } case *ast.InsertPathOp: if err := e.applyInsertPath(rawData, o); err != nil { - return fmt.Errorf("INSERT PATH failed: %w", err) + return mdlerrors.NewBackend("INSERT PATH", err) } case *ast.DropPathOp: if err := applyDropPath(rawData, o); err != nil { - return fmt.Errorf("DROP PATH failed: %w", err) + return mdlerrors.NewBackend("DROP PATH", err) } case *ast.InsertBranchOp: if err := e.applyInsertBranch(rawData, o); err != nil { - return fmt.Errorf("INSERT BRANCH failed: %w", err) + return mdlerrors.NewBackend("INSERT BRANCH", err) } case *ast.DropBranchOp: if err := applyDropBranch(rawData, o); err != nil { - return fmt.Errorf("DROP BRANCH failed: %w", err) + return mdlerrors.NewBackend("DROP BRANCH", err) } case *ast.InsertBoundaryEventOp: if err := e.applyInsertBoundaryEvent(rawData, o); err != nil { - return fmt.Errorf("INSERT BOUNDARY EVENT failed: %w", err) + return mdlerrors.NewBackend("INSERT BOUNDARY EVENT", err) } case *ast.DropBoundaryEventOp: if err := applyDropBoundaryEvent(rawData, o); err != nil { - return fmt.Errorf("DROP BOUNDARY EVENT failed: %w", err) + return mdlerrors.NewBackend("DROP BOUNDARY EVENT", err) } default: - return fmt.Errorf("unknown ALTER WORKFLOW operation type: %T", op) + return mdlerrors.NewUnsupported(fmt.Sprintf("unknown ALTER WORKFLOW operation type: %T", op)) } } // Marshal back to BSON bytes outBytes, err := bson.Marshal(rawData) if err != nil { - return fmt.Errorf("failed to marshal modified workflow: %w", err) + return mdlerrors.NewBackend("marshal modified workflow", err) } // Save if err := e.writer.UpdateRawUnit(string(wfID), outBytes); err != nil { - return fmt.Errorf("failed to save modified workflow: %w", err) + return mdlerrors.NewBackend("save modified workflow", err) } e.invalidateHierarchy() @@ -241,7 +242,7 @@ func applySetWorkflowProperty(doc *bson.D, op *ast.SetWorkflowPropertyOp) error return nil default: - return fmt.Errorf("unsupported workflow property: %s", op.Property) + return mdlerrors.NewUnsupported("unsupported workflow property: " + op.Property) } } @@ -302,7 +303,7 @@ func applySetActivityProperty(doc bson.D, op *ast.SetActivityPropertyOp) error { return nil default: - return fmt.Errorf("unsupported activity property: %s", op.Property) + return mdlerrors.NewUnsupported("unsupported activity property: " + op.Property) } } @@ -312,29 +313,29 @@ func applySetActivityProperty(doc bson.D, op *ast.SetActivityPropertyOp) error { func findActivityByCaption(doc bson.D, caption string, atPosition int) (bson.D, error) { flow := dGetDoc(doc, "Flow") if flow == nil { - return nil, fmt.Errorf("workflow has no Flow") + return nil, mdlerrors.NewValidation("workflow has no Flow") } var matches []bson.D findActivitiesRecursive(flow, caption, &matches) if len(matches) == 0 { - return nil, fmt.Errorf("activity %q not found in workflow", caption) + return nil, mdlerrors.NewNotFound("activity", caption) } if len(matches) == 1 || atPosition == 0 { if atPosition > 0 && atPosition > len(matches) { - return nil, fmt.Errorf("activity %q at position %d not found (found %d matches)", caption, atPosition, len(matches)) + return nil, mdlerrors.NewNotFoundMsg("activity", caption, fmt.Sprintf("activity %q at position %d not found (found %d matches)", caption, atPosition, len(matches))) } if atPosition > 0 { return matches[atPosition-1], nil } if len(matches) > 1 { - return nil, fmt.Errorf("ambiguous activity %q — %d matches. Use @N to disambiguate", caption, len(matches)) + return nil, mdlerrors.NewValidation(fmt.Sprintf("ambiguous activity %q — %d matches. Use @N to disambiguate", caption, len(matches))) } return matches[0], nil } if atPosition > len(matches) { - return nil, fmt.Errorf("activity %q at position %d not found (found %d matches)", caption, atPosition, len(matches)) + return nil, mdlerrors.NewNotFoundMsg("activity", caption, fmt.Sprintf("activity %q at position %d not found (found %d matches)", caption, atPosition, len(matches))) } return matches[atPosition-1], nil } @@ -392,23 +393,23 @@ func getNestedFlows(actDoc bson.D) []bson.D { func findActivityIndex(doc bson.D, caption string, atPosition int) (int, []any, bson.D, error) { flow := dGetDoc(doc, "Flow") if flow == nil { - return -1, nil, nil, fmt.Errorf("workflow has no Flow") + return -1, nil, nil, mdlerrors.NewValidation("workflow has no Flow") } var matches []activityMatch findActivityIndexRecursive(flow, caption, &matches) if len(matches) == 0 { - return -1, nil, nil, fmt.Errorf("activity %q not found in workflow", caption) + return -1, nil, nil, mdlerrors.NewNotFound("activity", caption) } pos := 0 if atPosition > 0 { pos = atPosition - 1 } else if len(matches) > 1 { - return -1, nil, nil, fmt.Errorf("ambiguous activity %q — %d matches. Use @N to disambiguate", caption, len(matches)) + return -1, nil, nil, mdlerrors.NewValidation(fmt.Sprintf("ambiguous activity %q — %d matches. Use @N to disambiguate", caption, len(matches))) } if pos >= len(matches) { - return -1, nil, nil, fmt.Errorf("activity %q at position %d not found (found %d matches)", caption, atPosition, len(matches)) + return -1, nil, nil, mdlerrors.NewNotFoundMsg("activity", caption, fmt.Sprintf("activity %q at position %d not found (found %d matches)", caption, atPosition, len(matches))) } m := matches[pos] return m.idx, m.activities, m.flow, nil @@ -513,7 +514,7 @@ func (e *Executor) applyInsertAfterActivity(doc bson.D, op *ast.InsertAfterOp) e newActs := buildWorkflowActivities([]ast.WorkflowActivityNode{op.NewActivity}) if len(newActs) == 0 { - return fmt.Errorf("failed to build new activity") + return mdlerrors.NewValidation("failed to build new activity") } // Auto-bind parameters and deduplicate against existing workflow names @@ -565,7 +566,7 @@ func (e *Executor) applyReplaceActivity(doc bson.D, op *ast.ReplaceActivityOp) e newActs := buildWorkflowActivities([]ast.WorkflowActivityNode{op.NewActivity}) if len(newActs) == 0 { - return fmt.Errorf("failed to build replacement activity") + return mdlerrors.NewValidation("failed to build replacement activity") } e.autoBindActivitiesInFlow(newActs) @@ -651,7 +652,7 @@ func applyDropOutcome(doc bson.D, op *ast.DropOutcomeOp) error { kept = append(kept, elem) } if !found { - return fmt.Errorf("outcome %q not found on activity %q", op.OutcomeName, op.ActivityRef) + return mdlerrors.NewNotFoundMsg("outcome", op.OutcomeName, fmt.Sprintf("outcome %q not found on activity %q", op.OutcomeName, op.ActivityRef)) } dSetArray(actDoc, "Outcomes", kept) return nil @@ -706,7 +707,7 @@ func applyDropPath(doc bson.D, op *ast.DropPathOp) error { } } if pathIdx < 0 { - return fmt.Errorf("path %q not found on parallel split %q", op.PathCaption, op.ActivityRef) + return mdlerrors.NewNotFoundMsg("path", op.PathCaption, fmt.Sprintf("path %q not found on parallel split %q", op.PathCaption, op.ActivityRef)) } newOutcomes := make([]any, 0, len(outcomes)-1) @@ -811,7 +812,7 @@ func applyDropBranch(doc bson.D, op *ast.DropBranchOp) error { kept = append(kept, elem) } if !found { - return fmt.Errorf("branch %q not found on activity %q", op.BranchName, op.ActivityRef) + return mdlerrors.NewNotFoundMsg("branch", op.BranchName, fmt.Sprintf("branch %q not found on activity %q", op.BranchName, op.ActivityRef)) } dSetArray(actDoc, "Outcomes", kept) return nil @@ -871,7 +872,7 @@ func applyDropBoundaryEvent(doc bson.D, op *ast.DropBoundaryEventOp) error { events := dGetArrayElements(dGet(actDoc, "BoundaryEvents")) if len(events) == 0 { - return fmt.Errorf("activity %q has no boundary events", op.ActivityRef) + return mdlerrors.NewValidation(fmt.Sprintf("activity %q has no boundary events", op.ActivityRef)) } if len(events) > 1 { diff --git a/mdl/executor/cmd_associations.go b/mdl/executor/cmd_associations.go index 80de2d2b..b005b836 100644 --- a/mdl/executor/cmd_associations.go +++ b/mdl/executor/cmd_associations.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" ) @@ -16,7 +17,7 @@ import ( // execCreateAssociation handles CREATE ASSOCIATION statements. func (e *Executor) execCreateAssociation(s *ast.CreateAssociationStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Find or auto-create module @@ -27,7 +28,7 @@ func (e *Executor) execCreateAssociation(s *ast.CreateAssociationStmt) error { dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } // Find parent and child entities (supports cross-module associations) @@ -37,7 +38,7 @@ func (e *Executor) execCreateAssociation(s *ast.CreateAssociationStmt) error { } parentEntity, err := e.findEntity(parentModule, s.Parent.Name) if err != nil { - return fmt.Errorf("parent entity not found: %s", s.Parent) + return mdlerrors.NewNotFound("parent entity", s.Parent.String()) } parentID := parentEntity.ID @@ -47,7 +48,7 @@ func (e *Executor) execCreateAssociation(s *ast.CreateAssociationStmt) error { } childEntity, err := e.findEntity(childModule, s.Child.Name) if err != nil { - return fmt.Errorf("child entity not found: %s", s.Child) + return mdlerrors.NewNotFound("child entity", s.Child.String()) } childID := childEntity.ID @@ -105,7 +106,7 @@ func (e *Executor) execCreateAssociation(s *ast.CreateAssociationStmt) error { }, } if err := e.writer.CreateCrossAssociation(dm.ID, ca); err != nil { - return fmt.Errorf("failed to create cross-module association: %w", err) + return mdlerrors.NewBackend("create cross-module association", err) } } else { assoc := &domainmodel.Association{ @@ -120,7 +121,7 @@ func (e *Executor) execCreateAssociation(s *ast.CreateAssociationStmt) error { }, } if err := e.writer.CreateAssociation(dm.ID, assoc); err != nil { - return fmt.Errorf("failed to create association: %w", err) + return mdlerrors.NewBackend("create association", err) } } @@ -144,7 +145,7 @@ func (e *Executor) execCreateAssociation(s *ast.CreateAssociationStmt) error { // execAlterAssociation handles ALTER ASSOCIATION statements. func (e *Executor) execAlterAssociation(s *ast.AlterAssociationStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } module, err := e.findModule(s.Name.Module) @@ -154,7 +155,7 @@ func (e *Executor) execAlterAssociation(s *ast.AlterAssociationStmt) error { dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } // Try intra-module associations first @@ -173,7 +174,7 @@ func (e *Executor) execAlterAssociation(s *ast.AlterAssociationStmt) error { assoc.Documentation = s.Comment } if err := e.writer.UpdateDomainModel(dm); err != nil { - return fmt.Errorf("failed to update association: %w", err) + return mdlerrors.NewBackend("update association", err) } fmt.Fprintf(e.output, "Altered association: %s\n", s.Name) return nil @@ -196,20 +197,20 @@ func (e *Executor) execAlterAssociation(s *ast.AlterAssociationStmt) error { ca.Documentation = s.Comment } if err := e.writer.UpdateDomainModel(dm); err != nil { - return fmt.Errorf("failed to update cross-module association: %w", err) + return mdlerrors.NewBackend("update cross-module association", err) } fmt.Fprintf(e.output, "Altered association: %s\n", s.Name) return nil } } - return fmt.Errorf("association not found: %s", s.Name) + return mdlerrors.NewNotFound("association", s.Name.String()) } // execDropAssociation handles DROP ASSOCIATION statements. func (e *Executor) execDropAssociation(s *ast.DropAssociationStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Find module @@ -220,13 +221,13 @@ func (e *Executor) execDropAssociation(s *ast.DropAssociationStmt) error { dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } for _, assoc := range dm.Associations { if assoc.Name == s.Name.Name { if err := e.writer.DeleteAssociation(dm.ID, assoc.ID); err != nil { - return fmt.Errorf("failed to delete association: %w", err) + return mdlerrors.NewBackend("delete association", err) } fmt.Fprintf(e.output, "Dropped association: %s\n", s.Name) return nil @@ -235,14 +236,14 @@ func (e *Executor) execDropAssociation(s *ast.DropAssociationStmt) error { for _, ca := range dm.CrossAssociations { if ca.Name == s.Name.Name { if err := e.writer.DeleteCrossAssociation(dm.ID, ca.ID); err != nil { - return fmt.Errorf("failed to delete cross-module association: %w", err) + return mdlerrors.NewBackend("delete cross-module association", err) } fmt.Fprintf(e.output, "Dropped cross-module association: %s\n", s.Name) return nil } } - return fmt.Errorf("association not found: %s", s.Name) + return mdlerrors.NewNotFound("association", s.Name.String()) } // showAssociations handles SHOW ASSOCIATIONS command. @@ -250,7 +251,7 @@ func (e *Executor) showAssociations(moduleName string) error { // Build module ID -> name map (single query) modules, err := e.reader.ListModules() if err != nil { - return fmt.Errorf("failed to list modules: %w", err) + return mdlerrors.NewBackend("list modules", err) } moduleNames := make(map[model.ID]string) for _, m := range modules { @@ -260,7 +261,7 @@ func (e *Executor) showAssociations(moduleName string) error { // Get all domain models in a single query (avoids O(n²) behavior) domainModels, err := e.reader.ListDomainModels() if err != nil { - return fmt.Errorf("failed to list domain models: %w", err) + return mdlerrors.NewBackend("list domain models", err) } // Build entity ID -> qualified name map @@ -334,7 +335,7 @@ func (e *Executor) showAssociations(moduleName string) error { // showAssociation handles SHOW ASSOCIATION command. func (e *Executor) showAssociation(name *ast.QualifiedName) error { if name == nil { - return fmt.Errorf("association name required") + return mdlerrors.NewValidation("association name required") } module, err := e.findModule(name.Module) @@ -344,7 +345,7 @@ func (e *Executor) showAssociation(name *ast.QualifiedName) error { dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } for _, assoc := range dm.Associations { @@ -367,7 +368,7 @@ func (e *Executor) showAssociation(name *ast.QualifiedName) error { } } - return fmt.Errorf("association not found: %s", name) + return mdlerrors.NewNotFound("association", name.String()) } // describeAssociation handles DESCRIBE ASSOCIATION command. @@ -379,18 +380,18 @@ func (e *Executor) describeAssociation(name ast.QualifiedName) error { dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } // Build entity ID -> qualified name map across all modules entityNames := make(map[model.ID]string) allDomainModels, err := e.reader.ListDomainModels() if err != nil { - return fmt.Errorf("failed to list domain models: %w", err) + return mdlerrors.NewBackend("list domain models", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, otherDM := range allDomainModels { modName := h.GetModuleName(otherDM.ContainerID) @@ -467,5 +468,5 @@ func (e *Executor) describeAssociation(name ast.QualifiedName) error { } } - return fmt.Errorf("association not found: %s", name) + return mdlerrors.NewNotFound("association", name.String()) } diff --git a/mdl/executor/cmd_businessevents.go b/mdl/executor/cmd_businessevents.go index e586bee2..9844d33a 100644 --- a/mdl/executor/cmd_businessevents.go +++ b/mdl/executor/cmd_businessevents.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/mpr" ) @@ -14,12 +15,12 @@ import ( // showBusinessEventServices displays a table of all business event service documents. func (e *Executor) showBusinessEventServices(inModule string) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } services, err := e.reader.ListBusinessEventServices() if err != nil { - return fmt.Errorf("failed to list business event services: %w", err) + return mdlerrors.NewBackend("list business event services", err) } h, err := e.getHierarchy() @@ -94,12 +95,12 @@ func (e *Executor) showBusinessEventClients(inModule string) error { // showBusinessEvents displays a table of individual messages across all business event services. func (e *Executor) showBusinessEvents(inModule string) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } services, err := e.reader.ListBusinessEventServices() if err != nil { - return fmt.Errorf("failed to list business event services: %w", err) + return mdlerrors.NewBackend("list business event services", err) } h, err := e.getHierarchy() @@ -171,12 +172,12 @@ func (e *Executor) showBusinessEvents(inModule string) error { // describeBusinessEventService outputs the full MDL description of a business event service. func (e *Executor) describeBusinessEventService(name ast.QualifiedName) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } services, err := e.reader.ListBusinessEventServices() if err != nil { - return fmt.Errorf("failed to list business event services: %w", err) + return mdlerrors.NewBackend("list business event services", err) } // Use hierarchy to resolve container IDs to module names @@ -199,7 +200,7 @@ func (e *Executor) describeBusinessEventService(name ast.QualifiedName) error { } if found == nil { - return fmt.Errorf("business event service not found: %s", name) + return mdlerrors.NewNotFound("business event service", name.String()) } // Output MDL CREATE statement @@ -261,20 +262,20 @@ func (e *Executor) describeBusinessEventService(name ast.QualifiedName) error { // createBusinessEventService creates a new business event service from an AST statement. func (e *Executor) createBusinessEventService(stmt *ast.CreateBusinessEventServiceStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project (read-only mode)") + return mdlerrors.NewNotConnectedWrite() } moduleName := stmt.Name.Module module, err := e.findModule(moduleName) if err != nil { - return fmt.Errorf("module not found: %s", moduleName) + return mdlerrors.NewNotFound("module", moduleName) } // Check for existing service with same name (if not CREATE OR REPLACE) existingServices, _ := e.reader.ListBusinessEventServices() h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, existing := range existingServices { @@ -284,10 +285,10 @@ func (e *Executor) createBusinessEventService(stmt *ast.CreateBusinessEventServi if stmt.CreateOrReplace { // Delete existing if err := e.writer.DeleteBusinessEventService(existing.ID); err != nil { - return fmt.Errorf("failed to delete existing service: %w", err) + return mdlerrors.NewBackend("delete existing service", err) } } else { - return fmt.Errorf("business event service already exists: %s.%s (use CREATE OR REPLACE to overwrite)", moduleName, stmt.Name.Name) + return mdlerrors.NewAlreadyExistsMsg("business event service", moduleName+"."+stmt.Name.Name, fmt.Sprintf("business event service already exists: %s.%s (use CREATE OR REPLACE to overwrite)", moduleName, stmt.Name.Name)) } } } @@ -297,7 +298,7 @@ func (e *Executor) createBusinessEventService(stmt *ast.CreateBusinessEventServi if stmt.Folder != "" { folderID, err := e.resolveFolder(module.ID, stmt.Folder) if err != nil { - return fmt.Errorf("failed to resolve folder '%s': %w", stmt.Folder, err) + return mdlerrors.NewBackend(fmt.Sprintf("resolve folder '%s'", stmt.Folder), err) } containerID = folderID } @@ -365,7 +366,7 @@ func (e *Executor) createBusinessEventService(stmt *ast.CreateBusinessEventServi // Write to project if err := e.writer.CreateBusinessEventService(svc); err != nil { - return fmt.Errorf("failed to create business event service: %w", err) + return mdlerrors.NewBackend("create business event service", err) } fmt.Fprintf(e.output, "Created business event service: %s.%s\n", moduleName, stmt.Name.Name) @@ -375,17 +376,17 @@ func (e *Executor) createBusinessEventService(stmt *ast.CreateBusinessEventServi // dropBusinessEventService deletes a business event service. func (e *Executor) dropBusinessEventService(stmt *ast.DropBusinessEventServiceStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project (read-only mode)") + return mdlerrors.NewNotConnectedWrite() } services, err := e.reader.ListBusinessEventServices() if err != nil { - return fmt.Errorf("failed to list business event services: %w", err) + return mdlerrors.NewBackend("list business event services", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, svc := range services { @@ -393,14 +394,14 @@ func (e *Executor) dropBusinessEventService(stmt *ast.DropBusinessEventServiceSt moduleName := h.GetModuleName(modID) if strings.EqualFold(moduleName, stmt.Name.Module) && strings.EqualFold(svc.Name, stmt.Name.Name) { if err := e.writer.DeleteBusinessEventService(svc.ID); err != nil { - return fmt.Errorf("failed to delete business event service: %w", err) + return mdlerrors.NewBackend("delete business event service", err) } fmt.Fprintf(e.output, "Dropped business event service: %s.%s\n", moduleName, svc.Name) return nil } } - return fmt.Errorf("business event service not found: %s", stmt.Name) + return mdlerrors.NewNotFound("business event service", stmt.Name.String()) } // generateChannelName generates a hex channel name (similar to Mendix Studio Pro). diff --git a/mdl/executor/cmd_catalog.go b/mdl/executor/cmd_catalog.go index 446008a8..a2d8f44b 100644 --- a/mdl/executor/cmd_catalog.go +++ b/mdl/executor/cmd_catalog.go @@ -13,6 +13,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/catalog" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" ) @@ -131,7 +132,7 @@ func (e *Executor) execCatalogQuery(query string) error { // Execute query result, err := e.catalog.Query(query) if err != nil { - return fmt.Errorf("failed to execute catalog query\n%v", err) + return mdlerrors.NewBackend("execute catalog query", err) } // Output results @@ -170,7 +171,7 @@ func (e *Executor) execDescribeCatalogTable(stmt *ast.DescribeCatalogTableStmt) // Query column info using PRAGMA result, err := e.catalog.Query(fmt.Sprintf("PRAGMA table_info(%s)", tableName)) if err != nil || result.Count == 0 { - return fmt.Errorf("unknown catalog table: CATALOG.%s", strings.ToUpper(tableName)) + return mdlerrors.NewNotFoundMsg("catalog table", strings.ToUpper(tableName), "unknown catalog table: CATALOG."+strings.ToUpper(tableName)) } // Print table header @@ -334,7 +335,7 @@ func (e *Executor) buildCatalog(full bool, source ...bool) error { // Create new catalog cat, err := catalog.New() if err != nil { - return fmt.Errorf("failed to create catalog: %w", err) + return mdlerrors.NewBackend("create catalog", err) } // Set project metadata @@ -354,7 +355,7 @@ func (e *Executor) buildCatalog(full bool, source ...bool) error { }) if err != nil { cat.Close() - return fmt.Errorf("failed to build catalog: %w", err) + return mdlerrors.NewBackend("build catalog", err) } elapsed := time.Since(start) @@ -396,7 +397,7 @@ func (e *Executor) buildCatalog(full bool, source ...bool) error { // execRefreshCatalogStmt handles REFRESH CATALOG [FULL] [SOURCE] [FORCE] [BACKGROUND] command. func (e *Executor) execRefreshCatalogStmt(stmt *ast.RefreshCatalogStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } requiredMode := "fast" @@ -425,7 +426,7 @@ func (e *Executor) execRefreshCatalogStmt(stmt *ast.RefreshCatalogStmt) error { // If project file was modified, reconnect to get fresh database connection if reason == "project file modified" { if err := e.reconnect(); err != nil { - return fmt.Errorf("failed to reconnect after project modification: %w", err) + return mdlerrors.NewBackend("reconnect after project modification", err) } } } @@ -628,7 +629,7 @@ func (e *Executor) captureDescribe(objectType string, qualifiedName string) (str // Parse qualified name into ast.QualifiedName parts := strings.SplitN(qualifiedName, ".", 2) if len(parts) != 2 { - return "", fmt.Errorf("invalid qualified name: %s", qualifiedName) + return "", mdlerrors.NewValidationf("invalid qualified name: %s", qualifiedName) } qn := ast.QualifiedName{Module: parts[0], Name: parts[1]} @@ -653,7 +654,7 @@ func (e *Executor) captureDescribe(objectType string, qualifiedName string) (str case "WORKFLOW": err = e.describeWorkflow(qn) default: - return "", fmt.Errorf("unsupported object type for describe: %s", objectType) + return "", mdlerrors.NewUnsupported("object type for describe: " + objectType) } if err != nil { @@ -669,7 +670,7 @@ func (e *Executor) captureDescribe(objectType string, qualifiedName string) (str func (e *Executor) captureDescribeParallel(objectType string, qualifiedName string) (string, error) { parts := strings.SplitN(qualifiedName, ".", 2) if len(parts) != 2 { - return "", fmt.Errorf("invalid qualified name: %s", qualifiedName) + return "", mdlerrors.NewValidationf("invalid qualified name: %s", qualifiedName) } qn := ast.QualifiedName{Module: parts[0], Name: parts[1]} @@ -696,7 +697,7 @@ func (e *Executor) captureDescribeParallel(objectType string, qualifiedName stri case "WORKFLOW": err = local.describeWorkflow(qn) default: - return "", fmt.Errorf("unsupported object type for describe: %s", objectType) + return "", mdlerrors.NewUnsupported("object type for describe: " + objectType) } if err != nil { @@ -743,7 +744,7 @@ func (e *Executor) PreWarmCache() { // execSearch handles SEARCH 'query' command. func (e *Executor) execSearch(stmt *ast.SearchStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Ensure catalog is built (at least full mode for strings table) @@ -805,7 +806,7 @@ func escapeFTSQuery(q string) string { // Format can be: "table" (default), "names" (just qualified names), "json" func (e *Executor) Search(query, format string) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Ensure catalog is built (at least full mode for strings table) diff --git a/mdl/executor/cmd_constants.go b/mdl/executor/cmd_constants.go index b3efc746..b414333a 100644 --- a/mdl/executor/cmd_constants.go +++ b/mdl/executor/cmd_constants.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" ) @@ -15,13 +16,13 @@ import ( func (e *Executor) showConstants(moduleName string) error { constants, err := e.reader.ListConstants() if err != nil { - return fmt.Errorf("failed to list constants: %w", err) + return mdlerrors.NewBackend("list constants", err) } // Use hierarchy for proper module resolution (handles constants inside folders) h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Collect rows @@ -82,13 +83,13 @@ func (e *Executor) showConstants(moduleName string) error { func (e *Executor) describeConstant(name ast.QualifiedName) error { constants, err := e.reader.ListConstants() if err != nil { - return fmt.Errorf("failed to list constants: %w", err) + return mdlerrors.NewBackend("list constants", err) } // Use hierarchy for proper module resolution h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Find the constant @@ -100,7 +101,7 @@ func (e *Executor) describeConstant(name ast.QualifiedName) error { } } - return fmt.Errorf("constant not found: %s", name) + return mdlerrors.NewNotFound("constant", name.String()) } // outputConstantMDL outputs a constant definition in MDL format. @@ -248,12 +249,12 @@ func formatDefaultValue(dt model.ConstantDataType, value string) string { // createConstant handles CREATE CONSTANT command. func (e *Executor) createConstant(stmt *ast.CreateConstantStmt) error { if e.writer == nil { - return fmt.Errorf("not connected in write mode") + return mdlerrors.NewNotConnectedWrite() } // Validate module name is specified if stmt.Name.Module == "" { - return fmt.Errorf("module name required for constant: use CREATE CONSTANT Module.ConstantName") + return mdlerrors.NewValidation("module name required for constant: use CREATE CONSTANT Module.ConstantName") } // Find or auto-create module @@ -290,13 +291,13 @@ func (e *Executor) createConstant(stmt *ast.CreateConstantStmt) error { c.DefaultValue = defaultValue c.ExposedToClient = stmt.ExposedToClient if err := e.writer.UpdateConstant(c); err != nil { - return fmt.Errorf("failed to update constant: %w", err) + return mdlerrors.NewBackend("update constant", err) } e.invalidateHierarchy() fmt.Fprintf(e.output, "Modified constant: %s.%s\n", modName, c.Name) return nil } - return fmt.Errorf("constant already exists: %s.%s (use CREATE OR MODIFY to update)", modName, c.Name) + return mdlerrors.NewAlreadyExistsMsg("constant", modName+"."+c.Name, fmt.Sprintf("constant already exists: %s.%s (use CREATE OR MODIFY to update)", modName, c.Name)) } } } @@ -311,7 +312,7 @@ func (e *Executor) createConstant(stmt *ast.CreateConstantStmt) error { if stmt.Folder != "" { folderID, err := e.resolveFolder(module.ID, stmt.Folder) if err != nil { - return fmt.Errorf("failed to resolve folder %s: %w", stmt.Folder, err) + return mdlerrors.NewBackend(fmt.Sprintf("resolve folder %s", stmt.Folder), err) } containerID = folderID } @@ -326,7 +327,7 @@ func (e *Executor) createConstant(stmt *ast.CreateConstantStmt) error { } if err := e.writer.CreateConstant(constant); err != nil { - return fmt.Errorf("failed to create constant: %w", err) + return mdlerrors.NewBackend("create constant", err) } e.invalidateHierarchy() fmt.Fprintf(e.output, "Created constant: %s.%s\n", stmt.Name.Module, stmt.Name.Name) @@ -338,17 +339,17 @@ func (e *Executor) createConstant(stmt *ast.CreateConstantStmt) error { func (e *Executor) showConstantValues(moduleName string) error { constants, err := e.reader.ListConstants() if err != nil { - return fmt.Errorf("failed to list constants: %w", err) + return mdlerrors.NewBackend("list constants", err) } ps, err := e.reader.GetProjectSettings() if err != nil { - return fmt.Errorf("failed to read project settings: %w", err) + return mdlerrors.NewBackend("read project settings", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Build constant list with qualified names @@ -431,18 +432,18 @@ func (e *Executor) showConstantValues(moduleName string) error { // dropConstant handles DROP CONSTANT command. func (e *Executor) dropConstant(stmt *ast.DropConstantStmt) error { if e.writer == nil { - return fmt.Errorf("not connected in write mode") + return mdlerrors.NewNotConnectedWrite() } constants, err := e.reader.ListConstants() if err != nil { - return fmt.Errorf("failed to list constants: %w", err) + return mdlerrors.NewBackend("list constants", err) } // Use hierarchy for proper module resolution h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Find the constant @@ -451,7 +452,7 @@ func (e *Executor) dropConstant(stmt *ast.DropConstantStmt) error { modName := h.GetModuleName(modID) if strings.EqualFold(modName, stmt.Name.Module) && strings.EqualFold(c.Name, stmt.Name.Name) { if err := e.writer.DeleteConstant(c.ID); err != nil { - return fmt.Errorf("failed to drop constant: %w", err) + return mdlerrors.NewBackend("drop constant", err) } e.invalidateHierarchy() fmt.Fprintf(e.output, "Dropped constant: %s.%s\n", modName, c.Name) @@ -459,7 +460,7 @@ func (e *Executor) dropConstant(stmt *ast.DropConstantStmt) error { } } - return fmt.Errorf("constant not found: %s", stmt.Name) + return mdlerrors.NewNotFound("constant", stmt.Name.String()) } // astDataTypeToConstantDataType converts AST DataType to model.ConstantDataType. diff --git a/mdl/executor/cmd_context.go b/mdl/executor/cmd_context.go index 6fde598e..73d56c6c 100644 --- a/mdl/executor/cmd_context.go +++ b/mdl/executor/cmd_context.go @@ -7,18 +7,19 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" ) // execShowContext handles SHOW CONTEXT OF [DEPTH n] command. // It assembles relevant context information for LLM consumption. func (e *Executor) execShowContext(s *ast.ShowStmt) error { if s.Name == nil { - return fmt.Errorf("SHOW CONTEXT requires a qualified name") + return mdlerrors.NewValidation("SHOW CONTEXT requires a qualified name") } // Ensure catalog is built with full mode for refs if err := e.ensureCatalog(true); err != nil { - return fmt.Errorf("failed to build catalog: %w", err) + return mdlerrors.NewBackend("build catalog", err) } name := s.Name.String() @@ -90,7 +91,7 @@ func (e *Executor) detectElementType(name string) (string, error) { } } - return "", fmt.Errorf("element not found: %s", name) + return "", mdlerrors.NewNotFound("element", name) } // assembleMicroflowContext assembles context for a microflow. diff --git a/mdl/executor/cmd_contract.go b/mdl/executor/cmd_contract.go index 5b4c4b73..c9ed342d 100644 --- a/mdl/executor/cmd_contract.go +++ b/mdl/executor/cmd_contract.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/mpr" @@ -16,7 +17,7 @@ import ( // showContractEntities handles SHOW CONTRACT ENTITIES FROM Module.Service. func (e *Executor) showContractEntities(name *ast.QualifiedName) error { if name == nil { - return fmt.Errorf("service name required: SHOW CONTRACT ENTITIES FROM Module.Service") + return mdlerrors.NewValidation("service name required: SHOW CONTRACT ENTITIES FROM Module.Service") } doc, svcQN, err := e.parseServiceContract(*name) @@ -76,7 +77,7 @@ func (e *Executor) showContractEntities(name *ast.QualifiedName) error { // showContractActions handles SHOW CONTRACT ACTIONS FROM Module.Service. func (e *Executor) showContractActions(name *ast.QualifiedName) error { if name == nil { - return fmt.Errorf("service name required: SHOW CONTRACT ACTIONS FROM Module.Service") + return mdlerrors.NewValidation("service name required: SHOW CONTRACT ACTIONS FROM Module.Service") } doc, svcQN, err := e.parseServiceContract(*name) @@ -153,7 +154,7 @@ func (e *Executor) describeContractEntity(name ast.QualifiedName, format string) et := doc.FindEntityType(entityName) if et == nil { - return fmt.Errorf("entity type %q not found in contract for %s", entityName, svcQN) + return mdlerrors.NewNotFoundMsg("entity type", entityName, fmt.Sprintf("entity type %q not found in contract for %s", entityName, svcQN)) } if strings.EqualFold(format, "mdl") { @@ -233,7 +234,7 @@ func (e *Executor) describeContractAction(name ast.QualifiedName, format string) } } if action == nil { - return fmt.Errorf("action %q not found in contract for %s", actionName, svcQN) + return mdlerrors.NewNotFoundMsg("action", actionName, fmt.Sprintf("action %q not found in contract for %s", actionName, svcQN)) } fmt.Fprintf(e.output, "%s\n", action.Name) @@ -317,12 +318,12 @@ func (e *Executor) outputContractEntityMDL(et *mpr.EdmEntityType, svcQN string, func (e *Executor) parseServiceContract(name ast.QualifiedName) (*mpr.EdmxDocument, string, error) { services, err := e.reader.ListConsumedODataServices() if err != nil { - return nil, "", fmt.Errorf("failed to list consumed OData services: %w", err) + return nil, "", mdlerrors.NewBackend("list consumed OData services", err) } h, err := e.getHierarchy() if err != nil { - return nil, "", fmt.Errorf("failed to build hierarchy: %w", err) + return nil, "", mdlerrors.NewBackend("build hierarchy", err) } for _, svc := range services { @@ -336,18 +337,18 @@ func (e *Executor) parseServiceContract(name ast.QualifiedName) (*mpr.EdmxDocume svcQN := modName + "." + svc.Name if svc.Metadata == "" { - return nil, svcQN, fmt.Errorf("no cached contract metadata for %s (MetadataUrl: %s). The service metadata has not been downloaded yet", svcQN, svc.MetadataUrl) + return nil, svcQN, mdlerrors.NewValidationf("no cached contract metadata for %s (MetadataUrl: %s). The service metadata has not been downloaded yet", svcQN, svc.MetadataUrl) } doc, err := mpr.ParseEdmx(svc.Metadata) if err != nil { - return nil, svcQN, fmt.Errorf("failed to parse contract metadata for %s: %w", svcQN, err) + return nil, svcQN, mdlerrors.NewBackend(fmt.Sprintf("parse contract metadata for %s", svcQN), err) } return doc, svcQN, nil } - return nil, "", fmt.Errorf("consumed OData service not found: %s.%s", name.Module, name.Name) + return nil, "", mdlerrors.NewNotFound("consumed OData service", name.Module+"."+name.Name) } // splitContractRef splits Module.Service.EntityName into (Module.Service, EntityName). @@ -359,7 +360,7 @@ func splitContractRef(name ast.QualifiedName) (ast.QualifiedName, string, error) // We need to split Name into service name and entity name. parts := strings.SplitN(name.Name, ".", 2) if len(parts) != 2 { - return name, "", fmt.Errorf("expected Module.Service.EntityName, got %s.%s", name.Module, name.Name) + return name, "", mdlerrors.NewValidationf("expected Module.Service.EntityName, got %s.%s", name.Module, name.Name) } svcName := ast.QualifiedName{ @@ -446,7 +447,7 @@ var reservedEntityAttrNames = map[string]bool{ // what Studio Pro produces. func (e *Executor) createExternalEntities(s *ast.CreateExternalEntitiesStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } doc, svcQN, err := e.parseServiceContract(s.ServiceRef) @@ -480,7 +481,7 @@ func (e *Executor) createExternalEntities(s *ast.CreateExternalEntitiesStmt) err } dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } // Index existing entities by name for upsert @@ -1245,7 +1246,7 @@ func edmToAstDataType(p *mpr.EdmProperty) ast.DataType { // showContractChannels handles SHOW CONTRACT CHANNELS FROM Module.Service. func (e *Executor) showContractChannels(name *ast.QualifiedName) error { if name == nil { - return fmt.Errorf("service name required: SHOW CONTRACT CHANNELS FROM Module.Service") + return mdlerrors.NewValidation("service name required: SHOW CONTRACT CHANNELS FROM Module.Service") } doc, svcQN, err := e.parseAsyncAPIContract(*name) @@ -1284,7 +1285,7 @@ func (e *Executor) showContractChannels(name *ast.QualifiedName) error { // showContractMessages handles SHOW CONTRACT MESSAGES FROM Module.Service. func (e *Executor) showContractMessages(name *ast.QualifiedName) error { if name == nil { - return fmt.Errorf("service name required: SHOW CONTRACT MESSAGES FROM Module.Service") + return mdlerrors.NewValidation("service name required: SHOW CONTRACT MESSAGES FROM Module.Service") } doc, svcQN, err := e.parseAsyncAPIContract(*name) @@ -1338,7 +1339,7 @@ func (e *Executor) describeContractMessage(name ast.QualifiedName) error { msg := doc.FindMessage(msgName) if msg == nil { - return fmt.Errorf("message %q not found in contract for %s", msgName, svcQN) + return mdlerrors.NewNotFoundMsg("message", msgName, fmt.Sprintf("message %q not found in contract for %s", msgName, svcQN)) } fmt.Fprintf(e.output, "%s\n", msg.Name) @@ -1380,12 +1381,12 @@ func (e *Executor) describeContractMessage(name ast.QualifiedName) error { func (e *Executor) parseAsyncAPIContract(name ast.QualifiedName) (*mpr.AsyncAPIDocument, string, error) { services, err := e.reader.ListBusinessEventServices() if err != nil { - return nil, "", fmt.Errorf("failed to list business event services: %w", err) + return nil, "", mdlerrors.NewBackend("list business event services", err) } h, err := e.getHierarchy() if err != nil { - return nil, "", fmt.Errorf("failed to build hierarchy: %w", err) + return nil, "", mdlerrors.NewBackend("build hierarchy", err) } for _, svc := range services { @@ -1399,18 +1400,18 @@ func (e *Executor) parseAsyncAPIContract(name ast.QualifiedName) (*mpr.AsyncAPID svcQN := modName + "." + svc.Name if svc.Document == "" { - 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) + return nil, svcQN, mdlerrors.NewValidationf("no cached AsyncAPI contract for %s. This service has no Document field (it may be a publisher, not a consumer)", svcQN) } doc, err := mpr.ParseAsyncAPI(svc.Document) if err != nil { - return nil, svcQN, fmt.Errorf("failed to parse AsyncAPI contract for %s: %w", svcQN, err) + return nil, svcQN, mdlerrors.NewBackend(fmt.Sprintf("parse AsyncAPI contract for %s", svcQN), err) } return doc, svcQN, nil } - return nil, "", fmt.Errorf("business event service not found: %s.%s", name.Module, name.Name) + return nil, "", mdlerrors.NewNotFound("business event service", name.Module+"."+name.Name) } // asyncTypeString formats an AsyncAPI property type for display. diff --git a/mdl/executor/cmd_datatransformer.go b/mdl/executor/cmd_datatransformer.go index 8ee86541..77c919cb 100644 --- a/mdl/executor/cmd_datatransformer.go +++ b/mdl/executor/cmd_datatransformer.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" ) @@ -14,12 +15,12 @@ import ( func (e *Executor) listDataTransformers(moduleName string) error { transformers, err := e.reader.ListDataTransformers() if err != nil { - return fmt.Errorf("failed to list data transformers: %w", err) + return mdlerrors.NewBackend("list data transformers", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } var rows [][]any @@ -57,12 +58,12 @@ func (e *Executor) listDataTransformers(moduleName string) error { func (e *Executor) describeDataTransformer(name ast.QualifiedName) error { transformers, err := e.reader.ListDataTransformers() if err != nil { - return fmt.Errorf("failed to list data transformers: %w", err) + return mdlerrors.NewBackend("list data transformers", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, dt := range transformers { @@ -98,13 +99,13 @@ func (e *Executor) describeDataTransformer(name ast.QualifiedName) error { return nil } - return fmt.Errorf("data transformer not found: %s.%s", name.Module, name.Name) + return mdlerrors.NewNotFound("data transformer", name.Module+"."+name.Name) } // execCreateDataTransformer creates a new data transformer. func (e *Executor) execCreateDataTransformer(s *ast.CreateDataTransformerStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } if err := e.checkFeature("integration", "data_transformer", @@ -115,7 +116,7 @@ func (e *Executor) execCreateDataTransformer(s *ast.CreateDataTransformerStmt) e module, err := e.findModule(s.Name.Module) if err != nil { - return fmt.Errorf("module %s not found", s.Name.Module) + return mdlerrors.NewNotFound("module", s.Name.Module) } dt := &model.DataTransformer{ @@ -133,7 +134,7 @@ func (e *Executor) execCreateDataTransformer(s *ast.CreateDataTransformerStmt) e } if err := e.writer.CreateDataTransformer(dt); err != nil { - return fmt.Errorf("failed to create data transformer: %w", err) + return mdlerrors.NewBackend("create data transformer", err) } if !e.quiet { @@ -146,12 +147,12 @@ func (e *Executor) execCreateDataTransformer(s *ast.CreateDataTransformerStmt) e // execDropDataTransformer deletes a data transformer. func (e *Executor) execDropDataTransformer(s *ast.DropDataTransformerStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } transformers, err := e.reader.ListDataTransformers() if err != nil { - return fmt.Errorf("failed to list data transformers: %w", err) + return mdlerrors.NewBackend("list data transformers", err) } h, err := e.getHierarchy() @@ -164,7 +165,7 @@ func (e *Executor) execDropDataTransformer(s *ast.DropDataTransformerStmt) error modName := h.GetModuleName(modID) if modName == s.Name.Module && dt.Name == s.Name.Name { if err := e.writer.DeleteDataTransformer(dt.ID); err != nil { - return fmt.Errorf("failed to drop data transformer: %w", err) + return mdlerrors.NewBackend("drop data transformer", err) } if !e.quiet { fmt.Fprintf(e.output, "Dropped data transformer: %s.%s\n", s.Name.Module, s.Name.Name) @@ -173,5 +174,5 @@ func (e *Executor) execDropDataTransformer(s *ast.DropDataTransformerStmt) error } } - return fmt.Errorf("data transformer %s.%s not found", s.Name.Module, s.Name.Name) + return mdlerrors.NewNotFound("data transformer", s.Name.Module+"."+s.Name.Name) } diff --git a/mdl/executor/cmd_dbconnection.go b/mdl/executor/cmd_dbconnection.go index 3831a4a2..70de4ff5 100644 --- a/mdl/executor/cmd_dbconnection.go +++ b/mdl/executor/cmd_dbconnection.go @@ -8,17 +8,18 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" ) // createDatabaseConnection handles CREATE DATABASE CONNECTION command. func (e *Executor) createDatabaseConnection(stmt *ast.CreateDatabaseConnectionStmt) error { if e.writer == nil { - return fmt.Errorf("not connected in write mode") + return mdlerrors.NewNotConnectedWrite() } if stmt.Name.Module == "" { - return fmt.Errorf("module name required: use CREATE DATABASE CONNECTION Module.ConnectionName") + return mdlerrors.NewValidation("module name required: use CREATE DATABASE CONNECTION Module.ConnectionName") } module, err := e.findModule(stmt.Name.Module) @@ -36,11 +37,10 @@ func (e *Executor) createDatabaseConnection(stmt *ast.CreateDatabaseConnectionSt if strings.EqualFold(modName, stmt.Name.Module) && strings.EqualFold(ex.Name, stmt.Name.Name) { if stmt.CreateOrModify { if err := e.writer.DeleteDatabaseConnection(ex.ID); err != nil { - return fmt.Errorf("failed to delete existing connection: %w", err) + return mdlerrors.NewBackend("delete existing connection", err) } } else { - return fmt.Errorf("database connection already exists: %s.%s (use CREATE OR MODIFY to update)", - modName, ex.Name) + return mdlerrors.NewAlreadyExistsMsg("database connection", modName+"."+ex.Name, "use CREATE OR MODIFY to update") } } } @@ -112,7 +112,7 @@ func (e *Executor) createDatabaseConnection(stmt *ast.CreateDatabaseConnectionSt } if err := e.writer.CreateDatabaseConnection(conn); err != nil { - return fmt.Errorf("failed to create database connection: %w", err) + return mdlerrors.NewBackend("create database connection", err) } e.invalidateHierarchy() @@ -124,12 +124,12 @@ func (e *Executor) createDatabaseConnection(stmt *ast.CreateDatabaseConnectionSt func (e *Executor) showDatabaseConnections(moduleName string) error { connections, err := e.reader.ListDatabaseConnections() if err != nil { - return fmt.Errorf("failed to list database connections: %w", err) + return mdlerrors.NewBackend("list database connections", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } type row struct { @@ -178,12 +178,12 @@ func (e *Executor) showDatabaseConnections(moduleName string) error { func (e *Executor) describeDatabaseConnection(name ast.QualifiedName) error { connections, err := e.reader.ListDatabaseConnections() if err != nil { - return fmt.Errorf("failed to list database connections: %w", err) + return mdlerrors.NewBackend("list database connections", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, conn := range connections { @@ -194,7 +194,7 @@ func (e *Executor) describeDatabaseConnection(name ast.QualifiedName) error { } } - return fmt.Errorf("database connection not found: %s", name) + return mdlerrors.NewNotFound("database connection", name.String()) } // outputDatabaseConnectionMDL outputs a database connection definition in MDL format. diff --git a/mdl/executor/cmd_diff.go b/mdl/executor/cmd_diff.go index 8a0c51bb..87824a9b 100644 --- a/mdl/executor/cmd_diff.go +++ b/mdl/executor/cmd_diff.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" ) // DiffFormat represents the output format for diff results @@ -67,7 +68,7 @@ const ( // DiffProgram compares an MDL program against the current project state func (e *Executor) DiffProgram(prog *ast.Program, opts DiffOptions) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Set defaults diff --git a/mdl/executor/cmd_diff_local.go b/mdl/executor/cmd_diff_local.go index c6dd0c03..b3adb348 100644 --- a/mdl/executor/cmd_diff_local.go +++ b/mdl/executor/cmd_diff_local.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/mpr" "go.mongodb.org/mongo-driver/bson" @@ -29,17 +30,17 @@ import ( // - A range "base..target" — compares two revisions (no working tree) func (e *Executor) DiffLocal(ref string, opts DiffOptions) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Check MPR version if e.reader.Version() != 2 { - return fmt.Errorf("diff-local only supports MPR v2 format (Mendix 10.18+)") + return mdlerrors.NewUnsupported("diff-local only supports MPR v2 format (Mendix 10.18+)") } contentsDir := e.reader.ContentsDir() if contentsDir == "" { - return fmt.Errorf("mprcontents directory not found") + return mdlerrors.NewValidation("mprcontents directory not found") } // Set defaults @@ -53,7 +54,7 @@ func (e *Executor) DiffLocal(ref string, opts DiffOptions) error { // Find changed mxunit files using git changedFiles, err := e.findChangedMxunitFiles(contentsDir, ref) if err != nil { - return fmt.Errorf("failed to find changed files: %w", err) + return mdlerrors.NewBackend("find changed files", err) } if len(changedFiles) == 0 { @@ -114,7 +115,7 @@ func (e *Executor) findChangedMxunitFiles(contentsDir, ref string) ([]gitChange, cmd := execCommand("git", "diff", "--name-status", ref, "--", contentsDir) output, err := cmd.Output() if err != nil { - return nil, fmt.Errorf("git diff failed: %w", err) + return nil, mdlerrors.NewBackend("git diff", err) } var changes []gitChange @@ -163,14 +164,14 @@ func (e *Executor) diffMxunitFile(change gitChange, contentsDir, ref string) (*D cmd := execCommand("git", "show", targetRef+":"+change.FilePath) currentContent, err = cmd.Output() if err != nil { - return nil, fmt.Errorf("failed to read %s version of %s: %w", targetRef, change.FilePath, err) + return nil, mdlerrors.NewBackend(fmt.Sprintf("read %s version of %s", targetRef, change.FilePath), err) } } if change.Status != "A" { cmd := execCommand("git", "show", baseRef+":"+change.FilePath) gitContent, err = cmd.Output() if err != nil { - return nil, fmt.Errorf("failed to read %s version of %s: %w", baseRef, change.FilePath, err) + return nil, mdlerrors.NewBackend(fmt.Sprintf("read %s version of %s", baseRef, change.FilePath), err) } } } else { @@ -178,14 +179,14 @@ func (e *Executor) diffMxunitFile(change gitChange, contentsDir, ref string) (*D if change.Status != "D" { currentContent, err = readFile(change.FilePath) if err != nil { - return nil, fmt.Errorf("failed to read current file %s: %w", change.FilePath, err) + return nil, mdlerrors.NewBackend(fmt.Sprintf("read current file %s", change.FilePath), err) } } if change.Status != "A" { cmd := execCommand("git", "show", ref+":"+change.FilePath) gitContent, err = cmd.Output() if err != nil { - return nil, fmt.Errorf("failed to read git version of %s: %w", change.FilePath, err) + return nil, mdlerrors.NewBackend(fmt.Sprintf("read git version of %s", change.FilePath), err) } } } diff --git a/mdl/executor/cmd_domainmodel_elk.go b/mdl/executor/cmd_domainmodel_elk.go index 59e8e66b..9ae04c22 100644 --- a/mdl/executor/cmd_domainmodel_elk.go +++ b/mdl/executor/cmd_domainmodel_elk.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" ) @@ -67,7 +68,7 @@ const ( // If name contains a dot (e.g. "Module.Entity"), it delegates to EntityFocusELK for a focused view. func (e *Executor) DomainModelELK(name string) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // If name is qualified (Module.Entity), render focused entity diagram @@ -83,7 +84,7 @@ func (e *Executor) DomainModelELK(name string) error { dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } allEntityNames, _ := e.buildAllEntityNames() @@ -155,12 +156,12 @@ func (e *Executor) DomainModelELK(name string) error { // and entities directly connected to it via associations or generalization. func (e *Executor) EntityFocusELK(qualifiedName string) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } parts := strings.SplitN(qualifiedName, ".", 2) if len(parts) != 2 { - return fmt.Errorf("expected qualified name Module.Entity, got: %s", qualifiedName) + return mdlerrors.NewValidationf("expected qualified name Module.Entity, got: %s", qualifiedName) } moduleName, entityName := parts[0], parts[1] @@ -171,7 +172,7 @@ func (e *Executor) EntityFocusELK(qualifiedName string) error { dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } // Find the focus entity @@ -183,7 +184,7 @@ func (e *Executor) EntityFocusELK(qualifiedName string) error { } } if focusEntity == nil { - return fmt.Errorf("entity not found: %s", qualifiedName) + return mdlerrors.NewNotFound("entity", qualifiedName) } // If this is a view entity with an OQL query, render query plan instead @@ -485,7 +486,7 @@ func (e *Executor) buildDomainModelMdlSource(entities []*domainmodel.Entity, mod func (e *Executor) emitDomainModelELK(data domainModelELKData) error { out, err := json.MarshalIndent(data, "", " ") if err != nil { - return fmt.Errorf("failed to marshal JSON: %w", err) + return mdlerrors.NewBackend("marshal JSON", err) } fmt.Fprint(e.output, string(out)) return nil diff --git a/mdl/executor/cmd_entities.go b/mdl/executor/cmd_entities.go index dd319305..477b22bb 100644 --- a/mdl/executor/cmd_entities.go +++ b/mdl/executor/cmd_entities.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/mpr" @@ -26,7 +27,7 @@ func (e *Executor) buildEventHandlers(defs []ast.EventHandlerDef) ([]*domainmode mfQN := d.Microflow.String() mfID, err := e.resolveMicroflowByName(mfQN) if err != nil { - return nil, fmt.Errorf("event handler microflow not found: %s", mfQN) + return nil, mdlerrors.NewNotFound("microflow", mfQN) } handlers = append(handlers, &domainmodel.EventHandler{ BaseElement: model.BaseElement{ @@ -46,7 +47,7 @@ func (e *Executor) buildEventHandlers(defs []ast.EventHandlerDef) ([]*domainmode func (e *Executor) execCreateEntity(s *ast.CreateEntityStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Find or auto-create module @@ -58,7 +59,7 @@ func (e *Executor) execCreateEntity(s *ast.CreateEntityStmt) error { // Get domain model dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } // Check if entity already exists @@ -72,7 +73,7 @@ func (e *Executor) execCreateEntity(s *ast.CreateEntityStmt) error { // If entity exists and not using CREATE OR MODIFY, return error if existingEntity != nil && !s.CreateOrModify { - return fmt.Errorf("entity already exists: %s.%s (use CREATE OR MODIFY to update)", s.Name.Module, s.Name.Name) + return mdlerrors.NewAlreadyExistsMsg("entity", s.Name.Module+"."+s.Name.Name, fmt.Sprintf("entity already exists: %s.%s (use CREATE OR MODIFY to update)", s.Name.Module, s.Name.Name)) } // Calculate position @@ -122,7 +123,7 @@ func (e *Executor) execCreateEntity(s *ast.CreateEntityStmt) error { // CALCULATED attributes are only supported on persistent entities if a.Calculated && !persistable { - return fmt.Errorf("attribute '%s': CALCULATED attributes are only supported on persistent entities", a.Name) + return mdlerrors.NewValidationf("attribute '%s': CALCULATED attributes are only supported on persistent entities", a.Name) } // Use Documentation if available, fall back to Comment @@ -269,7 +270,7 @@ func (e *Executor) execCreateEntity(s *ast.CreateEntityStmt) error { // Update existing entity entity.ID = existingEntity.ID if err := e.writer.UpdateEntity(dm.ID, entity); err != nil { - return fmt.Errorf("failed to update entity: %w", err) + return mdlerrors.NewBackend("update entity", err) } // Invalidate caches so updated entity is visible e.invalidateHierarchy() @@ -278,7 +279,7 @@ func (e *Executor) execCreateEntity(s *ast.CreateEntityStmt) error { } else { // Create new entity if err := e.writer.CreateEntity(dm.ID, entity); err != nil { - return fmt.Errorf("failed to create entity: %w", err) + return mdlerrors.NewBackend("create entity", err) } // Invalidate caches so new entity is visible e.invalidateHierarchy() @@ -293,7 +294,7 @@ func (e *Executor) execCreateEntity(s *ast.CreateEntityStmt) error { // execCreateViewEntity handles CREATE VIEW ENTITY statements. func (e *Executor) execCreateViewEntity(s *ast.CreateViewEntityStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Version pre-check @@ -311,7 +312,7 @@ func (e *Executor) execCreateViewEntity(s *ast.CreateViewEntityStmt) error { for _, v := range oqlViolations { msgs = append(msgs, v.Message) } - return fmt.Errorf("invalid OQL in view entity '%s':\n - %s", + return mdlerrors.NewValidationf("invalid OQL in view entity '%s':\n - %s", s.Name.String(), strings.Join(msgs, "\n - ")) } } @@ -325,7 +326,7 @@ func (e *Executor) execCreateViewEntity(s *ast.CreateViewEntityStmt) error { // Get domain model dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } // Check if entity already exists @@ -339,7 +340,7 @@ func (e *Executor) execCreateViewEntity(s *ast.CreateViewEntityStmt) error { // If entity exists: REPLACE drops and recreates, MODIFY updates in place if existingEntity != nil && !s.CreateOrModify && !s.CreateOrReplace { - return fmt.Errorf("entity already exists: %s.%s (use CREATE OR MODIFY to update, or CREATE OR REPLACE to drop and recreate)", s.Name.Module, s.Name.Name) + return mdlerrors.NewAlreadyExistsMsg("entity", s.Name.Module+"."+s.Name.Name, fmt.Sprintf("entity already exists: %s.%s (use CREATE OR MODIFY to update, or CREATE OR REPLACE to drop and recreate)", s.Name.Module, s.Name.Name)) } // CREATE OR REPLACE: delete existing entity and source doc, then recreate @@ -350,17 +351,17 @@ func (e *Executor) execCreateViewEntity(s *ast.CreateViewEntityStmt) error { } // Delete ViewEntitySourceDocument if err := e.writer.DeleteViewEntitySourceDocumentByName(s.Name.Module, s.Name.Name); err != nil { - return fmt.Errorf("failed to delete existing ViewEntitySourceDocument: %w", err) + return mdlerrors.NewBackend("delete existing ViewEntitySourceDocument", err) } // Delete the entity itself if err := e.writer.DeleteEntity(dm.ID, existingEntity.ID); err != nil { - return fmt.Errorf("failed to delete existing entity for replace: %w", err) + return mdlerrors.NewBackend("delete existing entity for replace", err) } existingEntity = nil // Re-fetch domain model after deletion so entity count is correct for positioning dm, err = e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model after delete: %w", err) + return mdlerrors.NewBackend("get domain model after delete", err) } } @@ -380,7 +381,7 @@ func (e *Executor) execCreateViewEntity(s *ast.CreateViewEntityStmt) error { // This prevents duplicate OQL documents from accumulating (e.g., from re-running // scripts or after a previous DROP that didn't clean up properly). if err := e.writer.DeleteViewEntitySourceDocumentByName(s.Name.Module, s.Name.Name); err != nil { - return fmt.Errorf("failed to delete existing ViewEntitySourceDocument: %w", err) + return mdlerrors.NewBackend("delete existing ViewEntitySourceDocument", err) } _, err = e.writer.CreateViewEntitySourceDocument( module.ID, @@ -390,7 +391,7 @@ func (e *Executor) execCreateViewEntity(s *ast.CreateViewEntityStmt) error { s.Documentation, ) if err != nil { - return fmt.Errorf("failed to create ViewEntitySourceDocument: %w", err) + return mdlerrors.NewBackend("create ViewEntitySourceDocument", err) } // Create view attributes with OqlViewValue references. @@ -439,7 +440,7 @@ func (e *Executor) execCreateViewEntity(s *ast.CreateViewEntityStmt) error { entity.ID = existingEntity.ID entity.SourceObjectID = existingEntity.SourceObjectID if err := e.writer.UpdateEntity(dm.ID, entity); err != nil { - return fmt.Errorf("failed to update view entity: %w", err) + return mdlerrors.NewBackend("update view entity", err) } // Invalidate caches so updated entity is visible e.invalidateHierarchy() @@ -448,7 +449,7 @@ func (e *Executor) execCreateViewEntity(s *ast.CreateViewEntityStmt) error { } else { // Create new entity if err := e.writer.CreateEntity(dm.ID, entity); err != nil { - return fmt.Errorf("failed to create view entity: %w", err) + return mdlerrors.NewBackend("create view entity", err) } // Invalidate caches so new entity is visible e.invalidateHierarchy() @@ -462,7 +463,7 @@ func (e *Executor) execCreateViewEntity(s *ast.CreateViewEntityStmt) error { // execAlterEntity handles ALTER ENTITY statements. func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Find module @@ -474,7 +475,7 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { // Get domain model dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } // Find entity @@ -486,14 +487,14 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { } } if entity == nil { - return fmt.Errorf("entity not found: %s", s.Name) + return mdlerrors.NewNotFoundMsg("entity", fmt.Sprint(s.Name), fmt.Sprintf("entity not found: %s", s.Name)) } switch s.Operation { case ast.AlterEntityAddAttribute: a := s.Attribute if a == nil { - return fmt.Errorf("no attribute definition provided") + return mdlerrors.NewValidation("no attribute definition provided") } // Pseudo-types: set entity flags instead of adding real attributes switch a.Type.Kind { @@ -512,7 +513,7 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { } // CALCULATED attributes are only supported on persistent entities if a.Calculated && !entity.Persistable { - return fmt.Errorf("attribute '%s': CALCULATED attributes are only supported on persistent entities", a.Name) + return mdlerrors.NewValidationf("attribute '%s': CALCULATED attributes are only supported on persistent entities", a.Name) } // Auto-default Boolean attributes to false if no DEFAULT specified if a.Type.Kind == ast.TypeBoolean && !a.HasDefault { @@ -522,7 +523,7 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { // Check for duplicate attribute name for _, existing := range entity.Attributes { if existing.Name == a.Name { - return fmt.Errorf("attribute '%s' already exists on entity %s", a.Name, s.Name) + return mdlerrors.NewAlreadyExistsMsg("attribute", a.Name, fmt.Sprintf("attribute '%s' already exists on entity %s", a.Name, s.Name)) } } @@ -591,7 +592,7 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { } if err := e.writer.UpdateEntity(dm.ID, entity); err != nil { - return fmt.Errorf("failed to add attribute: %w", err) + return mdlerrors.NewBackend("add attribute", err) } e.invalidateHierarchy() e.invalidateDomainModelsCache() @@ -607,10 +608,10 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { } } if !found { - return fmt.Errorf("attribute '%s' not found on entity %s", s.AttributeName, s.Name) + return mdlerrors.NewNotFoundMsg("attribute", s.AttributeName, fmt.Sprintf("attribute '%s' not found on entity %s", s.AttributeName, s.Name)) } if err := e.writer.UpdateEntity(dm.ID, entity); err != nil { - return fmt.Errorf("failed to rename attribute: %w", err) + return mdlerrors.NewBackend("rename attribute", err) } e.invalidateHierarchy() e.invalidateDomainModelsCache() @@ -619,7 +620,7 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { case ast.AlterEntityModifyAttribute: // CALCULATED attributes are only supported on persistent entities if s.Calculated && !entity.Persistable { - return fmt.Errorf("attribute '%s': CALCULATED attributes are only supported on persistent entities", s.AttributeName) + return mdlerrors.NewValidationf("attribute '%s': CALCULATED attributes are only supported on persistent entities", s.AttributeName) } found := false for _, attr := range entity.Attributes { @@ -644,10 +645,10 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { } } if !found { - return fmt.Errorf("attribute '%s' not found on entity %s", s.AttributeName, s.Name) + return mdlerrors.NewNotFoundMsg("attribute", s.AttributeName, fmt.Sprintf("attribute '%s' not found on entity %s", s.AttributeName, s.Name)) } if err := e.writer.UpdateEntity(dm.ID, entity); err != nil { - return fmt.Errorf("failed to modify attribute: %w", err) + return mdlerrors.NewBackend("modify attribute", err) } e.invalidateHierarchy() e.invalidateDomainModelsCache() @@ -686,7 +687,7 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { } } if idx < 0 { - return fmt.Errorf("attribute '%s' not found on entity %s", s.AttributeName, s.Name) + return mdlerrors.NewNotFoundMsg("attribute", s.AttributeName, fmt.Sprintf("attribute '%s' not found on entity %s", s.AttributeName, s.Name)) } // Clean up entity-level references to the dropped attribute droppedID := entity.Attributes[idx].ID @@ -746,7 +747,7 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { // Remove the attribute entity.Attributes = append(entity.Attributes[:idx], entity.Attributes[idx+1:]...) if err := e.writer.UpdateEntity(dm.ID, entity); err != nil { - return fmt.Errorf("failed to drop attribute: %w", err) + return mdlerrors.NewBackend("drop attribute", err) } e.invalidateHierarchy() e.invalidateDomainModelsCache() @@ -771,7 +772,7 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { case ast.AlterEntitySetDocumentation: entity.Documentation = s.Documentation if err := e.writer.UpdateEntity(dm.ID, entity); err != nil { - return fmt.Errorf("failed to set documentation: %w", err) + return mdlerrors.NewBackend("set documentation", err) } e.invalidateDomainModelsCache() fmt.Fprintf(e.output, "Set documentation on entity %s\n", s.Name) @@ -780,25 +781,25 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { // Comments are stored as documentation in the Mendix model entity.Documentation = s.Comment if err := e.writer.UpdateEntity(dm.ID, entity); err != nil { - return fmt.Errorf("failed to set comment: %w", err) + return mdlerrors.NewBackend("set comment", err) } e.invalidateDomainModelsCache() fmt.Fprintf(e.output, "Set comment on entity %s\n", s.Name) case ast.AlterEntitySetPosition: if s.Position == nil { - return fmt.Errorf("no position provided") + return mdlerrors.NewValidation("no position provided") } entity.Location = model.Point{X: s.Position.X, Y: s.Position.Y} if err := e.writer.UpdateEntity(dm.ID, entity); err != nil { - return fmt.Errorf("failed to set position: %w", err) + return mdlerrors.NewBackend("set position", err) } e.invalidateDomainModelsCache() fmt.Fprintf(e.output, "Set position of entity %s to (%d, %d)\n", s.Name, s.Position.X, s.Position.Y) case ast.AlterEntityAddIndex: if s.Index == nil { - return fmt.Errorf("no index definition provided") + return mdlerrors.NewValidation("no index definition provided") } // Build name-to-ID map for attribute lookup attrNameToID := make(map[string]model.ID) @@ -816,7 +817,7 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { ia.ID = model.ID(mpr.GenerateID()) indexAttrs = append(indexAttrs, ia) } else { - return fmt.Errorf("attribute '%s' not found for index on entity %s", col.Name, s.Name) + return mdlerrors.NewNotFoundMsg("attribute", col.Name, fmt.Sprintf("attribute '%s' not found for index on entity %s", col.Name, s.Name)) } } index := &domainmodel.Index{ @@ -825,7 +826,7 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { index.ID = idxID entity.Indexes = append(entity.Indexes, index) if err := e.writer.UpdateEntity(dm.ID, entity); err != nil { - return fmt.Errorf("failed to add index: %w", err) + return mdlerrors.NewBackend("add index", err) } e.invalidateDomainModelsCache() fmt.Fprintf(e.output, "Added index to entity %s\n", s.Name) @@ -833,7 +834,7 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { case ast.AlterEntityDropIndex: // Find and remove the index by position (Mendix indexes don't have user-visible names) if len(entity.Indexes) == 0 { - return fmt.Errorf("no indexes on entity %s", s.Name) + return mdlerrors.NewValidationf("no indexes on entity %s", s.Name) } // For now, drop by ordinal name ("idx1", "idx2", etc.) or drop all idx := -1 @@ -845,18 +846,18 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { } } if idx < 0 { - return fmt.Errorf("index '%s' not found on entity %s", s.IndexName, s.Name) + return mdlerrors.NewNotFoundMsg("index", s.IndexName, fmt.Sprintf("index '%s' not found on entity %s", s.IndexName, s.Name)) } entity.Indexes = append(entity.Indexes[:idx], entity.Indexes[idx+1:]...) if err := e.writer.UpdateEntity(dm.ID, entity); err != nil { - return fmt.Errorf("failed to drop index: %w", err) + return mdlerrors.NewBackend("drop index", err) } e.invalidateDomainModelsCache() fmt.Fprintf(e.output, "Dropped index '%s' from entity %s\n", s.IndexName, s.Name) case ast.AlterEntityAddEventHandler: if s.EventHandler == nil { - return fmt.Errorf("missing event handler definition") + return mdlerrors.NewValidation("missing event handler definition") } ehs, err := e.buildEventHandlers([]ast.EventHandlerDef{*s.EventHandler}) if err != nil { @@ -865,13 +866,14 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { // Reject duplicate (same Moment + Event) for _, existing := range entity.EventHandlers { if existing.Moment == ehs[0].Moment && existing.Event == ehs[0].Event { - return fmt.Errorf("event handler already exists for %s %s on %s", - s.EventHandler.Moment, s.EventHandler.Event, s.Name) + return mdlerrors.NewAlreadyExistsMsg("event handler", + fmt.Sprintf("%s %s", s.EventHandler.Moment, s.EventHandler.Event), + fmt.Sprintf("event handler already exists for %s %s on %s", s.EventHandler.Moment, s.EventHandler.Event, s.Name)) } } entity.EventHandlers = append(entity.EventHandlers, ehs[0]) if err := e.writer.UpdateEntity(dm.ID, entity); err != nil { - return fmt.Errorf("failed to add event handler: %w", err) + return mdlerrors.NewBackend("add event handler", err) } e.invalidateDomainModelsCache() fmt.Fprintf(e.output, "Added event handler %s %s on %s\n", @@ -879,7 +881,7 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { case ast.AlterEntityDropEventHandler: if s.EventHandler == nil { - return fmt.Errorf("missing event handler reference") + return mdlerrors.NewValidation("missing event handler reference") } moment := domainmodel.EventMoment(s.EventHandler.Moment) event := domainmodel.EventType(s.EventHandler.Event) @@ -891,19 +893,20 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { } } if idx < 0 { - return fmt.Errorf("event handler %s %s not found on %s", - s.EventHandler.Moment, s.EventHandler.Event, s.Name) + return mdlerrors.NewNotFoundMsg("event handler", + fmt.Sprintf("%s %s", s.EventHandler.Moment, s.EventHandler.Event), + fmt.Sprintf("event handler %s %s not found on %s", s.EventHandler.Moment, s.EventHandler.Event, s.Name)) } entity.EventHandlers = append(entity.EventHandlers[:idx], entity.EventHandlers[idx+1:]...) if err := e.writer.UpdateEntity(dm.ID, entity); err != nil { - return fmt.Errorf("failed to drop event handler: %w", err) + return mdlerrors.NewBackend("drop event handler", err) } e.invalidateDomainModelsCache() fmt.Fprintf(e.output, "Dropped event handler %s %s from %s\n", s.EventHandler.Moment, s.EventHandler.Event, s.Name) default: - return fmt.Errorf("unsupported ALTER ENTITY operation") + return mdlerrors.NewUnsupported("unsupported ALTER ENTITY operation") } e.trackModifiedDomainModel(module.ID, module.Name) @@ -913,7 +916,7 @@ func (e *Executor) execAlterEntity(s *ast.AlterEntityStmt) error { // execDropEntity handles DROP ENTITY statements. func (e *Executor) execDropEntity(s *ast.DropEntityStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Find module and entity @@ -924,7 +927,7 @@ func (e *Executor) execDropEntity(s *ast.DropEntityStmt) error { dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } for _, entity := range dm.Entities { @@ -935,11 +938,11 @@ func (e *Executor) execDropEntity(s *ast.DropEntityStmt) error { // If this is a view entity, also delete the associated ViewEntitySourceDocument if entity.Source == "DomainModels$OqlViewEntitySource" { if err := e.writer.DeleteViewEntitySourceDocumentByName(s.Name.Module, s.Name.Name); err != nil { - return fmt.Errorf("failed to delete view entity source document: %w", err) + return mdlerrors.NewBackend("delete view entity source document", err) } } if err := e.writer.DeleteEntity(dm.ID, entity.ID); err != nil { - return fmt.Errorf("failed to delete entity: %w", err) + return mdlerrors.NewBackend("delete entity", err) } e.invalidateDomainModelsCache() fmt.Fprintf(e.output, "Dropped entity: %s\n", s.Name) @@ -947,7 +950,7 @@ func (e *Executor) execDropEntity(s *ast.DropEntityStmt) error { } } - return fmt.Errorf("entity not found: %s", s.Name) + return mdlerrors.NewNotFoundMsg("entity", fmt.Sprint(s.Name), fmt.Sprintf("entity not found: %s", s.Name)) } // warnEntityReferences prints a warning if the entity is referenced by other elements. diff --git a/mdl/executor/cmd_entities_describe.go b/mdl/executor/cmd_entities_describe.go index fcaeabee..0999df46 100644 --- a/mdl/executor/cmd_entities_describe.go +++ b/mdl/executor/cmd_entities_describe.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" ) @@ -18,7 +19,7 @@ func (e *Executor) showEntities(moduleName string) error { // Build module ID -> name map (single query) modules, err := e.reader.ListModules() if err != nil { - return fmt.Errorf("failed to list modules: %w", err) + return mdlerrors.NewBackend("list modules", err) } moduleNames := make(map[model.ID]string) for _, m := range modules { @@ -28,7 +29,7 @@ func (e *Executor) showEntities(moduleName string) error { // Get all domain models in a single query (avoids O(n²) behavior) domainModels, err := e.reader.ListDomainModels() if err != nil { - return fmt.Errorf("failed to list domain models: %w", err) + return mdlerrors.NewBackend("list domain models", err) } // Build entity ID -> association count map @@ -158,7 +159,7 @@ func (e *Executor) showEntities(moduleName string) error { // showEntity handles SHOW ENTITY command. func (e *Executor) showEntity(name *ast.QualifiedName) error { if name == nil { - return fmt.Errorf("entity name required") + return mdlerrors.NewValidation("entity name required") } module, err := e.findModule(name.Module) @@ -168,7 +169,7 @@ func (e *Executor) showEntity(name *ast.QualifiedName) error { dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } for _, entity := range dm.Entities { @@ -209,7 +210,7 @@ func (e *Executor) showEntity(name *ast.QualifiedName) error { } } - return fmt.Errorf("entity not found: %s", name) + return mdlerrors.NewNotFound("entity", name.String()) } // describeEntity handles DESCRIBE ENTITY command. @@ -221,7 +222,7 @@ func (e *Executor) describeEntity(name ast.QualifiedName) error { dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } for _, entity := range dm.Entities { @@ -431,7 +432,7 @@ func (e *Executor) describeEntity(name ast.QualifiedName) error { } } - return fmt.Errorf("entity not found: %s", name) + return mdlerrors.NewNotFound("entity", name.String()) } // describeEntityToString generates MDL source for an entity and returns it as a string. @@ -463,7 +464,7 @@ func extractAttrNameFromQualified(qualifiedName string) string { func (e *Executor) resolveMicroflowByName(qualifiedName string) (model.ID, error) { parts := strings.Split(qualifiedName, ".") if len(parts) < 2 { - return "", fmt.Errorf("invalid microflow name: %s (expected Module.Name)", qualifiedName) + return "", mdlerrors.NewValidationf("invalid microflow name: %s (expected Module.Name)", qualifiedName) } moduleName := parts[0] mfName := strings.Join(parts[1:], ".") @@ -478,12 +479,12 @@ func (e *Executor) resolveMicroflowByName(qualifiedName string) (model.ID, error // Search existing microflows allMicroflows, err := e.reader.ListMicroflows() if err != nil { - return "", fmt.Errorf("failed to list microflows: %w", err) + return "", mdlerrors.NewBackend("list microflows", err) } h, err := e.getHierarchy() if err != nil { - return "", fmt.Errorf("failed to build hierarchy: %w", err) + return "", mdlerrors.NewBackend("build hierarchy", err) } for _, mf := range allMicroflows { @@ -494,7 +495,7 @@ func (e *Executor) resolveMicroflowByName(qualifiedName string) (model.ID, error } } - return "", fmt.Errorf("microflow not found: %s", qualifiedName) + return "", mdlerrors.NewNotFound("microflow", qualifiedName) } // lookupMicroflowName reverse-looks up a microflow ID to its qualified name. diff --git a/mdl/executor/cmd_enumerations.go b/mdl/executor/cmd_enumerations.go index 4262ede1..c2b88b6d 100644 --- a/mdl/executor/cmd_enumerations.go +++ b/mdl/executor/cmd_enumerations.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/mdl/linter" "github.com/mendixlabs/mxcli/model" ) @@ -16,7 +17,7 @@ import ( // execCreateEnumeration handles CREATE ENUMERATION statements. func (e *Executor) execCreateEnumeration(s *ast.CreateEnumerationStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Validate enumeration values for reserved words @@ -25,7 +26,7 @@ func (e *Executor) execCreateEnumeration(s *ast.CreateEnumerationStmt) error { for _, v := range violations { msgs = append(msgs, v.Message) } - return fmt.Errorf("invalid enumeration '%s':\n - %s", + return mdlerrors.NewValidationf("invalid enumeration '%s':\n - %s", s.Name.String(), strings.Join(msgs, "\n - ")) } @@ -38,7 +39,7 @@ func (e *Executor) execCreateEnumeration(s *ast.CreateEnumerationStmt) error { // Check if enumeration already exists existingEnum := e.findEnumeration(s.Name.Module, s.Name.Name) if existingEnum != nil && !s.CreateOrModify { - return fmt.Errorf("enumeration already exists: %s.%s (use CREATE OR MODIFY to update)", s.Name.Module, s.Name.Name) + return mdlerrors.NewAlreadyExistsMsg("enumeration", s.Name.Module+"."+s.Name.Name, "use CREATE OR MODIFY to update") } // Create enumeration values @@ -55,7 +56,7 @@ func (e *Executor) execCreateEnumeration(s *ast.CreateEnumerationStmt) error { // If enumeration exists and CREATE OR MODIFY, delete it first if existingEnum != nil && s.CreateOrModify { if err := e.writer.DeleteEnumeration(existingEnum.ID); err != nil { - return fmt.Errorf("failed to delete existing enumeration: %w", err) + return mdlerrors.NewBackend("delete existing enumeration", err) } } @@ -68,7 +69,7 @@ func (e *Executor) execCreateEnumeration(s *ast.CreateEnumerationStmt) error { } if err := e.writer.CreateEnumeration(enum); err != nil { - return fmt.Errorf("failed to create enumeration: %w", err) + return mdlerrors.NewBackend("create enumeration", err) } // Invalidate hierarchy cache so the new enumeration's container is visible @@ -103,19 +104,19 @@ func (e *Executor) findEnumeration(moduleName, enumName string) *model.Enumerati // execAlterEnumeration handles ALTER ENUMERATION statements. func (e *Executor) execAlterEnumeration(s *ast.AlterEnumerationStmt) error { // TODO: Implement ALTER ENUMERATION - return fmt.Errorf("ALTER ENUMERATION not yet implemented") + return mdlerrors.NewUnsupported("ALTER ENUMERATION not yet implemented") } // execDropEnumeration handles DROP ENUMERATION statements. func (e *Executor) execDropEnumeration(s *ast.DropEnumerationStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Find enumeration enums, err := e.reader.ListEnumerations() if err != nil { - return fmt.Errorf("failed to list enumerations: %w", err) + return mdlerrors.NewBackend("list enumerations", err) } for _, enum := range enums { @@ -124,7 +125,7 @@ func (e *Executor) execDropEnumeration(s *ast.DropEnumerationStmt) error { module, err := e.findModuleByID(enum.ContainerID) if err == nil && (s.Name.Module == "" || module.Name == s.Name.Module) { if err := e.writer.DeleteEnumeration(enum.ID); err != nil { - return fmt.Errorf("failed to delete enumeration: %w", err) + return mdlerrors.NewBackend("delete enumeration", err) } fmt.Fprintf(e.output, "Dropped enumeration: %s\n", s.Name) return nil @@ -132,20 +133,20 @@ func (e *Executor) execDropEnumeration(s *ast.DropEnumerationStmt) error { } } - return fmt.Errorf("enumeration not found: %s", s.Name) + return mdlerrors.NewNotFound("enumeration", s.Name.String()) } // showEnumerations handles SHOW ENUMERATIONS command. func (e *Executor) showEnumerations(moduleName string) error { enums, err := e.reader.ListEnumerations() if err != nil { - return fmt.Errorf("failed to list enumerations: %w", err) + return mdlerrors.NewBackend("list enumerations", err) } // Get hierarchy for module/folder resolution h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Collect rows @@ -189,12 +190,12 @@ func (e *Executor) showEnumerations(moduleName string) error { func (e *Executor) describeEnumeration(name ast.QualifiedName) error { enums, err := e.reader.ListEnumerations() if err != nil { - return fmt.Errorf("failed to list enumerations: %w", err) + return mdlerrors.NewBackend("list enumerations", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, enum := range enums { @@ -224,7 +225,7 @@ func (e *Executor) describeEnumeration(name ast.QualifiedName) error { } } - return fmt.Errorf("enumeration not found: %s", name) + return mdlerrors.NewNotFound("enumeration", name.String()) } // mendixReservedWords contains words that cannot be used as enumeration value names. diff --git a/mdl/executor/cmd_export_mappings.go b/mdl/executor/cmd_export_mappings.go index 98b2fd2d..2886368d 100644 --- a/mdl/executor/cmd_export_mappings.go +++ b/mdl/executor/cmd_export_mappings.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/mpr" ) @@ -15,12 +16,12 @@ import ( // showExportMappings prints a table of all export mapping documents. func (e *Executor) showExportMappings(inModule string) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } all, err := e.reader.ListExportMappings() if err != nil { - return fmt.Errorf("failed to list export mappings: %w", err) + return mdlerrors.NewBackend("list export mappings", err) } h, err := e.getHierarchy() @@ -78,12 +79,12 @@ func (e *Executor) showExportMappings(inModule string) error { // describeExportMapping prints the MDL representation of an export mapping. func (e *Executor) describeExportMapping(name ast.QualifiedName) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } em, err := e.reader.GetExportMappingByQualifiedName(name.Module, name.Name) if err != nil { - return fmt.Errorf("export mapping %s not found", name) + return mdlerrors.NewNotFoundMsg("export mapping", name.String(), err.Error()) } if em.Documentation != "" { @@ -173,12 +174,12 @@ func printExportMappingElement(e *Executor, elem *model.ExportMappingElement, de // execCreateExportMapping creates a new export mapping. func (e *Executor) execCreateExportMapping(s *ast.CreateExportMappingStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } module, err := e.findModule(s.Name.Module) if err != nil { - return fmt.Errorf("module %s not found", s.Name.Module) + return mdlerrors.NewNotFound("module", s.Name.Module) } containerID := module.ID @@ -215,7 +216,7 @@ func (e *Executor) execCreateExportMapping(s *ast.CreateExportMappingStmt) error } if err := e.writer.CreateExportMapping(em); err != nil { - return fmt.Errorf("failed to create export mapping: %w", err) + return mdlerrors.NewBackend("create export mapping", err) } if !e.quiet { @@ -357,16 +358,16 @@ func buildExportMappingElementModel(moduleName string, def *ast.ExportMappingEle // execDropExportMapping deletes an export mapping. func (e *Executor) execDropExportMapping(s *ast.DropExportMappingStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } em, err := e.reader.GetExportMappingByQualifiedName(s.Name.Module, s.Name.Name) if err != nil { - return fmt.Errorf("export mapping %s not found", s.Name) + return mdlerrors.NewNotFoundMsg("export mapping", s.Name.String(), err.Error()) } if err := e.writer.DeleteExportMapping(em.ID); err != nil { - return fmt.Errorf("failed to drop export mapping: %w", err) + return mdlerrors.NewBackend("drop export mapping", err) } if !e.quiet { diff --git a/mdl/executor/cmd_features.go b/mdl/executor/cmd_features.go index eff0175d..e7004c52 100644 --- a/mdl/executor/cmd_features.go +++ b/mdl/executor/cmd_features.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/sdk/versions" ) @@ -41,7 +42,7 @@ func (e *Executor) checkFeature(area, name, statement, hint string) error { if hint != "" { msg += "\n hint: " + hint } - return fmt.Errorf("%s", msg) + return mdlerrors.NewUnsupported(msg) } // execShowFeatures handles SHOW FEATURES, SHOW FEATURES FOR VERSION, and @@ -49,7 +50,7 @@ func (e *Executor) checkFeature(area, name, statement, hint string) error { func (e *Executor) execShowFeatures(s *ast.ShowFeaturesStmt) error { reg, err := versions.Load() if err != nil { - return fmt.Errorf("failed to load version registry: %w", err) + return mdlerrors.NewBackend("load version registry", err) } // Determine the project version to use. @@ -60,7 +61,7 @@ func (e *Executor) execShowFeatures(s *ast.ShowFeaturesStmt) error { // SHOW FEATURES ADDED SINCE x.y sinceV, err := versions.ParseSemVer(s.AddedSince) if err != nil { - return fmt.Errorf("invalid version %q: %w", s.AddedSince, err) + return mdlerrors.NewValidationf("invalid version %q: %v", s.AddedSince, err) } return e.showFeaturesAddedSince(reg, sinceV) @@ -68,13 +69,13 @@ func (e *Executor) execShowFeatures(s *ast.ShowFeaturesStmt) error { // SHOW FEATURES FOR VERSION x.y — no project connection needed pv, err = versions.ParseSemVer(s.ForVersion) if err != nil { - return fmt.Errorf("invalid version %q: %w", s.ForVersion, err) + return mdlerrors.NewValidationf("invalid version %q: %v", s.ForVersion, err) } default: // SHOW FEATURES [IN area] — requires project connection if e.reader == nil { - return fmt.Errorf("not connected to a project\n hint: use SHOW FEATURES FOR VERSION x.y without a project connection") + return mdlerrors.NewNotConnected() } rpv := e.reader.ProjectVersion() pv = versions.SemVer{Major: rpv.MajorVersion, Minor: rpv.MinorVersion, Patch: rpv.PatchVersion} diff --git a/mdl/executor/cmd_folders.go b/mdl/executor/cmd_folders.go index 12e7a432..0b7c04b0 100644 --- a/mdl/executor/cmd_folders.go +++ b/mdl/executor/cmd_folders.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/mpr" ) @@ -36,12 +37,12 @@ func (e *Executor) findFolderByPath(moduleID model.ID, folderPath string, folder } if !found { - return "", fmt.Errorf("folder not found: '%s'", folderPath) + return "", mdlerrors.NewNotFound("folder", folderPath) } } if targetFolderID == "" { - return "", fmt.Errorf("folder not found: '%s'", folderPath) + return "", mdlerrors.NewNotFound("folder", folderPath) } return targetFolderID, nil @@ -51,17 +52,17 @@ func (e *Executor) findFolderByPath(moduleID model.ID, folderPath string, folder // The folder must be empty (no child documents or sub-folders). func (e *Executor) execDropFolder(s *ast.DropFolderStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } module, err := e.findModule(s.Module) if err != nil { - return fmt.Errorf("module not found: %s", s.Module) + return mdlerrors.NewNotFound("module", s.Module) } folders, err := e.reader.ListFolders() if err != nil { - return fmt.Errorf("failed to list folders: %w", err) + return mdlerrors.NewBackend("list folders", err) } folderID, err := e.findFolderByPath(module.ID, s.FolderPath, folders) @@ -70,7 +71,7 @@ func (e *Executor) execDropFolder(s *ast.DropFolderStmt) error { } if err := e.writer.DeleteFolder(folderID); err != nil { - return fmt.Errorf("failed to delete folder '%s': %w", s.FolderPath, err) + return mdlerrors.NewBackend(fmt.Sprintf("delete folder '%s'", s.FolderPath), err) } e.invalidateHierarchy() @@ -81,19 +82,19 @@ func (e *Executor) execDropFolder(s *ast.DropFolderStmt) error { // execMoveFolder handles MOVE FOLDER Module.FolderName TO ... statements. func (e *Executor) execMoveFolder(s *ast.MoveFolderStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Find the source module sourceModule, err := e.findModule(s.Name.Module) if err != nil { - return fmt.Errorf("source module not found: %s", s.Name.Module) + return mdlerrors.NewNotFound("source module", s.Name.Module) } // Find the source folder folders, err := e.reader.ListFolders() if err != nil { - return fmt.Errorf("failed to list folders: %w", err) + return mdlerrors.NewBackend("list folders", err) } folderID, err := e.findFolderByPath(sourceModule.ID, s.Name.Name, folders) @@ -106,7 +107,7 @@ func (e *Executor) execMoveFolder(s *ast.MoveFolderStmt) error { if s.TargetModule != "" { targetModule, err = e.findModule(s.TargetModule) if err != nil { - return fmt.Errorf("target module not found: %s", s.TargetModule) + return mdlerrors.NewNotFound("target module", s.TargetModule) } } else { targetModule = sourceModule @@ -117,7 +118,7 @@ func (e *Executor) execMoveFolder(s *ast.MoveFolderStmt) error { if s.TargetFolder != "" { targetContainerID, err = e.resolveFolder(targetModule.ID, s.TargetFolder) if err != nil { - return fmt.Errorf("failed to resolve target folder: %w", err) + return mdlerrors.NewBackend("resolve target folder", err) } } else { targetContainerID = targetModule.ID @@ -125,7 +126,7 @@ func (e *Executor) execMoveFolder(s *ast.MoveFolderStmt) error { // Move the folder if err := e.writer.MoveFolder(folderID, targetContainerID); err != nil { - return fmt.Errorf("failed to move folder: %w", err) + return mdlerrors.NewBackend("move folder", err) } e.invalidateHierarchy() diff --git a/mdl/executor/cmd_fragments.go b/mdl/executor/cmd_fragments.go index 46181a58..62441387 100644 --- a/mdl/executor/cmd_fragments.go +++ b/mdl/executor/cmd_fragments.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/sdk/pages" ) @@ -18,7 +19,7 @@ func (e *Executor) execDefineFragment(s *ast.DefineFragmentStmt) error { e.fragments = make(map[string]*ast.DefineFragmentStmt) } if _, exists := e.fragments[s.Name]; exists { - return fmt.Errorf("fragment %q already defined", s.Name) + return mdlerrors.NewAlreadyExists("fragment", s.Name) } e.fragments[s.Name] = s fmt.Fprintf(e.output, "Defined fragment %s (%d widgets)\n", s.Name, len(s.Widgets)) @@ -51,11 +52,11 @@ func (e *Executor) showFragments() error { // describeFragment outputs a fragment's definition as MDL. func (e *Executor) describeFragment(name ast.QualifiedName) error { if e.fragments == nil { - return fmt.Errorf("fragment %q not found", name.Name) + return mdlerrors.NewNotFound("fragment", name.Name) } frag, ok := e.fragments[name.Name] if !ok { - return fmt.Errorf("fragment %q not found", name.Name) + return mdlerrors.NewNotFound("fragment", name.Name) } fmt.Fprintf(e.output, "DEFINE FRAGMENT %s AS {\n", frag.Name) @@ -70,12 +71,12 @@ func (e *Executor) describeFragment(name ast.QualifiedName) error { // It finds a named widget in a page or snippet and outputs it as MDL. func (e *Executor) describeFragmentFrom(s *ast.DescribeFragmentFromStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } var rawWidgets []rawWidget @@ -84,7 +85,7 @@ func (e *Executor) describeFragmentFrom(s *ast.DescribeFragmentFromStmt) error { case "PAGE": allPages, err := e.reader.ListPages() if err != nil { - return fmt.Errorf("failed to list pages: %w", err) + return mdlerrors.NewBackend("list pages", err) } var foundPage *pages.Page for _, p := range allPages { @@ -96,14 +97,14 @@ func (e *Executor) describeFragmentFrom(s *ast.DescribeFragmentFromStmt) error { } } if foundPage == nil { - return fmt.Errorf("page %s not found", s.ContainerName.String()) + return mdlerrors.NewNotFound("page", s.ContainerName.String()) } rawWidgets = e.getPageWidgetsFromRaw(foundPage.ID) case "SNIPPET": allSnippets, err := e.reader.ListSnippets() if err != nil { - return fmt.Errorf("failed to list snippets: %w", err) + return mdlerrors.NewBackend("list snippets", err) } var foundSnippet *pages.Snippet for _, sn := range allSnippets { @@ -115,7 +116,7 @@ func (e *Executor) describeFragmentFrom(s *ast.DescribeFragmentFromStmt) error { } } if foundSnippet == nil { - return fmt.Errorf("snippet %s not found", s.ContainerName.String()) + return mdlerrors.NewNotFound("snippet", s.ContainerName.String()) } rawWidgets = e.getSnippetWidgetsFromRaw(foundSnippet.ID) } @@ -123,7 +124,7 @@ func (e *Executor) describeFragmentFrom(s *ast.DescribeFragmentFromStmt) error { // Find the widget by name target := findRawWidgetByName(rawWidgets, s.WidgetName) if target == nil { - return fmt.Errorf("widget %q not found in %s %s", s.WidgetName, strings.ToLower(s.ContainerType), s.ContainerName.String()) + return mdlerrors.NewNotFoundMsg("widget", s.WidgetName, fmt.Sprintf("not found in %s %s", strings.ToLower(s.ContainerType), s.ContainerName.String())) } // Output as MDL diff --git a/mdl/executor/cmd_imagecollections.go b/mdl/executor/cmd_imagecollections.go index af1bc70a..126ad7c6 100644 --- a/mdl/executor/cmd_imagecollections.go +++ b/mdl/executor/cmd_imagecollections.go @@ -10,13 +10,14 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/sdk/mpr" ) // execCreateImageCollection handles CREATE IMAGE COLLECTION statements. func (e *Executor) execCreateImageCollection(s *ast.CreateImageCollectionStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Find or auto-create module @@ -28,7 +29,7 @@ func (e *Executor) execCreateImageCollection(s *ast.CreateImageCollectionStmt) e // Check if image collection already exists existing := e.findImageCollection(s.Name.Module, s.Name.Name) if existing != nil { - return fmt.Errorf("image collection already exists: %s.%s", s.Name.Module, s.Name.Name) + return mdlerrors.NewAlreadyExists("image collection", s.Name.Module+"."+s.Name.Name) } // Build ImageCollection @@ -45,13 +46,13 @@ func (e *Executor) execCreateImageCollection(s *ast.CreateImageCollectionStmt) e if !filepath.IsAbs(filePath) { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("failed to get working directory: %w", err) + return mdlerrors.NewBackend("get working directory", err) } filePath = filepath.Join(cwd, filePath) } data, err := os.ReadFile(filePath) if err != nil { - return fmt.Errorf("failed to read image file %q: %w", item.FilePath, err) + return mdlerrors.NewBackend(fmt.Sprintf("read image file %q", item.FilePath), err) } format := extToImageFormat(filepath.Ext(filePath)) ic.Images = append(ic.Images, mpr.Image{ @@ -62,7 +63,7 @@ func (e *Executor) execCreateImageCollection(s *ast.CreateImageCollectionStmt) e } if err := e.writer.CreateImageCollection(ic); err != nil { - return fmt.Errorf("failed to create image collection: %w", err) + return mdlerrors.NewBackend("create image collection", err) } // Invalidate hierarchy cache so the new collection's container is visible @@ -75,16 +76,16 @@ func (e *Executor) execCreateImageCollection(s *ast.CreateImageCollectionStmt) e // execDropImageCollection handles DROP IMAGE COLLECTION statements. func (e *Executor) execDropImageCollection(s *ast.DropImageCollectionStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } ic := e.findImageCollection(s.Name.Module, s.Name.Name) if ic == nil { - return fmt.Errorf("image collection not found: %s", s.Name) + return mdlerrors.NewNotFound("image collection", s.Name.String()) } if err := e.writer.DeleteImageCollection(string(ic.ID)); err != nil { - return fmt.Errorf("failed to delete image collection: %w", err) + return mdlerrors.NewBackend("delete image collection", err) } fmt.Fprintf(e.output, "Dropped image collection: %s\n", s.Name) @@ -95,7 +96,7 @@ func (e *Executor) execDropImageCollection(s *ast.DropImageCollectionStmt) error func (e *Executor) describeImageCollection(name ast.QualifiedName) error { ic := e.findImageCollection(name.Module, name.Name) if ic == nil { - return fmt.Errorf("image collection not found: %s", name) + return mdlerrors.NewNotFound("image collection", name.String()) } h, err := e.getHierarchy() @@ -129,7 +130,7 @@ func (e *Executor) describeImageCollection(name ast.QualifiedName) error { // Write image data to temp files and output CREATE statement with IMAGE lines previewDir := filepath.Join("/tmp/mxcli-preview", qualifiedName) if err := os.MkdirAll(previewDir, 0o755); err != nil { - return fmt.Errorf("failed to create preview directory: %w", err) + return mdlerrors.NewBackend("create preview directory", err) } fmt.Fprintf(e.output, "CREATE OR REPLACE IMAGE COLLECTION %s", qualifiedName) @@ -143,7 +144,7 @@ func (e *Executor) describeImageCollection(name ast.QualifiedName) error { filePath := filepath.Join(previewDir, img.Name+ext) if len(img.Data) > 0 { if err := os.WriteFile(filePath, img.Data, 0o644); err != nil { - return fmt.Errorf("failed to write image %s: %w", img.Name, err) + return mdlerrors.NewBackend(fmt.Sprintf("write image %s", img.Name), err) } } @@ -199,7 +200,7 @@ func extToImageFormat(ext string) string { func (e *Executor) showImageCollections(moduleName string) error { collections, err := e.reader.ListImageCollections() if err != nil { - return fmt.Errorf("failed to list image collections: %w", err) + return mdlerrors.NewBackend("list image collections", err) } h, err := e.getHierarchy() diff --git a/mdl/executor/cmd_import.go b/mdl/executor/cmd_import.go index 750c06d7..d856cb5d 100644 --- a/mdl/executor/cmd_import.go +++ b/mdl/executor/cmd_import.go @@ -9,6 +9,7 @@ import ( "time" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/sdk/domainmodel" sqllib "github.com/mendixlabs/mxcli/sql" ) @@ -16,7 +17,7 @@ import ( // execImport handles IMPORT FROM QUERY '' INTO Module.Entity MAP (...) [LINK (...)] [BATCH n] [LIMIT n] func (e *Executor) execImport(s *ast.ImportStmt) error { if e.reader == nil { - return fmt.Errorf("no project connected (use CONNECT LOCAL '' first)") + return mdlerrors.NewNotConnected() } // Validate entity exists @@ -73,7 +74,7 @@ func (e *Executor) execImport(s *ast.ImportStmt) error { fmt.Fprintf(e.output, " batch %d: %d rows imported\n", batch, rows) }) if err != nil { - return fmt.Errorf("import failed: %w", err) + return mdlerrors.NewBackend("import", err) } elapsed := time.Since(start) @@ -107,19 +108,19 @@ func (e *Executor) resolveImportLinks(ctx context.Context, mendixConn *sqllib.Co // Parse target entity module targetParts := strings.SplitN(s.TargetEntity, ".", 2) if len(targetParts) != 2 { - return nil, fmt.Errorf("invalid target entity %q", s.TargetEntity) + return nil, mdlerrors.NewValidationf("invalid target entity %q", s.TargetEntity) } targetModule := targetParts[0] // Load domain models to find associations dms, err := e.reader.ListDomainModels() if err != nil { - return nil, fmt.Errorf("failed to list domain models: %w", err) + return nil, mdlerrors.NewBackend("list domain models", err) } h, err := e.getHierarchy() if err != nil { - return nil, fmt.Errorf("failed to get hierarchy: %w", err) + return nil, mdlerrors.NewBackend("get hierarchy", err) } // Build entity ID → qualified name map @@ -196,7 +197,7 @@ func (e *Executor) resolveOneLink( } if foundAssoc == nil && foundCross == nil { - return nil, fmt.Errorf("association %q not found in module %q", link.AssociationName, targetModule) + return nil, mdlerrors.NewNotFoundMsg("association", link.AssociationName, fmt.Sprintf("not found in module %q", targetModule)) } // Extract association info @@ -222,11 +223,11 @@ func (e *Executor) resolveOneLink( // Reject ReferenceSet associations (not supported in MVP) if assocType == string(domainmodel.AssociationTypeReferenceSet) { - return nil, fmt.Errorf("association %q is ReferenceSet — not supported in IMPORT LINK (use manual SQL)", assocQualName) + return nil, mdlerrors.NewUnsupported(fmt.Sprintf("association %q is ReferenceSet — not supported in IMPORT LINK (use manual SQL)", assocQualName)) } if childEntity == "" { - return nil, fmt.Errorf("could not resolve child entity for association %q", assocQualName) + return nil, mdlerrors.NewValidationf("could not resolve child entity for association %q", assocQualName) } info := &sqllib.AssocInfo{ @@ -277,12 +278,13 @@ func (e *Executor) resolveOneLink( if link.LookupAttr != "" { childTable, err := sqllib.EntityToTableName(childEntity) if err != nil { + // Kept as fmt.Errorf: wraps a cause with entity-specific context, not a standard "failed to" pattern. return nil, fmt.Errorf("invalid child entity %q: %w", childEntity, err) } lookupCol := sqllib.AttributeToColumnName(link.LookupAttr) cache, err := sqllib.BuildLookupCache(ctx, mendixConn, childTable, lookupCol) if err != nil { - return nil, fmt.Errorf("failed to build lookup cache for %s: %w", assocQualName, err) + return nil, mdlerrors.NewBackend(fmt.Sprintf("build lookup cache for %s", assocQualName), err) } info.LookupCache = cache if len(cache) == 0 { @@ -323,11 +325,11 @@ func (e *Executor) ensureMendixDBConnection() (*sqllib.Connection, error) { // Read project settings to get DB configuration ps, err := e.reader.GetProjectSettings() if err != nil { - return nil, fmt.Errorf("failed to read project settings: %w", err) + return nil, mdlerrors.NewBackend("read project settings", err) } if ps.Configuration == nil || len(ps.Configuration.Configurations) == 0 { - return nil, fmt.Errorf("no server configurations found in project settings") + return nil, mdlerrors.NewValidation("no server configurations found in project settings") } // Use the first configuration (typically "default") @@ -340,7 +342,7 @@ func (e *Executor) ensureMendixDBConnection() (*sqllib.Connection, error) { } if err := mgr.Connect(sqllib.DriverPostgres, dsn, sqllib.MendixDBAlias); err != nil { - return nil, fmt.Errorf("failed to connect to Mendix app database: %w", err) + return nil, mdlerrors.NewBackend("connect to Mendix app database", err) } fmt.Fprintf(e.output, "Auto-connected to Mendix app database as '%s'\n", sqllib.MendixDBAlias) diff --git a/mdl/executor/cmd_import_mappings.go b/mdl/executor/cmd_import_mappings.go index acabe487..021669fa 100644 --- a/mdl/executor/cmd_import_mappings.go +++ b/mdl/executor/cmd_import_mappings.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/mpr" ) @@ -15,12 +16,12 @@ import ( // showImportMappings prints a table of all import mapping documents. func (e *Executor) showImportMappings(inModule string) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } all, err := e.reader.ListImportMappings() if err != nil { - return fmt.Errorf("failed to list import mappings: %w", err) + return mdlerrors.NewBackend("list import mappings", err) } h, err := e.getHierarchy() @@ -78,12 +79,12 @@ func (e *Executor) showImportMappings(inModule string) error { // describeImportMapping prints the MDL representation of an import mapping. func (e *Executor) describeImportMapping(name ast.QualifiedName) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } im, err := e.reader.GetImportMappingByQualifiedName(name.Module, name.Name) if err != nil { - return fmt.Errorf("import mapping %s not found", name) + return mdlerrors.NewNotFoundMsg("import mapping", name.String(), err.Error()) } if im.Documentation != "" { @@ -186,12 +187,12 @@ func printImportMappingElement(e *Executor, elem *model.ImportMappingElement, de // execCreateImportMapping creates a new import mapping. func (e *Executor) execCreateImportMapping(s *ast.CreateImportMappingStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } module, err := e.findModule(s.Name.Module) if err != nil { - return fmt.Errorf("module %s not found", s.Name.Module) + return mdlerrors.NewNotFound("module", s.Name.Module) } containerID := module.ID @@ -224,7 +225,7 @@ func (e *Executor) execCreateImportMapping(s *ast.CreateImportMappingStmt) error } if err := e.writer.CreateImportMapping(im); err != nil { - return fmt.Errorf("failed to create import mapping: %w", err) + return mdlerrors.NewBackend("create import mapping", err) } if !e.quiet { @@ -371,16 +372,16 @@ func resolveAttributeType(entityQN, attrName string, reader *mpr.Reader) string // execDropImportMapping deletes an import mapping. func (e *Executor) execDropImportMapping(s *ast.DropImportMappingStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } im, err := e.reader.GetImportMappingByQualifiedName(s.Name.Module, s.Name.Name) if err != nil { - return fmt.Errorf("import mapping %s not found", s.Name) + return mdlerrors.NewNotFoundMsg("import mapping", s.Name.String(), err.Error()) } if err := e.writer.DeleteImportMapping(im.ID); err != nil { - return fmt.Errorf("failed to drop import mapping: %w", err) + return mdlerrors.NewBackend("drop import mapping", err) } if !e.quiet { diff --git a/mdl/executor/cmd_javaactions.go b/mdl/executor/cmd_javaactions.go index 56157e1a..1cfadf18 100644 --- a/mdl/executor/cmd_javaactions.go +++ b/mdl/executor/cmd_javaactions.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/javaactions" "github.com/mendixlabs/mxcli/sdk/mpr" @@ -21,13 +22,13 @@ func (e *Executor) showJavaActions(moduleName string) error { // Get hierarchy for module/folder resolution h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Get all Java actions javaActions, err := e.reader.ListJavaActions() if err != nil { - return fmt.Errorf("failed to list java actions: %w", err) + return mdlerrors.NewBackend("list java actions", err) } // Collect rows @@ -69,7 +70,7 @@ func (e *Executor) describeJavaAction(name ast.QualifiedName) error { qualifiedName := name.Module + "." + name.Name ja, err := e.reader.ReadJavaActionByName(qualifiedName) if err != nil { - return fmt.Errorf("java action not found: %s", qualifiedName) + return mdlerrors.NewNotFound("java action", qualifiedName) } // Generate MDL-style output for CREATE JAVA ACTION format @@ -247,19 +248,19 @@ func formatJavaActionReturnType(t javaactions.CodeActionReturnType) string { // execDropJavaAction handles DROP JAVA ACTION statements. func (e *Executor) execDropJavaAction(s *ast.DropJavaActionStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Get hierarchy for module/folder resolution h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Find and delete the Java action jas, err := e.reader.ListJavaActions() if err != nil { - return fmt.Errorf("failed to list java actions: %w", err) + return mdlerrors.NewBackend("list java actions", err) } for _, ja := range jas { @@ -267,32 +268,32 @@ func (e *Executor) execDropJavaAction(s *ast.DropJavaActionStmt) error { modName := h.GetModuleName(modID) if modName == s.Name.Module && ja.Name == s.Name.Name { if err := e.writer.DeleteJavaAction(ja.ID); err != nil { - return fmt.Errorf("failed to delete java action: %w", err) + return mdlerrors.NewBackend("delete java action", err) } fmt.Fprintf(e.output, "Dropped java action: %s.%s\n", s.Name.Module, s.Name.Name) return nil } } - return fmt.Errorf("java action not found: %s.%s", s.Name.Module, s.Name.Name) + return mdlerrors.NewNotFound("java action", s.Name.Module+"."+s.Name.Name) } // execCreateJavaAction handles CREATE JAVA ACTION statements. func (e *Executor) execCreateJavaAction(s *ast.CreateJavaActionStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Get hierarchy for module/folder resolution h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Find the module modules, err := e.reader.ListModules() if err != nil { - return fmt.Errorf("failed to get modules: %w", err) + return mdlerrors.NewBackend("get modules", err) } var containerID model.ID @@ -305,19 +306,19 @@ func (e *Executor) execCreateJavaAction(s *ast.CreateJavaActionStmt) error { } } if containerID == "" { - return fmt.Errorf("module not found: %s", s.Name.Module) + return mdlerrors.NewNotFound("module", s.Name.Module) } // Check if Java action already exists jas, err := e.reader.ListJavaActions() if err != nil { - return fmt.Errorf("failed to list java actions: %w", err) + return mdlerrors.NewBackend("list java actions", err) } for _, existing := range jas { existingModID := h.FindModuleID(existing.ContainerID) existingModName := h.GetModuleName(existingModID) if existingModName == s.Name.Module && existing.Name == s.Name.Name { - return fmt.Errorf("java action already exists: %s.%s", s.Name.Module, s.Name.Name) + return mdlerrors.NewAlreadyExists("java action", s.Name.Module+"."+s.Name.Name) } } @@ -410,13 +411,13 @@ func (e *Executor) execCreateJavaAction(s *ast.CreateJavaActionStmt) error { // Create in MPR if err := e.writer.CreateJavaAction(ja); err != nil { - return fmt.Errorf("failed to create java action: %w", err) + return mdlerrors.NewBackend("create java action", err) } // Write Java source file if code is provided if s.JavaCode != "" { if err := e.writer.WriteJavaSourceFile(moduleName, s.Name.Name, s.JavaCode, ja.Parameters, ja.ReturnType); err != nil { - return fmt.Errorf("failed to write java source file: %w", err) + return mdlerrors.NewBackend("write java source file", err) } } diff --git a/mdl/executor/cmd_javascript_actions.go b/mdl/executor/cmd_javascript_actions.go index 649026a3..b813437a 100644 --- a/mdl/executor/cmd_javascript_actions.go +++ b/mdl/executor/cmd_javascript_actions.go @@ -11,6 +11,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/sdk/javaactions" ) @@ -18,12 +19,12 @@ import ( func (e *Executor) showJavaScriptActions(moduleName string) error { h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } jsActions, err := e.reader.ListJavaScriptActions() if err != nil { - return fmt.Errorf("failed to list javascript actions: %w", err) + return mdlerrors.NewBackend("list javascript actions", err) } type row struct { @@ -68,7 +69,7 @@ func (e *Executor) describeJavaScriptAction(name ast.QualifiedName) error { qualifiedName := name.Module + "." + name.Name jsa, err := e.reader.ReadJavaScriptActionByName(qualifiedName) if err != nil { - return fmt.Errorf("javascript action not found: %s", qualifiedName) + return mdlerrors.NewNotFound("javascript action", qualifiedName) } var sb strings.Builder diff --git a/mdl/executor/cmd_jsonstructures.go b/mdl/executor/cmd_jsonstructures.go index 46069167..4b49ea96 100644 --- a/mdl/executor/cmd_jsonstructures.go +++ b/mdl/executor/cmd_jsonstructures.go @@ -10,6 +10,7 @@ import ( "unicode" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/sdk/mpr" ) @@ -17,7 +18,7 @@ import ( func (e *Executor) showJsonStructures(moduleName string) error { structures, err := e.reader.ListJsonStructures() if err != nil { - return fmt.Errorf("failed to list JSON structures: %w", err) + return mdlerrors.NewBackend("list JSON structures", err) } h, err := e.getHierarchy() @@ -72,7 +73,7 @@ func (e *Executor) showJsonStructures(moduleName string) error { func (e *Executor) describeJsonStructure(name ast.QualifiedName) error { js := e.findJsonStructure(name.Module, name.Name) if js == nil { - return fmt.Errorf("JSON structure not found: %s", name) + return mdlerrors.NewNotFound("JSON structure", name.String()) } h, err := e.getHierarchy() @@ -173,7 +174,7 @@ func capitalizeFirstRune(s string) string { // execCreateJsonStructure handles CREATE [OR REPLACE] JSON STRUCTURE statements. func (e *Executor) execCreateJsonStructure(s *ast.CreateJsonStructureStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Find or auto-create module @@ -187,7 +188,7 @@ func (e *Executor) execCreateJsonStructure(s *ast.CreateJsonStructureStmt) error if s.Folder != "" { folderID, err := e.resolveFolder(module.ID, s.Folder) if err != nil { - return fmt.Errorf("failed to resolve folder %s: %w", s.Folder, err) + return mdlerrors.NewBackend("resolve folder "+s.Folder, err) } containerID = folderID } @@ -198,17 +199,17 @@ func (e *Executor) execCreateJsonStructure(s *ast.CreateJsonStructureStmt) error if s.CreateOrReplace { // Delete existing before recreating if err := e.writer.DeleteJsonStructure(string(existing.ID)); err != nil { - return fmt.Errorf("failed to delete existing JSON structure: %w", err) + return mdlerrors.NewBackend("delete existing JSON structure", err) } } else { - return fmt.Errorf("JSON structure already exists: %s.%s", s.Name.Module, s.Name.Name) + return mdlerrors.NewAlreadyExists("JSON structure", s.Name.Module+"."+s.Name.Name) } } // Build element tree from JSON snippet, applying custom name mappings elements, err := mpr.BuildJsonElementsFromSnippet(s.JsonSnippet, s.CustomNameMap) if err != nil { - return fmt.Errorf("failed to build element tree: %w", err) + return mdlerrors.NewBackend("build element tree", err) } // For CREATE OR REPLACE, keep original folder unless a new one is specified @@ -225,7 +226,7 @@ func (e *Executor) execCreateJsonStructure(s *ast.CreateJsonStructureStmt) error } if err := e.writer.CreateJsonStructure(js); err != nil { - return fmt.Errorf("failed to create JSON structure: %w", err) + return mdlerrors.NewBackend("create JSON structure", err) } // Invalidate hierarchy cache @@ -242,16 +243,16 @@ func (e *Executor) execCreateJsonStructure(s *ast.CreateJsonStructureStmt) error // execDropJsonStructure handles DROP JSON STRUCTURE statements. func (e *Executor) execDropJsonStructure(s *ast.DropJsonStructureStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } js := e.findJsonStructure(s.Name.Module, s.Name.Name) if js == nil { - return fmt.Errorf("JSON structure not found: %s", s.Name) + return mdlerrors.NewNotFound("JSON structure", s.Name.String()) } if err := e.writer.DeleteJsonStructure(string(js.ID)); err != nil { - return fmt.Errorf("failed to delete JSON structure: %w", err) + return mdlerrors.NewBackend("delete JSON structure", err) } fmt.Fprintf(e.output, "Dropped JSON structure: %s\n", s.Name) diff --git a/mdl/executor/cmd_languages.go b/mdl/executor/cmd_languages.go index 96581f3e..b1144879 100644 --- a/mdl/executor/cmd_languages.go +++ b/mdl/executor/cmd_languages.go @@ -4,13 +4,15 @@ package executor import ( "fmt" + + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" ) // showLanguages lists all languages found in the project's translatable strings. // Requires REFRESH CATALOG FULL to populate the strings table. func (e *Executor) showLanguages() error { if e.catalog == nil { - return fmt.Errorf("no catalog available — run REFRESH CATALOG FULL first") + return mdlerrors.NewValidation("no catalog available — run REFRESH CATALOG FULL first") } result, err := e.catalog.Query(` @@ -21,7 +23,7 @@ func (e *Executor) showLanguages() error { ORDER BY StringCount DESC `) if err != nil { - return fmt.Errorf("failed to query languages: %w", err) + return mdlerrors.NewBackend("query languages", err) } if len(result.Rows) == 0 { diff --git a/mdl/executor/cmd_layouts.go b/mdl/executor/cmd_layouts.go index cdc236b3..bf02e62d 100644 --- a/mdl/executor/cmd_layouts.go +++ b/mdl/executor/cmd_layouts.go @@ -7,6 +7,8 @@ import ( "fmt" "sort" "strings" + + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" ) // showLayouts handles SHOW LAYOUTS command. @@ -14,13 +16,13 @@ func (e *Executor) showLayouts(moduleName string) error { // Get hierarchy for module/folder resolution h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Get all layouts layouts, err := e.reader.ListLayouts() if err != nil { - return fmt.Errorf("failed to list layouts: %w", err) + return mdlerrors.NewBackend("list layouts", err) } // Collect rows diff --git a/mdl/executor/cmd_lint.go b/mdl/executor/cmd_lint.go index 554041d2..eb36bd29 100644 --- a/mdl/executor/cmd_lint.go +++ b/mdl/executor/cmd_lint.go @@ -8,6 +8,7 @@ import ( "path/filepath" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/mdl/linter" "github.com/mendixlabs/mxcli/mdl/linter/rules" ) @@ -15,7 +16,7 @@ import ( // execLint executes a LINT statement. func (e *Executor) execLint(s *ast.LintStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Handle SHOW LINT RULES @@ -27,7 +28,7 @@ func (e *Executor) execLint(s *ast.LintStmt) error { if e.catalog == nil { fmt.Fprintln(e.output, "Building catalog for linting...") if err := e.buildCatalog(false); err != nil { - return fmt.Errorf("failed to build catalog: %w", err) + return mdlerrors.NewBackend("build catalog", err) } } @@ -82,7 +83,7 @@ func (e *Executor) execLint(s *ast.LintStmt) error { // Run linting violations, err := lint.Run(context.Background()) if err != nil { - return fmt.Errorf("linting failed: %w", err) + return mdlerrors.NewBackend("lint", err) } // Filter violations if targeting specific module diff --git a/mdl/executor/cmd_mermaid.go b/mdl/executor/cmd_mermaid.go index b8fa0457..6f746ea2 100644 --- a/mdl/executor/cmd_mermaid.go +++ b/mdl/executor/cmd_mermaid.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/microflows" @@ -17,7 +18,7 @@ import ( // Supported types: entity (renders full domain model), microflow, page. func (e *Executor) DescribeMermaid(objectType, name string) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } parts := strings.SplitN(name, ".", 2) @@ -36,7 +37,7 @@ func (e *Executor) DescribeMermaid(objectType, name string) error { case "PAGE": return e.pageToMermaid(qn) default: - return fmt.Errorf("mermaid format not supported for type: %s", objectType) + return mdlerrors.NewUnsupported(fmt.Sprintf("mermaid format not supported for type: %s", objectType)) } } @@ -49,7 +50,7 @@ func (e *Executor) domainModelToMermaid(moduleName string) error { dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } // Build entity ID-to-name map for this module @@ -183,7 +184,7 @@ func (e *Executor) domainModelToMermaid(moduleName string) error { func (e *Executor) microflowToMermaid(name ast.QualifiedName) error { h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Build entity name lookup @@ -199,7 +200,7 @@ func (e *Executor) microflowToMermaid(name ast.QualifiedName) error { // Find the microflow allMicroflows, err := e.reader.ListMicroflows() if err != nil { - return fmt.Errorf("failed to list microflows: %w", err) + return mdlerrors.NewBackend("list microflows", err) } var targetMf *microflows.Microflow @@ -213,7 +214,7 @@ func (e *Executor) microflowToMermaid(name ast.QualifiedName) error { } if targetMf == nil { - return fmt.Errorf("microflow not found: %s", name) + return mdlerrors.NewNotFound("microflow", name.String()) } return e.renderMicroflowMermaid(targetMf, entityNames) @@ -361,12 +362,12 @@ func (e *Executor) renderMicroflowMermaid(mf *microflows.Microflow, entityNames func (e *Executor) pageToMermaid(name ast.QualifiedName) error { h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } allPages, err := e.reader.ListPages() if err != nil { - return fmt.Errorf("failed to list pages: %w", err) + return mdlerrors.NewBackend("list pages", err) } var foundPage *pages.Page @@ -380,7 +381,7 @@ func (e *Executor) pageToMermaid(name ast.QualifiedName) error { } if foundPage == nil { - return fmt.Errorf("page not found: %s", name) + return mdlerrors.NewNotFound("page", name.String()) } // Use raw widget data (same approach as describePage) diff --git a/mdl/executor/cmd_microflow_elk.go b/mdl/executor/cmd_microflow_elk.go index 32d97780..d07a4202 100644 --- a/mdl/executor/cmd_microflow_elk.go +++ b/mdl/executor/cmd_microflow_elk.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/microflows" ) @@ -61,19 +62,19 @@ type microflowELKEdge struct { // MicroflowELK generates a JSON graph of a microflow for rendering with ELK.js. func (e *Executor) MicroflowELK(name string) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } parts := strings.SplitN(name, ".", 2) if len(parts) != 2 { - return fmt.Errorf("expected qualified name Module.Microflow, got: %s", name) + return mdlerrors.NewValidationf("expected qualified name Module.Microflow, got: %s", name) } qn := ast.QualifiedName{Module: parts[0], Name: parts[1]} h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Build entity name lookup @@ -89,7 +90,7 @@ func (e *Executor) MicroflowELK(name string) error { // Find the microflow allMicroflows, err := e.reader.ListMicroflows() if err != nil { - return fmt.Errorf("failed to list microflows: %w", err) + return mdlerrors.NewBackend("list microflows", err) } var targetMf *microflows.Microflow @@ -103,7 +104,7 @@ func (e *Executor) MicroflowELK(name string) error { } if targetMf == nil { - return fmt.Errorf("microflow not found: %s", name) + return mdlerrors.NewNotFound("microflow", name) } // Generate MDL source with source map @@ -405,7 +406,7 @@ func collectAllObjectsAndFlows(oc *microflows.MicroflowObjectCollection) ([]micr func (e *Executor) emitMicroflowELK(data microflowELKData) error { out, err := json.MarshalIndent(data, "", " ") if err != nil { - return fmt.Errorf("failed to marshal JSON: %w", err) + return mdlerrors.NewBackend("marshal JSON", err) } fmt.Fprint(e.output, string(out)) return nil diff --git a/mdl/executor/cmd_microflows_builder.go b/mdl/executor/cmd_microflows_builder.go index 130e4f9f..e4f49597 100644 --- a/mdl/executor/cmd_microflows_builder.go +++ b/mdl/executor/cmd_microflows_builder.go @@ -22,17 +22,17 @@ type flowBuilder struct { posY int baseY int // Base Y position (for returning after ELSE branches) spacing int - returnValue string // Return value expression for RETURN statement (used by buildFlowGraph final EndEvent) - endsWithReturn bool // True if the flow already ends with EndEvent(s) from RETURN statements - varTypes map[string]string // Variable name -> entity qualified name (for CHANGE statements) - declaredVars map[string]string // Declared primitive variables: name -> type (e.g., "$IsValid" -> "Boolean") - errors []string // Validation errors collected during build - measurer *layoutMeasurer // For measuring statement dimensions - nextConnectionPoint model.ID // For compound statements: the exit point differs from entry point - nextFlowCase string // If set, next connecting flow uses this case value (for merge-less splits) - reader *mpr.Reader // For looking up page/microflow references - hierarchy *ContainerHierarchy // For resolving container IDs to module names - pendingAnnotations *ast.ActivityAnnotations // Pending annotations to attach to next activity + returnValue string // Return value expression for RETURN statement (used by buildFlowGraph final EndEvent) + endsWithReturn bool // True if the flow already ends with EndEvent(s) from RETURN statements + varTypes map[string]string // Variable name -> entity qualified name (for CHANGE statements) + declaredVars map[string]string // Declared primitive variables: name -> type (e.g., "$IsValid" -> "Boolean") + errors []string // Validation errors collected during build + measurer *layoutMeasurer // For measuring statement dimensions + nextConnectionPoint model.ID // For compound statements: the exit point differs from entry point + nextFlowCase string // If set, next connecting flow uses this case value (for merge-less splits) + reader *mpr.Reader // For looking up page/microflow references + hierarchy *ContainerHierarchy // For resolving container IDs to module names + pendingAnnotations *ast.ActivityAnnotations // Pending annotations to attach to next activity restServices []*model.ConsumedRestService // Cached REST services for parameter classification } diff --git a/mdl/executor/cmd_microflows_create.go b/mdl/executor/cmd_microflows_create.go index ac4ba445..c93309a3 100644 --- a/mdl/executor/cmd_microflows_create.go +++ b/mdl/executor/cmd_microflows_create.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/microflows" "github.com/mendixlabs/mxcli/sdk/mpr" @@ -33,7 +34,7 @@ func (e *Executor) loadRestServices() ([]*model.ConsumedRestService, error) { func (e *Executor) execCreateMicroflow(s *ast.CreateMicroflowStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Find or auto-create module @@ -47,7 +48,7 @@ func (e *Executor) execCreateMicroflow(s *ast.CreateMicroflowStmt) error { if s.Folder != "" { folderID, err := e.resolveFolder(module.ID, s.Folder) if err != nil { - return fmt.Errorf("failed to resolve folder %s: %w", s.Folder, err) + return mdlerrors.NewBackend("resolve folder "+s.Folder, err) } containerID = folderID } @@ -57,12 +58,12 @@ func (e *Executor) execCreateMicroflow(s *ast.CreateMicroflowStmt) error { var existingContainerID model.ID existingMicroflows, err := e.reader.ListMicroflows() if err != nil { - return fmt.Errorf("failed to check existing microflows: %w", err) + return mdlerrors.NewBackend("check existing microflows", err) } for _, existing := range existingMicroflows { if existing.Name == s.Name.Name && e.getModuleID(existing.ContainerID) == module.ID { if !s.CreateOrModify { - return fmt.Errorf("microflow '%s.%s' already exists (use CREATE OR REPLACE to overwrite)", s.Name.Module, s.Name.Name) + return mdlerrors.NewAlreadyExistsMsg("microflow", s.Name.Module+"."+s.Name.Name, "microflow '"+s.Name.Module+"."+s.Name.Name+"' already exists (use CREATE OR REPLACE to overwrite)") } existingID = existing.ID existingContainerID = existing.ContainerID @@ -128,15 +129,15 @@ func (e *Executor) execCreateMicroflow(s *ast.CreateMicroflowStmt) error { if p.Type.EntityRef != nil && !isBuiltinModuleEntity(p.Type.EntityRef.Module) { entityID := entityResolver(*p.Type.EntityRef) if entityID == "" { - return fmt.Errorf("entity '%s.%s' not found for parameter '%s'", - p.Type.EntityRef.Module, p.Type.EntityRef.Name, p.Name) + return mdlerrors.NewNotFoundMsg("entity", p.Type.EntityRef.Module+"."+p.Type.EntityRef.Name, + fmt.Sprintf("entity '%s.%s' not found for parameter '%s'", p.Type.EntityRef.Module, p.Type.EntityRef.Name, p.Name)) } } // Validate enumeration references for Enumeration types if p.Type.Kind == ast.TypeEnumeration && p.Type.EnumRef != nil { if found := e.findEnumeration(p.Type.EnumRef.Module, p.Type.EnumRef.Name); found == nil { - return fmt.Errorf("enumeration '%s.%s' not found for parameter '%s'", - p.Type.EnumRef.Module, p.Type.EnumRef.Name, p.Name) + return mdlerrors.NewNotFoundMsg("enumeration", p.Type.EnumRef.Module+"."+p.Type.EnumRef.Name, + fmt.Sprintf("enumeration '%s.%s' not found for parameter '%s'", p.Type.EnumRef.Module, p.Type.EnumRef.Name, p.Name)) } } param := µflows.MicroflowParameter{ @@ -158,15 +159,15 @@ func (e *Executor) execCreateMicroflow(s *ast.CreateMicroflowStmt) error { if s.ReturnType.Type.EntityRef != nil && !isBuiltinModuleEntity(s.ReturnType.Type.EntityRef.Module) { entityID := entityResolver(*s.ReturnType.Type.EntityRef) if entityID == "" { - return fmt.Errorf("entity '%s.%s' not found for return type", - s.ReturnType.Type.EntityRef.Module, s.ReturnType.Type.EntityRef.Name) + return mdlerrors.NewNotFoundMsg("entity", s.ReturnType.Type.EntityRef.Module+"."+s.ReturnType.Type.EntityRef.Name, + fmt.Sprintf("entity '%s.%s' not found for return type", s.ReturnType.Type.EntityRef.Module, s.ReturnType.Type.EntityRef.Name)) } } // Validate enumeration references for return type if s.ReturnType.Type.Kind == ast.TypeEnumeration && s.ReturnType.Type.EnumRef != nil { if found := e.findEnumeration(s.ReturnType.Type.EnumRef.Module, s.ReturnType.Type.EnumRef.Name); found == nil { - return fmt.Errorf("enumeration '%s.%s' not found for return type", - s.ReturnType.Type.EnumRef.Module, s.ReturnType.Type.EnumRef.Name) + return mdlerrors.NewNotFoundMsg("enumeration", s.ReturnType.Type.EnumRef.Module+"."+s.ReturnType.Type.EnumRef.Name, + fmt.Sprintf("enumeration '%s.%s' not found for return type", s.ReturnType.Type.EnumRef.Module, s.ReturnType.Type.EnumRef.Name)) } } mf.ReturnType = convertASTToMicroflowDataType(s.ReturnType.Type, entityResolver) @@ -232,12 +233,12 @@ func (e *Executor) execCreateMicroflow(s *ast.CreateMicroflowStmt) error { // Create or update the microflow if existingID != "" { if err := e.writer.UpdateMicroflow(mf); err != nil { - return fmt.Errorf("failed to update microflow: %w", err) + return mdlerrors.NewBackend("update microflow", err) } fmt.Fprintf(e.output, "Replaced microflow: %s.%s\n", s.Name.Module, s.Name.Name) } else { if err := e.writer.CreateMicroflow(mf); err != nil { - return fmt.Errorf("failed to create microflow: %w", err) + return mdlerrors.NewBackend("create microflow", err) } fmt.Fprintf(e.output, "Created microflow: %s.%s\n", s.Name.Module, s.Name.Name) } diff --git a/mdl/executor/cmd_microflows_drop.go b/mdl/executor/cmd_microflows_drop.go index cb53c03e..0f9a59c6 100644 --- a/mdl/executor/cmd_microflows_drop.go +++ b/mdl/executor/cmd_microflows_drop.go @@ -7,24 +7,25 @@ import ( "fmt" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" ) // execDropMicroflow handles DROP MICROFLOW statements. func (e *Executor) execDropMicroflow(s *ast.DropMicroflowStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnectedWrite() } // Get hierarchy for module/folder resolution h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Find and delete the microflow mfs, err := e.reader.ListMicroflows() if err != nil { - return fmt.Errorf("failed to list microflows: %w", err) + return mdlerrors.NewBackend("list microflows", err) } for _, mf := range mfs { @@ -32,7 +33,7 @@ func (e *Executor) execDropMicroflow(s *ast.DropMicroflowStmt) error { modName := h.GetModuleName(modID) if modName == s.Name.Module && mf.Name == s.Name.Name { if err := e.writer.DeleteMicroflow(mf.ID); err != nil { - return fmt.Errorf("failed to delete microflow: %w", err) + return mdlerrors.NewBackend("delete microflow", err) } // Clear executor-level caches so subsequent CREATE sees fresh state qualifiedName := s.Name.Module + "." + s.Name.Name @@ -45,5 +46,5 @@ func (e *Executor) execDropMicroflow(s *ast.DropMicroflowStmt) error { } } - return fmt.Errorf("microflow not found: %s.%s", s.Name.Module, s.Name.Name) + return mdlerrors.NewNotFound("microflow", s.Name.Module+"."+s.Name.Name) } diff --git a/mdl/executor/cmd_microflows_show.go b/mdl/executor/cmd_microflows_show.go index c2a52388..6b286acc 100644 --- a/mdl/executor/cmd_microflows_show.go +++ b/mdl/executor/cmd_microflows_show.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/microflows" ) @@ -18,13 +19,13 @@ func (e *Executor) showMicroflows(moduleName string) error { // Get hierarchy for module/folder resolution h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Get all microflows microflows, err := e.reader.ListMicroflows() if err != nil { - return fmt.Errorf("failed to list microflows: %w", err) + return mdlerrors.NewBackend("list microflows", err) } // Collect rows and calculate column widths @@ -82,13 +83,13 @@ func (e *Executor) showNanoflows(moduleName string) error { // Get hierarchy for module/folder resolution h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Get all nanoflows nanoflows, err := e.reader.ListNanoflows() if err != nil { - return fmt.Errorf("failed to list nanoflows: %w", err) + return mdlerrors.NewBackend("list nanoflows", err) } // Collect rows and calculate column widths @@ -180,7 +181,7 @@ func (e *Executor) describeMicroflow(name ast.QualifiedName) error { // Get hierarchy for module/folder resolution h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Use pre-warmed cache if available (from PreWarmCache), otherwise build on demand @@ -190,7 +191,7 @@ func (e *Executor) describeMicroflow(name ast.QualifiedName) error { // Find the microflow allMicroflows, err := e.reader.ListMicroflows() if err != nil { - return fmt.Errorf("failed to list microflows: %w", err) + return mdlerrors.NewBackend("list microflows", err) } // Supplement microflow name lookup if not pre-warmed @@ -211,7 +212,7 @@ func (e *Executor) describeMicroflow(name ast.QualifiedName) error { } if targetMf == nil { - return fmt.Errorf("microflow not found: %s", name) + return mdlerrors.NewNotFound("microflow", name.String()) } // Generate MDL output @@ -307,7 +308,7 @@ func (e *Executor) describeMicroflow(name ast.QualifiedName) error { func (e *Executor) describeNanoflow(name ast.QualifiedName) error { h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Build entity name lookup @@ -330,7 +331,7 @@ func (e *Executor) describeNanoflow(name ast.QualifiedName) error { // Find the nanoflow allNanoflows, err := e.reader.ListNanoflows() if err != nil { - return fmt.Errorf("failed to list nanoflows: %w", err) + return mdlerrors.NewBackend("list nanoflows", err) } for _, nf := range allNanoflows { @@ -348,7 +349,7 @@ func (e *Executor) describeNanoflow(name ast.QualifiedName) error { } if targetNf == nil { - return fmt.Errorf("nanoflow not found: %s", name) + return mdlerrors.NewNotFound("nanoflow", name.String()) } var lines []string @@ -423,7 +424,7 @@ func (e *Executor) describeNanoflow(name ast.QualifiedName) error { func (e *Executor) describeMicroflowToString(name ast.QualifiedName) (string, map[string]elkSourceRange, error) { h, err := e.getHierarchy() if err != nil { - return "", nil, fmt.Errorf("failed to build hierarchy: %w", err) + return "", nil, mdlerrors.NewBackend("build hierarchy", err) } entityNames := make(map[model.ID]string) @@ -438,7 +439,7 @@ func (e *Executor) describeMicroflowToString(name ast.QualifiedName) (string, ma microflowNames := make(map[model.ID]string) allMicroflows, err := e.reader.ListMicroflows() if err != nil { - return "", nil, fmt.Errorf("failed to list microflows: %w", err) + return "", nil, mdlerrors.NewBackend("list microflows", err) } for _, mf := range allMicroflows { microflowNames[mf.ID] = h.GetQualifiedName(mf.ContainerID, mf.Name) @@ -455,7 +456,7 @@ func (e *Executor) describeMicroflowToString(name ast.QualifiedName) (string, ma } if targetMf == nil { - return "", nil, fmt.Errorf("microflow not found: %s", name) + return "", nil, mdlerrors.NewNotFound("microflow", name.String()) } sourceMap := make(map[string]elkSourceRange) diff --git a/mdl/executor/cmd_misc.go b/mdl/executor/cmd_misc.go index e91c4c4c..be003093 100644 --- a/mdl/executor/cmd_misc.go +++ b/mdl/executor/cmd_misc.go @@ -11,17 +11,18 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/mdl/visitor" ) // ErrExit is a sentinel error indicating clean script/session termination. // Use errors.Is(err, ErrExit) to detect exit requests. -var ErrExit = errors.New("exit") +var ErrExit = mdlerrors.ErrExit // execUpdate handles UPDATE statements (refresh from disk). func (e *Executor) execUpdate() error { if e.mprPath == "" { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Reconnect to refresh @@ -323,7 +324,7 @@ Statement Terminator: // showVersion displays Mendix project version information. func (e *Executor) showVersion() error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } pv := e.reader.ProjectVersion() @@ -352,7 +353,7 @@ func (e *Executor) execExecuteScript(s *ast.ExecuteScriptStmt) error { if !filepath.IsAbs(scriptPath) { cwd, err := os.Getwd() if err != nil { - return fmt.Errorf("failed to get current directory: %w", err) + return mdlerrors.NewBackend("get current directory", err) } scriptPath = filepath.Join(cwd, scriptPath) } @@ -360,7 +361,7 @@ func (e *Executor) execExecuteScript(s *ast.ExecuteScriptStmt) error { // Read the script file content, err := os.ReadFile(scriptPath) if err != nil { - return fmt.Errorf("failed to read script file '%s': %w", s.Path, err) + return mdlerrors.NewBackend("read script file '"+s.Path+"'", err) } // Pre-process: remove "/" statement separators (SQL*Plus style) @@ -373,7 +374,7 @@ func (e *Executor) execExecuteScript(s *ast.ExecuteScriptStmt) error { for _, err := range errs { fmt.Fprintf(e.output, "Parse error in %s: %v\n", s.Path, err) } - return fmt.Errorf("script '%s' has parse errors", s.Path) + return mdlerrors.NewValidationf("script '%s' has parse errors", s.Path) } // Execute all statements in the script diff --git a/mdl/executor/cmd_module_overview.go b/mdl/executor/cmd_module_overview.go index 335764be..31f5e1c0 100644 --- a/mdl/executor/cmd_module_overview.go +++ b/mdl/executor/cmd_module_overview.go @@ -5,6 +5,8 @@ package executor import ( "encoding/json" "fmt" + + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" ) // moduleOverviewData is the JSON output schema for the module overview ELK diagram. @@ -46,18 +48,18 @@ var systemModuleNames = map[string]bool{ // cross-module dependencies, suitable for rendering with ELK.js. func (e *Executor) ModuleOverview() error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Ensure catalog is built with full mode for refs if err := e.ensureCatalog(true); err != nil { - return fmt.Errorf("failed to build catalog: %w", err) + return mdlerrors.NewBackend("build catalog", err) } // Get all module names moduleResult, err := e.catalog.Query("SELECT Name FROM modules") if err != nil { - return fmt.Errorf("failed to query modules: %w", err) + return mdlerrors.NewBackend("query modules", err) } moduleNames := make(map[string]bool) @@ -129,7 +131,7 @@ func (e *Executor) ModuleOverview() error { HAVING SourceModule != TargetModule `) if err != nil { - return fmt.Errorf("failed to query refs: %w", err) + return mdlerrors.NewBackend("query refs", err) } // Aggregate edges by source/target pair @@ -181,7 +183,7 @@ func (e *Executor) ModuleOverview() error { out, err := json.MarshalIndent(data, "", " ") if err != nil { - return fmt.Errorf("failed to marshal JSON: %w", err) + return mdlerrors.NewBackend("marshal JSON", err) } fmt.Fprint(e.output, string(out)) diff --git a/mdl/executor/cmd_modules.go b/mdl/executor/cmd_modules.go index 8c473e0e..b43b5b1e 100644 --- a/mdl/executor/cmd_modules.go +++ b/mdl/executor/cmd_modules.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" ) @@ -16,13 +17,13 @@ import ( // execCreateModule handles CREATE MODULE statements. func (e *Executor) execCreateModule(s *ast.CreateModuleStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Check if module already exists modules, err := e.reader.ListModules() if err != nil { - return fmt.Errorf("failed to list modules: %w", err) + return mdlerrors.NewBackend("list modules", err) } for _, m := range modules { @@ -38,7 +39,7 @@ func (e *Executor) execCreateModule(s *ast.CreateModuleStmt) error { } if err := e.writer.CreateModule(module); err != nil { - return fmt.Errorf("failed to create module: %w", err) + return mdlerrors.NewBackend("create module", err) } // Invalidate cache so new module is visible @@ -59,13 +60,13 @@ func (e *Executor) execCreateModule(s *ast.CreateModuleStmt) error { // - Constants func (e *Executor) execDropModule(s *ast.DropModuleStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Find the module modules, err := e.reader.ListModules() if err != nil { - return fmt.Errorf("failed to list modules: %w", err) + return mdlerrors.NewBackend("list modules", err) } var targetModule *model.Module @@ -77,7 +78,7 @@ func (e *Executor) execDropModule(s *ast.DropModuleStmt) error { } if targetModule == nil { - return fmt.Errorf("module not found: %s", s.Name) + return mdlerrors.NewNotFound("module", s.Name) } // Build set of all container IDs belonging to this module (including nested folders) @@ -280,7 +281,7 @@ func (e *Executor) execDropModule(s *ast.DropModuleStmt) error { // Delete the module itself (and clean up themesource directory) if err := e.writer.DeleteModuleWithCleanup(targetModule.ID, s.Name); err != nil { - return fmt.Errorf("failed to delete module: %w", err) + return mdlerrors.NewBackend("delete module", err) } // Build summary of what was removed @@ -375,26 +376,26 @@ func (e *Executor) getModuleContainers(moduleID model.ID) map[model.ID]bool { // showModules handles SHOW MODULES command. func (e *Executor) showModules() error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Always get fresh module list and update cache e.invalidateModuleCache() modules, err := e.getModulesFromCache() if err != nil { - return fmt.Errorf("failed to list modules: %w", err) + return mdlerrors.NewBackend("list modules", err) } // Get hierarchy for module resolution h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Get units for type-based counting units, err := e.reader.ListUnits() if err != nil { - return fmt.Errorf("failed to list units: %w", err) + return mdlerrors.NewBackend("list units", err) } // Count elements per module using unit types @@ -568,13 +569,13 @@ func (e *Executor) showModules() error { // describeModule handles DESCRIBE MODULE [WITH ALL] command. func (e *Executor) describeModule(moduleName string, withAll bool) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Find the module modules, err := e.reader.ListModules() if err != nil { - return fmt.Errorf("failed to list modules: %w", err) + return mdlerrors.NewBackend("list modules", err) } var targetModule *model.Module @@ -586,7 +587,7 @@ func (e *Executor) describeModule(moduleName string, withAll bool) error { } if targetModule == nil { - return fmt.Errorf("module not found: %s", moduleName) + return mdlerrors.NewNotFound("module", moduleName) } // Output basic CREATE MODULE statement diff --git a/mdl/executor/cmd_move.go b/mdl/executor/cmd_move.go index 8923525b..b531c2fe 100644 --- a/mdl/executor/cmd_move.go +++ b/mdl/executor/cmd_move.go @@ -7,6 +7,7 @@ import ( "fmt" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" ) @@ -14,13 +15,13 @@ import ( // execMove handles MOVE PAGE/MICROFLOW/SNIPPET/NANOFLOW/ENTITY/ENUMERATION statements. func (e *Executor) execMove(s *ast.MoveStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Find the source module sourceModule, err := e.findModule(s.Name.Module) if err != nil { - return fmt.Errorf("source module not found: %w", err) + return mdlerrors.NewBackend("find source module", err) } // Determine target module @@ -29,7 +30,7 @@ func (e *Executor) execMove(s *ast.MoveStmt) error { if s.TargetModule != "" { targetModule, err = e.findModule(s.TargetModule) if err != nil { - return fmt.Errorf("target module not found: %w", err) + return mdlerrors.NewBackend("find target module", err) } isCrossModuleMove = targetModule.ID != sourceModule.ID } else { @@ -46,7 +47,7 @@ func (e *Executor) execMove(s *ast.MoveStmt) error { if s.Folder != "" { targetContainerID, err = e.resolveFolder(targetModule.ID, s.Folder) if err != nil { - return fmt.Errorf("failed to resolve target folder: %w", err) + return mdlerrors.NewBackend("resolve target folder", err) } } else { targetContainerID = targetModule.ID @@ -81,7 +82,7 @@ func (e *Executor) execMove(s *ast.MoveStmt) error { return err } default: - return fmt.Errorf("unsupported document type: %s", s.DocumentType) + return mdlerrors.NewUnsupported("unsupported document type: " + string(s.DocumentType)) } // For cross-module moves, update all BY_NAME references throughout the project @@ -100,7 +101,7 @@ func (e *Executor) updateQualifiedNameRefs(name ast.QualifiedName, newModule str newQN := newModule + "." + name.Name // "NewModule.ElementName" updated, err := e.writer.UpdateQualifiedNameInAllUnits(oldQN, newQN) if err != nil { - return fmt.Errorf("failed to update references: %w", err) + return mdlerrors.NewBackend("update references", err) } if updated > 0 { fmt.Fprintf(e.output, "Updated references in %d document(s): %s → %s\n", updated, oldQN, newQN) @@ -113,12 +114,12 @@ func (e *Executor) movePage(name ast.QualifiedName, targetContainerID model.ID) // Find the page pages, err := e.reader.ListPages() if err != nil { - return fmt.Errorf("failed to list pages: %w", err) + return mdlerrors.NewBackend("list pages", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, p := range pages { @@ -128,14 +129,14 @@ func (e *Executor) movePage(name ast.QualifiedName, targetContainerID model.ID) // Update container ID and move the unit p.ContainerID = targetContainerID if err := e.writer.MovePage(p); err != nil { - return fmt.Errorf("failed to move page: %w", err) + return mdlerrors.NewBackend("move page", err) } fmt.Fprintf(e.output, "Moved page %s to new location\n", name.String()) return nil } } - return fmt.Errorf("page not found: %s", name.String()) + return mdlerrors.NewNotFound("page", name.String()) } // moveMicroflow moves a microflow to a new container. @@ -143,12 +144,12 @@ func (e *Executor) moveMicroflow(name ast.QualifiedName, targetContainerID model // Find the microflow mfs, err := e.reader.ListMicroflows() if err != nil { - return fmt.Errorf("failed to list microflows: %w", err) + return mdlerrors.NewBackend("list microflows", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, mf := range mfs { @@ -158,14 +159,14 @@ func (e *Executor) moveMicroflow(name ast.QualifiedName, targetContainerID model // Update container ID and move the unit mf.ContainerID = targetContainerID if err := e.writer.MoveMicroflow(mf); err != nil { - return fmt.Errorf("failed to move microflow: %w", err) + return mdlerrors.NewBackend("move microflow", err) } fmt.Fprintf(e.output, "Moved microflow %s to new location\n", name.String()) return nil } } - return fmt.Errorf("microflow not found: %s", name.String()) + return mdlerrors.NewNotFound("microflow", name.String()) } // moveSnippet moves a snippet to a new container. @@ -173,12 +174,12 @@ func (e *Executor) moveSnippet(name ast.QualifiedName, targetContainerID model.I // Find the snippet snippets, err := e.reader.ListSnippets() if err != nil { - return fmt.Errorf("failed to list snippets: %w", err) + return mdlerrors.NewBackend("list snippets", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, s := range snippets { @@ -188,14 +189,14 @@ func (e *Executor) moveSnippet(name ast.QualifiedName, targetContainerID model.I // Update container ID and move the unit s.ContainerID = targetContainerID if err := e.writer.MoveSnippet(s); err != nil { - return fmt.Errorf("failed to move snippet: %w", err) + return mdlerrors.NewBackend("move snippet", err) } fmt.Fprintf(e.output, "Moved snippet %s to new location\n", name.String()) return nil } } - return fmt.Errorf("snippet not found: %s", name.String()) + return mdlerrors.NewNotFound("snippet", name.String()) } // moveNanoflow moves a nanoflow to a new container. @@ -203,12 +204,12 @@ func (e *Executor) moveNanoflow(name ast.QualifiedName, targetContainerID model. // Find the nanoflow nfs, err := e.reader.ListNanoflows() if err != nil { - return fmt.Errorf("failed to list nanoflows: %w", err) + return mdlerrors.NewBackend("list nanoflows", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, nf := range nfs { @@ -218,14 +219,14 @@ func (e *Executor) moveNanoflow(name ast.QualifiedName, targetContainerID model. // Update container ID and move the unit nf.ContainerID = targetContainerID if err := e.writer.MoveNanoflow(nf); err != nil { - return fmt.Errorf("failed to move nanoflow: %w", err) + return mdlerrors.NewBackend("move nanoflow", err) } fmt.Fprintf(e.output, "Moved nanoflow %s to new location\n", name.String()) return nil } } - return fmt.Errorf("nanoflow not found: %s", name.String()) + return mdlerrors.NewNotFound("nanoflow", name.String()) } // moveEntity moves an entity from one domain model to another. @@ -236,7 +237,7 @@ func (e *Executor) moveEntity(name ast.QualifiedName, sourceModule, targetModule // Get source domain model sourceDM, err := e.reader.GetDomainModel(sourceModule.ID) if err != nil { - return fmt.Errorf("failed to get source domain model: %w", err) + return mdlerrors.NewBackend("get source domain model", err) } // Find the entity in the source domain model @@ -248,19 +249,19 @@ func (e *Executor) moveEntity(name ast.QualifiedName, sourceModule, targetModule } } if entity == nil { - return fmt.Errorf("entity not found: %s", name.String()) + return mdlerrors.NewNotFound("entity", name.String()) } // Get target domain model targetDM, err := e.reader.GetDomainModel(targetModule.ID) if err != nil { - return fmt.Errorf("failed to get target domain model: %w", err) + return mdlerrors.NewBackend("get target domain model", err) } // Move entity via writer (converts associations to CrossAssociations, updates validation rule refs) convertedAssocs, err := e.writer.MoveEntity(entity, sourceDM.ID, targetDM.ID, sourceModule.Name, targetModule.Name) if err != nil { - return fmt.Errorf("failed to move entity: %w", err) + return mdlerrors.NewBackend("move entity", err) } // Move ViewEntitySourceDocument for view entities @@ -297,13 +298,13 @@ func (e *Executor) moveEntity(name ast.QualifiedName, sourceModule, targetModule func (e *Executor) moveEnumeration(name ast.QualifiedName, targetContainerID model.ID, targetModuleName string) error { enum := e.findEnumeration(name.Module, name.Name) if enum == nil { - return fmt.Errorf("enumeration not found: %s", name.String()) + return mdlerrors.NewNotFound("enumeration", name.String()) } oldQualifiedName := name.String() // e.g., "DmTest.Country" enum.ContainerID = targetContainerID if err := e.writer.MoveEnumeration(enum); err != nil { - return fmt.Errorf("failed to move enumeration: %w", err) + return mdlerrors.NewBackend("move enumeration", err) } // For cross-module moves, update enumeration references in all domain models @@ -324,12 +325,12 @@ func (e *Executor) moveEnumeration(name ast.QualifiedName, targetContainerID mod func (e *Executor) moveConstant(name ast.QualifiedName, targetContainerID model.ID) error { constants, err := e.reader.ListConstants() if err != nil { - return fmt.Errorf("failed to list constants: %w", err) + return mdlerrors.NewBackend("list constants", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, c := range constants { @@ -338,26 +339,26 @@ func (e *Executor) moveConstant(name ast.QualifiedName, targetContainerID model. if modName == name.Module && c.Name == name.Name { c.ContainerID = targetContainerID if err := e.writer.MoveConstant(c); err != nil { - return fmt.Errorf("failed to move constant: %w", err) + return mdlerrors.NewBackend("move constant", err) } fmt.Fprintf(e.output, "Moved constant %s to new location\n", name.String()) return nil } } - return fmt.Errorf("constant not found: %s", name.String()) + return mdlerrors.NewNotFound("constant", name.String()) } // moveDatabaseConnection moves a database connection to a new container. func (e *Executor) moveDatabaseConnection(name ast.QualifiedName, targetContainerID model.ID) error { connections, err := e.reader.ListDatabaseConnections() if err != nil { - return fmt.Errorf("failed to list database connections: %w", err) + return mdlerrors.NewBackend("list database connections", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, conn := range connections { @@ -366,12 +367,12 @@ func (e *Executor) moveDatabaseConnection(name ast.QualifiedName, targetContaine if modName == name.Module && conn.Name == name.Name { conn.ContainerID = targetContainerID if err := e.writer.MoveDatabaseConnection(conn); err != nil { - return fmt.Errorf("failed to move database connection: %w", err) + return mdlerrors.NewBackend("move database connection", err) } fmt.Fprintf(e.output, "Moved database connection %s to new location\n", name.String()) return nil } } - return fmt.Errorf("database connection not found: %s", name.String()) + return mdlerrors.NewNotFound("database connection", name.String()) } diff --git a/mdl/executor/cmd_navigation.go b/mdl/executor/cmd_navigation.go index 3097ba8f..590463ab 100644 --- a/mdl/executor/cmd_navigation.go +++ b/mdl/executor/cmd_navigation.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/sdk/mpr" ) @@ -15,12 +16,12 @@ import ( // It fully replaces the profile's home pages, login page, not-found page, and menu tree. func (e *Executor) execAlterNavigation(s *ast.AlterNavigationStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project (read-write required)") + return mdlerrors.NewNotConnectedWrite() } nav, err := e.reader.GetNavigation() if err != nil { - return fmt.Errorf("failed to get navigation: %w", err) + return mdlerrors.NewBackend("get navigation", err) } // Verify the profile exists @@ -32,8 +33,8 @@ func (e *Executor) execAlterNavigation(s *ast.AlterNavigationStmt) error { } } if !profileFound { - return fmt.Errorf("navigation profile not found: %s (available: %s)", - s.ProfileName, profileNames(nav)) + return mdlerrors.NewNotFoundMsg("navigation profile", s.ProfileName, + fmt.Sprintf("navigation profile not found: %s (available: %s)", s.ProfileName, profileNames(nav))) } // Convert AST types to writer spec @@ -64,7 +65,7 @@ func (e *Executor) execAlterNavigation(s *ast.AlterNavigationStmt) error { } if err := e.writer.UpdateNavigationProfile(nav.ID, s.ProfileName, spec); err != nil { - return fmt.Errorf("failed to update navigation profile: %w", err) + return mdlerrors.NewBackend("update navigation profile", err) } fmt.Fprintf(e.output, "Navigation profile '%s' updated.\n", s.ProfileName) @@ -102,7 +103,7 @@ func profileNames(nav *mpr.NavigationDocument) string { func (e *Executor) showNavigation() error { nav, err := e.reader.GetNavigation() if err != nil { - return fmt.Errorf("failed to get navigation: %w", err) + return mdlerrors.NewBackend("get navigation", err) } if len(nav.Profiles) == 0 { @@ -160,7 +161,7 @@ func (e *Executor) showNavigation() error { func (e *Executor) showNavigationMenu(profileName *ast.QualifiedName) error { nav, err := e.reader.GetNavigation() if err != nil { - return fmt.Errorf("failed to get navigation: %w", err) + return mdlerrors.NewBackend("get navigation", err) } for _, p := range nav.Profiles { @@ -185,7 +186,7 @@ func (e *Executor) showNavigationMenu(profileName *ast.QualifiedName) error { func (e *Executor) showNavigationHomes() error { nav, err := e.reader.GetNavigation() if err != nil { - return fmt.Errorf("failed to get navigation: %w", err) + return mdlerrors.NewBackend("get navigation", err) } for _, p := range nav.Profiles { @@ -227,7 +228,7 @@ func (e *Executor) showNavigationHomes() error { func (e *Executor) describeNavigation(name ast.QualifiedName) error { nav, err := e.reader.GetNavigation() if err != nil { - return fmt.Errorf("failed to get navigation: %w", err) + return mdlerrors.NewBackend("get navigation", err) } // If no profile name, describe all profiles @@ -246,7 +247,7 @@ func (e *Executor) describeNavigation(name ast.QualifiedName) error { } } - return fmt.Errorf("navigation profile not found: %s", name.Name) + return mdlerrors.NewNotFound("navigation profile", name.Name) } // outputNavigationProfile outputs a single profile in round-trippable CREATE OR REPLACE NAVIGATION format. diff --git a/mdl/executor/cmd_odata.go b/mdl/executor/cmd_odata.go index 673d0908..95b84626 100644 --- a/mdl/executor/cmd_odata.go +++ b/mdl/executor/cmd_odata.go @@ -12,6 +12,7 @@ import ( "time" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/microflows" @@ -37,12 +38,12 @@ func outputJavadocIndented(w io.Writer, text string, indent string) { func (e *Executor) showODataClients(moduleName string) error { services, err := e.reader.ListConsumedODataServices() if err != nil { - return fmt.Errorf("failed to list consumed OData services: %w", err) + return mdlerrors.NewBackend("list consumed OData services", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } type row struct { @@ -100,12 +101,12 @@ func (e *Executor) showODataClients(moduleName string) error { func (e *Executor) describeODataClient(name ast.QualifiedName) error { services, err := e.reader.ListConsumedODataServices() if err != nil { - return fmt.Errorf("failed to list consumed OData services: %w", err) + return mdlerrors.NewBackend("list consumed OData services", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, svc := range services { @@ -117,7 +118,7 @@ func (e *Executor) describeODataClient(name ast.QualifiedName) error { } } - return fmt.Errorf("consumed OData service not found: %s", name) + return mdlerrors.NewNotFoundMsg("consumed OData service", fmt.Sprint(name), fmt.Sprintf("consumed OData service not found: %s", name)) } // outputConsumedODataServiceMDL outputs a consumed OData service in MDL format. @@ -217,12 +218,12 @@ func (e *Executor) outputConsumedODataServiceMDL(svc *model.ConsumedODataService func (e *Executor) showODataServices(moduleName string) error { services, err := e.reader.ListPublishedODataServices() if err != nil { - return fmt.Errorf("failed to list published OData services: %w", err) + return mdlerrors.NewBackend("list published OData services", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } type row struct { @@ -277,12 +278,12 @@ func (e *Executor) showODataServices(moduleName string) error { func (e *Executor) describeODataService(name ast.QualifiedName) error { services, err := e.reader.ListPublishedODataServices() if err != nil { - return fmt.Errorf("failed to list published OData services: %w", err) + return mdlerrors.NewBackend("list published OData services", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, svc := range services { @@ -294,7 +295,7 @@ func (e *Executor) describeODataService(name ast.QualifiedName) error { } } - return fmt.Errorf("published OData service not found: %s", name) + return mdlerrors.NewNotFoundMsg("published OData service", fmt.Sprint(name), fmt.Sprintf("published OData service not found: %s", name)) } // outputPublishedODataServiceMDL outputs a published OData service in MDL format. @@ -453,12 +454,12 @@ func (e *Executor) outputPublishedODataServiceMDL(svc *model.PublishedODataServi func (e *Executor) showExternalEntities(moduleName string) error { domainModels, err := e.reader.ListDomainModels() if err != nil { - return fmt.Errorf("failed to list domain models: %w", err) + return mdlerrors.NewBackend("list domain models", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } type row struct { @@ -519,16 +520,16 @@ func (e *Executor) showExternalEntities(moduleName string) error { func (e *Executor) showExternalActions(moduleName string) error { mfs, err := e.reader.ListMicroflows() if err != nil { - return fmt.Errorf("failed to list microflows: %w", err) + return mdlerrors.NewBackend("list microflows", err) } nfs, err := e.reader.ListNanoflows() if err != nil { - return fmt.Errorf("failed to list nanoflows: %w", err) + return mdlerrors.NewBackend("list nanoflows", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Collect unique actions: key = service + "." + action name @@ -650,12 +651,12 @@ func (e *Executor) showExternalActions(moduleName string) error { func (e *Executor) describeExternalEntity(name ast.QualifiedName) error { domainModels, err := e.reader.ListDomainModels() if err != nil { - return fmt.Errorf("failed to list domain models: %w", err) + return mdlerrors.NewBackend("list domain models", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, dm := range domainModels { @@ -671,14 +672,14 @@ func (e *Executor) describeExternalEntity(name ast.QualifiedName) error { } if entity.Source != "Rest$ODataRemoteEntitySource" { - return fmt.Errorf("%s.%s is not an external entity (source: %s)", modName, entity.Name, entity.Source) + return mdlerrors.NewValidationf("%s.%s is not an external entity (source: %s)", modName, entity.Name, entity.Source) } return e.outputExternalEntityMDL(entity, modName) } } - return fmt.Errorf("external entity not found: %s", name) + return mdlerrors.NewNotFoundMsg("external entity", fmt.Sprint(name), fmt.Sprintf("external entity not found: %s", name)) } // outputExternalEntityMDL outputs an external entity in MDL format. @@ -741,11 +742,11 @@ func (e *Executor) outputExternalEntityMDL(entity *domainmodel.Entity, moduleNam // execCreateExternalEntity handles CREATE [OR MODIFY] EXTERNAL ENTITY statements. func (e *Executor) execCreateExternalEntity(s *ast.CreateExternalEntityStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } if s.Name.Module == "" { - return fmt.Errorf("module name required: use CREATE EXTERNAL ENTITY Module.Name FROM ODATA CLIENT ...") + return mdlerrors.NewValidation("module name required: use CREATE EXTERNAL ENTITY Module.Name FROM ODATA CLIENT ...") } // Find module @@ -757,7 +758,7 @@ func (e *Executor) execCreateExternalEntity(s *ast.CreateExternalEntityStmt) err // Get domain model dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } // Check if entity already exists @@ -770,7 +771,7 @@ func (e *Executor) execCreateExternalEntity(s *ast.CreateExternalEntityStmt) err } if existingEntity != nil && !s.CreateOrModify { - return fmt.Errorf("entity already exists: %s.%s (use CREATE OR MODIFY to update)", s.Name.Module, s.Name.Name) + return mdlerrors.NewAlreadyExistsMsg("entity", s.Name.Module+"."+s.Name.Name, fmt.Sprintf("entity already exists: %s.%s (use CREATE OR MODIFY to update)", s.Name.Module, s.Name.Name)) } // Build attributes @@ -804,7 +805,7 @@ func (e *Executor) execCreateExternalEntity(s *ast.CreateExternalEntityStmt) err existingEntity.Documentation = s.Documentation } if err := e.writer.UpdateEntity(dm.ID, existingEntity); err != nil { - return fmt.Errorf("failed to update external entity: %w", err) + return mdlerrors.NewBackend("update external entity", err) } fmt.Fprintf(e.output, "Modified external entity: %s.%s\n", s.Name.Module, s.Name.Name) return nil @@ -831,7 +832,7 @@ func (e *Executor) execCreateExternalEntity(s *ast.CreateExternalEntityStmt) err newEntity.ID = model.ID(mpr.GenerateID()) if err := e.writer.CreateEntity(dm.ID, newEntity); err != nil { - return fmt.Errorf("failed to create external entity: %w", err) + return mdlerrors.NewBackend("create external entity", err) } fmt.Fprintf(e.output, "Created external entity: %s.%s\n", s.Name.Module, s.Name.Name) return nil @@ -844,11 +845,11 @@ func (e *Executor) execCreateExternalEntity(s *ast.CreateExternalEntityStmt) err // createODataClient handles CREATE ODATA CLIENT command. func (e *Executor) createODataClient(stmt *ast.CreateODataClientStmt) error { if e.writer == nil { - return fmt.Errorf("not connected in write mode") + return mdlerrors.NewNotConnectedWrite() } if stmt.Name.Module == "" { - return fmt.Errorf("module name required: use CREATE ODATA CLIENT Module.Name (...)") + return mdlerrors.NewValidation("module name required: use CREATE ODATA CLIENT Module.Name (...)") } module, err := e.findModule(stmt.Name.Module) @@ -933,13 +934,13 @@ func (e *Executor) createODataClient(stmt *ast.CreateODataClientStmt) error { } } if err := e.writer.UpdateConsumedODataService(svc); err != nil { - return fmt.Errorf("failed to update OData client: %w", err) + return mdlerrors.NewBackend("update OData client", err) } e.invalidateHierarchy() fmt.Fprintf(e.output, "Modified OData client: %s.%s\n", modName, svc.Name) return nil } - return fmt.Errorf("OData client already exists: %s.%s (use CREATE OR MODIFY to update)", modName, svc.Name) + return mdlerrors.NewAlreadyExistsMsg("OData client", modName+"."+svc.Name, fmt.Sprintf("OData client already exists: %s.%s (use CREATE OR MODIFY to update)", modName, svc.Name)) } } } @@ -949,7 +950,7 @@ func (e *Executor) createODataClient(stmt *ast.CreateODataClientStmt) error { if stmt.Folder != "" { folderID, err := e.resolveFolder(module.ID, stmt.Folder) if err != nil { - return fmt.Errorf("failed to resolve folder %s: %w", stmt.Folder, err) + return mdlerrors.NewBackend(fmt.Sprintf("resolve folder %s", stmt.Folder), err) } containerID = folderID } @@ -1013,7 +1014,7 @@ func (e *Executor) createODataClient(stmt *ast.CreateODataClientStmt) error { } if err := e.writer.CreateConsumedODataService(newSvc); err != nil { - return fmt.Errorf("failed to create OData client: %w", err) + return mdlerrors.NewBackend("create OData client", err) } e.invalidateHierarchy() fmt.Fprintf(e.output, "Created OData client: %s.%s\n", stmt.Name.Module, stmt.Name.Name) @@ -1035,17 +1036,17 @@ func (e *Executor) createODataClient(stmt *ast.CreateODataClientStmt) error { // alterODataClient handles ALTER ODATA CLIENT command. func (e *Executor) alterODataClient(stmt *ast.AlterODataClientStmt) error { if e.writer == nil { - return fmt.Errorf("not connected in write mode") + return mdlerrors.NewNotConnectedWrite() } services, err := e.reader.ListConsumedODataServices() if err != nil { - return fmt.Errorf("failed to list consumed OData services: %w", err) + return mdlerrors.NewBackend("list consumed OData services", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, svc := range services { @@ -1106,11 +1107,11 @@ func (e *Executor) alterODataClient(stmt *ast.AlterODataClientStmt) error { case "proxypassword": svc.ProxyPassword = strVal default: - return fmt.Errorf("unknown OData client property: %s", key) + return mdlerrors.NewUnsupported(fmt.Sprintf("unknown OData client property: %s", key)) } } if err := e.writer.UpdateConsumedODataService(svc); err != nil { - return fmt.Errorf("failed to alter OData client: %w", err) + return mdlerrors.NewBackend("alter OData client", err) } e.invalidateHierarchy() fmt.Fprintf(e.output, "Altered OData client: %s.%s\n", modName, svc.Name) @@ -1118,23 +1119,23 @@ func (e *Executor) alterODataClient(stmt *ast.AlterODataClientStmt) error { } } - return fmt.Errorf("OData client not found: %s", stmt.Name) + return mdlerrors.NewNotFoundMsg("OData client", fmt.Sprint(stmt.Name), fmt.Sprintf("OData client not found: %s", stmt.Name)) } // dropODataClient handles DROP ODATA CLIENT command. func (e *Executor) dropODataClient(stmt *ast.DropODataClientStmt) error { if e.writer == nil { - return fmt.Errorf("not connected in write mode") + return mdlerrors.NewNotConnectedWrite() } services, err := e.reader.ListConsumedODataServices() if err != nil { - return fmt.Errorf("failed to list consumed OData services: %w", err) + return mdlerrors.NewBackend("list consumed OData services", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, svc := range services { @@ -1142,7 +1143,7 @@ func (e *Executor) dropODataClient(stmt *ast.DropODataClientStmt) error { modName := h.GetModuleName(modID) if strings.EqualFold(modName, stmt.Name.Module) && strings.EqualFold(svc.Name, stmt.Name.Name) { if err := e.writer.DeleteConsumedODataService(svc.ID); err != nil { - return fmt.Errorf("failed to drop OData client: %w", err) + return mdlerrors.NewBackend("drop OData client", err) } e.invalidateHierarchy() fmt.Fprintf(e.output, "Dropped OData client: %s.%s\n", modName, svc.Name) @@ -1150,17 +1151,17 @@ func (e *Executor) dropODataClient(stmt *ast.DropODataClientStmt) error { } } - return fmt.Errorf("OData client not found: %s", stmt.Name) + return mdlerrors.NewNotFoundMsg("OData client", fmt.Sprint(stmt.Name), fmt.Sprintf("OData client not found: %s", stmt.Name)) } // createODataService handles CREATE ODATA SERVICE command. func (e *Executor) createODataService(stmt *ast.CreateODataServiceStmt) error { if e.writer == nil { - return fmt.Errorf("not connected in write mode") + return mdlerrors.NewNotConnectedWrite() } if stmt.Name.Module == "" { - return fmt.Errorf("module name required: use CREATE ODATA SERVICE Module.Name (...)") + return mdlerrors.NewValidation("module name required: use CREATE ODATA SERVICE Module.Name (...)") } module, err := e.findModule(stmt.Name.Module) @@ -1204,13 +1205,13 @@ func (e *Executor) createODataService(stmt *ast.CreateODataServiceStmt) error { svc.AuthenticationTypes = stmt.AuthenticationTypes } if err := e.writer.UpdatePublishedODataService(svc); err != nil { - return fmt.Errorf("failed to update OData service: %w", err) + return mdlerrors.NewBackend("update OData service", err) } e.invalidateHierarchy() fmt.Fprintf(e.output, "Modified OData service: %s.%s\n", modName, svc.Name) return nil } - return fmt.Errorf("OData service already exists: %s.%s (use CREATE OR MODIFY to update)", modName, svc.Name) + return mdlerrors.NewAlreadyExistsMsg("OData service", modName+"."+svc.Name, fmt.Sprintf("OData service already exists: %s.%s (use CREATE OR MODIFY to update)", modName, svc.Name)) } } } @@ -1220,7 +1221,7 @@ func (e *Executor) createODataService(stmt *ast.CreateODataServiceStmt) error { if stmt.Folder != "" { folderID, err := e.resolveFolder(module.ID, stmt.Folder) if err != nil { - return fmt.Errorf("failed to resolve folder %s: %w", stmt.Folder, err) + return mdlerrors.NewBackend(fmt.Sprintf("resolve folder %s", stmt.Folder), err) } containerID = folderID } @@ -1248,7 +1249,7 @@ func (e *Executor) createODataService(stmt *ast.CreateODataServiceStmt) error { } if err := e.writer.CreatePublishedODataService(newSvc); err != nil { - return fmt.Errorf("failed to create OData service: %w", err) + return mdlerrors.NewBackend("create OData service", err) } e.invalidateHierarchy() fmt.Fprintf(e.output, "Created OData service: %s.%s\n", stmt.Name.Module, stmt.Name.Name) @@ -1258,17 +1259,17 @@ func (e *Executor) createODataService(stmt *ast.CreateODataServiceStmt) error { // alterODataService handles ALTER ODATA SERVICE command. func (e *Executor) alterODataService(stmt *ast.AlterODataServiceStmt) error { if e.writer == nil { - return fmt.Errorf("not connected in write mode") + return mdlerrors.NewNotConnectedWrite() } services, err := e.reader.ListPublishedODataServices() if err != nil { - return fmt.Errorf("failed to list published OData services: %w", err) + return mdlerrors.NewBackend("list published OData services", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, svc := range services { @@ -1295,11 +1296,11 @@ func (e *Executor) alterODataService(stmt *ast.AlterODataServiceStmt) error { case "publishassociations": svc.PublishAssociations = strings.EqualFold(strVal, "true") || strings.EqualFold(strVal, "yes") default: - return fmt.Errorf("unknown OData service property: %s", key) + return mdlerrors.NewUnsupported(fmt.Sprintf("unknown OData service property: %s", key)) } } if err := e.writer.UpdatePublishedODataService(svc); err != nil { - return fmt.Errorf("failed to alter OData service: %w", err) + return mdlerrors.NewBackend("alter OData service", err) } e.invalidateHierarchy() fmt.Fprintf(e.output, "Altered OData service: %s.%s\n", modName, svc.Name) @@ -1307,23 +1308,23 @@ func (e *Executor) alterODataService(stmt *ast.AlterODataServiceStmt) error { } } - return fmt.Errorf("OData service not found: %s", stmt.Name) + return mdlerrors.NewNotFoundMsg("OData service", fmt.Sprint(stmt.Name), fmt.Sprintf("OData service not found: %s", stmt.Name)) } // dropODataService handles DROP ODATA SERVICE command. func (e *Executor) dropODataService(stmt *ast.DropODataServiceStmt) error { if e.writer == nil { - return fmt.Errorf("not connected in write mode") + return mdlerrors.NewNotConnectedWrite() } services, err := e.reader.ListPublishedODataServices() if err != nil { - return fmt.Errorf("failed to list published OData services: %w", err) + return mdlerrors.NewBackend("list published OData services", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, svc := range services { @@ -1331,7 +1332,7 @@ func (e *Executor) dropODataService(stmt *ast.DropODataServiceStmt) error { modName := h.GetModuleName(modID) if strings.EqualFold(modName, stmt.Name.Module) && strings.EqualFold(svc.Name, stmt.Name.Name) { if err := e.writer.DeletePublishedODataService(svc.ID); err != nil { - return fmt.Errorf("failed to drop OData service: %w", err) + return mdlerrors.NewBackend("drop OData service", err) } e.invalidateHierarchy() fmt.Fprintf(e.output, "Dropped OData service: %s.%s\n", modName, svc.Name) @@ -1339,7 +1340,7 @@ func (e *Executor) dropODataService(stmt *ast.DropODataServiceStmt) error { } } - return fmt.Errorf("OData service not found: %s", stmt.Name) + return mdlerrors.NewNotFoundMsg("OData service", fmt.Sprint(stmt.Name), fmt.Sprintf("OData service not found: %s", stmt.Name)) } // formatExprValue formats a Mendix expression value for MDL output. @@ -1414,17 +1415,17 @@ func fetchODataMetadata(metadataUrl string) (metadata string, hash string, err e client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Get(metadataUrl) if err != nil { - return "", "", fmt.Errorf("failed to fetch $metadata from %s: %w", metadataUrl, err) + return "", "", mdlerrors.NewBackend(fmt.Sprintf("fetch $metadata from %s", metadataUrl), err) } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { - return "", "", fmt.Errorf("$metadata fetch returned HTTP %d from %s", resp.StatusCode, metadataUrl) + return "", "", mdlerrors.NewValidationf("$metadata fetch returned HTTP %d from %s", resp.StatusCode, metadataUrl) } body, err := io.ReadAll(resp.Body) if err != nil { - return "", "", fmt.Errorf("failed to read $metadata response: %w", err) + return "", "", mdlerrors.NewBackend("read $metadata response", err) } metadata = string(body) diff --git a/mdl/executor/cmd_oql_plan.go b/mdl/executor/cmd_oql_plan.go index db36e4e3..72958f23 100644 --- a/mdl/executor/cmd_oql_plan.go +++ b/mdl/executor/cmd_oql_plan.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/sdk/domainmodel" ) @@ -64,7 +65,7 @@ func (e *Executor) OqlQueryPlanELK(qualifiedName string, entity *domainmodel.Ent out, err := json.MarshalIndent(plan, "", " ") if err != nil { - return fmt.Errorf("failed to marshal JSON: %w", err) + return mdlerrors.NewBackend("marshal JSON", err) } fmt.Fprint(e.output, string(out)) return nil diff --git a/mdl/executor/cmd_page_wireframe.go b/mdl/executor/cmd_page_wireframe.go index 3664f486..48305885 100644 --- a/mdl/executor/cmd_page_wireframe.go +++ b/mdl/executor/cmd_page_wireframe.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/pages" ) @@ -89,25 +90,25 @@ func (c *wireframeCounter) next() string { // PageWireframeJSON generates wireframe JSON for a page. func (e *Executor) PageWireframeJSON(name string) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } parts := strings.SplitN(name, ".", 2) if len(parts) != 2 { - return fmt.Errorf("expected qualified name Module.Page, got: %s", name) + return mdlerrors.NewValidationf("expected qualified name Module.Page, got: %s", name) } qn := ast.QualifiedName{Module: parts[0], Name: parts[1]} h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Find the page allPages, err := e.reader.ListPages() if err != nil { - return fmt.Errorf("failed to list pages: %w", err) + return mdlerrors.NewBackend("list pages", err) } var foundPage *pages.Page @@ -121,7 +122,7 @@ func (e *Executor) PageWireframeJSON(name string) error { } if foundPage == nil { - return fmt.Errorf("page %s not found", name) + return mdlerrors.NewNotFound("page", name) } modID := h.FindModuleID(foundPage.ContainerID) @@ -192,7 +193,7 @@ func (e *Executor) PageWireframeJSON(name string) error { jsonBytes, err := json.Marshal(data) if err != nil { - return fmt.Errorf("failed to marshal wireframe JSON: %w", err) + return mdlerrors.NewBackend("marshal wireframe JSON", err) } fmt.Fprint(e.output, string(jsonBytes)) @@ -202,23 +203,23 @@ func (e *Executor) PageWireframeJSON(name string) error { // SnippetWireframeJSON generates wireframe JSON for a snippet. func (e *Executor) SnippetWireframeJSON(name string) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } parts := strings.SplitN(name, ".", 2) if len(parts) != 2 { - return fmt.Errorf("expected qualified name Module.Snippet, got: %s", name) + return mdlerrors.NewValidationf("expected qualified name Module.Snippet, got: %s", name) } qn := ast.QualifiedName{Module: parts[0], Name: parts[1]} allSnippets, err := e.reader.ListSnippets() if err != nil { - return fmt.Errorf("failed to list snippets: %w", err) + return mdlerrors.NewBackend("list snippets", err) } var foundSnippet *pages.Snippet @@ -232,7 +233,7 @@ func (e *Executor) SnippetWireframeJSON(name string) error { } if foundSnippet == nil { - return fmt.Errorf("snippet %s not found", name) + return mdlerrors.NewNotFound("snippet", name) } modID := h.FindModuleID(foundSnippet.ContainerID) @@ -256,7 +257,7 @@ func (e *Executor) SnippetWireframeJSON(name string) error { jsonBytes, err := json.Marshal(data) if err != nil { - return fmt.Errorf("failed to marshal wireframe JSON: %w", err) + return mdlerrors.NewBackend("marshal wireframe JSON", err) } fmt.Fprint(e.output, string(jsonBytes)) diff --git a/mdl/executor/cmd_pages_builder.go b/mdl/executor/cmd_pages_builder.go index ffb7d394..c7a7a8a7 100644 --- a/mdl/executor/cmd_pages_builder.go +++ b/mdl/executor/cmd_pages_builder.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/microflows" @@ -55,7 +56,7 @@ func (pb *pageBuilder) initPluggableEngine() { } registry, err := NewWidgetRegistry() if err != nil { - pb.pluggableEngineErr = fmt.Errorf("widget registry init failed: %w", err) + pb.pluggableEngineErr = mdlerrors.NewBackend("widget registry init", err) log.Printf("warning: %v", pb.pluggableEngineErr) return } @@ -75,7 +76,7 @@ func (pb *pageBuilder) registerWidgetName(name string, id model.ID) error { return nil // Anonymous widgets are allowed } if existingID, exists := pb.widgetScope[name]; exists { - return fmt.Errorf("duplicate widget name '%s': widget names must be unique within a page (existing ID: %s)", name, existingID) + return mdlerrors.NewAlreadyExistsMsg("widget", name, fmt.Sprintf("duplicate widget name '%s': widget names must be unique within a page (existing ID: %s)", name, existingID)) } pb.widgetScope[name] = id return nil @@ -163,12 +164,12 @@ func (pb *pageBuilder) getMicroflows() ([]*microflows.Microflow, error) { func (pb *pageBuilder) resolveLayout(layoutName string) (model.ID, error) { layouts, err := pb.getLayouts() if err != nil { - return "", fmt.Errorf("failed to list layouts: %w", err) + return "", mdlerrors.NewBackend("list layouts", err) } h, err := pb.getHierarchy() if err != nil { - return "", fmt.Errorf("failed to build hierarchy: %w", err) + return "", mdlerrors.NewBackend("build hierarchy", err) } // Parse qualified name @@ -190,7 +191,7 @@ func (pb *pageBuilder) resolveLayout(layoutName string) (model.ID, error) { } } - return "", fmt.Errorf("layout %s not found", layoutName) + return "", mdlerrors.NewNotFound("layout", layoutName) } // resolveEntity finds an entity by qualified name. @@ -198,12 +199,12 @@ func (pb *pageBuilder) resolveEntity(entityRef ast.QualifiedName) (model.ID, err // Get domain models which contain entities domainModels, err := pb.getDomainModels() if err != nil { - return "", fmt.Errorf("failed to list domain models: %w", err) + return "", mdlerrors.NewBackend("list domain models", err) } h, err := pb.getHierarchy() if err != nil { - return "", fmt.Errorf("failed to build hierarchy: %w", err) + return "", mdlerrors.NewBackend("build hierarchy", err) } // Search for entity in domain models @@ -216,7 +217,7 @@ func (pb *pageBuilder) resolveEntity(entityRef ast.QualifiedName) (model.ID, err } } - return "", fmt.Errorf("entity %s not found", entityRef.String()) + return "", mdlerrors.NewNotFound("entity", entityRef.String()) } // getModuleID returns the module ID for any container by using the hierarchy. @@ -271,7 +272,7 @@ func (pb *pageBuilder) resolveFolder(folderPath string) (model.ID, error) { folders, err := pb.getFolders() if err != nil { - return "", fmt.Errorf("failed to list folders: %w", err) + return "", mdlerrors.NewBackend("list folders", err) } // Split path into parts @@ -298,7 +299,7 @@ func (pb *pageBuilder) resolveFolder(folderPath string) (model.ID, error) { // Create the folder newFolderID, err := pb.createFolder(part, currentContainerID) if err != nil { - return "", fmt.Errorf("failed to create folder %s: %w", part, err) + return "", mdlerrors.NewBackend(fmt.Sprintf("create folder %s", part), err) } currentContainerID = newFolderID @@ -335,12 +336,12 @@ func (pb *pageBuilder) createFolder(name string, containerID model.ID) (model.ID // execDropPage handles DROP PAGE statement. func (e *Executor) execDropPage(s *ast.DropPageStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } pages, err := e.reader.ListPages() if err != nil { - return fmt.Errorf("failed to list pages: %w", err) + return mdlerrors.NewBackend("list pages", err) } for _, p := range pages { @@ -348,25 +349,25 @@ func (e *Executor) execDropPage(s *ast.DropPageStmt) error { modName := e.getModuleName(modID) if modName == s.Name.Module && p.Name == s.Name.Name { if err := e.writer.DeletePage(p.ID); err != nil { - return fmt.Errorf("failed to delete page: %w", err) + return mdlerrors.NewBackend("delete page", err) } fmt.Fprintf(e.output, "Dropped page %s\n", s.Name.String()) return nil } } - return fmt.Errorf("page %s not found", s.Name.String()) + return mdlerrors.NewNotFound("page", s.Name.String()) } // execDropSnippet handles DROP SNIPPET statement. func (e *Executor) execDropSnippet(s *ast.DropSnippetStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } snippets, err := e.reader.ListSnippets() if err != nil { - return fmt.Errorf("failed to list snippets: %w", err) + return mdlerrors.NewBackend("list snippets", err) } for _, snip := range snippets { @@ -374,12 +375,12 @@ func (e *Executor) execDropSnippet(s *ast.DropSnippetStmt) error { modName := e.getModuleName(modID) if modName == s.Name.Module && snip.Name == s.Name.Name { if err := e.writer.DeleteSnippet(snip.ID); err != nil { - return fmt.Errorf("failed to delete snippet: %w", err) + return mdlerrors.NewBackend("delete snippet", err) } fmt.Fprintf(e.output, "Dropped snippet %s\n", s.Name.String()) return nil } } - return fmt.Errorf("snippet %s not found", s.Name.String()) + return mdlerrors.NewNotFound("snippet", s.Name.String()) } diff --git a/mdl/executor/cmd_pages_builder_input.go b/mdl/executor/cmd_pages_builder_input.go index be49c22c..16bb6fcb 100644 --- a/mdl/executor/cmd_pages_builder_input.go +++ b/mdl/executor/cmd_pages_builder_input.go @@ -7,6 +7,7 @@ import ( "log" "strings" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/pages" @@ -262,7 +263,7 @@ func convertPropertyTypeIDs(src map[string]widgets.PropertyTypeIDEntry) map[stri // resolveSnippetRef resolves a snippet qualified name to its ID. func (pb *pageBuilder) resolveSnippetRef(snippetRef string) (model.ID, error) { if snippetRef == "" { - return "", fmt.Errorf("empty snippet reference") + return "", mdlerrors.NewValidation("empty snippet reference") } snippetRef = unquoteQualifiedName(snippetRef) @@ -282,7 +283,7 @@ func (pb *pageBuilder) resolveSnippetRef(snippetRef string) (model.ID, error) { h, err := pb.getHierarchy() if err != nil { - return "", fmt.Errorf("failed to build hierarchy: %w", err) + return "", mdlerrors.NewBackend("build hierarchy", err) } for _, s := range snippets { @@ -293,7 +294,7 @@ func (pb *pageBuilder) resolveSnippetRef(snippetRef string) (model.ID, error) { } } - return "", fmt.Errorf("snippet %s not found", snippetRef) + return "", mdlerrors.NewNotFound("snippet", snippetRef) } func (pb *pageBuilder) resolveMicroflow(qualifiedName string) (model.ID, error) { @@ -301,7 +302,7 @@ func (pb *pageBuilder) resolveMicroflow(qualifiedName string) (model.ID, error) // Parse qualified name parts := strings.Split(qualifiedName, ".") if len(parts) < 2 { - return "", fmt.Errorf("invalid microflow name: %s", qualifiedName) + return "", mdlerrors.NewValidationf("invalid microflow name: %s", qualifiedName) } moduleName := parts[0] mfName := strings.Join(parts[1:], ".") @@ -317,13 +318,13 @@ func (pb *pageBuilder) resolveMicroflow(qualifiedName string) (model.ID, error) // Get microflows from reader cache mfs, err := pb.getMicroflows() if err != nil { - return "", fmt.Errorf("failed to list microflows: %w", err) + return "", mdlerrors.NewBackend("list microflows", err) } // Use hierarchy to resolve module names (handles microflows in folders) h, err := pb.getHierarchy() if err != nil { - return "", fmt.Errorf("failed to build hierarchy: %w", err) + return "", mdlerrors.NewBackend("build hierarchy", err) } // Find matching microflow @@ -335,12 +336,12 @@ func (pb *pageBuilder) resolveMicroflow(qualifiedName string) (model.ID, error) } } - return "", fmt.Errorf("microflow not found: %s", qualifiedName) + return "", mdlerrors.NewNotFound("microflow", qualifiedName) } func (pb *pageBuilder) resolvePageRef(pageRef string) (model.ID, error) { if pageRef == "" { - return "", fmt.Errorf("empty page reference") + return "", mdlerrors.NewValidation("empty page reference") } pageRef = unquoteQualifiedName(pageRef) @@ -374,7 +375,7 @@ func (pb *pageBuilder) resolvePageRef(pageRef string) (model.ID, error) { h, err := pb.getHierarchy() if err != nil { - return "", fmt.Errorf("failed to build hierarchy: %w", err) + return "", mdlerrors.NewBackend("build hierarchy", err) } for _, p := range pgs { @@ -385,5 +386,5 @@ func (pb *pageBuilder) resolvePageRef(pageRef string) (model.ID, error) { } } - return "", fmt.Errorf("page %s not found", pageRef) + return "", mdlerrors.NewNotFound("page", pageRef) } diff --git a/mdl/executor/cmd_pages_builder_v3.go b/mdl/executor/cmd_pages_builder_v3.go index abc4176d..a5838ac9 100644 --- a/mdl/executor/cmd_pages_builder_v3.go +++ b/mdl/executor/cmd_pages_builder_v3.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/microflows" @@ -25,7 +26,7 @@ func (pb *pageBuilder) buildPageV3(s *ast.CreatePageStmtV3) (*pages.Page, error) if s.Folder != "" { folderID, err := pb.resolveFolder(s.Folder) if err != nil { - return nil, fmt.Errorf("failed to resolve folder %s: %w", s.Folder, err) + return nil, mdlerrors.NewBackend("resolve folder "+s.Folder, err) } containerID = folderID } @@ -95,7 +96,7 @@ func (pb *pageBuilder) buildPageV3(s *ast.CreatePageStmtV3) (*pages.Page, error) // Entity type parameter entityID, err := pb.resolveEntity(param.EntityType) if err != nil { - return nil, fmt.Errorf("failed to resolve entity %s: %w", param.EntityType.String(), err) + return nil, mdlerrors.NewBackend("resolve entity "+param.EntityType.String(), err) } entityName := param.EntityType.String() pageParam.EntityID = entityID @@ -153,7 +154,7 @@ func (pb *pageBuilder) buildPageV3(s *ast.CreatePageStmtV3) (*pages.Page, error) for _, astWidget := range expanded { w, err := pb.buildWidgetV3(astWidget) if err != nil { - return nil, fmt.Errorf("failed to build widget: %w", err) + return nil, mdlerrors.NewBackend("build widget", err) } containerWidget.Widgets = append(containerWidget.Widgets, w) } @@ -174,7 +175,7 @@ func (pb *pageBuilder) buildSnippetV3(s *ast.CreateSnippetStmtV3) (*pages.Snippe if s.Folder != "" { folderID, err := pb.resolveFolder(s.Folder) if err != nil { - return nil, fmt.Errorf("failed to resolve folder %s: %w", s.Folder, err) + return nil, mdlerrors.NewBackend("resolve folder "+s.Folder, err) } containerID = folderID } @@ -204,7 +205,7 @@ func (pb *pageBuilder) buildSnippetV3(s *ast.CreateSnippetStmtV3) (*pages.Snippe if param.EntityType.Name != "" { entityID, err := pb.resolveEntity(param.EntityType) if err != nil { - return nil, fmt.Errorf("failed to resolve entity %s: %w", param.EntityType.String(), err) + return nil, mdlerrors.NewBackend("resolve entity "+param.EntityType.String(), err) } entityName := param.EntityType.String() snippetParam.EntityID = entityID @@ -242,7 +243,7 @@ func (pb *pageBuilder) buildSnippetV3(s *ast.CreateSnippetStmtV3) (*pages.Snippe for _, astWidget := range expanded { w, err := pb.buildWidgetV3(astWidget) if err != nil { - return nil, fmt.Errorf("failed to build widget: %w", err) + return nil, mdlerrors.NewBackend("build widget", err) } snippet.Widgets = append(snippet.Widgets, w) } @@ -294,7 +295,7 @@ func (pb *pageBuilder) buildWidgetV3(w *ast.WidgetV3) (pages.Widget, error) { widget, err = pb.buildTabContainerV3(w) case "TABPAGE": // Tab pages are handled inside TabContainer - return nil, fmt.Errorf("TABPAGE must be a direct child of TABCONTAINER") + return nil, mdlerrors.NewValidation("TABPAGE must be a direct child of TABCONTAINER") case "GROUPBOX": widget, err = pb.buildGroupBoxV3(w) case "RADIOBUTTONS": @@ -303,7 +304,7 @@ func (pb *pageBuilder) buildWidgetV3(w *ast.WidgetV3) (pages.Widget, error) { widget, err = pb.buildNavigationListV3(w) case "ITEM": // Items are handled inside NavigationList - return nil, fmt.Errorf("ITEM must be a direct child of NAVIGATIONLIST") + return nil, mdlerrors.NewValidation("ITEM must be a direct child of NAVIGATIONLIST") case "SNIPPETCALL": widget, err = pb.buildSnippetCallV3(w) case "FOOTER": @@ -343,14 +344,14 @@ func (pb *pageBuilder) buildWidgetV3(w *ast.WidgetV3) (pages.Widget, error) { if def, ok := pb.widgetRegistry.GetByWidgetID(widgetType); ok { return pb.pluggableEngine.Build(def, w) } - return nil, fmt.Errorf("no definition for widget %s (run 'mxcli widget init -p app.mpr')", widgetType) + return nil, mdlerrors.NewNotFoundMsg("widget", widgetType, "no definition for widget "+widgetType+" (run 'mxcli widget init -p app.mpr')") } } } if pb.pluggableEngineErr != nil { - return nil, fmt.Errorf("unsupported widget type: %s (%v)", w.Type, pb.pluggableEngineErr) + return nil, mdlerrors.NewUnsupported(fmt.Sprintf("unsupported widget type: %s (%v)", w.Type, pb.pluggableEngineErr)) } - return nil, fmt.Errorf("unsupported widget type: %s", w.Type) + return nil, mdlerrors.NewUnsupported("unsupported widget type: " + w.Type) } if err != nil { @@ -484,7 +485,7 @@ func (pb *pageBuilder) buildDataSourceV3(ds *ast.DataSourceV3) (pages.DataSource entityName = pb.paramEntityNames["$"+paramName] } if !ok { - return nil, "", fmt.Errorf("parameter not found: %s", ds.Reference) + return nil, "", mdlerrors.NewNotFound("parameter", ds.Reference) } // Fallback to lookup if entity name not stored @@ -511,7 +512,7 @@ func (pb *pageBuilder) buildDataSourceV3(ds *ast.DataSourceV3) (pages.DataSource Name: pb.extractName(ds.Reference), }) if err != nil { - return nil, "", fmt.Errorf("failed to resolve entity: %w", err) + return nil, "", mdlerrors.NewBackend("resolve entity", err) } dbSource := &pages.DatabaseSource{ @@ -551,7 +552,7 @@ func (pb *pageBuilder) buildDataSourceV3(ds *ast.DataSourceV3) (pages.DataSource // Microflow source mfID, err := pb.resolveMicroflow(ds.Reference) if err != nil { - return nil, "", fmt.Errorf("failed to resolve microflow: %w", err) + return nil, "", mdlerrors.NewBackend("resolve microflow", err) } // Get entity name from microflow's return type for context resolution @@ -570,7 +571,7 @@ func (pb *pageBuilder) buildDataSourceV3(ds *ast.DataSourceV3) (pages.DataSource // Nanoflow source - resolve by listing all nanoflows nfID, err := pb.resolveNanoflowByName(ds.Reference) if err != nil { - return nil, "", fmt.Errorf("failed to resolve nanoflow: %w", err) + return nil, "", mdlerrors.NewBackend("resolve nanoflow", err) } // Get entity name from nanoflow's return type for context resolution @@ -621,7 +622,7 @@ func (pb *pageBuilder) buildDataSourceV3(ds *ast.DataSourceV3) (pages.DataSource widgetName := ds.Reference widgetID, ok := pb.widgetScope[widgetName] if !ok { - return nil, "", fmt.Errorf("widget not found for selection: %s", widgetName) + return nil, "", mdlerrors.NewNotFound("widget", widgetName) } // Get the entity context from the source widget if available @@ -637,7 +638,7 @@ func (pb *pageBuilder) buildDataSourceV3(ds *ast.DataSourceV3) (pages.DataSource }, entityName, nil default: - return nil, "", fmt.Errorf("unsupported datasource type: %s", ds.Type) + return nil, "", mdlerrors.NewUnsupported("unsupported datasource type: " + ds.Type) } } @@ -878,7 +879,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc Name: pb.extractName(action.Target), }) if err != nil { - return nil, fmt.Errorf("failed to resolve entity for create: %w", err) + return nil, mdlerrors.NewBackend("resolve entity for create", err) } createAct := &pages.CreateObjectClientAction{ @@ -894,7 +895,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc if action.ThenAction != nil && action.ThenAction.Type == "showPage" { pageID, err := pb.resolvePageRef(action.ThenAction.Target) if err != nil { - return nil, fmt.Errorf("failed to resolve page: %w", err) + return nil, mdlerrors.NewBackend("resolve page", err) } createAct.PageID = pageID createAct.PageName = action.ThenAction.Target @@ -905,7 +906,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc case "showPage": _, err := pb.resolvePageRef(action.Target) if err != nil { - return nil, fmt.Errorf("failed to resolve page: %w", err) + return nil, mdlerrors.NewBackend("resolve page", err) } pageAction := &pages.PageClientAction{ @@ -944,7 +945,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc case "microflow": mfID, err := pb.resolveMicroflow(action.Target) if err != nil { - return nil, fmt.Errorf("failed to resolve microflow: %w", err) + return nil, mdlerrors.NewBackend("resolve microflow", err) } mfAction := &pages.MicroflowClientAction{ @@ -984,7 +985,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc case "nanoflow": nfID, err := pb.resolveNanoflowByName(action.Target) if err != nil { - return nil, fmt.Errorf("failed to resolve nanoflow: %w", err) + return nil, mdlerrors.NewBackend("resolve nanoflow", err) } nfAction := &pages.NanoflowClientAction{ @@ -1051,7 +1052,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc }, nil default: - return nil, fmt.Errorf("unsupported action type: %s", action.Type) + return nil, mdlerrors.NewUnsupported("unsupported action type: " + action.Type) } } @@ -1097,7 +1098,7 @@ func (pb *pageBuilder) getEntityNameByID(entityID model.ID) (string, error) { } } } - return "", fmt.Errorf("entity not found by ID: %s", entityID) + return "", mdlerrors.NewNotFound("entity", string(entityID)) } // pageParamBSONType maps a DataType to the BSON $Type string for primitive page parameters. @@ -1135,7 +1136,7 @@ func (pb *pageBuilder) resolveNanoflowByName(nfName string) (model.ID, error) { nanoflows, err := pb.reader.ListNanoflows() if err != nil { - return "", fmt.Errorf("failed to list nanoflows: %w", err) + return "", mdlerrors.NewBackend("list nanoflows", err) } h, err := pb.getHierarchy() @@ -1157,7 +1158,7 @@ func (pb *pageBuilder) resolveNanoflowByName(nfName string) (model.ID, error) { } } - return "", fmt.Errorf("nanoflow not found: %s", nfName) + return "", mdlerrors.NewNotFound("nanoflow", nfName) } // mdlTypeToBsonType converts an MDL type name to a BSON DataTypes$* type string. @@ -1351,11 +1352,11 @@ func (pb *pageBuilder) expandIfFragment(w *ast.WidgetV3) ([]*ast.WidgetV3, error } if pb.fragments == nil { - return nil, fmt.Errorf("fragment %q not found", w.Name) + return nil, mdlerrors.NewNotFound("fragment", w.Name) } frag, ok := pb.fragments[w.Name] if !ok { - return nil, fmt.Errorf("fragment %q not found", w.Name) + return nil, mdlerrors.NewNotFound("fragment", w.Name) } widgets := cloneWidgets(frag.Widgets) diff --git a/mdl/executor/cmd_pages_builder_v3_pluggable.go b/mdl/executor/cmd_pages_builder_v3_pluggable.go index 62e71fb1..072959f7 100644 --- a/mdl/executor/cmd_pages_builder_v3_pluggable.go +++ b/mdl/executor/cmd_pages_builder_v3_pluggable.go @@ -9,6 +9,7 @@ import ( "go.mongodb.org/mongo-driver/bson" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/sdk/mpr" ) @@ -97,7 +98,7 @@ func (pb *pageBuilder) buildWidgetV3ToBSON(w *ast.WidgetV3) (bson.D, error) { // TypePointers reference the Type's PropertyType IDs (not regenerated). func (pb *pageBuilder) createAttributeObject(attributePath string, objectTypeID, propertyTypeID, valueTypeID string) (bson.D, error) { if strings.Count(attributePath, ".") < 2 { - return nil, fmt.Errorf("invalid attribute path %q: expected Module.Entity.Attribute format", attributePath) + return nil, mdlerrors.NewValidationf("invalid attribute path %q: expected Module.Entity.Attribute format", attributePath) } return bson.D{ {Key: "$ID", Value: hexToBytes(mpr.GenerateID())}, diff --git a/mdl/executor/cmd_pages_builder_v3_widgets.go b/mdl/executor/cmd_pages_builder_v3_widgets.go index db026dd5..27d98581 100644 --- a/mdl/executor/cmd_pages_builder_v3_widgets.go +++ b/mdl/executor/cmd_pages_builder_v3_widgets.go @@ -10,6 +10,7 @@ import ( "go.mongodb.org/mongo-driver/bson" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/pages" @@ -31,7 +32,7 @@ func (pb *pageBuilder) buildDataViewV3(w *ast.WidgetV3) (*pages.DataView, error) if ds := w.GetDataSource(); ds != nil { dataSource, entityName, err := pb.buildDataSourceV3(ds) if err != nil { - return nil, fmt.Errorf("failed to build datasource: %w", err) + return nil, mdlerrors.NewBackend("build datasource", err) } dv.DataSource = dataSource @@ -95,10 +96,10 @@ func (pb *pageBuilder) buildDataGridV3(w *ast.WidgetV3) (*pages.CustomWidget, er // Load embedded template (required for pluggable widgets to work) embeddedType, embeddedObject, embeddedIDs, embeddedObjectTypeID, err := widgets.GetTemplateFullBSON(pages.WidgetIDDataGrid2, mpr.GenerateID, pb.reader.Path()) if err != nil { - return nil, fmt.Errorf("failed to load DataGrid2 template: %w", err) + return nil, mdlerrors.NewBackend("load DataGrid2 template", err) } if embeddedType == nil || embeddedObject == nil { - return nil, fmt.Errorf("DataGrid2 template not found") + return nil, mdlerrors.NewNotFound("widget template", "DataGrid2") } // Convert widget IDs to pages.PropertyTypeIDEntry format @@ -109,7 +110,7 @@ func (pb *pageBuilder) buildDataGridV3(w *ast.WidgetV3) (*pages.CustomWidget, er if ds := w.GetDataSource(); ds != nil { dataSource, entityName, err := pb.buildDataSourceV3(ds) if err != nil { - return nil, fmt.Errorf("failed to build datasource: %w", err) + return nil, mdlerrors.NewBackend("build datasource", err) } datasource = dataSource @@ -145,7 +146,7 @@ func (pb *pageBuilder) buildDataGridV3(w *ast.WidgetV3) (*pages.CustomWidget, er for _, controlBarChild := range child.Children { widgetBSON, err := pb.buildWidgetV3ToBSON(controlBarChild) if err != nil { - return nil, fmt.Errorf("failed to build controlbar widget: %w", err) + return nil, mdlerrors.NewBackend("build controlbar widget", err) } if widgetBSON != nil { headerWidgets = append(headerWidgets, widgetBSON) @@ -239,7 +240,7 @@ func (pb *pageBuilder) buildListViewV3(w *ast.WidgetV3) (*pages.ListView, error) if ds := w.GetDataSource(); ds != nil { dataSource, entityName, err := pb.buildDataSourceV3(ds) if err != nil { - return nil, fmt.Errorf("failed to build datasource: %w", err) + return nil, mdlerrors.NewBackend("build datasource", err) } lv.DataSource = dataSource @@ -674,7 +675,7 @@ func (pb *pageBuilder) buildButtonV3(w *ast.WidgetV3) (*pages.ActionButton, erro if action := w.GetAction(); action != nil { act, err := pb.buildClientActionV3(action) if err != nil { - return nil, fmt.Errorf("failed to build action: %w", err) + return nil, mdlerrors.NewBackend("build action", err) } btn.Action = act } @@ -719,7 +720,7 @@ func (pb *pageBuilder) buildNavigationListV3(w *ast.WidgetV3) (*pages.Navigation // buildNavigationListItemV3 creates a NavigationListItem from V3 syntax. func (pb *pageBuilder) buildNavigationListItemV3(w *ast.WidgetV3) (*pages.NavigationListItem, error) { if w.Name == "" { - return nil, fmt.Errorf("ITEM inside NAVIGATIONLIST requires a name") + return nil, mdlerrors.NewValidation("ITEM inside NAVIGATIONLIST requires a name") } item := &pages.NavigationListItem{ @@ -782,7 +783,7 @@ func (pb *pageBuilder) buildSnippetCallV3(w *ast.WidgetV3) (*pages.SnippetCallWi if snippetName := w.GetSnippet(); snippetName != "" { snippetID, err := pb.resolveSnippetRef(snippetName) if err != nil { - return nil, fmt.Errorf("failed to resolve snippet %s: %w", snippetName, err) + return nil, mdlerrors.NewBackend(fmt.Sprintf("resolve snippet %s", snippetName), err) } sc.SnippetID = snippetID sc.SnippetName = snippetName // Store qualified name for BY_NAME_REFERENCE serialization diff --git a/mdl/executor/cmd_pages_create_v3.go b/mdl/executor/cmd_pages_create_v3.go index d4b2fa8d..3166eedc 100644 --- a/mdl/executor/cmd_pages_create_v3.go +++ b/mdl/executor/cmd_pages_create_v3.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" ) @@ -16,7 +17,7 @@ import ( // execCreatePageV3 handles CREATE PAGE statement with V3 syntax. func (e *Executor) execCreatePageV3(s *ast.CreatePageStmtV3) error { if e.writer == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Version pre-check: page parameters require 11.0+ @@ -31,7 +32,7 @@ func (e *Executor) execCreatePageV3(s *ast.CreatePageStmtV3) error { // Find or auto-create module module, err := e.findOrCreateModule(s.Name.Module) if err != nil { - return fmt.Errorf("failed to find module %s: %w", s.Name.Module, err) + return mdlerrors.NewBackend(fmt.Sprintf("find module %s", s.Name.Module), err) } moduleID := module.ID @@ -43,7 +44,7 @@ func (e *Executor) execCreatePageV3(s *ast.CreatePageStmtV3) error { modName := e.getModuleName(modID) if modName == s.Name.Module && p.Name == s.Name.Name { if !s.IsReplace && !s.IsModify && len(pagesToDelete) == 0 { - return fmt.Errorf("page %s already exists", s.Name.String()) + return mdlerrors.NewAlreadyExists("page", s.Name.String()) } pagesToDelete = append(pagesToDelete, p.ID) } @@ -65,7 +66,7 @@ func (e *Executor) execCreatePageV3(s *ast.CreatePageStmtV3) error { page, err := pb.buildPageV3(s) if err != nil { - return fmt.Errorf("failed to build page: %w", err) + return mdlerrors.NewBackend("build page", err) } // Replace or create the page in the MPR @@ -73,17 +74,17 @@ func (e *Executor) execCreatePageV3(s *ast.CreatePageStmtV3) error { // Reuse first existing page's UUID to avoid git delete+add (which crashes Studio Pro RevStatusCache) page.ID = pagesToDelete[0] if err := e.writer.UpdatePage(page); err != nil { - return fmt.Errorf("failed to update page: %w", err) + return mdlerrors.NewBackend("update page", err) } // Delete any additional duplicates for _, id := range pagesToDelete[1:] { if err := e.writer.DeletePage(id); err != nil { - return fmt.Errorf("failed to delete duplicate page: %w", err) + return mdlerrors.NewBackend("delete duplicate page", err) } } } else { if err := e.writer.CreatePage(page); err != nil { - return fmt.Errorf("failed to create page: %w", err) + return mdlerrors.NewBackend("create page", err) } } @@ -100,13 +101,13 @@ func (e *Executor) execCreatePageV3(s *ast.CreatePageStmtV3) error { // execCreateSnippetV3 handles CREATE SNIPPET statement with V3 syntax. func (e *Executor) execCreateSnippetV3(s *ast.CreateSnippetStmtV3) error { if e.writer == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Find or auto-create module module, err := e.findOrCreateModule(s.Name.Module) if err != nil { - return fmt.Errorf("failed to find module %s: %w", s.Name.Module, err) + return mdlerrors.NewBackend(fmt.Sprintf("find module %s", s.Name.Module), err) } moduleID := module.ID @@ -118,7 +119,7 @@ func (e *Executor) execCreateSnippetV3(s *ast.CreateSnippetStmtV3) error { modName := e.getModuleName(modID) if modName == s.Name.Module && snip.Name == s.Name.Name { if !s.IsReplace && !s.IsModify && len(snippetsToDelete) == 0 { - return fmt.Errorf("snippet %s already exists", s.Name.String()) + return mdlerrors.NewAlreadyExists("snippet", s.Name.String()) } snippetsToDelete = append(snippetsToDelete, snip.ID) } @@ -140,19 +141,19 @@ func (e *Executor) execCreateSnippetV3(s *ast.CreateSnippetStmtV3) error { snippet, err := pb.buildSnippetV3(s) if err != nil { - return fmt.Errorf("failed to build snippet: %w", err) + return mdlerrors.NewBackend("build snippet", err) } // Delete old snippets only after successful build for _, id := range snippetsToDelete { if err := e.writer.DeleteSnippet(id); err != nil { - return fmt.Errorf("failed to delete existing snippet: %w", err) + return mdlerrors.NewBackend("delete existing snippet", err) } } // Create the snippet in the MPR if err := e.writer.CreateSnippet(snippet); err != nil { - return fmt.Errorf("failed to create snippet: %w", err) + return mdlerrors.NewBackend("create snippet", err) } // Track the created snippet so it can be resolved by subsequent snippet references diff --git a/mdl/executor/cmd_pages_describe.go b/mdl/executor/cmd_pages_describe.go index 752c8c3e..bf2d4917 100644 --- a/mdl/executor/cmd_pages_describe.go +++ b/mdl/executor/cmd_pages_describe.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/pages" @@ -23,13 +24,13 @@ func (e *Executor) describePage(name ast.QualifiedName) error { // Get hierarchy for module/folder resolution h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Find the page allPages, err := e.reader.ListPages() if err != nil { - return fmt.Errorf("failed to list pages: %w", err) + return mdlerrors.NewBackend("list pages", err) } var foundPage *pages.Page @@ -43,7 +44,7 @@ func (e *Executor) describePage(name ast.QualifiedName) error { } if foundPage == nil { - return fmt.Errorf("page %s not found", name.String()) + return mdlerrors.NewNotFound("page", name.String()) } // Get module name for the page @@ -178,13 +179,13 @@ func (e *Executor) describeSnippet(name ast.QualifiedName) error { // Get hierarchy for module/folder resolution h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Find the snippet allSnippets, err := e.reader.ListSnippets() if err != nil { - return fmt.Errorf("failed to list snippets: %w", err) + return mdlerrors.NewBackend("list snippets", err) } var foundSnippet *pages.Snippet @@ -198,7 +199,7 @@ func (e *Executor) describeSnippet(name ast.QualifiedName) error { } if foundSnippet == nil { - return fmt.Errorf("snippet %s not found", name.String()) + return mdlerrors.NewNotFound("snippet", name.String()) } // Get module name for the snippet @@ -261,13 +262,13 @@ func (e *Executor) describeLayout(name ast.QualifiedName) error { // Get hierarchy for module/folder resolution h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Find the layout allLayouts, err := e.reader.ListLayouts() if err != nil { - return fmt.Errorf("failed to list layouts: %w", err) + return mdlerrors.NewBackend("list layouts", err) } var foundLayout *pages.Layout @@ -281,7 +282,7 @@ func (e *Executor) describeLayout(name ast.QualifiedName) error { } if foundLayout == nil { - return fmt.Errorf("layout %s not found", name.String()) + return mdlerrors.NewNotFound("layout", name.String()) } // Get module name for the layout diff --git a/mdl/executor/cmd_pages_show.go b/mdl/executor/cmd_pages_show.go index ae8190e4..9befd535 100644 --- a/mdl/executor/cmd_pages_show.go +++ b/mdl/executor/cmd_pages_show.go @@ -6,6 +6,8 @@ import ( "fmt" "sort" "strings" + + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" ) // showPages handles SHOW PAGES command. @@ -13,13 +15,13 @@ func (e *Executor) showPages(moduleName string) error { // Get hierarchy for module/folder resolution h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Get all pages pages, err := e.reader.ListPages() if err != nil { - return fmt.Errorf("failed to list pages: %w", err) + return mdlerrors.NewBackend("list pages", err) } // Collect rows diff --git a/mdl/executor/cmd_published_rest.go b/mdl/executor/cmd_published_rest.go index c8b7d8fb..d118bf52 100644 --- a/mdl/executor/cmd_published_rest.go +++ b/mdl/executor/cmd_published_rest.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" ) @@ -15,12 +16,12 @@ import ( func (e *Executor) showPublishedRestServices(moduleName string) error { services, err := e.reader.ListPublishedRestServices() if err != nil { - return fmt.Errorf("failed to list published REST services: %w", err) + return mdlerrors.NewBackend("list published REST services", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } type row struct { @@ -77,12 +78,12 @@ func (e *Executor) showPublishedRestServices(moduleName string) error { func (e *Executor) describePublishedRestService(name ast.QualifiedName) error { services, err := e.reader.ListPublishedRestServices() if err != nil { - return fmt.Errorf("failed to list published REST services: %w", err) + return mdlerrors.NewBackend("list published REST services", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, svc := range services { @@ -150,7 +151,7 @@ func (e *Executor) describePublishedRestService(name ast.QualifiedName) error { return nil } - return fmt.Errorf("published REST service not found: %s", name) + return mdlerrors.NewNotFound("published REST service", name.String()) } // findPublishedRestService looks up a published REST service by module and name. @@ -170,13 +171,13 @@ func (e *Executor) findPublishedRestService(moduleName, name string) (*model.Pub return svc, nil } } - return nil, fmt.Errorf("not found") + return nil, mdlerrors.NewNotFoundMsg("published REST service", "", "not found") } // execCreatePublishedRestService creates a new published REST service. func (e *Executor) execCreatePublishedRestService(s *ast.CreatePublishedRestServiceStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } if err := e.checkFeature("integration", "published_rest_service", @@ -189,21 +190,21 @@ func (e *Executor) execCreatePublishedRestService(s *ast.CreatePublishedRestServ if s.CreateOrReplace { if existing, _ := e.findPublishedRestService(s.Name.Module, s.Name.Name); existing != nil { if err := e.writer.DeletePublishedRestService(existing.ID); err != nil { - return fmt.Errorf("failed to replace existing service: %w", err) + return mdlerrors.NewBackend("replace existing service", err) } } } module, err := e.findModule(s.Name.Module) if err != nil { - return fmt.Errorf("module %s not found", s.Name.Module) + return mdlerrors.NewNotFound("module", s.Name.Module) } containerID := module.ID if s.Folder != "" { folderID, err := e.resolveFolder(module.ID, s.Folder) if err != nil { - return fmt.Errorf("failed to resolve folder '%s': %w", s.Folder, err) + return mdlerrors.NewBackend(fmt.Sprintf("resolve folder '%s'", s.Folder), err) } containerID = folderID } @@ -234,7 +235,7 @@ func (e *Executor) execCreatePublishedRestService(s *ast.CreatePublishedRestServ } if err := e.writer.CreatePublishedRestService(svc); err != nil { - return fmt.Errorf("failed to create published REST service: %w", err) + return mdlerrors.NewBackend("create published REST service", err) } if !e.quiet { @@ -246,12 +247,12 @@ func (e *Executor) execCreatePublishedRestService(s *ast.CreatePublishedRestServ // execDropPublishedRestService deletes a published REST service. func (e *Executor) execDropPublishedRestService(s *ast.DropPublishedRestServiceStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } services, err := e.reader.ListPublishedRestServices() if err != nil { - return fmt.Errorf("failed to list published REST services: %w", err) + return mdlerrors.NewBackend("list published REST services", err) } h, err := e.getHierarchy() @@ -264,7 +265,7 @@ func (e *Executor) execDropPublishedRestService(s *ast.DropPublishedRestServiceS modName := h.GetModuleName(modID) if modName == s.Name.Module && svc.Name == s.Name.Name { if err := e.writer.DeletePublishedRestService(svc.ID); err != nil { - return fmt.Errorf("failed to drop published REST service: %w", err) + return mdlerrors.NewBackend("drop published REST service", err) } if !e.quiet { fmt.Fprintf(e.output, "Dropped published REST service %s.%s\n", s.Name.Module, s.Name.Name) @@ -273,7 +274,7 @@ func (e *Executor) execDropPublishedRestService(s *ast.DropPublishedRestServiceS } } - return fmt.Errorf("published REST service %s.%s not found", s.Name.Module, s.Name.Name) + return mdlerrors.NewNotFound("published REST service", s.Name.Module+"."+s.Name.Name) } // astResourceDefToModel converts an AST PublishedRestResourceDef to the @@ -295,7 +296,7 @@ func astResourceDefToModel(def *ast.PublishedRestResourceDef) *model.PublishedRe // actions to an existing published REST service. func (e *Executor) execAlterPublishedRestService(s *ast.AlterPublishedRestServiceStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } if err := e.checkFeature("integration", "published_rest_alter", @@ -306,7 +307,7 @@ func (e *Executor) execAlterPublishedRestService(s *ast.AlterPublishedRestServic svc, err := e.findPublishedRestService(s.Name.Module, s.Name.Name) if err != nil { - return fmt.Errorf("published REST service %s.%s not found", s.Name.Module, s.Name.Name) + return mdlerrors.NewNotFound("published REST service", s.Name.Module+"."+s.Name.Name) } for _, action := range s.Actions { @@ -321,7 +322,7 @@ func (e *Executor) execAlterPublishedRestService(s *ast.AlterPublishedRestServic case "servicename": svc.ServiceName = val default: - return fmt.Errorf("unknown published REST service property: %s (allowed: Path, Version, ServiceName)", key) + return mdlerrors.NewUnsupported(fmt.Sprintf("unknown published REST service property: %s (allowed: Path, Version, ServiceName)", key)) } } @@ -329,7 +330,7 @@ func (e *Executor) execAlterPublishedRestService(s *ast.AlterPublishedRestServic // Reject duplicate resource names for _, existing := range svc.Resources { if existing.Name == a.Resource.Name { - return fmt.Errorf("resource '%s' already exists on %s.%s", a.Resource.Name, s.Name.Module, s.Name.Name) + return mdlerrors.NewAlreadyExistsMsg("resource", a.Resource.Name, fmt.Sprintf("resource '%s' already exists on %s.%s", a.Resource.Name, s.Name.Module, s.Name.Name)) } } svc.Resources = append(svc.Resources, astResourceDefToModel(a.Resource)) @@ -343,17 +344,17 @@ func (e *Executor) execAlterPublishedRestService(s *ast.AlterPublishedRestServic } } if idx == -1 { - return fmt.Errorf("resource '%s' not found on %s.%s", a.Name, s.Name.Module, s.Name.Name) + return mdlerrors.NewNotFoundMsg("resource", a.Name, fmt.Sprintf("resource '%s' not found on %s.%s", a.Name, s.Name.Module, s.Name.Name)) } svc.Resources = append(svc.Resources[:idx], svc.Resources[idx+1:]...) default: - return fmt.Errorf("unsupported alter action: %T", action) + return mdlerrors.NewUnsupported(fmt.Sprintf("unsupported alter action: %T", action)) } } if err := e.writer.UpdatePublishedRestService(svc); err != nil { - return fmt.Errorf("failed to alter published REST service: %w", err) + return mdlerrors.NewBackend("alter published REST service", err) } if !e.quiet { diff --git a/mdl/executor/cmd_rename.go b/mdl/executor/cmd_rename.go index 1204c94a..66b05062 100644 --- a/mdl/executor/cmd_rename.go +++ b/mdl/executor/cmd_rename.go @@ -8,13 +8,14 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/sdk/mpr" ) // execRename handles RENAME statements for all document types. func (e *Executor) execRename(s *ast.RenameStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } switch s.ObjectType { @@ -35,7 +36,7 @@ func (e *Executor) execRename(s *ast.RenameStmt) error { case "MODULE": return e.execRenameModule(s) default: - return fmt.Errorf("RENAME not supported for %s", s.ObjectType) + return mdlerrors.NewUnsupported(fmt.Sprintf("RENAME not supported for %s", s.ObjectType)) } } @@ -49,7 +50,7 @@ func (e *Executor) execRenameEntity(s *ast.RenameStmt) error { dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } found := false @@ -60,7 +61,7 @@ func (e *Executor) execRenameEntity(s *ast.RenameStmt) error { } } if !found { - return fmt.Errorf("entity not found: %s", s.Name) + return mdlerrors.NewNotFound("entity", s.Name.String()) } oldQualifiedName := s.Name.Module + "." + s.Name.Name @@ -69,7 +70,7 @@ func (e *Executor) execRenameEntity(s *ast.RenameStmt) error { // Scan for references hits, err := e.writer.RenameReferences(oldQualifiedName, newQualifiedName, s.DryRun) if err != nil { - return fmt.Errorf("failed to scan references: %w", err) + return mdlerrors.NewBackend("scan references", err) } if s.DryRun { @@ -85,7 +86,7 @@ func (e *Executor) execRenameEntity(s *ast.RenameStmt) error { } } if err := e.writer.UpdateDomainModel(dm); err != nil { - return fmt.Errorf("failed to update entity name: %w", err) + return mdlerrors.NewBackend("update entity name", err) } e.invalidateHierarchy() @@ -112,13 +113,13 @@ func (e *Executor) execRenameModule(s *ast.RenameStmt) error { // Module rename replaces "OldModule." with "NewModule." in all qualified names hits, err := e.writer.RenameReferences(oldModuleName+".", newModuleName+".", s.DryRun) if err != nil { - return fmt.Errorf("failed to scan references: %w", err) + return mdlerrors.NewBackend("scan references", err) } // Also scan for exact module name matches (e.g., in navigation, security role refs) exactHits, err := e.writer.RenameReferences(oldModuleName, newModuleName, s.DryRun) if err != nil { - return fmt.Errorf("failed to scan exact module references: %w", err) + return mdlerrors.NewBackend("scan exact module references", err) } // Merge hit lists (deduplicate by unit ID) @@ -132,7 +133,7 @@ func (e *Executor) execRenameModule(s *ast.RenameStmt) error { // Update the module name module.Name = newModuleName if err := e.writer.UpdateModule(module); err != nil { - return fmt.Errorf("failed to update module name: %w", err) + return mdlerrors.NewBackend("update module name", err) } e.invalidateHierarchy() @@ -200,7 +201,7 @@ func (e *Executor) execRenameDocument(s *ast.RenameStmt, docType string) error { } if !found { - return fmt.Errorf("%s not found: %s", s.ObjectType, oldQualifiedName) + return mdlerrors.NewNotFound(s.ObjectType, oldQualifiedName) } // The reference scanner will also update the document's own Name field @@ -210,7 +211,7 @@ func (e *Executor) execRenameDocument(s *ast.RenameStmt, docType string) error { // update the Name field directly. hits, err := e.writer.RenameReferences(oldQualifiedName, newQualifiedName, s.DryRun) if err != nil { - return fmt.Errorf("failed to scan references: %w", err) + return mdlerrors.NewBackend("scan references", err) } if s.DryRun { @@ -220,7 +221,7 @@ func (e *Executor) execRenameDocument(s *ast.RenameStmt, docType string) error { // Update the document's own Name field via the raw BSON name updater if err := e.writer.RenameDocumentByName(s.Name.Module, s.Name.Name, s.NewName); err != nil { - return fmt.Errorf("failed to rename %s: %w", docType, err) + return mdlerrors.NewBackend(fmt.Sprintf("rename %s", docType), err) } e.invalidateHierarchy() @@ -240,7 +241,7 @@ func (e *Executor) execRenameEnumeration(s *ast.RenameStmt) error { // Verify it exists enums, err := e.reader.ListEnumerations() if err != nil { - return fmt.Errorf("failed to list enumerations: %w", err) + return mdlerrors.NewBackend("list enumerations", err) } h, err := e.getHierarchy() if err != nil { @@ -255,12 +256,12 @@ func (e *Executor) execRenameEnumeration(s *ast.RenameStmt) error { } } if !found { - return fmt.Errorf("enumeration not found: %s", oldQualifiedName) + return mdlerrors.NewNotFound("enumeration", oldQualifiedName) } hits, err := e.writer.RenameReferences(oldQualifiedName, newQualifiedName, s.DryRun) if err != nil { - return fmt.Errorf("failed to scan references: %w", err) + return mdlerrors.NewBackend("scan references", err) } if s.DryRun { @@ -270,7 +271,7 @@ func (e *Executor) execRenameEnumeration(s *ast.RenameStmt) error { // Update enumeration name via raw BSON if err := e.writer.RenameDocumentByName(s.Name.Module, s.Name.Name, s.NewName); err != nil { - return fmt.Errorf("failed to rename enumeration: %w", err) + return mdlerrors.NewBackend("rename enumeration", err) } // Also update enumeration refs in domain models (attribute types store qualified enum names) @@ -298,7 +299,7 @@ func (e *Executor) execRenameAssociation(s *ast.RenameStmt) error { dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } found := false @@ -309,12 +310,12 @@ func (e *Executor) execRenameAssociation(s *ast.RenameStmt) error { } } if !found { - return fmt.Errorf("association not found: %s", oldQualifiedName) + return mdlerrors.NewNotFound("association", oldQualifiedName) } hits, err := e.writer.RenameReferences(oldQualifiedName, newQualifiedName, s.DryRun) if err != nil { - return fmt.Errorf("failed to scan references: %w", err) + return mdlerrors.NewBackend("scan references", err) } if s.DryRun { @@ -330,7 +331,7 @@ func (e *Executor) execRenameAssociation(s *ast.RenameStmt) error { } } if err := e.writer.UpdateDomainModel(dm); err != nil { - return fmt.Errorf("failed to update association name: %w", err) + return mdlerrors.NewBackend("update association name", err) } e.invalidateHierarchy() diff --git a/mdl/executor/cmd_rest_clients.go b/mdl/executor/cmd_rest_clients.go index ca44d48e..c4b304a5 100644 --- a/mdl/executor/cmd_rest_clients.go +++ b/mdl/executor/cmd_rest_clients.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" ) @@ -24,12 +25,12 @@ func safeIdent(name string) string { func (e *Executor) showRestClients(moduleName string) error { services, err := e.reader.ListConsumedRestServices() if err != nil { - return fmt.Errorf("failed to list consumed REST services: %w", err) + return mdlerrors.NewBackend("list consumed REST services", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } type row struct { @@ -85,12 +86,12 @@ func (e *Executor) showRestClients(moduleName string) error { func (e *Executor) describeRestClient(name ast.QualifiedName) error { services, err := e.reader.ListConsumedRestServices() if err != nil { - return fmt.Errorf("failed to list consumed REST services: %w", err) + return mdlerrors.NewBackend("list consumed REST services", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, svc := range services { @@ -101,7 +102,7 @@ func (e *Executor) describeRestClient(name ast.QualifiedName) error { } } - return fmt.Errorf("consumed REST service not found: %s", name) + return mdlerrors.NewNotFound("consumed REST service", name.String()) } // outputConsumedRestServiceMDL outputs a consumed REST service in the property-based { } format. @@ -277,7 +278,7 @@ func writeExportMappings(w io.Writer, mappings []*model.RestResponseMapping, ind // createRestClient handles CREATE REST CLIENT statement. func (e *Executor) createRestClient(stmt *ast.CreateRestClientStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project (read-only mode)") + return mdlerrors.NewNotConnectedWrite() } // Version pre-check: REST clients require 10.1+ @@ -290,14 +291,14 @@ func (e *Executor) createRestClient(stmt *ast.CreateRestClientStmt) error { moduleName := stmt.Name.Module module, err := e.findModule(moduleName) if err != nil { - return fmt.Errorf("module not found: %s", moduleName) + return mdlerrors.NewNotFound("module", moduleName) } // Check for existing service with same name existingServices, _ := e.reader.ListConsumedRestServices() h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, existing := range existingServices { @@ -307,10 +308,10 @@ func (e *Executor) createRestClient(stmt *ast.CreateRestClientStmt) error { if stmt.CreateOrModify { // Delete existing and recreate if err := e.writer.DeleteConsumedRestService(existing.ID); err != nil { - return fmt.Errorf("failed to delete existing REST client: %w", err) + return mdlerrors.NewBackend("delete existing REST client", err) } } else { - return fmt.Errorf("REST client already exists: %s.%s (use CREATE OR MODIFY to overwrite)", moduleName, stmt.Name.Name) + return mdlerrors.NewAlreadyExistsMsg("REST client", moduleName+"."+stmt.Name.Name, fmt.Sprintf("REST client already exists: %s.%s (use CREATE OR MODIFY to overwrite)", moduleName, stmt.Name.Name)) } } } @@ -320,7 +321,7 @@ func (e *Executor) createRestClient(stmt *ast.CreateRestClientStmt) error { if stmt.Folder != "" { folderID, err := e.resolveFolder(module.ID, stmt.Folder) if err != nil { - return fmt.Errorf("failed to resolve folder '%s': %w", stmt.Folder, err) + return mdlerrors.NewBackend(fmt.Sprintf("resolve folder '%s'", stmt.Folder), err) } containerID = folderID } @@ -350,7 +351,7 @@ func (e *Executor) createRestClient(stmt *ast.CreateRestClientStmt) error { // Write to project if err := e.writer.CreateConsumedRestService(svc); err != nil { - return fmt.Errorf("failed to create REST client: %w", err) + return mdlerrors.NewBackend("create REST client", err) } fmt.Fprintf(e.output, "Created REST client: %s.%s (%d operations)\n", moduleName, stmt.Name.Name, len(svc.Operations)) @@ -441,10 +442,10 @@ func convertMappingEntries(entries []ast.RestMappingEntry, importDirection bool) // Value mapping — direction determines which side is attribute vs JSON field m := &model.RestResponseMapping{} if importDirection { - m.Attribute = e.Left // EntityAttr = jsonField + m.Attribute = e.Left // EntityAttr = jsonField m.ExposedName = e.Right } else { - m.Attribute = e.Right // jsonField = EntityAttr + m.Attribute = e.Right // jsonField = EntityAttr m.ExposedName = e.Left } result = append(result, m) @@ -456,17 +457,17 @@ func convertMappingEntries(entries []ast.RestMappingEntry, importDirection bool) // dropRestClient handles DROP REST CLIENT statement. func (e *Executor) dropRestClient(stmt *ast.DropRestClientStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project (read-only mode)") + return mdlerrors.NewNotConnectedWrite() } services, err := e.reader.ListConsumedRestServices() if err != nil { - return fmt.Errorf("failed to list consumed REST services: %w", err) + return mdlerrors.NewBackend("list consumed REST services", err) } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } for _, svc := range services { @@ -474,14 +475,14 @@ func (e *Executor) dropRestClient(stmt *ast.DropRestClientStmt) error { moduleName := h.GetModuleName(modID) if strings.EqualFold(moduleName, stmt.Name.Module) && strings.EqualFold(svc.Name, stmt.Name.Name) { if err := e.writer.DeleteConsumedRestService(svc.ID); err != nil { - return fmt.Errorf("failed to delete REST client: %w", err) + return mdlerrors.NewBackend("delete REST client", err) } fmt.Fprintf(e.output, "Dropped REST client: %s.%s\n", moduleName, svc.Name) return nil } } - return fmt.Errorf("REST client not found: %s", stmt.Name) + return mdlerrors.NewNotFound("REST client", stmt.Name.String()) } // formatRestAuthValue formats an authentication value for MDL output. diff --git a/mdl/executor/cmd_search.go b/mdl/executor/cmd_search.go index 58747bed..ccef9093 100644 --- a/mdl/executor/cmd_search.go +++ b/mdl/executor/cmd_search.go @@ -7,12 +7,13 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" ) // execShowCallers handles SHOW CALLERS OF Module.Microflow [TRANSITIVE]. func (e *Executor) execShowCallers(s *ast.ShowStmt) error { if s.Name == nil { - return fmt.Errorf("target name required for SHOW CALLERS") + return mdlerrors.NewValidation("target name required for SHOW CALLERS") } // Ensure catalog is available with full mode for refs @@ -59,7 +60,7 @@ func (e *Executor) execShowCallers(s *ast.ShowStmt) error { result, err := e.catalog.Query(strings.Replace(query, "?", "'"+targetName+"'", 1)) if err != nil { - return fmt.Errorf("failed to query callers: %w", err) + return mdlerrors.NewBackend("query callers", err) } if result.Count == 0 { @@ -75,7 +76,7 @@ func (e *Executor) execShowCallers(s *ast.ShowStmt) error { // execShowCallees handles SHOW CALLEES OF Module.Microflow [TRANSITIVE]. func (e *Executor) execShowCallees(s *ast.ShowStmt) error { if s.Name == nil { - return fmt.Errorf("target name required for SHOW CALLEES") + return mdlerrors.NewValidation("target name required for SHOW CALLEES") } // Ensure catalog is available with full mode for refs @@ -122,7 +123,7 @@ func (e *Executor) execShowCallees(s *ast.ShowStmt) error { result, err := e.catalog.Query(strings.Replace(query, "?", "'"+sourceName+"'", 1)) if err != nil { - return fmt.Errorf("failed to query callees: %w", err) + return mdlerrors.NewBackend("query callees", err) } if result.Count == 0 { @@ -138,7 +139,7 @@ func (e *Executor) execShowCallees(s *ast.ShowStmt) error { // execShowReferences handles SHOW REFERENCES TO Module.Entity. func (e *Executor) execShowReferences(s *ast.ShowStmt) error { if s.Name == nil { - return fmt.Errorf("target name required for SHOW REFERENCES") + return mdlerrors.NewValidation("target name required for SHOW REFERENCES") } // Ensure catalog is available with full mode for refs @@ -159,7 +160,7 @@ func (e *Executor) execShowReferences(s *ast.ShowStmt) error { result, err := e.catalog.Query(strings.Replace(query, "?", "'"+targetName+"'", 1)) if err != nil { - return fmt.Errorf("failed to query references: %w", err) + return mdlerrors.NewBackend("query references", err) } if result.Count == 0 { @@ -176,7 +177,7 @@ func (e *Executor) execShowReferences(s *ast.ShowStmt) error { // This shows all elements that would be affected by changing the target. func (e *Executor) execShowImpact(s *ast.ShowStmt) error { if s.Name == nil { - return fmt.Errorf("target name required for SHOW IMPACT") + return mdlerrors.NewValidation("target name required for SHOW IMPACT") } // Ensure catalog is available with full mode for refs @@ -197,7 +198,7 @@ func (e *Executor) execShowImpact(s *ast.ShowStmt) error { result, err := e.catalog.Query(strings.Replace(directQuery, "?", "'"+targetName+"'", 1)) if err != nil { - return fmt.Errorf("failed to query impact: %w", err) + return mdlerrors.NewBackend("query impact", err) } if result.Count == 0 { diff --git a/mdl/executor/cmd_security.go b/mdl/executor/cmd_security.go index 1b2958d5..e7977594 100644 --- a/mdl/executor/cmd_security.go +++ b/mdl/executor/cmd_security.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/security" ) @@ -16,7 +17,7 @@ import ( func (e *Executor) showProjectSecurity() error { ps, err := e.reader.GetProjectSecurity() if err != nil { - return fmt.Errorf("failed to read project security: %w", err) + return mdlerrors.NewBackend("read project security", err) } fmt.Fprintf(e.output, "Security Level: %s\n", security.SecurityLevelDisplay(ps.SecurityLevel)) @@ -49,12 +50,12 @@ func (e *Executor) showProjectSecurity() error { func (e *Executor) showModuleRoles(moduleName string) error { h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } allMS, err := e.reader.ListModuleSecurity() if err != nil { - return fmt.Errorf("failed to read module security: %w", err) + return mdlerrors.NewBackend("read module security", err) } result := &TableResult{ @@ -83,7 +84,7 @@ func (e *Executor) showModuleRoles(moduleName string) error { func (e *Executor) showUserRoles() error { ps, err := e.reader.GetProjectSecurity() if err != nil { - return fmt.Errorf("failed to read project security: %w", err) + return mdlerrors.NewBackend("read project security", err) } result := &TableResult{ @@ -110,7 +111,7 @@ func (e *Executor) showUserRoles() error { func (e *Executor) showDemoUsers() error { ps, err := e.reader.GetProjectSecurity() if err != nil { - return fmt.Errorf("failed to read project security: %w", err) + return mdlerrors.NewBackend("read project security", err) } if !ps.EnableDemoUsers { @@ -135,7 +136,7 @@ func (e *Executor) showDemoUsers() error { // showAccessOnEntity handles SHOW ACCESS ON Module.Entity. func (e *Executor) showAccessOnEntity(name *ast.QualifiedName) error { if name == nil { - return fmt.Errorf("entity name required") + return mdlerrors.NewValidation("entity name required") } module, err := e.findModule(name.Module) @@ -145,7 +146,7 @@ func (e *Executor) showAccessOnEntity(name *ast.QualifiedName) error { dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } var entity *domainmodel.Entity @@ -156,7 +157,7 @@ func (e *Executor) showAccessOnEntity(name *ast.QualifiedName) error { } } if entity == nil { - return fmt.Errorf("entity not found: %s", name) + return mdlerrors.NewNotFound("entity", name.String()) } if len(entity.AccessRules) == 0 { @@ -246,17 +247,17 @@ func (e *Executor) showAccessOnEntity(name *ast.QualifiedName) error { // showAccessOnMicroflow handles SHOW ACCESS ON MICROFLOW Module.MF. func (e *Executor) showAccessOnMicroflow(name *ast.QualifiedName) error { if name == nil { - return fmt.Errorf("microflow name required") + return mdlerrors.NewValidation("microflow name required") } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } mfs, err := e.reader.ListMicroflows() if err != nil { - return fmt.Errorf("failed to list microflows: %w", err) + return mdlerrors.NewBackend("list microflows", err) } for _, mf := range mfs { @@ -274,23 +275,23 @@ func (e *Executor) showAccessOnMicroflow(name *ast.QualifiedName) error { } } - return fmt.Errorf("microflow not found: %s", name) + return mdlerrors.NewNotFound("microflow", name.String()) } // showAccessOnPage handles SHOW ACCESS ON PAGE Module.Page. func (e *Executor) showAccessOnPage(name *ast.QualifiedName) error { if name == nil { - return fmt.Errorf("page name required") + return mdlerrors.NewValidation("page name required") } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } pages, err := e.reader.ListPages() if err != nil { - return fmt.Errorf("failed to list pages: %w", err) + return mdlerrors.NewBackend("list pages", err) } for _, pg := range pages { @@ -308,12 +309,12 @@ func (e *Executor) showAccessOnPage(name *ast.QualifiedName) error { } } - return fmt.Errorf("page not found: %s", name) + return mdlerrors.NewNotFound("page", name.String()) } // showAccessOnWorkflow handles SHOW ACCESS ON WORKFLOW Module.WF. func (e *Executor) showAccessOnWorkflow(name *ast.QualifiedName) error { - return fmt.Errorf("SHOW ACCESS ON WORKFLOW is not supported: Mendix workflows do not have document-level AllowedModuleRoles (unlike microflows and pages). Workflow access is controlled through the microflow that triggers the workflow and UserTask targeting") + return mdlerrors.NewUnsupported("SHOW ACCESS ON WORKFLOW is not supported: Mendix workflows do not have document-level AllowedModuleRoles (unlike microflows and pages). Workflow access is controlled through the microflow that triggers the workflow and UserTask targeting") } // showSecurityMatrix handles SHOW SECURITY MATRIX [IN module]. @@ -324,13 +325,13 @@ func (e *Executor) showSecurityMatrix(moduleName string) error { h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Collect all module roles allMS, err := e.reader.ListModuleSecurity() if err != nil { - return fmt.Errorf("failed to read module security: %w", err) + return mdlerrors.NewBackend("read module security", err) } // Build role list for the target module(s) @@ -370,7 +371,7 @@ func (e *Executor) showSecurityMatrix(moduleName string) error { // Collect entities with access rules dms, err := e.reader.ListDomainModels() if err != nil { - return fmt.Errorf("failed to list domain models: %w", err) + return mdlerrors.NewBackend("list domain models", err) } fmt.Fprintf(e.output, "Security Matrix") @@ -451,7 +452,7 @@ func (e *Executor) showSecurityMatrix(moduleName string) error { mfs, err := e.reader.ListMicroflows() if err != nil { - return fmt.Errorf("failed to list microflows: %w", err) + return mdlerrors.NewBackend("list microflows", err) } mfFound := false @@ -482,7 +483,7 @@ func (e *Executor) showSecurityMatrix(moduleName string) error { pages, err := e.reader.ListPages() if err != nil { - return fmt.Errorf("failed to list pages: %w", err) + return mdlerrors.NewBackend("list pages", err) } pgFound := false @@ -521,7 +522,7 @@ func (e *Executor) showSecurityMatrix(moduleName string) error { func (e *Executor) showSecurityMatrixJSON(moduleName string) error { h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } tr := &TableResult{ @@ -629,7 +630,6 @@ func (e *Executor) showSecurityMatrixJSON(moduleName string) error { }) } - return e.writeResult(tr) } @@ -637,12 +637,12 @@ func (e *Executor) showSecurityMatrixJSON(moduleName string) error { func (e *Executor) describeModuleRole(name ast.QualifiedName) error { h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } allMS, err := e.reader.ListModuleSecurity() if err != nil { - return fmt.Errorf("failed to read module security: %w", err) + return mdlerrors.NewBackend("read module security", err) } for _, ms := range allMS { @@ -681,14 +681,14 @@ func (e *Executor) describeModuleRole(name ast.QualifiedName) error { } } - return fmt.Errorf("module role not found: %s", name) + return mdlerrors.NewNotFound("module role", name.String()) } // describeDemoUser handles DESCRIBE DEMO USER 'name'. func (e *Executor) describeDemoUser(userName string) error { ps, err := e.reader.GetProjectSecurity() if err != nil { - return fmt.Errorf("failed to read project security: %w", err) + return mdlerrors.NewBackend("read project security", err) } for _, du := range ps.DemoUsers { @@ -706,14 +706,14 @@ func (e *Executor) describeDemoUser(userName string) error { } } - return fmt.Errorf("demo user not found: %s", userName) + return mdlerrors.NewNotFound("demo user", userName) } // describeUserRole handles DESCRIBE USER ROLE Name. func (e *Executor) describeUserRole(name ast.QualifiedName) error { ps, err := e.reader.GetProjectSecurity() if err != nil { - return fmt.Errorf("failed to read project security: %w", err) + return mdlerrors.NewBackend("read project security", err) } for _, ur := range ps.UserRoles { @@ -746,5 +746,5 @@ func (e *Executor) describeUserRole(name ast.QualifiedName) error { } } - return fmt.Errorf("user role not found: %s", name.Name) + return mdlerrors.NewNotFound("user role", name.Name) } diff --git a/mdl/executor/cmd_security_write.go b/mdl/executor/cmd_security_write.go index 22066f7e..23f81b6d 100644 --- a/mdl/executor/cmd_security_write.go +++ b/mdl/executor/cmd_security_write.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/security" @@ -16,7 +17,7 @@ import ( // execCreateModuleRole handles CREATE MODULE ROLE Module.RoleName [DESCRIPTION '...']. func (e *Executor) execCreateModuleRole(s *ast.CreateModuleRoleStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } module, err := e.findModule(s.Name.Module) @@ -26,18 +27,18 @@ func (e *Executor) execCreateModuleRole(s *ast.CreateModuleRoleStmt) error { ms, err := e.reader.GetModuleSecurity(module.ID) if err != nil { - return fmt.Errorf("failed to read module security for %s: %w", s.Name.Module, err) + return mdlerrors.NewBackend(fmt.Sprintf("read module security for %s", s.Name.Module), err) } // Check if role already exists for _, mr := range ms.ModuleRoles { if mr.Name == s.Name.Name { - return fmt.Errorf("module role already exists: %s.%s", s.Name.Module, s.Name.Name) + return mdlerrors.NewAlreadyExists("module role", s.Name.Module+"."+s.Name.Name) } } if err := e.writer.AddModuleRole(ms.ID, s.Name.Name, s.Description); err != nil { - return fmt.Errorf("failed to create module role: %w", err) + return mdlerrors.NewBackend("create module role", err) } fmt.Fprintf(e.output, "Created module role: %s.%s\n", s.Name.Module, s.Name.Name) @@ -49,7 +50,7 @@ func (e *Executor) execCreateModuleRole(s *ast.CreateModuleRoleStmt) error { // allowed roles, and OData service allowed roles before deleting the role itself. func (e *Executor) execDropModuleRole(s *ast.DropModuleRoleStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } module, err := e.findModule(s.Name.Module) @@ -59,7 +60,7 @@ func (e *Executor) execDropModuleRole(s *ast.DropModuleRoleStmt) error { ms, err := e.reader.GetModuleSecurity(module.ID) if err != nil { - return fmt.Errorf("failed to read module security for %s: %w", s.Name.Module, err) + return mdlerrors.NewBackend(fmt.Sprintf("read module security for %s", s.Name.Module), err) } // Check role exists @@ -71,7 +72,7 @@ func (e *Executor) execDropModuleRole(s *ast.DropModuleRoleStmt) error { } } if !found { - return fmt.Errorf("module role not found: %s.%s", s.Name.Module, s.Name.Name) + return mdlerrors.NewNotFound("module role", s.Name.Module+"."+s.Name.Name) } qualifiedRole := s.Name.Module + "." + s.Name.Name @@ -80,7 +81,7 @@ func (e *Executor) execDropModuleRole(s *ast.DropModuleRoleStmt) error { dm, err := e.reader.GetDomainModel(module.ID) if err == nil { if n, err := e.writer.RemoveRoleFromAllEntities(dm.ID, qualifiedRole); err != nil { - return fmt.Errorf("failed to cascade-remove entity access rules: %w", err) + return mdlerrors.NewBackend("cascade-remove entity access rules", err) } else if n > 0 { fmt.Fprintf(e.output, "Removed %s from %d entity access rule(s)\n", qualifiedRole, n) } @@ -151,7 +152,7 @@ func (e *Executor) execDropModuleRole(s *ast.DropModuleRoleStmt) error { // Finally, remove the role itself if err := e.writer.RemoveModuleRole(ms.ID, s.Name.Name); err != nil { - return fmt.Errorf("failed to drop module role: %w", err) + return mdlerrors.NewBackend("drop module role", err) } fmt.Fprintf(e.output, "Dropped module role: %s.%s\n", s.Name.Module, s.Name.Name) @@ -161,12 +162,12 @@ func (e *Executor) execDropModuleRole(s *ast.DropModuleRoleStmt) error { // execCreateUserRole handles CREATE [OR MODIFY] USER ROLE Name (ModuleRoles) [MANAGE ALL ROLES]. func (e *Executor) execCreateUserRole(s *ast.CreateUserRoleStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } ps, err := e.reader.GetProjectSecurity() if err != nil { - return fmt.Errorf("failed to read project security: %w", err) + return mdlerrors.NewBackend("read project security", err) } // Build qualified module role names @@ -180,11 +181,11 @@ func (e *Executor) execCreateUserRole(s *ast.CreateUserRoleStmt) error { for _, ur := range ps.UserRoles { if ur.Name == s.Name { if !s.CreateOrModify { - return fmt.Errorf("user role already exists: %s", s.Name) + return mdlerrors.NewAlreadyExists("user role", s.Name) } // Additive: ensure specified module roles are present if err := e.writer.AlterUserRoleModuleRoles(ps.ID, s.Name, true, moduleRoleNames); err != nil { - return fmt.Errorf("failed to update user role: %w", err) + return mdlerrors.NewBackend("update user role", err) } fmt.Fprintf(e.output, "Modified user role: %s\n", s.Name) return nil @@ -192,7 +193,7 @@ func (e *Executor) execCreateUserRole(s *ast.CreateUserRoleStmt) error { } if err := e.writer.AddUserRole(ps.ID, s.Name, moduleRoleNames, s.ManageAllRoles); err != nil { - return fmt.Errorf("failed to create user role: %w", err) + return mdlerrors.NewBackend("create user role", err) } fmt.Fprintf(e.output, "Created user role: %s\n", s.Name) @@ -202,12 +203,12 @@ func (e *Executor) execCreateUserRole(s *ast.CreateUserRoleStmt) error { // execAlterUserRole handles ALTER USER ROLE Name ADD/REMOVE MODULE ROLES (...). func (e *Executor) execAlterUserRole(s *ast.AlterUserRoleStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } ps, err := e.reader.GetProjectSecurity() if err != nil { - return fmt.Errorf("failed to read project security: %w", err) + return mdlerrors.NewBackend("read project security", err) } // Check user role exists @@ -219,7 +220,7 @@ func (e *Executor) execAlterUserRole(s *ast.AlterUserRoleStmt) error { } } if !found { - return fmt.Errorf("user role not found: %s", s.Name) + return mdlerrors.NewNotFound("user role", s.Name) } // Build qualified module role names @@ -229,7 +230,7 @@ func (e *Executor) execAlterUserRole(s *ast.AlterUserRoleStmt) error { } if err := e.writer.AlterUserRoleModuleRoles(ps.ID, s.Name, s.Add, moduleRoleNames); err != nil { - return fmt.Errorf("failed to alter user role: %w", err) + return mdlerrors.NewBackend("alter user role", err) } action := "Added" @@ -245,12 +246,12 @@ func (e *Executor) execAlterUserRole(s *ast.AlterUserRoleStmt) error { // execDropUserRole handles DROP USER ROLE Name. func (e *Executor) execDropUserRole(s *ast.DropUserRoleStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } ps, err := e.reader.GetProjectSecurity() if err != nil { - return fmt.Errorf("failed to read project security: %w", err) + return mdlerrors.NewBackend("read project security", err) } // Check user role exists @@ -262,11 +263,11 @@ func (e *Executor) execDropUserRole(s *ast.DropUserRoleStmt) error { } } if !found { - return fmt.Errorf("user role not found: %s", s.Name) + return mdlerrors.NewNotFound("user role", s.Name) } if err := e.writer.RemoveUserRole(ps.ID, s.Name); err != nil { - return fmt.Errorf("failed to drop user role: %w", err) + return mdlerrors.NewBackend("drop user role", err) } fmt.Fprintf(e.output, "Dropped user role: %s\n", s.Name) @@ -276,7 +277,7 @@ func (e *Executor) execDropUserRole(s *ast.DropUserRoleStmt) error { // execGrantEntityAccess handles GRANT roles ON Module.Entity (rights) [WHERE '...']. func (e *Executor) execGrantEntityAccess(s *ast.GrantEntityAccessStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } module, err := e.findModule(s.Entity.Module) @@ -286,13 +287,13 @@ func (e *Executor) execGrantEntityAccess(s *ast.GrantEntityAccessStmt) error { dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } // Verify entity exists entity := dm.FindEntityByName(s.Entity.Name) if entity == nil { - return fmt.Errorf("entity not found: %s.%s", s.Entity.Module, s.Entity.Name) + return mdlerrors.NewNotFound("entity", s.Entity.Module+"."+s.Entity.Name) } // Build role name list @@ -411,12 +412,12 @@ func (e *Executor) execGrantEntityAccess(s *ast.GrantEntityAccessStmt) error { if err := e.writer.AddEntityAccessRule(dm.ID, s.Entity.Name, roleNames, allowCreate, allowDelete, defaultMemberAccess, s.XPathConstraint, memberAccesses); err != nil { - return fmt.Errorf("failed to grant entity access: %w", err) + return mdlerrors.NewBackend("grant entity access", err) } // Reconcile MemberAccesses on pre-existing rules for this entity's domain model if count, err := e.writer.ReconcileMemberAccesses(dm.ID, module.Name); err != nil { - return fmt.Errorf("failed to reconcile member accesses: %w", err) + return mdlerrors.NewBackend("reconcile member accesses", err) } else if count > 0 && !e.quiet { fmt.Fprintf(e.output, "Reconciled %d access rule(s) in module %s\n", count, module.Name) } @@ -432,7 +433,7 @@ func (e *Executor) execGrantEntityAccess(s *ast.GrantEntityAccessStmt) error { // execRevokeEntityAccess handles REVOKE roles ON Module.Entity [(rights...)]. func (e *Executor) execRevokeEntityAccess(s *ast.RevokeEntityAccessStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } module, err := e.findModule(s.Entity.Module) @@ -442,13 +443,13 @@ func (e *Executor) execRevokeEntityAccess(s *ast.RevokeEntityAccessStmt) error { dm, err := e.reader.GetDomainModel(module.ID) if err != nil { - return fmt.Errorf("failed to get domain model: %w", err) + return mdlerrors.NewBackend("get domain model", err) } // Verify entity exists entity := dm.FindEntityByName(s.Entity.Name) if entity == nil { - return fmt.Errorf("entity not found: %s.%s", s.Entity.Module, s.Entity.Name) + return mdlerrors.NewNotFound("entity", s.Entity.Module+"."+s.Entity.Name) } // Build role name list @@ -485,7 +486,7 @@ func (e *Executor) execRevokeEntityAccess(s *ast.RevokeEntityAccessStmt) error { modified, err := e.writer.RevokeEntityMemberAccess(dm.ID, s.Entity.Name, roleNames, revocation) if err != nil { - return fmt.Errorf("failed to revoke entity access: %w", err) + return mdlerrors.NewBackend("revoke entity access", err) } if modified == 0 { @@ -500,7 +501,7 @@ func (e *Executor) execRevokeEntityAccess(s *ast.RevokeEntityAccessStmt) error { // Full revoke — remove entire access rule modified, err := e.writer.RemoveEntityAccessRule(dm.ID, s.Entity.Name, roleNames) if err != nil { - return fmt.Errorf("failed to revoke entity access: %w", err) + return mdlerrors.NewBackend("revoke entity access", err) } if modified == 0 { @@ -519,18 +520,18 @@ func (e *Executor) execRevokeEntityAccess(s *ast.RevokeEntityAccessStmt) error { // execGrantMicroflowAccess handles GRANT EXECUTE ON MICROFLOW Module.MF TO roles. func (e *Executor) execGrantMicroflowAccess(s *ast.GrantMicroflowAccessStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Find the microflow mfs, err := e.reader.ListMicroflows() if err != nil { - return fmt.Errorf("failed to list microflows: %w", err) + return mdlerrors.NewBackend("list microflows", err) } for _, mf := range mfs { @@ -564,7 +565,7 @@ func (e *Executor) execGrantMicroflowAccess(s *ast.GrantMicroflowAccessStmt) err } if err := e.writer.UpdateAllowedRoles(mf.ID, merged); err != nil { - return fmt.Errorf("failed to update microflow access: %w", err) + return mdlerrors.NewBackend("update microflow access", err) } if len(added) == 0 { @@ -575,24 +576,24 @@ func (e *Executor) execGrantMicroflowAccess(s *ast.GrantMicroflowAccessStmt) err return nil } - return fmt.Errorf("microflow not found: %s.%s", s.Microflow.Module, s.Microflow.Name) + return mdlerrors.NewNotFound("microflow", s.Microflow.Module+"."+s.Microflow.Name) } // execRevokeMicroflowAccess handles REVOKE EXECUTE ON MICROFLOW Module.MF FROM roles. func (e *Executor) execRevokeMicroflowAccess(s *ast.RevokeMicroflowAccessStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Find the microflow mfs, err := e.reader.ListMicroflows() if err != nil { - return fmt.Errorf("failed to list microflows: %w", err) + return mdlerrors.NewBackend("list microflows", err) } for _, mf := range mfs { @@ -620,7 +621,7 @@ func (e *Executor) execRevokeMicroflowAccess(s *ast.RevokeMicroflowAccessStmt) e } if err := e.writer.UpdateAllowedRoles(mf.ID, remaining); err != nil { - return fmt.Errorf("failed to update microflow access: %w", err) + return mdlerrors.NewBackend("update microflow access", err) } if len(removed) == 0 { @@ -631,24 +632,24 @@ func (e *Executor) execRevokeMicroflowAccess(s *ast.RevokeMicroflowAccessStmt) e return nil } - return fmt.Errorf("microflow not found: %s.%s", s.Microflow.Module, s.Microflow.Name) + return mdlerrors.NewNotFound("microflow", s.Microflow.Module+"."+s.Microflow.Name) } // execGrantPageAccess handles GRANT VIEW ON PAGE Module.Page TO roles. func (e *Executor) execGrantPageAccess(s *ast.GrantPageAccessStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Find the page pages, err := e.reader.ListPages() if err != nil { - return fmt.Errorf("failed to list pages: %w", err) + return mdlerrors.NewBackend("list pages", err) } for _, pg := range pages { @@ -682,7 +683,7 @@ func (e *Executor) execGrantPageAccess(s *ast.GrantPageAccessStmt) error { } if err := e.writer.UpdateAllowedRoles(pg.ID, merged); err != nil { - return fmt.Errorf("failed to update page access: %w", err) + return mdlerrors.NewBackend("update page access", err) } if len(added) == 0 { @@ -693,24 +694,24 @@ func (e *Executor) execGrantPageAccess(s *ast.GrantPageAccessStmt) error { return nil } - return fmt.Errorf("page not found: %s.%s", s.Page.Module, s.Page.Name) + return mdlerrors.NewNotFound("page", s.Page.Module+"."+s.Page.Name) } // execRevokePageAccess handles REVOKE VIEW ON PAGE Module.Page FROM roles. func (e *Executor) execRevokePageAccess(s *ast.RevokePageAccessStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Find the page pages, err := e.reader.ListPages() if err != nil { - return fmt.Errorf("failed to list pages: %w", err) + return mdlerrors.NewBackend("list pages", err) } for _, pg := range pages { @@ -738,7 +739,7 @@ func (e *Executor) execRevokePageAccess(s *ast.RevokePageAccessStmt) error { } if err := e.writer.UpdateAllowedRoles(pg.ID, remaining); err != nil { - return fmt.Errorf("failed to update page access: %w", err) + return mdlerrors.NewBackend("update page access", err) } if len(removed) == 0 { @@ -749,21 +750,21 @@ func (e *Executor) execRevokePageAccess(s *ast.RevokePageAccessStmt) error { return nil } - return fmt.Errorf("page not found: %s.%s", s.Page.Module, s.Page.Name) + return mdlerrors.NewNotFound("page", s.Page.Module+"."+s.Page.Name) } // execGrantWorkflowAccess handles GRANT EXECUTE ON WORKFLOW Module.WF TO roles. // Mendix workflows do not have a document-level AllowedModuleRoles field (unlike // microflows and pages), so this operation is not supported. func (e *Executor) execGrantWorkflowAccess(s *ast.GrantWorkflowAccessStmt) error { - return fmt.Errorf("GRANT EXECUTE ON WORKFLOW is not supported: Mendix workflows do not have document-level AllowedModuleRoles (unlike microflows and pages). Workflow access is controlled through the microflow that triggers the workflow and UserTask targeting") + return mdlerrors.NewUnsupported("GRANT EXECUTE ON WORKFLOW is not supported: Mendix workflows do not have document-level AllowedModuleRoles (unlike microflows and pages). Workflow access is controlled through the microflow that triggers the workflow and UserTask targeting") } // execRevokeWorkflowAccess handles REVOKE EXECUTE ON WORKFLOW Module.WF FROM roles. // Mendix workflows do not have a document-level AllowedModuleRoles field (unlike // microflows and pages), so this operation is not supported. func (e *Executor) execRevokeWorkflowAccess(s *ast.RevokeWorkflowAccessStmt) error { - return fmt.Errorf("REVOKE EXECUTE ON WORKFLOW is not supported: Mendix workflows do not have document-level AllowedModuleRoles (unlike microflows and pages). Workflow access is controlled through the microflow that triggers the workflow and UserTask targeting") + return mdlerrors.NewUnsupported("REVOKE EXECUTE ON WORKFLOW is not supported: Mendix workflows do not have document-level AllowedModuleRoles (unlike microflows and pages). Workflow access is controlled through the microflow that triggers the workflow and UserTask targeting") } // validateModuleRole checks that a module role exists in the project. @@ -775,7 +776,7 @@ func (e *Executor) validateModuleRole(role ast.QualifiedName) error { ms, err := e.reader.GetModuleSecurity(module.ID) if err != nil { - return fmt.Errorf("failed to read module security for %s: %w", role.Module, err) + return mdlerrors.NewBackend(fmt.Sprintf("read module security for %s", role.Module), err) } for _, mr := range ms.ModuleRoles { @@ -784,18 +785,18 @@ func (e *Executor) validateModuleRole(role ast.QualifiedName) error { } } - return fmt.Errorf("module role not found: %s.%s", role.Module, role.Name) + return mdlerrors.NewNotFound("module role", role.Module+"."+role.Name) } // execAlterProjectSecurity handles ALTER PROJECT SECURITY LEVEL/DEMO USERS. func (e *Executor) execAlterProjectSecurity(s *ast.AlterProjectSecurityStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } ps, err := e.reader.GetProjectSecurity() if err != nil { - return fmt.Errorf("failed to read project security: %w", err) + return mdlerrors.NewBackend("read project security", err) } if s.SecurityLevel != "" { @@ -809,18 +810,18 @@ func (e *Executor) execAlterProjectSecurity(s *ast.AlterProjectSecurityStmt) err case "Off": bsonLevel = security.SecurityLevelOff default: - return fmt.Errorf("unknown security level: %s", s.SecurityLevel) + return mdlerrors.NewUnsupported(fmt.Sprintf("unknown security level: %s", s.SecurityLevel)) } if err := e.writer.SetProjectSecurityLevel(ps.ID, bsonLevel); err != nil { - return fmt.Errorf("failed to set security level: %w", err) + return mdlerrors.NewBackend("set security level", err) } fmt.Fprintf(e.output, "Set project security level to %s\n", s.SecurityLevel) } if s.DemoUsersEnabled != nil { if err := e.writer.SetProjectDemoUsersEnabled(ps.ID, *s.DemoUsersEnabled); err != nil { - return fmt.Errorf("failed to set demo users: %w", err) + return mdlerrors.NewBackend("set demo users", err) } state := "disabled" if *s.DemoUsersEnabled { @@ -835,12 +836,12 @@ func (e *Executor) execAlterProjectSecurity(s *ast.AlterProjectSecurityStmt) err // execCreateDemoUser handles CREATE [OR MODIFY] DEMO USER 'name' PASSWORD 'pw' [ENTITY Module.Entity] (Roles). func (e *Executor) execCreateDemoUser(s *ast.CreateDemoUserStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } ps, err := e.reader.GetProjectSecurity() if err != nil { - return fmt.Errorf("failed to read project security: %w", err) + return mdlerrors.NewBackend("read project security", err) } // Validate password against project password policy @@ -852,7 +853,7 @@ func (e *Executor) execCreateDemoUser(s *ast.CreateDemoUserStmt) error { for _, du := range ps.DemoUsers { if du.UserName == s.UserName { if !s.CreateOrModify { - return fmt.Errorf("demo user already exists: %s", s.UserName) + return mdlerrors.NewAlreadyExists("demo user", s.UserName) } // Additive: merge roles, update password. Drop and re-create with merged roles. mergedRoles := du.UserRoles @@ -870,10 +871,10 @@ func (e *Executor) execCreateDemoUser(s *ast.CreateDemoUserStmt) error { entity = s.Entity } if err := e.writer.RemoveDemoUser(ps.ID, s.UserName); err != nil { - return fmt.Errorf("failed to update demo user: %w", err) + return mdlerrors.NewBackend("update demo user", err) } if err := e.writer.AddDemoUser(ps.ID, s.UserName, s.Password, entity, mergedRoles); err != nil { - return fmt.Errorf("failed to update demo user: %w", err) + return mdlerrors.NewBackend("update demo user", err) } fmt.Fprintf(e.output, "Modified demo user: %s\n", s.UserName) return nil @@ -891,7 +892,7 @@ func (e *Executor) execCreateDemoUser(s *ast.CreateDemoUserStmt) error { } if err := e.writer.AddDemoUser(ps.ID, s.UserName, s.Password, entity, s.UserRoles); err != nil { - return fmt.Errorf("failed to create demo user: %w", err) + return mdlerrors.NewBackend("create demo user", err) } fmt.Fprintf(e.output, "Created demo user: %s (entity: %s)\n", s.UserName, entity) @@ -902,7 +903,7 @@ func (e *Executor) execCreateDemoUser(s *ast.CreateDemoUserStmt) error { func (e *Executor) detectUserEntity() (string, error) { modules, err := e.reader.ListModules() if err != nil { - return "", fmt.Errorf("failed to list modules: %w", err) + return "", mdlerrors.NewBackend("list modules", err) } moduleNameByID := make(map[model.ID]string, len(modules)) for _, m := range modules { @@ -911,7 +912,7 @@ func (e *Executor) detectUserEntity() (string, error) { dms, err := e.reader.ListDomainModels() if err != nil { - return "", fmt.Errorf("failed to list domain models: %w", err) + return "", mdlerrors.NewBackend("list domain models", err) } var candidates []string @@ -926,11 +927,11 @@ func (e *Executor) detectUserEntity() (string, error) { switch len(candidates) { case 0: - return "", fmt.Errorf("no entity found that generalizes System.User; use ENTITY clause to specify one") + return "", mdlerrors.NewValidation("no entity found that generalizes System.User; use ENTITY clause to specify one") case 1: return candidates[0], nil default: - return "", fmt.Errorf("multiple entities generalize System.User: %s; use ENTITY clause to specify one", joinCandidates(candidates)) + return "", mdlerrors.NewValidationf("multiple entities generalize System.User: %s; use ENTITY clause to specify one", joinCandidates(candidates)) } } @@ -945,12 +946,12 @@ func joinCandidates(candidates []string) string { // execDropDemoUser handles DROP DEMO USER 'name'. func (e *Executor) execDropDemoUser(s *ast.DropDemoUserStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } ps, err := e.reader.GetProjectSecurity() if err != nil { - return fmt.Errorf("failed to read project security: %w", err) + return mdlerrors.NewBackend("read project security", err) } // Check if user exists @@ -962,11 +963,11 @@ func (e *Executor) execDropDemoUser(s *ast.DropDemoUserStmt) error { } } if !found { - return fmt.Errorf("demo user not found: %s", s.UserName) + return mdlerrors.NewNotFound("demo user", s.UserName) } if err := e.writer.RemoveDemoUser(ps.ID, s.UserName); err != nil { - return fmt.Errorf("failed to drop demo user: %w", err) + return mdlerrors.NewBackend("drop demo user", err) } fmt.Fprintf(e.output, "Dropped demo user: %s\n", s.UserName) @@ -980,18 +981,18 @@ func (e *Executor) execDropDemoUser(s *ast.DropDemoUserStmt) error { // execGrantODataServiceAccess handles GRANT ACCESS ON ODATA SERVICE Module.Svc TO roles. func (e *Executor) execGrantODataServiceAccess(s *ast.GrantODataServiceAccessStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Find the published OData service services, err := e.reader.ListPublishedODataServices() if err != nil { - return fmt.Errorf("failed to list published OData services: %w", err) + return mdlerrors.NewBackend("list published OData services", err) } for _, svc := range services { @@ -1025,7 +1026,7 @@ func (e *Executor) execGrantODataServiceAccess(s *ast.GrantODataServiceAccessStm } if err := e.writer.UpdateAllowedRoles(svc.ID, merged); err != nil { - return fmt.Errorf("failed to update OData service access: %w", err) + return mdlerrors.NewBackend("update OData service access", err) } if len(added) == 0 { @@ -1036,24 +1037,24 @@ func (e *Executor) execGrantODataServiceAccess(s *ast.GrantODataServiceAccessStm return nil } - return fmt.Errorf("published OData service not found: %s.%s", s.Service.Module, s.Service.Name) + return mdlerrors.NewNotFound("published OData service", s.Service.Module+"."+s.Service.Name) } // execRevokeODataServiceAccess handles REVOKE ACCESS ON ODATA SERVICE Module.Svc FROM roles. func (e *Executor) execRevokeODataServiceAccess(s *ast.RevokeODataServiceAccessStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Find the published OData service services, err := e.reader.ListPublishedODataServices() if err != nil { - return fmt.Errorf("failed to list published OData services: %w", err) + return mdlerrors.NewBackend("list published OData services", err) } for _, svc := range services { @@ -1081,7 +1082,7 @@ func (e *Executor) execRevokeODataServiceAccess(s *ast.RevokeODataServiceAccessS } if err := e.writer.UpdateAllowedRoles(svc.ID, remaining); err != nil { - return fmt.Errorf("failed to update OData service access: %w", err) + return mdlerrors.NewBackend("update OData service access", err) } if len(removed) == 0 { @@ -1092,7 +1093,7 @@ func (e *Executor) execRevokeODataServiceAccess(s *ast.RevokeODataServiceAccessS return nil } - return fmt.Errorf("published OData service not found: %s.%s", s.Service.Module, s.Service.Name) + return mdlerrors.NewNotFound("published OData service", s.Service.Module+"."+s.Service.Name) } // ============================================================================ @@ -1102,7 +1103,7 @@ func (e *Executor) execRevokeODataServiceAccess(s *ast.RevokeODataServiceAccessS // execGrantPublishedRestServiceAccess handles GRANT ACCESS ON PUBLISHED REST SERVICE Module.Svc TO roles. func (e *Executor) execGrantPublishedRestServiceAccess(s *ast.GrantPublishedRestServiceAccessStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } if err := e.checkFeature("integration", "published_rest_grant_revoke", @@ -1113,12 +1114,12 @@ func (e *Executor) execGrantPublishedRestServiceAccess(s *ast.GrantPublishedRest h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } services, err := e.reader.ListPublishedRestServices() if err != nil { - return fmt.Errorf("failed to list published REST services: %w", err) + return mdlerrors.NewBackend("list published REST services", err) } for _, svc := range services { @@ -1152,7 +1153,7 @@ func (e *Executor) execGrantPublishedRestServiceAccess(s *ast.GrantPublishedRest } if err := e.writer.UpdatePublishedRestServiceRoles(svc.ID, merged); err != nil { - return fmt.Errorf("failed to update published REST service access: %w", err) + return mdlerrors.NewBackend("update published REST service access", err) } if len(added) == 0 { @@ -1163,23 +1164,23 @@ func (e *Executor) execGrantPublishedRestServiceAccess(s *ast.GrantPublishedRest return nil } - return fmt.Errorf("published REST service not found: %s.%s", s.Service.Module, s.Service.Name) + return mdlerrors.NewNotFound("published REST service", s.Service.Module+"."+s.Service.Name) } // execRevokePublishedRestServiceAccess handles REVOKE ACCESS ON PUBLISHED REST SERVICE Module.Svc FROM roles. func (e *Executor) execRevokePublishedRestServiceAccess(s *ast.RevokePublishedRestServiceAccessStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } services, err := e.reader.ListPublishedRestServices() if err != nil { - return fmt.Errorf("failed to list published REST services: %w", err) + return mdlerrors.NewBackend("list published REST services", err) } for _, svc := range services { @@ -1207,7 +1208,7 @@ func (e *Executor) execRevokePublishedRestServiceAccess(s *ast.RevokePublishedRe } if err := e.writer.UpdatePublishedRestServiceRoles(svc.ID, remaining); err != nil { - return fmt.Errorf("failed to update published REST service access: %w", err) + return mdlerrors.NewBackend("update published REST service access", err) } if len(removed) == 0 { @@ -1218,13 +1219,13 @@ func (e *Executor) execRevokePublishedRestServiceAccess(s *ast.RevokePublishedRe return nil } - return fmt.Errorf("published REST service not found: %s.%s", s.Service.Module, s.Service.Name) + return mdlerrors.NewNotFound("published REST service", s.Service.Module+"."+s.Service.Name) } // execUpdateSecurity handles UPDATE SECURITY [IN Module]. func (e *Executor) execUpdateSecurity(s *ast.UpdateSecurityStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project in write mode") + return mdlerrors.NewNotConnectedWrite() } modules, err := e.getModulesFromCache() @@ -1245,7 +1246,7 @@ func (e *Executor) execUpdateSecurity(s *ast.UpdateSecurityStmt) error { count, err := e.writer.ReconcileMemberAccesses(dm.ID, mod.Name) if err != nil { - return fmt.Errorf("failed to reconcile security for module %s: %w", mod.Name, err) + return mdlerrors.NewBackend(fmt.Sprintf("reconcile security for module %s", mod.Name), err) } if count > 0 { fmt.Fprintf(e.output, "Reconciled %d access rule(s) in module %s\n", count, mod.Name) diff --git a/mdl/executor/cmd_settings.go b/mdl/executor/cmd_settings.go index 3426325d..edcbe66a 100644 --- a/mdl/executor/cmd_settings.go +++ b/mdl/executor/cmd_settings.go @@ -8,18 +8,19 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" ) // showSettings displays an overview table of all settings parts. func (e *Executor) showSettings() error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } ps, err := e.reader.GetProjectSettings() if err != nil { - return fmt.Errorf("failed to read project settings: %w", err) + return mdlerrors.NewBackend("read project settings", err) } tr := &TableResult{ @@ -84,12 +85,12 @@ func (e *Executor) showSettings() error { // describeSettings outputs the full MDL description of all settings. func (e *Executor) describeSettings() error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } ps, err := e.reader.GetProjectSettings() if err != nil { - return fmt.Errorf("failed to read project settings: %w", err) + return mdlerrors.NewBackend("read project settings", err) } // Model settings @@ -169,19 +170,19 @@ func (e *Executor) describeSettings() error { // alterSettings modifies project settings based on ALTER SETTINGS statement. func (e *Executor) alterSettings(stmt *ast.AlterSettingsStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project (read-only mode)") + return mdlerrors.NewNotConnectedWrite() } ps, err := e.reader.GetProjectSettings() if err != nil { - return fmt.Errorf("failed to read project settings: %w", err) + return mdlerrors.NewBackend("read project settings", err) } section := strings.ToUpper(stmt.Section) switch section { case "MODEL": if ps.Model == nil { - return fmt.Errorf("model settings not found in project") + return mdlerrors.NewNotFound("settings section", "model") } for key, val := range stmt.Properties { valStr := settingsValueToString(val) @@ -207,13 +208,13 @@ func (e *Executor) alterSettings(stmt *ast.AlterSettingsStmt) error { case "ScheduledEventTimeZoneCode": ps.Model.ScheduledEventTimeZoneCode = valStr default: - return fmt.Errorf("unknown model setting: %s", key) + return mdlerrors.NewUnsupported("unknown model setting: " + key) } } case "LANGUAGE": if ps.Language == nil { - return fmt.Errorf("language settings not found in project") + return mdlerrors.NewNotFound("settings section", "language") } for key, val := range stmt.Properties { valStr := settingsValueToString(val) @@ -221,13 +222,13 @@ func (e *Executor) alterSettings(stmt *ast.AlterSettingsStmt) error { case "DefaultLanguageCode": ps.Language.DefaultLanguageCode = valStr default: - return fmt.Errorf("unknown language setting: %s", key) + return mdlerrors.NewUnsupported("unknown language setting: " + key) } } case "WORKFLOWS": if ps.Workflows == nil { - return fmt.Errorf("workflow settings not found in project") + return mdlerrors.NewNotFound("settings section", "workflows") } for key, val := range stmt.Properties { valStr := settingsValueToString(val) @@ -243,7 +244,7 @@ func (e *Executor) alterSettings(stmt *ast.AlterSettingsStmt) error { ps.Workflows.WorkflowEngineParallelism = v } default: - return fmt.Errorf("unknown workflow setting: %s", key) + return mdlerrors.NewUnsupported("unknown workflow setting: " + key) } } @@ -254,12 +255,12 @@ func (e *Executor) alterSettings(stmt *ast.AlterSettingsStmt) error { return e.alterSettingsConstant(ps, stmt) default: - return fmt.Errorf("unknown settings section: %s (expected MODEL, CONFIGURATION, CONSTANT, LANGUAGE, or WORKFLOWS)", section) + return mdlerrors.NewUnsupported(fmt.Sprintf("unknown settings section: %s (expected MODEL, CONFIGURATION, CONSTANT, LANGUAGE, or WORKFLOWS)", section)) } // Write updated settings if err := e.writer.UpdateProjectSettings(ps); err != nil { - return fmt.Errorf("failed to update project settings: %w", err) + return mdlerrors.NewBackend("update project settings", err) } fmt.Fprintf(e.output, "Updated %s settings\n", section) @@ -268,7 +269,7 @@ func (e *Executor) alterSettings(stmt *ast.AlterSettingsStmt) error { func (e *Executor) alterSettingsConfiguration(ps *model.ProjectSettings, stmt *ast.AlterSettingsStmt) error { if ps.Configuration == nil { - return fmt.Errorf("configuration settings not found in project") + return mdlerrors.NewNotFound("settings section", "configuration") } // Find the named configuration @@ -280,7 +281,7 @@ func (e *Executor) alterSettingsConfiguration(ps *model.ProjectSettings, stmt *a } } if cfg == nil { - return fmt.Errorf("configuration not found: %s", stmt.ConfigName) + return mdlerrors.NewNotFound("configuration", stmt.ConfigName) } for key, val := range stmt.Properties { @@ -307,12 +308,12 @@ func (e *Executor) alterSettingsConfiguration(ps *model.ProjectSettings, stmt *a case "ApplicationRootUrl": cfg.ApplicationRootUrl = valStr default: - return fmt.Errorf("unknown configuration setting: %s", key) + return mdlerrors.NewUnsupported("unknown configuration setting: " + key) } } if err := e.writer.UpdateProjectSettings(ps); err != nil { - return fmt.Errorf("failed to update project settings: %w", err) + return mdlerrors.NewBackend("update project settings", err) } fmt.Fprintf(e.output, "Updated configuration '%s'\n", stmt.ConfigName) @@ -321,7 +322,7 @@ func (e *Executor) alterSettingsConfiguration(ps *model.ProjectSettings, stmt *a func (e *Executor) alterSettingsConstant(ps *model.ProjectSettings, stmt *ast.AlterSettingsStmt) error { if ps.Configuration == nil { - return fmt.Errorf("configuration settings not found in project") + return mdlerrors.NewNotFound("settings section", "configuration") } // Find the target configuration @@ -331,7 +332,7 @@ func (e *Executor) alterSettingsConstant(ps *model.ProjectSettings, stmt *ast.Al if len(ps.Configuration.Configurations) > 0 { targetConfig = ps.Configuration.Configurations[0].Name } else { - return fmt.Errorf("no configurations found") + return mdlerrors.NewValidation("no configurations found") } } @@ -343,7 +344,7 @@ func (e *Executor) alterSettingsConstant(ps *model.ProjectSettings, stmt *ast.Al } } if cfg == nil { - return fmt.Errorf("configuration not found: %s", targetConfig) + return mdlerrors.NewNotFound("configuration", targetConfig) } if stmt.DropConstant { @@ -352,14 +353,14 @@ func (e *Executor) alterSettingsConstant(ps *model.ProjectSettings, stmt *ast.Al if cv.ConstantId == stmt.ConstantId { cfg.ConstantValues = append(cfg.ConstantValues[:i], cfg.ConstantValues[i+1:]...) if err := e.writer.UpdateProjectSettings(ps); err != nil { - return fmt.Errorf("failed to update project settings: %w", err) + return mdlerrors.NewBackend("update project settings", err) } fmt.Fprintf(e.output, "Dropped constant '%s' from configuration '%s'\n", stmt.ConstantId, targetConfig) return nil } } - return fmt.Errorf("constant '%s' not found in configuration '%s'", stmt.ConstantId, targetConfig) + return mdlerrors.NewNotFoundMsg("constant", stmt.ConstantId, fmt.Sprintf("constant '%s' not found in configuration '%s'", stmt.ConstantId, targetConfig)) } // Find or create the constant value @@ -381,7 +382,7 @@ func (e *Executor) alterSettingsConstant(ps *model.ProjectSettings, stmt *ast.Al } if err := e.writer.UpdateProjectSettings(ps); err != nil { - return fmt.Errorf("failed to update project settings: %w", err) + return mdlerrors.NewBackend("update project settings", err) } fmt.Fprintf(e.output, "Updated constant '%s' = '%s' in configuration '%s'\n", @@ -392,22 +393,22 @@ func (e *Executor) alterSettingsConstant(ps *model.ProjectSettings, stmt *ast.Al // createConfiguration handles CREATE CONFIGURATION 'name' [properties...]. func (e *Executor) createConfiguration(stmt *ast.CreateConfigurationStmt) error { if e.writer == nil { - return fmt.Errorf("not connected in write mode") + return mdlerrors.NewNotConnectedWrite() } ps, err := e.reader.GetProjectSettings() if err != nil { - return fmt.Errorf("failed to read project settings: %w", err) + return mdlerrors.NewBackend("read project settings", err) } if ps.Configuration == nil { - return fmt.Errorf("configuration settings not found in project") + return mdlerrors.NewNotFound("settings section", "configuration") } // Check if configuration already exists for _, cfg := range ps.Configuration.Configurations { if strings.EqualFold(cfg.Name, stmt.Name) { - return fmt.Errorf("configuration already exists: %s", stmt.Name) + return mdlerrors.NewAlreadyExists("configuration", stmt.Name) } } @@ -444,14 +445,14 @@ func (e *Executor) createConfiguration(stmt *ast.CreateConfigurationStmt) error case "ApplicationRootUrl": newCfg.ApplicationRootUrl = valStr default: - return fmt.Errorf("unknown configuration property: %s", key) + return mdlerrors.NewUnsupported("unknown configuration property: " + key) } } ps.Configuration.Configurations = append(ps.Configuration.Configurations, newCfg) if err := e.writer.UpdateProjectSettings(ps); err != nil { - return fmt.Errorf("failed to update project settings: %w", err) + return mdlerrors.NewBackend("update project settings", err) } fmt.Fprintf(e.output, "Created configuration: %s\n", stmt.Name) @@ -461,16 +462,16 @@ func (e *Executor) createConfiguration(stmt *ast.CreateConfigurationStmt) error // dropConfiguration handles DROP CONFIGURATION 'name'. func (e *Executor) dropConfiguration(stmt *ast.DropConfigurationStmt) error { if e.writer == nil { - return fmt.Errorf("not connected in write mode") + return mdlerrors.NewNotConnectedWrite() } ps, err := e.reader.GetProjectSettings() if err != nil { - return fmt.Errorf("failed to read project settings: %w", err) + return mdlerrors.NewBackend("read project settings", err) } if ps.Configuration == nil { - return fmt.Errorf("configuration settings not found in project") + return mdlerrors.NewNotFound("settings section", "configuration") } for i, cfg := range ps.Configuration.Configurations { @@ -480,14 +481,14 @@ func (e *Executor) dropConfiguration(stmt *ast.DropConfigurationStmt) error { ps.Configuration.Configurations[i+1:]..., ) if err := e.writer.UpdateProjectSettings(ps); err != nil { - return fmt.Errorf("failed to update project settings: %w", err) + return mdlerrors.NewBackend("update project settings", err) } fmt.Fprintf(e.output, "Dropped configuration: %s\n", stmt.Name) return nil } } - return fmt.Errorf("configuration not found: %s", stmt.Name) + return mdlerrors.NewNotFound("configuration", stmt.Name) } // settingsValueToString converts an AST settings value to string. diff --git a/mdl/executor/cmd_snippets.go b/mdl/executor/cmd_snippets.go index 5dd5af85..3eb51989 100644 --- a/mdl/executor/cmd_snippets.go +++ b/mdl/executor/cmd_snippets.go @@ -7,6 +7,8 @@ import ( "fmt" "sort" "strings" + + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" ) // showSnippets handles SHOW SNIPPETS command. @@ -14,13 +16,13 @@ func (e *Executor) showSnippets(moduleName string) error { // Get hierarchy for module/folder resolution h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Get all snippets snippets, err := e.reader.ListSnippets() if err != nil { - return fmt.Errorf("failed to list snippets: %w", err) + return mdlerrors.NewBackend("list snippets", err) } // Collect rows diff --git a/mdl/executor/cmd_sql.go b/mdl/executor/cmd_sql.go index 3b76468d..4e0b7de3 100644 --- a/mdl/executor/cmd_sql.go +++ b/mdl/executor/cmd_sql.go @@ -9,6 +9,7 @@ import ( "time" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/mdl/visitor" sqllib "github.com/mendixlabs/mxcli/sql" ) @@ -31,7 +32,7 @@ func (e *Executor) getOrAutoConnect(alias string) (*sqllib.Connection, error) { // Not connected yet — try auto-connect from config if acErr := e.autoConnect(alias); acErr != nil { - return nil, fmt.Errorf("no connection '%s' (and auto-connect failed: %v)", alias, acErr) + return nil, mdlerrors.NewNotFoundMsg("connection", alias, fmt.Sprintf("no connection '%s' (and auto-connect failed: %v)", alias, acErr)) } return mgr.Get(alias) } @@ -242,7 +243,7 @@ func (e *Executor) execSQLGenerateConnector(s *ast.SQLGenerateConnectorStmt) err func (e *Executor) executeGeneratedMDL(mdl string) error { prog, errs := visitor.Build(mdl) if len(errs) > 0 { - return fmt.Errorf("failed to parse generated MDL: %v", errs[0]) + return mdlerrors.NewBackend("parse generated MDL", fmt.Errorf("%v", errs[0])) } return e.ExecuteProgram(prog) } diff --git a/mdl/executor/cmd_structure.go b/mdl/executor/cmd_structure.go index 8ac60a27..eebfb157 100644 --- a/mdl/executor/cmd_structure.go +++ b/mdl/executor/cmd_structure.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/javaactions" @@ -18,14 +19,14 @@ import ( // execShowStructure handles SHOW STRUCTURE [DEPTH n] [IN module] [ALL]. func (e *Executor) execShowStructure(s *ast.ShowStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } depth := min(max(s.Depth, 1), 3) // Ensure catalog is built (fast mode is sufficient) if err := e.ensureCatalog(false); err != nil { - return fmt.Errorf("failed to build catalog: %w", err) + return mdlerrors.NewBackend("build catalog", err) } // Get modules from catalog @@ -115,7 +116,7 @@ type structureModule struct { func (e *Executor) getStructureModules(filterModule string, includeAll bool) ([]structureModule, error) { result, err := e.catalog.Query("SELECT Id, Name, Source, AppStoreGuid FROM modules ORDER BY Name") if err != nil { - return nil, fmt.Errorf("failed to query modules: %w", err) + return nil, mdlerrors.NewBackend("query modules", err) } var modules []structureModule @@ -316,7 +317,7 @@ func (e *Executor) structureDepth2(modules []structureModule) error { // Pre-load data that needs the reader h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } // Load domain models for associations @@ -477,7 +478,7 @@ func (e *Executor) structureDepth3(modules []structureModule) error { // Same data loading as depth 2 h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } domainModels, _ := e.reader.ListDomainModels() diff --git a/mdl/executor/cmd_styling.go b/mdl/executor/cmd_styling.go index 278f2338..2d7de41b 100644 --- a/mdl/executor/cmd_styling.go +++ b/mdl/executor/cmd_styling.go @@ -10,6 +10,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/pages" ) @@ -20,13 +21,13 @@ import ( func (e *Executor) execShowDesignProperties(s *ast.ShowDesignPropertiesStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } projectDir := filepath.Dir(e.mprPath) registry, err := loadThemeRegistry(projectDir) if err != nil { - return fmt.Errorf("failed to load theme registry: %w", err) + return mdlerrors.NewBackend("load theme registry", err) } if len(registry.WidgetProperties) == 0 { @@ -111,12 +112,12 @@ func (e *Executor) printOneProperty(p ThemeProperty) { func (e *Executor) execDescribeStyling(s *ast.DescribeStylingStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } var rawWidgets []rawWidget @@ -125,7 +126,7 @@ func (e *Executor) execDescribeStyling(s *ast.DescribeStylingStmt) error { // Find page allPages, err := e.reader.ListPages() if err != nil { - return fmt.Errorf("failed to list pages: %w", err) + return mdlerrors.NewBackend("list pages", err) } var foundPage *pages.Page @@ -138,14 +139,14 @@ func (e *Executor) execDescribeStyling(s *ast.DescribeStylingStmt) error { } } if foundPage == nil { - return fmt.Errorf("page %s not found", s.ContainerName.String()) + return mdlerrors.NewNotFound("page", s.ContainerName.String()) } rawWidgets = e.getPageWidgetsFromRaw(foundPage.ID) } else if s.ContainerType == "SNIPPET" { // Find snippet allSnippets, err := e.reader.ListSnippets() if err != nil { - return fmt.Errorf("failed to list snippets: %w", err) + return mdlerrors.NewBackend("list snippets", err) } var foundSnippet *pages.Snippet @@ -158,7 +159,7 @@ func (e *Executor) execDescribeStyling(s *ast.DescribeStylingStmt) error { } } if foundSnippet == nil { - return fmt.Errorf("snippet %s not found", s.ContainerName.String()) + return mdlerrors.NewNotFound("snippet", s.ContainerName.String()) } rawWidgets = e.getSnippetWidgetsFromRaw(foundSnippet.ID) } @@ -173,7 +174,7 @@ func (e *Executor) execDescribeStyling(s *ast.DescribeStylingStmt) error { if len(styledWidgets) == 0 { if s.WidgetName != "" { - return fmt.Errorf("widget %q not found in %s %s", s.WidgetName, s.ContainerType, s.ContainerName.String()) + return mdlerrors.NewNotFoundMsg("widget", s.WidgetName, fmt.Sprintf("widget %q not found in %s %s", s.WidgetName, s.ContainerType, s.ContainerName.String())) } fmt.Fprintf(e.output, "No styled widgets found in %s %s\n", s.ContainerType, s.ContainerName.String()) return nil @@ -253,15 +254,15 @@ func collectStyledWidgets(widgets []rawWidget, widgetName string) []rawWidget { func (e *Executor) execAlterStyling(s *ast.AlterStylingStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } if e.writer == nil { - return fmt.Errorf("project not opened for writing") + return mdlerrors.NewNotConnectedWrite() } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } if s.ContainerType == "PAGE" { @@ -270,14 +271,14 @@ func (e *Executor) execAlterStyling(s *ast.AlterStylingStmt) error { return e.alterStylingOnSnippet(s, h) } - return fmt.Errorf("unsupported container type: %s", s.ContainerType) + return mdlerrors.NewUnsupported("unsupported container type: " + s.ContainerType) } func (e *Executor) alterStylingOnPage(s *ast.AlterStylingStmt, h *ContainerHierarchy) error { // Find page allPages, err := e.reader.ListPages() if err != nil { - return fmt.Errorf("failed to list pages: %w", err) + return mdlerrors.NewBackend("list pages", err) } var page *pages.Page @@ -290,7 +291,7 @@ func (e *Executor) alterStylingOnPage(s *ast.AlterStylingStmt, h *ContainerHiera } } if page == nil { - return fmt.Errorf("page %s not found", s.ContainerName.String()) + return mdlerrors.NewNotFound("page", s.ContainerName.String()) } // Walk the page to find the widget by name @@ -308,12 +309,12 @@ func (e *Executor) alterStylingOnPage(s *ast.AlterStylingStmt, h *ContainerHiera } if !found { - return fmt.Errorf("widget %q not found in page %s", s.WidgetName, s.ContainerName.String()) + return mdlerrors.NewNotFoundMsg("widget", s.WidgetName, fmt.Sprintf("widget %q not found in page %s", s.WidgetName, s.ContainerName.String())) } // Save the page if err := e.writer.UpdatePage(page); err != nil { - return fmt.Errorf("failed to save page: %w", err) + return mdlerrors.NewBackend("save page", err) } fmt.Fprintf(e.output, "Updated styling on widget %q in page %s\n", s.WidgetName, s.ContainerName.String()) @@ -324,7 +325,7 @@ func (e *Executor) alterStylingOnSnippet(s *ast.AlterStylingStmt, h *ContainerHi // Find snippet allSnippets, err := e.reader.ListSnippets() if err != nil { - return fmt.Errorf("failed to list snippets: %w", err) + return mdlerrors.NewBackend("list snippets", err) } var snippet *pages.Snippet @@ -337,7 +338,7 @@ func (e *Executor) alterStylingOnSnippet(s *ast.AlterStylingStmt, h *ContainerHi } } if snippet == nil { - return fmt.Errorf("snippet %s not found", s.ContainerName.String()) + return mdlerrors.NewNotFound("snippet", s.ContainerName.String()) } // Walk the snippet to find the widget by name @@ -355,12 +356,12 @@ func (e *Executor) alterStylingOnSnippet(s *ast.AlterStylingStmt, h *ContainerHi } if !found { - return fmt.Errorf("widget %q not found in snippet %s", s.WidgetName, s.ContainerName.String()) + return mdlerrors.NewNotFoundMsg("widget", s.WidgetName, fmt.Sprintf("widget %q not found in snippet %s", s.WidgetName, s.ContainerName.String())) } // Save the snippet if err := e.writer.UpdateSnippet(snippet); err != nil { - return fmt.Errorf("failed to save snippet: %w", err) + return mdlerrors.NewBackend("save snippet", err) } fmt.Fprintf(e.output, "Updated styling on widget %q in snippet %s\n", s.WidgetName, s.ContainerName.String()) @@ -409,13 +410,13 @@ func applyStylingAssignments(widget any, assignments []ast.StylingAssignment, cl v = v.Elem() } if v.Kind() != reflect.Struct { - return fmt.Errorf("widget is not a struct") + return mdlerrors.NewValidation("widget is not a struct") } // Get BaseWidget baseWidget := v.FieldByName("BaseWidget") if !baseWidget.IsValid() { - return fmt.Errorf("widget has no BaseWidget field") + return mdlerrors.NewValidation("widget has no BaseWidget field") } // Clear design properties if requested @@ -453,7 +454,7 @@ func applyStylingAssignments(widget any, assignments []ast.StylingAssignment, cl func setDesignProperty(baseWidget reflect.Value, a ast.StylingAssignment) error { dpField := baseWidget.FieldByName("DesignProperties") if !dpField.IsValid() || !dpField.CanSet() { - return fmt.Errorf("widget does not support design properties") + return mdlerrors.NewUnsupported("widget does not support design properties") } // Get existing design properties @@ -511,7 +512,7 @@ func setDesignProperty(baseWidget reflect.Value, a ast.StylingAssignment) error func (e *Executor) findPageByName(name ast.QualifiedName, h *ContainerHierarchy) (*pages.Page, error) { allPages, err := e.reader.ListPages() if err != nil { - return nil, fmt.Errorf("failed to list pages: %w", err) + return nil, mdlerrors.NewBackend("list pages", err) } for _, p := range allPages { modID := h.FindModuleID(p.ContainerID) @@ -520,14 +521,14 @@ func (e *Executor) findPageByName(name ast.QualifiedName, h *ContainerHierarchy) return p, nil } } - return nil, fmt.Errorf("page %s not found", name.String()) + return nil, mdlerrors.NewNotFound("page", name.String()) } // findSnippetByName looks up a snippet by qualified name. func (e *Executor) findSnippetByName(name ast.QualifiedName, h *ContainerHierarchy) (*pages.Snippet, model.ID, error) { allSnippets, err := e.reader.ListSnippets() if err != nil { - return nil, "", fmt.Errorf("failed to list snippets: %w", err) + return nil, "", mdlerrors.NewBackend("list snippets", err) } for _, s := range allSnippets { modID := h.FindModuleID(s.ContainerID) @@ -536,5 +537,5 @@ func (e *Executor) findSnippetByName(name ast.QualifiedName, h *ContainerHierarc return s, modID, nil } } - return nil, "", fmt.Errorf("snippet %s not found", name.String()) + return nil, "", mdlerrors.NewNotFound("snippet", name.String()) } diff --git a/mdl/executor/cmd_widgets.go b/mdl/executor/cmd_widgets.go index 5b516799..6fa4212b 100644 --- a/mdl/executor/cmd_widgets.go +++ b/mdl/executor/cmd_widgets.go @@ -10,18 +10,19 @@ import ( "go.mongodb.org/mongo-driver/bson" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" ) // execShowWidgets handles the SHOW WIDGETS statement. func (e *Executor) execShowWidgets(s *ast.ShowWidgetsStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Ensure catalog is built (full mode for widgets) if err := e.ensureCatalog(true); err != nil { - return fmt.Errorf("failed to build catalog: %w", err) + return mdlerrors.NewBackend("build catalog", err) } // Build SQL query from filters @@ -49,7 +50,7 @@ func (e *Executor) execShowWidgets(s *ast.ShowWidgetsStmt) error { // Execute query using SQLite parameterization result, err := e.executeCatalogQueryWithArgs(query.String(), args...) if err != nil { - return fmt.Errorf("failed to query widgets: %w", err) + return mdlerrors.NewBackend("query widgets", err) } // Output results as table @@ -79,21 +80,21 @@ func (e *Executor) execShowWidgets(s *ast.ShowWidgetsStmt) error { // execUpdateWidgets handles the UPDATE WIDGETS statement. func (e *Executor) execUpdateWidgets(s *ast.UpdateWidgetsStmt) error { if e.reader == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } if e.writer == nil { - return fmt.Errorf("project not opened for writing") + return mdlerrors.NewNotConnectedWrite() } // Ensure catalog is built (full mode for widgets) if err := e.ensureCatalog(true); err != nil { - return fmt.Errorf("failed to build catalog: %w", err) + return mdlerrors.NewBackend("build catalog", err) } // Find matching widgets widgets, err := e.findMatchingWidgets(s.Filters, s.InModule) if err != nil { - return fmt.Errorf("failed to find widgets: %w", err) + return mdlerrors.NewBackend("find widgets", err) } if len(widgets) == 0 { @@ -211,7 +212,7 @@ func (e *Executor) updateWidgetsInContainer(containerID string, widgetRefs []wid return e.updateWidgetsInSnippet(containerID, containerName, widgetRefs, assignments, dryRun) } - return 0, fmt.Errorf("unsupported container type: %s", containerType) + return 0, mdlerrors.NewUnsupported(fmt.Sprintf("unsupported container type: %s", containerType)) } // updateWidgetsInPage updates widgets in a page using raw BSON. @@ -219,11 +220,11 @@ func (e *Executor) updateWidgetsInPage(containerID, containerName string, widget // Load raw BSON as ordered document (preserves field ordering) rawBytes, err := e.reader.GetRawUnitBytes(model.ID(containerID)) if err != nil { - return 0, fmt.Errorf("failed to load page %s: %w", containerName, err) + return 0, mdlerrors.NewBackend(fmt.Sprintf("load page %s", containerName), err) } var rawData bson.D if err := bson.Unmarshal(rawBytes, &rawData); err != nil { - return 0, fmt.Errorf("failed to unmarshal page %s: %w", containerName, err) + return 0, mdlerrors.NewBackend(fmt.Sprintf("unmarshal page %s", containerName), err) } updated := 0 @@ -251,10 +252,10 @@ func (e *Executor) updateWidgetsInPage(containerID, containerName string, widget if !dryRun && updated > 0 { outBytes, err := bson.Marshal(rawData) if err != nil { - return updated, fmt.Errorf("failed to marshal page %s: %w", containerName, err) + return updated, mdlerrors.NewBackend(fmt.Sprintf("marshal page %s", containerName), err) } if err := e.writer.UpdateRawUnit(containerID, outBytes); err != nil { - return updated, fmt.Errorf("failed to save page %s: %w", containerName, err) + return updated, mdlerrors.NewBackend(fmt.Sprintf("save page %s", containerName), err) } } @@ -266,11 +267,11 @@ func (e *Executor) updateWidgetsInSnippet(containerID, containerName string, wid // Load raw BSON as ordered document (preserves field ordering) rawBytes, err := e.reader.GetRawUnitBytes(model.ID(containerID)) if err != nil { - return 0, fmt.Errorf("failed to load snippet %s: %w", containerName, err) + return 0, mdlerrors.NewBackend(fmt.Sprintf("load snippet %s", containerName), err) } var rawData bson.D if err := bson.Unmarshal(rawBytes, &rawData); err != nil { - return 0, fmt.Errorf("failed to unmarshal snippet %s: %w", containerName, err) + return 0, mdlerrors.NewBackend(fmt.Sprintf("unmarshal snippet %s", containerName), err) } updated := 0 @@ -298,10 +299,10 @@ func (e *Executor) updateWidgetsInSnippet(containerID, containerName string, wid if !dryRun && updated > 0 { outBytes, err := bson.Marshal(rawData) if err != nil { - return updated, fmt.Errorf("failed to marshal snippet %s: %w", containerName, err) + return updated, mdlerrors.NewBackend(fmt.Sprintf("marshal snippet %s", containerName), err) } if err := e.writer.UpdateRawUnit(containerID, outBytes); err != nil { - return updated, fmt.Errorf("failed to save snippet %s: %w", containerName, err) + return updated, mdlerrors.NewBackend(fmt.Sprintf("save snippet %s", containerName), err) } } diff --git a/mdl/executor/cmd_workflows.go b/mdl/executor/cmd_workflows.go index b2641fe9..fdd8d6ae 100644 --- a/mdl/executor/cmd_workflows.go +++ b/mdl/executor/cmd_workflows.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/sdk/workflows" ) @@ -16,12 +17,12 @@ import ( func (e *Executor) showWorkflows(moduleName string) error { h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } wfs, err := e.reader.ListWorkflows() if err != nil { - return fmt.Errorf("failed to list workflows: %w", err) + return mdlerrors.NewBackend("list workflows", err) } type row struct { @@ -143,12 +144,12 @@ func (e *Executor) describeWorkflow(name ast.QualifiedName) error { func (e *Executor) describeWorkflowToString(name ast.QualifiedName) (string, map[string]elkSourceRange, error) { h, err := e.getHierarchy() if err != nil { - return "", nil, fmt.Errorf("failed to build hierarchy: %w", err) + return "", nil, mdlerrors.NewBackend("build hierarchy", err) } allWorkflows, err := e.reader.ListWorkflows() if err != nil { - return "", nil, fmt.Errorf("failed to list workflows: %w", err) + return "", nil, mdlerrors.NewBackend("list workflows", err) } var targetWf *workflows.Workflow @@ -162,7 +163,7 @@ func (e *Executor) describeWorkflowToString(name ast.QualifiedName) (string, map } if targetWf == nil { - return "", nil, fmt.Errorf("workflow not found: %s", name) + return "", nil, mdlerrors.NewNotFound("workflow", name.String()) } var lines []string diff --git a/mdl/executor/cmd_workflows_write.go b/mdl/executor/cmd_workflows_write.go index 5fb1b783..eb5cef32 100644 --- a/mdl/executor/cmd_workflows_write.go +++ b/mdl/executor/cmd_workflows_write.go @@ -9,6 +9,7 @@ import ( "unicode" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/workflows" @@ -17,7 +18,7 @@ import ( // execCreateWorkflow handles CREATE WORKFLOW statements. func (e *Executor) execCreateWorkflow(s *ast.CreateWorkflowStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } module, err := e.findOrCreateModule(s.Name.Module) @@ -28,12 +29,12 @@ func (e *Executor) execCreateWorkflow(s *ast.CreateWorkflowStmt) error { // Check if workflow already exists h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } existingWorkflows, err := e.reader.ListWorkflows() if err != nil { - return fmt.Errorf("failed to list workflows: %w", err) + return mdlerrors.NewBackend("list workflows", err) } var existingID model.ID @@ -42,7 +43,7 @@ func (e *Executor) execCreateWorkflow(s *ast.CreateWorkflowStmt) error { modName := h.GetModuleName(modID) if modName == s.Name.Module && existing.Name == s.Name.Name { if !s.CreateOrModify { - return fmt.Errorf("workflow '%s.%s' already exists (use CREATE OR REPLACE to overwrite)", s.Name.Module, s.Name.Name) + return mdlerrors.NewAlreadyExistsMsg("workflow", s.Name.Module+"."+s.Name.Name, "workflow '"+s.Name.Module+"."+s.Name.Name+"' already exists (use CREATE OR REPLACE to overwrite)") } existingID = existing.ID break @@ -113,12 +114,12 @@ func (e *Executor) execCreateWorkflow(s *ast.CreateWorkflowStmt) error { if existingID != "" { // Delete existing and recreate if err := e.writer.DeleteWorkflow(existingID); err != nil { - return fmt.Errorf("failed to delete existing workflow: %w", err) + return mdlerrors.NewBackend("delete existing workflow", err) } } if err := e.writer.CreateWorkflow(wf); err != nil { - return fmt.Errorf("failed to create workflow: %w", err) + return mdlerrors.NewBackend("create workflow", err) } e.invalidateHierarchy() @@ -129,17 +130,17 @@ func (e *Executor) execCreateWorkflow(s *ast.CreateWorkflowStmt) error { // execDropWorkflow handles DROP WORKFLOW statements. func (e *Executor) execDropWorkflow(s *ast.DropWorkflowStmt) error { if e.writer == nil { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } h, err := e.getHierarchy() if err != nil { - return fmt.Errorf("failed to build hierarchy: %w", err) + return mdlerrors.NewBackend("build hierarchy", err) } wfs, err := e.reader.ListWorkflows() if err != nil { - return fmt.Errorf("failed to list workflows: %w", err) + return mdlerrors.NewBackend("list workflows", err) } for _, wf := range wfs { @@ -147,7 +148,7 @@ func (e *Executor) execDropWorkflow(s *ast.DropWorkflowStmt) error { modName := h.GetModuleName(modID) if modName == s.Name.Module && wf.Name == s.Name.Name { if err := e.writer.DeleteWorkflow(wf.ID); err != nil { - return fmt.Errorf("failed to delete workflow: %w", err) + return mdlerrors.NewBackend("delete workflow", err) } e.invalidateHierarchy() fmt.Fprintf(e.output, "Dropped workflow: %s.%s\n", s.Name.Module, s.Name.Name) @@ -155,7 +156,7 @@ func (e *Executor) execDropWorkflow(s *ast.DropWorkflowStmt) error { } } - return fmt.Errorf("workflow not found: %s.%s", s.Name.Module, s.Name.Name) + return mdlerrors.NewNotFound("workflow", s.Name.Module+"."+s.Name.Name) } // generateWorkflowUUID generates a UUID for workflow elements. diff --git a/mdl/executor/executor.go b/mdl/executor/executor.go index aeb7c696..6bb4fafa 100644 --- a/mdl/executor/executor.go +++ b/mdl/executor/executor.go @@ -12,6 +12,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/catalog" "github.com/mendixlabs/mxcli/mdl/diaglog" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/mpr" @@ -256,7 +257,7 @@ func (e *Executor) finalizeProgramExecution() error { count, err := e.writer.ReconcileMemberAccesses(dm.ID, moduleName) if err != nil { - return fmt.Errorf("finalization: failed to reconcile security for module %s: %w", moduleName, err) + return mdlerrors.NewBackend(fmt.Sprintf("reconcile security for module %s", moduleName), err) } if count > 0 && !e.quiet { fmt.Fprintf(e.output, "Reconciled %d access rule(s) in module %s\n", count, moduleName) diff --git a/mdl/executor/executor_connect.go b/mdl/executor/executor_connect.go index dba280f9..a0d482bf 100644 --- a/mdl/executor/executor_connect.go +++ b/mdl/executor/executor_connect.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/sdk/mpr" ) @@ -16,7 +17,7 @@ func (e *Executor) execConnect(s *ast.ConnectStmt) error { writer, err := mpr.NewWriter(s.Path) if err != nil { - return fmt.Errorf("failed to connect: %w", err) + return mdlerrors.NewBackend("connect", err) } e.writer = writer @@ -39,7 +40,7 @@ func (e *Executor) execConnect(s *ast.ConnectStmt) error { // This is needed when the project file has been modified externally. func (e *Executor) reconnect() error { if e.mprPath == "" { - return fmt.Errorf("no project path to reconnect to") + return mdlerrors.NewNotConnected() } // Close existing connection @@ -50,7 +51,7 @@ func (e *Executor) reconnect() error { // Reopen connection writer, err := mpr.NewWriter(e.mprPath) if err != nil { - return fmt.Errorf("failed to reconnect: %w", err) + return mdlerrors.NewBackend("reconnect", err) } e.writer = writer diff --git a/mdl/executor/executor_dispatch.go b/mdl/executor/executor_dispatch.go index 270fc521..2b11e4c0 100644 --- a/mdl/executor/executor_dispatch.go +++ b/mdl/executor/executor_dispatch.go @@ -6,6 +6,7 @@ import ( "fmt" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" ) // executeInner dispatches a statement to its handler. @@ -302,6 +303,6 @@ func (e *Executor) executeInner(stmt ast.Statement) error { return e.execImport(s) default: - return fmt.Errorf("unknown statement type: %T", stmt) + return mdlerrors.NewUnsupported(fmt.Sprintf("unknown statement type: %T", stmt)) } } diff --git a/mdl/executor/executor_query.go b/mdl/executor/executor_query.go index 2108a5e7..3d65b6c5 100644 --- a/mdl/executor/executor_query.go +++ b/mdl/executor/executor_query.go @@ -3,14 +3,13 @@ package executor import ( - "fmt" - "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" ) func (e *Executor) execShow(s *ast.ShowStmt) error { if e.reader == nil && s.ObjectType != ast.ShowModules && s.ObjectType != ast.ShowFragments { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } switch s.ObjectType { @@ -139,13 +138,13 @@ func (e *Executor) execShow(s *ast.ShowStmt) error { case ast.ShowExportMappings: return e.showExportMappings(s.InModule) default: - return fmt.Errorf("unknown show object type") + return mdlerrors.NewUnsupported("unknown show object type") } } func (e *Executor) execDescribe(s *ast.DescribeStmt) error { if e.reader == nil && s.ObjectType != ast.DescribeFragment { - return fmt.Errorf("not connected to a project") + return mdlerrors.NewNotConnected() } // Determine the object type label and name for JSON wrapping. @@ -231,7 +230,7 @@ func (e *Executor) execDescribe(s *ast.DescribeStmt) error { case ast.DescribeExportMapping: return e.describeExportMapping(s.Name) default: - return fmt.Errorf("unknown describe object type") + return mdlerrors.NewUnsupported("unknown describe object type") } }) } diff --git a/mdl/executor/fragment_test.go b/mdl/executor/fragment_test.go index b76ec7e3..08ab1736 100644 --- a/mdl/executor/fragment_test.go +++ b/mdl/executor/fragment_test.go @@ -68,8 +68,8 @@ func TestExecDefineFragmentDuplicate(t *testing.T) { if err == nil { t.Fatal("Expected error for duplicate fragment, got nil") } - if !strings.Contains(err.Error(), "already defined") { - t.Errorf("Expected 'already defined' error, got: %v", err) + if !strings.Contains(err.Error(), "already exists") { + t.Errorf("Expected 'already exists' error, got: %v", err) } } diff --git a/mdl/executor/helpers.go b/mdl/executor/helpers.go index 35ac72a6..3d97e80e 100644 --- a/mdl/executor/helpers.go +++ b/mdl/executor/helpers.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/mpr" @@ -45,12 +46,12 @@ func (e *Executor) invalidateModuleCache() { func (e *Executor) findModule(name string) (*model.Module, error) { // Module name is required - objects must always belong to a module if name == "" { - return nil, fmt.Errorf("module name is required: objects must be created within a module (use ModuleName.ObjectName syntax)") + return nil, mdlerrors.NewValidation("module name is required: objects must be created within a module (use ModuleName.ObjectName syntax)") } modules, err := e.getModulesFromCache() if err != nil { - return nil, fmt.Errorf("failed to list modules: %w", err) + return nil, mdlerrors.NewBackend("list modules", err) } for _, m := range modules { @@ -59,7 +60,7 @@ func (e *Executor) findModule(name string) (*model.Module, error) { } } - return nil, fmt.Errorf("module not found: %s", name) + return nil, mdlerrors.NewNotFound("module", name) } // findOrCreateModule looks up a module by name, auto-creating it if it doesn't exist @@ -75,7 +76,7 @@ func (e *Executor) findOrCreateModule(name string) (*model.Module, error) { } // Auto-create the module if createErr := e.execCreateModule(&ast.CreateModuleStmt{Name: name}); createErr != nil { - return nil, fmt.Errorf("auto-create module %s failed: %w", name, createErr) + return nil, mdlerrors.NewBackend("auto-create module "+name, createErr) } return e.findModule(name) } @@ -83,7 +84,7 @@ func (e *Executor) findOrCreateModule(name string) (*model.Module, error) { func (e *Executor) findModuleByID(id model.ID) (*model.Module, error) { modules, err := e.getModulesFromCache() if err != nil { - return nil, fmt.Errorf("failed to list modules: %w", err) + return nil, mdlerrors.NewBackend("list modules", err) } for _, m := range modules { @@ -92,7 +93,7 @@ func (e *Executor) findModuleByID(id model.ID) (*model.Module, error) { } } - return nil, fmt.Errorf("module not found with ID: %s", id) + return nil, mdlerrors.NewNotFoundMsg("module", string(id), "module not found with ID: "+string(id)) } // resolveFolder resolves a folder path (e.g., "Resources/Images") to a folder ID. @@ -104,7 +105,7 @@ func (e *Executor) resolveFolder(moduleID model.ID, folderPath string) (model.ID folders, err := e.reader.ListFolders() if err != nil { - return "", fmt.Errorf("failed to list folders: %w", err) + return "", mdlerrors.NewBackend("list folders", err) } // Split path into parts @@ -132,7 +133,7 @@ func (e *Executor) resolveFolder(moduleID model.ID, folderPath string) (model.ID parentID := currentContainerID newFolderID, err := e.createFolder(part, parentID) if err != nil { - return "", fmt.Errorf("failed to create folder %s: %w", part, err) + return "", mdlerrors.NewBackend("create folder "+part, err) } currentContainerID = newFolderID diff --git a/mdl/executor/oql_type_inference.go b/mdl/executor/oql_type_inference.go index 536baeca..c314262c 100644 --- a/mdl/executor/oql_type_inference.go +++ b/mdl/executor/oql_type_inference.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/mdl/linter" "github.com/mendixlabs/mxcli/sdk/domainmodel" ) @@ -682,7 +683,7 @@ func (e *Executor) findEntity(moduleName, entityName string) (*domainmodel.Entit } } - return nil, fmt.Errorf("entity not found: %s.%s", moduleName, entityName) + return nil, mdlerrors.NewNotFound("entity", moduleName+"."+entityName) } // convertDomainModelTypeToAST converts a domainmodel.AttributeType to ast.DataType. diff --git a/mdl/executor/output_guard.go b/mdl/executor/output_guard.go index d05e0d8e..f0ae9898 100644 --- a/mdl/executor/output_guard.go +++ b/mdl/executor/output_guard.go @@ -4,9 +4,10 @@ package executor import ( "bytes" - "fmt" "io" "sync" + + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" ) // outputGuard wraps an io.Writer with a per-statement line limit. @@ -47,7 +48,7 @@ func (g *outputGuard) Write(p []byte) (int, error) { g.exceeded = true // Write the current chunk first so output isn't abruptly cut mid-line. _, _ = g.w.Write(p) - return len(p), fmt.Errorf("output line limit exceeded (%d lines); statement aborted", g.maxLines) + return len(p), mdlerrors.NewValidationf("output line limit exceeded (%d lines); statement aborted", g.maxLines) } return g.w.Write(p) diff --git a/mdl/executor/theme_reader.go b/mdl/executor/theme_reader.go index de897323..50d622f2 100644 --- a/mdl/executor/theme_reader.go +++ b/mdl/executor/theme_reader.go @@ -4,10 +4,11 @@ package executor import ( "encoding/json" - "fmt" "os" "path/filepath" "strings" + + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" ) // ThemeProperty represents a single design property definition from design-properties.json. @@ -47,7 +48,7 @@ func loadThemeRegistry(projectDir string) (*ThemeRegistry, error) { // Walk themesource/*/web/design-properties.json entries, err := os.ReadDir(themesourceDir) if err != nil { - return nil, fmt.Errorf("reading themesource directory: %w", err) + return nil, mdlerrors.NewBackend("read themesource directory", err) } for _, entry := range entries { diff --git a/mdl/executor/validate.go b/mdl/executor/validate.go index baf02097..2e24cb5d 100644 --- a/mdl/executor/validate.go +++ b/mdl/executor/validate.go @@ -8,6 +8,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" ) // scriptContext holds objects defined within a script for reference validation. @@ -153,7 +154,7 @@ func (sc *scriptContext) has(name string) bool { // to objects that are defined within the script itself. func (e *Executor) ValidateProgram(prog *ast.Program) []error { if e.reader == nil { - return []error{fmt.Errorf("not connected to a project")} + return []error{mdlerrors.NewNotConnected()} } // Collect all objects defined in the script @@ -177,7 +178,7 @@ func (e *Executor) validateWithContext(stmt ast.Statement, sc *scriptContext) er case *ast.CreateEntityStmt: if s.Name.Module != "" && !sc.modules[s.Name.Module] { if _, err := e.findModule(s.Name.Module); err != nil { - return fmt.Errorf("module not found: %s", s.Name.Module) + return mdlerrors.NewNotFound("module", s.Name.Module) } } // Validate enumeration references in attributes @@ -188,7 +189,7 @@ func (e *Executor) validateWithContext(stmt ast.Statement, sc *scriptContext) er enumRef := attr.Type.EnumRef // Check for missing module (common mistake - bare type name) if enumRef.Module == "" { - return fmt.Errorf("attribute '%s': enumeration reference '%s' is missing module prefix. "+ + return mdlerrors.NewValidationf("attribute '%s': enumeration reference '%s' is missing module prefix. "+ "Did you mean to use a built-in type like DateTime instead of DateAndTime?", attr.Name, enumRef.Name) } @@ -196,7 +197,7 @@ func (e *Executor) validateWithContext(stmt ast.Statement, sc *scriptContext) er enumQN := enumRef.String() if !sc.enumerations[enumQN] { if !e.enumerationExists(enumQN) { - return fmt.Errorf("attribute '%s': enumeration not found: %s", attr.Name, enumQN) + return mdlerrors.NewNotFoundMsg("enumeration", enumQN, fmt.Sprintf("attribute '%s': enumeration not found: %s", attr.Name, enumQN)) } } } @@ -206,111 +207,111 @@ func (e *Executor) validateWithContext(stmt ast.Statement, sc *scriptContext) er for _, col := range idx.Columns { dt, exists := attrTypes[col.Name] if !exists { - return fmt.Errorf("INDEX on unknown attribute '%s'", col.Name) + return mdlerrors.NewValidationf("INDEX on unknown attribute '%s'", col.Name) } if dt.Kind == ast.TypeString && dt.Length == 0 { - return fmt.Errorf("INDEX on attribute '%s' is not allowed — String(unlimited) maps to TEXT/CLOB which cannot be indexed. Use a fixed length, e.g. String(200)", col.Name) + return mdlerrors.NewValidationf("INDEX on attribute '%s' is not allowed — String(unlimited) maps to TEXT/CLOB which cannot be indexed. Use a fixed length, e.g. String(200)", col.Name) } } } case *ast.CreateAssociationStmt: if s.Name.Module != "" && !sc.modules[s.Name.Module] { if _, err := e.findModule(s.Name.Module); err != nil { - return fmt.Errorf("module not found: %s", s.Name.Module) + return mdlerrors.NewNotFound("module", s.Name.Module) } } // Check parent and child entity references if s.Parent.Module != "" && !sc.modules[s.Parent.Module] { if _, err := e.findModule(s.Parent.Module); err != nil { - return fmt.Errorf("parent entity module not found: %s", s.Parent.Module) + return mdlerrors.NewNotFoundMsg("module", s.Parent.Module, "parent entity module not found: "+s.Parent.Module) } } if s.Child.Module != "" && !sc.modules[s.Child.Module] { if _, err := e.findModule(s.Child.Module); err != nil { - return fmt.Errorf("child entity module not found: %s", s.Child.Module) + return mdlerrors.NewNotFoundMsg("module", s.Child.Module, "child entity module not found: "+s.Child.Module) } } case *ast.CreateImageCollectionStmt: if s.Name.Module != "" && !sc.modules[s.Name.Module] { if _, err := e.findModule(s.Name.Module); err != nil { - return fmt.Errorf("module not found: %s", s.Name.Module) + return mdlerrors.NewNotFound("module", s.Name.Module) } } case *ast.DropImageCollectionStmt: if s.Name.Module != "" && !sc.modules[s.Name.Module] { if _, err := e.findModule(s.Name.Module); err != nil { - return fmt.Errorf("module not found: %s", s.Name.Module) + return mdlerrors.NewNotFound("module", s.Name.Module) } } case *ast.CreateEnumerationStmt: if s.Name.Module != "" && !sc.modules[s.Name.Module] { if _, err := e.findModule(s.Name.Module); err != nil { - return fmt.Errorf("module not found: %s", s.Name.Module) + return mdlerrors.NewNotFound("module", s.Name.Module) } } case *ast.CreateMicroflowStmt: if s.Name.Module != "" && !sc.modules[s.Name.Module] { if _, err := e.findModule(s.Name.Module); err != nil { - return fmt.Errorf("module not found: %s", s.Name.Module) + return mdlerrors.NewNotFound("module", s.Name.Module) } } // Validate microflow body for semantic errors (e.g., undeclared variables) if validationErrors := ValidateMicroflowBody(s); len(validationErrors) > 0 { - return fmt.Errorf("microflow '%s' has validation errors:\n - %s", + return mdlerrors.NewValidationf("microflow '%s' has validation errors:\n - %s", s.Name.String(), strings.Join(validationErrors, "\n - ")) } // Validate references inside microflow body (pages, microflows, java actions, entities) if refErrors := e.validateMicroflowReferences(s, sc); len(refErrors) > 0 { - return fmt.Errorf("microflow '%s' has reference errors:\n - %s", + return mdlerrors.NewValidationf("microflow '%s' has reference errors:\n - %s", s.Name.String(), strings.Join(refErrors, "\n - ")) } case *ast.CreatePageStmtV3: if s.Name.Module != "" && !sc.modules[s.Name.Module] { if _, err := e.findModule(s.Name.Module); err != nil { - return fmt.Errorf("module not found: %s", s.Name.Module) + return mdlerrors.NewNotFound("module", s.Name.Module) } } // Validate widget references (DataSource, Action, Snippet) if refErrors := e.validateWidgetReferences(s.Widgets, sc); len(refErrors) > 0 { - return fmt.Errorf("page '%s' has reference errors:\n - %s", + return mdlerrors.NewValidationf("page '%s' has reference errors:\n - %s", s.Name.String(), strings.Join(refErrors, "\n - ")) } // Validate page context tree (parameter/selection/attribute bindings) if ctxErrors := validatePageContextTree(s.Parameters, s.Widgets); len(ctxErrors) > 0 { - return fmt.Errorf("page '%s' has context errors:\n - %s", + return mdlerrors.NewValidationf("page '%s' has context errors:\n - %s", s.Name.String(), strings.Join(ctxErrors, "\n - ")) } case *ast.CreateSnippetStmtV3: if s.Name.Module != "" && !sc.modules[s.Name.Module] { if _, err := e.findModule(s.Name.Module); err != nil { - return fmt.Errorf("module not found: %s", s.Name.Module) + return mdlerrors.NewNotFound("module", s.Name.Module) } } // Validate widget references (DataSource, Action, Snippet) if refErrors := e.validateWidgetReferences(s.Widgets, sc); len(refErrors) > 0 { - return fmt.Errorf("snippet '%s' has reference errors:\n - %s", + return mdlerrors.NewValidationf("snippet '%s' has reference errors:\n - %s", s.Name.String(), strings.Join(refErrors, "\n - ")) } // Validate snippet context tree (parameter/selection/attribute bindings) if ctxErrors := validatePageContextTree(s.Parameters, s.Widgets); len(ctxErrors) > 0 { - return fmt.Errorf("snippet '%s' has context errors:\n - %s", + return mdlerrors.NewValidationf("snippet '%s' has context errors:\n - %s", s.Name.String(), strings.Join(ctxErrors, "\n - ")) } case *ast.CreateViewEntityStmt: if s.Name.Module != "" && !sc.modules[s.Name.Module] { if _, err := e.findModule(s.Name.Module); err != nil { - return fmt.Errorf("module not found: %s", s.Name.Module) + return mdlerrors.NewNotFound("module", s.Name.Module) } } // Validate OQL types match declared attribute types if typeErrors := e.ValidateViewEntityTypes(s); len(typeErrors) > 0 { - return fmt.Errorf("view entity '%s' has type mismatches:\n - %s", + return mdlerrors.NewValidationf("view entity '%s' has type mismatches:\n - %s", s.Name.String(), strings.Join(typeErrors, "\n - ")) } case *ast.AlterEntityStmt: if s.Name.Module != "" && !sc.modules[s.Name.Module] { if _, err := e.findModule(s.Name.Module); err != nil { - return fmt.Errorf("module not found: %s", s.Name.Module) + return mdlerrors.NewNotFound("module", s.Name.Module) } } // Validate enumeration references in ADD ATTRIBUTE @@ -319,13 +320,13 @@ func (e *Executor) validateWithContext(stmt ast.Statement, sc *scriptContext) er if attr.Type.Kind == ast.TypeEnumeration && attr.Type.EnumRef != nil { enumRef := attr.Type.EnumRef if enumRef.Module == "" { - return fmt.Errorf("attribute '%s': enumeration reference '%s' is missing module prefix", + return mdlerrors.NewValidationf("attribute '%s': enumeration reference '%s' is missing module prefix", attr.Name, enumRef.Name) } enumQN := enumRef.String() if !sc.enumerations[enumQN] { if !e.enumerationExists(enumQN) { - return fmt.Errorf("attribute '%s': enumeration not found: %s", attr.Name, enumQN) + return mdlerrors.NewNotFoundMsg("enumeration", enumQN, fmt.Sprintf("attribute '%s': enumeration not found: %s", attr.Name, enumQN)) } } } @@ -333,14 +334,14 @@ func (e *Executor) validateWithContext(stmt ast.Statement, sc *scriptContext) er case *ast.DropEntityStmt: if s.Name.Module != "" && !sc.modules[s.Name.Module] { if _, err := e.findModule(s.Name.Module); err != nil { - return fmt.Errorf("module not found: %s", s.Name.Module) + return mdlerrors.NewNotFound("module", s.Name.Module) } } case *ast.DropModuleStmt: // For DROP, check if module exists in project OR will be created in script if !sc.modules[s.Name] { if _, err := e.findModule(s.Name); err != nil { - return fmt.Errorf("module not found: %s", s.Name) + return mdlerrors.NewNotFound("module", s.Name) } } diff --git a/mdl/executor/widget_engine.go b/mdl/executor/widget_engine.go index 392970fb..ad11effa 100644 --- a/mdl/executor/widget_engine.go +++ b/mdl/executor/widget_engine.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/pages" @@ -95,10 +96,10 @@ func (e *PluggableWidgetEngine) Build(def *WidgetDefinition, w *ast.WidgetV3) (* embeddedType, embeddedObject, embeddedIDs, embeddedObjectTypeID, err := widgets.GetTemplateFullBSON(def.WidgetID, mpr.GenerateID, e.pageBuilder.reader.Path()) if err != nil { - return nil, fmt.Errorf("failed to load %s template: %w", def.MDLName, err) + return nil, mdlerrors.NewBackend("load "+def.MDLName+" template", err) } if embeddedType == nil || embeddedObject == nil { - return nil, fmt.Errorf("%s template not found", def.MDLName) + return nil, mdlerrors.NewNotFound("template", def.MDLName) } propertyTypeIDs := convertPropertyTypeIDs(embeddedIDs) @@ -114,12 +115,12 @@ func (e *PluggableWidgetEngine) Build(def *WidgetDefinition, w *ast.WidgetV3) (* for _, mapping := range mappings { ctx, err := e.resolveMapping(mapping, w) if err != nil { - return nil, fmt.Errorf("failed to resolve mapping for %s: %w", mapping.PropertyKey, err) + return nil, mdlerrors.NewBackend("resolve mapping for "+mapping.PropertyKey, err) } op := e.operations.Lookup(mapping.Operation) if op == nil { - return nil, fmt.Errorf("unknown operation %q for property %s", mapping.Operation, mapping.PropertyKey) + return nil, mdlerrors.NewValidationf("unknown operation %q for property %s", mapping.Operation, mapping.PropertyKey) } updatedObject = op(updatedObject, propertyTypeIDs, mapping.PropertyKey, ctx) @@ -145,7 +146,7 @@ func (e *PluggableWidgetEngine) Build(def *WidgetDefinition, w *ast.WidgetV3) (* if entry.ValueType == "DataSource" { dataSource, entityName, err := e.pageBuilder.buildDataSourceV3(ds) if err != nil { - return nil, fmt.Errorf("auto datasource for %s: %w", propKey, err) + return nil, mdlerrors.NewBackend("auto datasource for "+propKey, err) } ctx := &BuildContext{DataSource: dataSource, EntityName: entityName} updatedObject = opDatasource(updatedObject, propertyTypeIDs, propKey, ctx) @@ -380,12 +381,12 @@ func (e *PluggableWidgetEngine) selectMappings(def *WidgetDefinition, w *ast.Wid // Use fallback mode if fallback != nil { if fallbackCount > 1 { - return nil, nil, fmt.Errorf("widget %s has %d modes without conditions; only one default mode is allowed", def.MDLName, fallbackCount) + return nil, nil, mdlerrors.NewValidationf("widget %s has %d modes without conditions; only one default mode is allowed", def.MDLName, fallbackCount) } return fallback.PropertyMappings, fallback.ChildSlots, nil } - return nil, nil, fmt.Errorf("no matching mode for widget %s", def.MDLName) + return nil, nil, mdlerrors.NewValidationf("no matching mode for widget %s", def.MDLName) } // evaluateCondition checks a built-in condition string against the AST widget. @@ -436,7 +437,7 @@ func (e *PluggableWidgetEngine) resolveMapping(mapping PropertyMapping, w *ast.W if ds := w.GetDataSource(); ds != nil { dataSource, entityName, err := e.pageBuilder.buildDataSourceV3(ds) if err != nil { - return nil, fmt.Errorf("failed to build datasource: %w", err) + return nil, mdlerrors.NewBackend("build datasource", err) } ctx.DataSource = dataSource ctx.EntityName = entityName @@ -472,7 +473,7 @@ func (e *PluggableWidgetEngine) resolveMapping(mapping PropertyMapping, w *ast.W // Entity name comes from DataSource context (must be resolved first by a DataSource mapping) ctx.EntityName = e.pageBuilder.entityContext if ctx.AssocPath != "" && ctx.EntityName == "" { - return nil, fmt.Errorf("association %q requires an entity context (add a DataSource mapping before Association)", ctx.AssocPath) + return nil, mdlerrors.NewValidationf("association %q requires an entity context (add a DataSource mapping before Association)", ctx.AssocPath) } case "OnClick": @@ -480,7 +481,7 @@ func (e *PluggableWidgetEngine) resolveMapping(mapping PropertyMapping, w *ast.W if action := w.GetAction(); action != nil { act, err := e.pageBuilder.buildClientActionV3(action) if err != nil { - return nil, fmt.Errorf("failed to build action: %w", err) + return nil, mdlerrors.NewBackend("build action", err) } ctx.ActionBSON = mpr.SerializeClientAction(act) } @@ -552,7 +553,7 @@ func (e *PluggableWidgetEngine) applyChildSlots(slots []ChildSlotMapping, w *ast op := e.operations.Lookup(slot.Operation) if op == nil { - return fmt.Errorf("unknown operation %q for child slot %s", slot.Operation, slot.PropertyKey) + return mdlerrors.NewValidationf("unknown operation %q for child slot %s", slot.Operation, slot.PropertyKey) } ctx := &BuildContext{ChildWidgets: childBSONs} diff --git a/mdl/executor/widget_property.go b/mdl/executor/widget_property.go index 69fa62a6..e44652a8 100644 --- a/mdl/executor/widget_property.go +++ b/mdl/executor/widget_property.go @@ -8,6 +8,7 @@ import ( "reflect" "strings" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/pages" "go.mongodb.org/mongo-driver/bson" @@ -46,7 +47,7 @@ func getWidgetID(widget any) string { // setWidgetProperty sets a property value on a widget by path. func setWidgetProperty(widget any, path string, value any) error { if widget == nil { - return fmt.Errorf("widget is nil") + return mdlerrors.NewValidation("widget is nil") } // Handle CustomWidget specifically @@ -69,7 +70,7 @@ func setCustomWidgetProperty(cw *pages.CustomWidget, path string, value any) err return updateStructuredWidgetProperty(cw.WidgetObject, cw.PropertyTypeIDMap, path, value) } - return fmt.Errorf("widget has no property data") + return mdlerrors.NewValidation("widget has no property data") } // updateBSONWidgetProperty updates a property in a BSON document. @@ -89,7 +90,7 @@ func updateBSONWidgetProperty(doc bson.D, path string, value any) error { } } } - return fmt.Errorf("property not found in BSON: %s", path) + return mdlerrors.NewNotFound("property", path) } // updateBSONPropertyByKey finds and updates a property by key in a BSON array. @@ -107,14 +108,14 @@ func updateBSONPropertyByKey(props bson.A, path string, value any) error { } } } - return fmt.Errorf("property not found: %s", path) + return mdlerrors.NewNotFound("property", path) } // updateBSONPropertyValueAtIndex updates the Value field of a property at the given index. func updateBSONPropertyValueAtIndex(props bson.A, index int, newValue any) error { propDoc, ok := props[index].(bson.D) if !ok { - return fmt.Errorf("property is not a BSON document") + return mdlerrors.NewValidation("property is not a BSON document") } for i := range propDoc { @@ -138,7 +139,7 @@ func updateBSONPropertyValueAtIndex(props bson.A, index int, newValue any) error } } - return fmt.Errorf("Value field not found in property") + return mdlerrors.NewValidation("Value field not found in property") } // convertToBSONValue converts a Go value to appropriate BSON format. @@ -163,7 +164,7 @@ func convertToBSONValue(value any) any { // updateStructuredWidgetProperty updates a property in a structured WidgetObject. func updateStructuredWidgetProperty(obj *pages.WidgetObject, typeMap map[string]pages.PropertyTypeIDEntry, path string, value any) error { if obj == nil || obj.Properties == nil { - return fmt.Errorf("widget object has no properties") + return mdlerrors.NewValidation("widget object has no properties") } bsonValue := convertToBSONValue(value) @@ -209,7 +210,7 @@ func updateStructuredWidgetProperty(obj *pages.WidgetObject, typeMap map[string] } } - return fmt.Errorf("property not found: %s", path) + return mdlerrors.NewNotFound("property", path) } // setWidgetFieldByReflection sets a simple field on a widget using reflection. @@ -219,15 +220,15 @@ func setWidgetFieldByReflection(widget any, fieldName string, value any) error { v = v.Elem() } if v.Kind() != reflect.Struct { - return fmt.Errorf("widget is not a struct") + return mdlerrors.NewValidation("widget is not a struct") } field := v.FieldByName(fieldName) if !field.IsValid() { - return fmt.Errorf("field not found: %s", fieldName) + return mdlerrors.NewNotFound("field", fieldName) } if !field.CanSet() { - return fmt.Errorf("field not settable: %s", fieldName) + return mdlerrors.NewValidationf("field not settable: %s", fieldName) } // Convert value to field type @@ -245,7 +246,7 @@ func setWidgetFieldByReflection(widget any, fieldName string, value any) error { return nil } - return fmt.Errorf("cannot convert %T to %s", value, fieldType) + return mdlerrors.NewValidationf("cannot convert %T to %s", value, fieldType) } // walkPageWidgets walks all widgets in a page and calls the visitor function. diff --git a/mdl/executor/widget_registry.go b/mdl/executor/widget_registry.go index 4b4d9d28..a0614d84 100644 --- a/mdl/executor/widget_registry.go +++ b/mdl/executor/widget_registry.go @@ -10,6 +10,7 @@ import ( "path/filepath" "strings" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/sdk/widgets/definitions" ) @@ -38,7 +39,7 @@ func NewWidgetRegistryWithOps(opReg *OperationRegistry) (*WidgetRegistry, error) entries, err := definitions.EmbeddedFS.ReadDir(".") if err != nil { - return nil, fmt.Errorf("read embedded definitions: %w", err) + return nil, mdlerrors.NewBackend("read embedded definitions", err) } for _, entry := range entries { @@ -48,12 +49,12 @@ func NewWidgetRegistryWithOps(opReg *OperationRegistry) (*WidgetRegistry, error) data, err := definitions.EmbeddedFS.ReadFile(entry.Name()) if err != nil { - return nil, fmt.Errorf("read definition %s: %w", entry.Name(), err) + return nil, mdlerrors.NewBackend(fmt.Sprintf("read definition %s", entry.Name()), err) } var def WidgetDefinition if err := json.Unmarshal(data, &def); err != nil { - return nil, fmt.Errorf("parse definition %s: %w", entry.Name(), err) + return nil, mdlerrors.NewBackend(fmt.Sprintf("parse definition %s", entry.Name()), err) } if err := validateDefinitionOperations(&def, entry.Name(), opReg); err != nil { @@ -139,16 +140,16 @@ func (r *WidgetRegistry) loadDefinitionsFromDir(dir string) error { filePath := filepath.Join(dir, entry.Name()) data, err := os.ReadFile(filePath) if err != nil { - return fmt.Errorf("read %s: %w", filePath, err) + return mdlerrors.NewBackend(fmt.Sprintf("read %s", filePath), err) } var def WidgetDefinition if err := json.Unmarshal(data, &def); err != nil { - return fmt.Errorf("parse %s: %w", filePath, err) + return mdlerrors.NewBackend(fmt.Sprintf("parse %s", filePath), err) } if def.WidgetID == "" || def.MDLName == "" { - return fmt.Errorf("invalid definition %s: widgetId and mdlName are required", entry.Name()) + return mdlerrors.NewValidationf("invalid definition %s: widgetId and mdlName are required", entry.Name()) } if err := validateDefinitionOperations(&def, entry.Name(), r.opReg); err != nil { @@ -181,7 +182,7 @@ func validateDefinitionOperations(def *WidgetDefinition, source string, opReg *O } for _, s := range def.ChildSlots { if !opReg.Has(s.Operation) { - return fmt.Errorf("%s: unknown operation %q in childSlots for key %q", source, s.Operation, s.PropertyKey) + return mdlerrors.NewValidationf("%s: unknown operation %q in childSlots for key %q", source, s.Operation, s.PropertyKey) } } for _, mode := range def.Modes { @@ -191,7 +192,7 @@ func validateDefinitionOperations(def *WidgetDefinition, source string, opReg *O } for _, s := range mode.ChildSlots { if !opReg.Has(s.Operation) { - return fmt.Errorf("%s: unknown operation %q in %schildSlots for key %q", source, s.Operation, ctx, s.PropertyKey) + return mdlerrors.NewValidationf("%s: unknown operation %q in %schildSlots for key %q", source, s.Operation, ctx, s.PropertyKey) } } } @@ -212,12 +213,12 @@ func validateMappings(mappings []PropertyMapping, source, modeCtx string, opReg hasDataSource := false for _, m := range mappings { if !opReg.Has(m.Operation) { - return fmt.Errorf("%s: unknown operation %q in %spropertyMappings for key %q", source, m.Operation, modeCtx, m.PropertyKey) + return mdlerrors.NewValidationf("%s: unknown operation %q in %spropertyMappings for key %q", source, m.Operation, modeCtx, m.PropertyKey) } // Check source/operation compatibility if incompatible, ok := incompatibleSourceOps[m.Source]; ok { if incompatible[m.Operation] { - return fmt.Errorf("%s: incompatible source %q with operation %q in %spropertyMappings for key %q", + return mdlerrors.NewValidationf("%s: incompatible source %q with operation %q in %spropertyMappings for key %q", source, m.Source, m.Operation, modeCtx, m.PropertyKey) } } @@ -227,7 +228,7 @@ func validateMappings(mappings []PropertyMapping, source, modeCtx string, opReg } // Association depends on entityContext set by a prior DataSource mapping if m.Source == "Association" && !hasDataSource { - return fmt.Errorf("%s: %spropertyMappings key %q uses source 'Association' before any 'DataSource' mapping — entityContext will be stale", + return mdlerrors.NewValidationf("%s: %spropertyMappings key %q uses source 'Association' before any 'DataSource' mapping — entityContext will be stale", source, modeCtx, m.PropertyKey) } } diff --git a/model/types.go b/model/types.go index 733db1af..63ca2a14 100644 --- a/model/types.go +++ b/model/types.go @@ -573,11 +573,11 @@ type ServiceOperation struct { // PublishedRestService represents a Rest$PublishedRestService document. type PublishedRestService struct { BaseElement - ContainerID ID `json:"containerId"` - Name string `json:"name"` - Path string `json:"path,omitempty"` - Version string `json:"version,omitempty"` - ServiceName string `json:"serviceName,omitempty"` + ContainerID ID `json:"containerId"` + Name string `json:"name"` + Path string `json:"path,omitempty"` + Version string `json:"version,omitempty"` + ServiceName string `json:"serviceName,omitempty"` Excluded bool `json:"excluded,omitempty"` AllowedRoles []string `json:"allowedRoles,omitempty"` Resources []*PublishedRestResource `json:"resources,omitempty"` @@ -658,9 +658,9 @@ type RestClientOperation struct { BodyMappings []*RestResponseMapping `json:"bodyMappings,omitempty"` // export mapping tree (Entity → JSON) for EXPORT_MAPPING bodies ResponseType string `json:"responseType"` // "JSON", "STRING", "FILE", "STATUS", "NONE", "MAPPING" ResponseVariable string `json:"responseVariable,omitempty"` - ResponseEntity string `json:"responseEntity,omitempty"` // target entity for implicit mapping response - ResponseMappings []*RestResponseMapping `json:"responseMappings,omitempty"` // JSON field → entity attribute - Timeout int `json:"timeout,omitempty"` // 0 = default (300s) + ResponseEntity string `json:"responseEntity,omitempty"` // target entity for implicit mapping response + ResponseMappings []*RestResponseMapping `json:"responseMappings,omitempty"` // JSON field → entity attribute + Timeout int `json:"timeout,omitempty"` // 0 = default (300s) } // RestResponseMapping represents one element in a response mapping tree. @@ -668,9 +668,9 @@ type RestClientOperation struct { // (Entity set, with its own Children). type RestResponseMapping struct { // Value mapping: maps a JSON field to an entity attribute - Attribute string `json:"attribute,omitempty"` // entity attribute short name - ExposedName string `json:"exposedName"` // JSON field name - JsonPath string `json:"jsonPath,omitempty"` // e.g. "(Object)|args|queryparam_1" + Attribute string `json:"attribute,omitempty"` // entity attribute short name + ExposedName string `json:"exposedName"` // JSON field name + JsonPath string `json:"jsonPath,omitempty"` // e.g. "(Object)|args|queryparam_1" // Object mapping: nested entity linked by association Entity string `json:"entity,omitempty"` // child entity (e.g. "RestDemo.Args") diff --git a/sdk/agenteditor/types.go b/sdk/agenteditor/types.go index 35250918..4e9dbfe2 100644 --- a/sdk/agenteditor/types.go +++ b/sdk/agenteditor/types.go @@ -81,7 +81,7 @@ type Model struct { // Portal-populated fields — usually empty on freshly-created documents. // They are filled by Studio Pro after the user clicks "Test Key". Type string `json:"type,omitempty"` - InnerName string `json:"innerName,omitempty"` // Contents.name field + InnerName string `json:"innerName,omitempty"` // Contents.name field DisplayName string `json:"displayName,omitempty"` // User-set: provider discriminator. Only observed value: "MxCloudGenAI". @@ -165,19 +165,19 @@ type Agent struct { Excluded bool `json:"excluded,omitempty"` ExportLevel string `json:"exportLevel,omitempty"` - Description string `json:"description,omitempty"` - SystemPrompt string `json:"systemPrompt,omitempty"` - UserPrompt string `json:"userPrompt,omitempty"` - UsageType string `json:"usageType,omitempty"` - Variables []AgentVar `json:"variables,omitempty"` - Tools []AgentTool `json:"tools,omitempty"` - KBTools []AgentKBTool `json:"knowledgebaseTools,omitempty"` - Model *DocRef `json:"model,omitempty"` - Entity *DocRef `json:"entity,omitempty"` - MaxTokens *int `json:"maxTokens,omitempty"` - ToolChoice string `json:"toolChoice,omitempty"` - Temperature *float64 `json:"temperature,omitempty"` - TopP *float64 `json:"topP,omitempty"` + Description string `json:"description,omitempty"` + SystemPrompt string `json:"systemPrompt,omitempty"` + UserPrompt string `json:"userPrompt,omitempty"` + UsageType string `json:"usageType,omitempty"` + Variables []AgentVar `json:"variables,omitempty"` + Tools []AgentTool `json:"tools,omitempty"` + KBTools []AgentKBTool `json:"knowledgebaseTools,omitempty"` + Model *DocRef `json:"model,omitempty"` + Entity *DocRef `json:"entity,omitempty"` + MaxTokens *int `json:"maxTokens,omitempty"` + ToolChoice string `json:"toolChoice,omitempty"` + Temperature *float64 `json:"temperature,omitempty"` + TopP *float64 `json:"topP,omitempty"` } // GetName returns the agent's name. diff --git a/sdk/microflows/microflows_actions.go b/sdk/microflows/microflows_actions.go index 374d1216..5b85d094 100644 --- a/sdk/microflows/microflows_actions.go +++ b/sdk/microflows/microflows_actions.go @@ -595,11 +595,11 @@ func (RestCallAction) isMicroflowAction() {} // BSON type: Microflows$RestOperationCallAction type RestOperationCallAction struct { model.BaseElement - ErrorHandlingType ErrorHandlingType `json:"errorHandlingType,omitempty"` - Operation string `json:"operation,omitempty"` // BY_NAME: Module.Service.Operation - OutputVariable *RestOutputVar `json:"outputVariable,omitempty"` // null or Microflows$OutputVariable - BodyVariable *RestBodyVar `json:"bodyVariable,omitempty"` // null or nested object - ParameterMappings []*RestParameterMapping `json:"parameterMappings,omitempty"` // path parameter bindings + ErrorHandlingType ErrorHandlingType `json:"errorHandlingType,omitempty"` + Operation string `json:"operation,omitempty"` // BY_NAME: Module.Service.Operation + OutputVariable *RestOutputVar `json:"outputVariable,omitempty"` // null or Microflows$OutputVariable + BodyVariable *RestBodyVar `json:"bodyVariable,omitempty"` // null or nested object + ParameterMappings []*RestParameterMapping `json:"parameterMappings,omitempty"` // path parameter bindings QueryParameterMappings []*RestQueryParameterMapping `json:"queryParameterMappings,omitempty"` // query parameter bindings } diff --git a/sdk/mpr/parser_customblob.go b/sdk/mpr/parser_customblob.go index b909bb3d..30b46e23 100644 --- a/sdk/mpr/parser_customblob.go +++ b/sdk/mpr/parser_customblob.go @@ -99,12 +99,12 @@ func (r *Reader) parseAgentEditorModel(unitID, containerID string, contents []by DisplayName string `json:"displayName"` Provider string `json:"provider"` ProviderFields struct { - Environment string `json:"environment"` - DeepLinkURL string `json:"deepLinkURL"` - KeyID string `json:"keyId"` - KeyName string `json:"keyName"` - ResourceName string `json:"resourceName"` - Key *agenteditor.ConstantRef `json:"key"` + Environment string `json:"environment"` + DeepLinkURL string `json:"deepLinkURL"` + KeyID string `json:"keyId"` + KeyName string `json:"keyName"` + ResourceName string `json:"resourceName"` + Key *agenteditor.ConstantRef `json:"key"` } `json:"providerFields"` } if err := json.Unmarshal([]byte(wrap.Contents), &payload); err != nil { @@ -259,19 +259,19 @@ func (r *Reader) parseAgentEditorAgent(unitID, containerID string, contents []by // Decode the fields we know about; unknown fields are ignored so // the parser stays forward-compatible with editor updates. var payload struct { - Description string `json:"description"` - SystemPrompt string `json:"systemPrompt"` - UserPrompt string `json:"userPrompt"` - UsageType string `json:"usageType"` - Variables []agenteditor.AgentVar `json:"variables"` - Tools []agenteditor.AgentTool `json:"tools"` + Description string `json:"description"` + SystemPrompt string `json:"systemPrompt"` + UserPrompt string `json:"userPrompt"` + UsageType string `json:"usageType"` + Variables []agenteditor.AgentVar `json:"variables"` + Tools []agenteditor.AgentTool `json:"tools"` KnowledgebaseTools []agenteditor.AgentKBTool `json:"knowledgebaseTools"` - Model *agenteditor.DocRef `json:"model"` - Entity *agenteditor.DocRef `json:"entity"` - MaxTokens *int `json:"maxTokens"` - ToolChoice string `json:"toolChoice"` - Temperature *float64 `json:"temperature"` - TopP *float64 `json:"topP"` + Model *agenteditor.DocRef `json:"model"` + Entity *agenteditor.DocRef `json:"entity"` + MaxTokens *int `json:"maxTokens"` + ToolChoice string `json:"toolChoice"` + Temperature *float64 `json:"temperature"` + TopP *float64 `json:"topP"` } if err := json.Unmarshal([]byte(wrap.Contents), &payload); err != nil { return nil, fmt.Errorf("failed to unmarshal agent-editor Agent Contents JSON: %w", err) diff --git a/sdk/mpr/writer_datatransformer.go b/sdk/mpr/writer_datatransformer.go index c7bf3c2e..90f8dab8 100644 --- a/sdk/mpr/writer_datatransformer.go +++ b/sdk/mpr/writer_datatransformer.go @@ -84,8 +84,8 @@ func serializeDataTransformer(dt *model.DataTransformer) ([]byte, error) { steps = append(steps, bson.M{ "$ID": idToBsonBinary(generateUUID()), - "$Type": "DataTransformers$Step", - "Action": action, + "$Type": "DataTransformers$Step", + "Action": action, "InputElementPointer": idToBsonBinary(rootElemID), "OutputElementPointer": idToBsonBinary(rootElemID), }) diff --git a/sdk/mpr/writer_rest.go b/sdk/mpr/writer_rest.go index 627e3478..970852f9 100644 --- a/sdk/mpr/writer_rest.go +++ b/sdk/mpr/writer_rest.go @@ -454,19 +454,19 @@ func (w *Writer) serializePublishedRestService(svc *model.PublishedRestService) ops := bson.A{int32(2)} for _, op := range res.Operations { opDoc := bson.M{ - "$ID": idToBsonBinary(GenerateID()), - "$Type": "Rest$PublishedRestServiceOperation", - "HttpMethod": httpMethodToMendix(op.HTTPMethod), - "Path": op.Path, - "Microflow": op.Microflow, - "Summary": op.Summary, - "Deprecated": op.Deprecated, - "Commit": "Yes", - "Documentation": "", - "ExportMapping": "", - "ImportMapping": "", + "$ID": idToBsonBinary(GenerateID()), + "$Type": "Rest$PublishedRestServiceOperation", + "HttpMethod": httpMethodToMendix(op.HTTPMethod), + "Path": op.Path, + "Microflow": op.Microflow, + "Summary": op.Summary, + "Deprecated": op.Deprecated, + "Commit": "Yes", + "Documentation": "", + "ExportMapping": "", + "ImportMapping": "", "ObjectHandlingBackup": "Create", - "Parameters": serializePublishedRestParams(op.Path, op.Microflow, op.Parameters), + "Parameters": serializePublishedRestParams(op.Path, op.Microflow, op.Parameters), } ops = append(ops, opDoc) } @@ -481,21 +481,21 @@ func (w *Writer) serializePublishedRestService(svc *model.PublishedRestService) } doc := bson.M{ - "$ID": idToBsonBinary(string(svc.ID)), - "$Type": "Rest$PublishedRestService", - "Name": svc.Name, - "Documentation": "", - "Excluded": svc.Excluded, - "ExportLevel": "Hidden", - "Path": svc.Path, - "Version": svc.Version, - "ServiceName": svc.ServiceName, - "AllowedRoles": makeMendixStringArray(svc.AllowedRoles), - "AuthenticationTypes": bson.A{int32(2)}, + "$ID": idToBsonBinary(string(svc.ID)), + "$Type": "Rest$PublishedRestService", + "Name": svc.Name, + "Documentation": "", + "Excluded": svc.Excluded, + "ExportLevel": "Hidden", + "Path": svc.Path, + "Version": svc.Version, + "ServiceName": svc.ServiceName, + "AllowedRoles": makeMendixStringArray(svc.AllowedRoles), + "AuthenticationTypes": bson.A{int32(2)}, "AuthenticationMicroflow": "", - "CorsConfiguration": nil, - "Parameters": bson.A{int32(2)}, - "Resources": resources, + "CorsConfiguration": nil, + "Parameters": bson.A{int32(2)}, + "Resources": resources, } return bson.Marshal(doc) diff --git a/sdk/pages/pages_datasources.go b/sdk/pages/pages_datasources.go index 594903d0..ee02ce9e 100644 --- a/sdk/pages/pages_datasources.go +++ b/sdk/pages/pages_datasources.go @@ -78,7 +78,7 @@ func (ListenToWidgetSource) isDataSource() {} // AssociationSource retrieves data via association. type AssociationSource struct { model.BaseElement - EntityPath string `json:"entityPath"` // "Module.Assoc" or "Module.Assoc/Module.DestEntity" + EntityPath string `json:"entityPath"` // "Module.Assoc" or "Module.Assoc/Module.DestEntity" ContextVariable string `json:"contextVariable,omitempty"` // page parameter name (without $) — empty for $currentObject }