From 1c7435b39057b93832eda19d96a073652792494a Mon Sep 17 00:00:00 2001 From: Andrew Vasilyev Date: Sun, 19 Apr 2026 15:33:50 +0200 Subject: [PATCH 1/5] refactor: extract shared types and utility functions to mdl/types Create mdl/types/ package with WASM-safe shared types extracted from sdk/mpr: domain model types, ID utilities, EDMX/AsyncAPI parsing, JSON formatting helpers. Migrate all executor handlers to use mdl/types directly, removing type aliases from sdk/mpr/reader_types.go. - Extract 16+ domain types to mdl/types/ (infrastructure, java, navigation, mapping) - Extract GenerateID, BlobToUUID, ValidateID to mdl/types/id.go - Extract ParseEdmx, ParseAsyncAPI to mdl/types/edmx.go, asyncapi.go - Extract PrettyPrintJSON, BuildJsonElementsFromSnippet to json_utils.go - Migrate 30+ executor handler files off sdk/mpr type references - sdk/mpr retains thin delegation wrappers for backward compatibility --- cmd/mxcli/project_tree.go | 3 +- mdl/backend/connection.go | 9 +- mdl/backend/doc.go | 7 +- mdl/backend/infrastructure.go | 16 +- mdl/backend/java.go | 8 +- mdl/backend/mapping.go | 8 +- mdl/backend/mock/backend.go | 47 +- mdl/backend/mock/mock_connection.go | 9 +- mdl/backend/mock/mock_infrastructure.go | 16 +- mdl/backend/mock/mock_java.go | 8 +- mdl/backend/mock/mock_mapping.go | 8 +- mdl/backend/mock/mock_module.go | 4 +- mdl/backend/mock/mock_navigation.go | 8 +- mdl/backend/mock/mock_security.go | 4 +- mdl/backend/mock/mock_workflow.go | 6 +- mdl/backend/mpr/backend.go | 86 +-- mdl/backend/mpr/convert.go | 457 +++++++++++++++ mdl/backend/navigation.go | 8 +- mdl/backend/security.go | 6 +- mdl/backend/workflow.go | 6 +- mdl/catalog/builder.go | 12 +- mdl/catalog/builder_contract.go | 6 +- mdl/catalog/builder_navigation.go | 6 +- mdl/catalog/builder_references.go | 4 +- mdl/executor/cmd_businessevents.go | 4 +- mdl/executor/cmd_contract.go | 68 +-- mdl/executor/cmd_entities.go | 32 +- mdl/executor/cmd_error_mock_test.go | 16 +- mdl/executor/cmd_export_mappings.go | 5 +- mdl/executor/cmd_folders.go | 4 +- mdl/executor/cmd_imagecollections.go | 8 +- .../cmd_imagecollections_mock_test.go | 10 +- mdl/executor/cmd_import_mappings.go | 7 +- mdl/executor/cmd_javaactions.go | 54 +- mdl/executor/cmd_javaactions_mock_test.go | 6 +- .../cmd_javascript_actions_mock_test.go | 10 +- mdl/executor/cmd_json_mock_test.go | 18 +- mdl/executor/cmd_jsonstructures.go | 16 +- mdl/executor/cmd_jsonstructures_mock_test.go | 6 +- .../cmd_microflows_builder_actions.go | 92 +-- .../cmd_microflows_builder_annotations.go | 12 +- mdl/executor/cmd_microflows_builder_calls.go | 120 ++-- .../cmd_microflows_builder_control.go | 20 +- mdl/executor/cmd_microflows_builder_flows.go | 16 +- mdl/executor/cmd_microflows_builder_graph.go | 8 +- .../cmd_microflows_builder_workflow.go | 38 +- mdl/executor/cmd_microflows_create.go | 6 +- mdl/executor/cmd_misc_mock_test.go | 6 +- mdl/executor/cmd_modules_mock_test.go | 6 +- mdl/executor/cmd_navigation.go | 22 +- mdl/executor/cmd_navigation_mock_test.go | 34 +- mdl/executor/cmd_odata.go | 8 +- mdl/executor/cmd_pages_builder.go | 9 +- mdl/executor/cmd_pages_builder_v3.go | 68 +-- mdl/executor/cmd_pages_builder_v3_layout.go | 36 +- mdl/executor/cmd_pages_builder_v3_widgets.go | 68 +-- mdl/executor/cmd_rename.go | 10 +- mdl/executor/cmd_security_write.go | 16 +- mdl/executor/cmd_workflows_write.go | 16 +- mdl/executor/cmd_write_handlers_mock_test.go | 14 +- mdl/executor/executor.go | 5 +- mdl/executor/helpers.go | 8 +- mdl/executor/widget_engine.go | 5 +- mdl/executor/widget_operations.go | 3 +- mdl/executor/widget_templates.go | 4 +- mdl/linter/rules/page_navigation_security.go | 3 +- mdl/types/asyncapi.go | 205 +++++++ mdl/types/doc.go | 7 + mdl/types/edmx.go | 541 +++++++++++++++++ mdl/types/id.go | 103 ++++ mdl/types/infrastructure.go | 93 +++ mdl/types/java.go | 54 ++ mdl/types/json_utils.go | 374 ++++++++++++ mdl/types/mapping.go | 65 +++ mdl/types/navigation.go | 85 +++ sdk/mpr/asyncapi.go | 203 +------ sdk/mpr/edmx.go | 548 +----------------- sdk/mpr/parser_misc.go | 51 +- sdk/mpr/reader.go | 15 +- sdk/mpr/reader_types.go | 249 +------- sdk/mpr/utils.go | 48 +- sdk/mpr/writer_core.go | 64 +- sdk/mpr/writer_imagecollection.go | 5 +- sdk/mpr/writer_imagecollection_test.go | 5 +- sdk/mpr/writer_jsonstructure.go | 376 +----------- 85 files changed, 2706 insertions(+), 2054 deletions(-) create mode 100644 mdl/backend/mpr/convert.go create mode 100644 mdl/types/asyncapi.go create mode 100644 mdl/types/doc.go create mode 100644 mdl/types/edmx.go create mode 100644 mdl/types/id.go create mode 100644 mdl/types/infrastructure.go create mode 100644 mdl/types/java.go create mode 100644 mdl/types/json_utils.go create mode 100644 mdl/types/mapping.go create mode 100644 mdl/types/navigation.go diff --git a/cmd/mxcli/project_tree.go b/cmd/mxcli/project_tree.go index 4238e8aa..86cb5859 100644 --- a/cmd/mxcli/project_tree.go +++ b/cmd/mxcli/project_tree.go @@ -9,6 +9,7 @@ import ( "sort" "github.com/mendixlabs/mxcli/mdl/executor" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/spf13/cobra" @@ -951,7 +952,7 @@ func buildDatabaseConnectionChildren(dbc *model.DatabaseConnection) []*TreeNode } // buildMenuTreeNodes recursively builds tree nodes from navigation menu items. -func buildMenuTreeNodes(parent *TreeNode, items []*mpr.NavMenuItem) { +func buildMenuTreeNodes(parent *TreeNode, items []*types.NavMenuItem) { for _, item := range items { label := item.Caption if label == "" { diff --git a/mdl/backend/connection.go b/mdl/backend/connection.go index 0bf5aa61..c1d3594a 100644 --- a/mdl/backend/connection.go +++ b/mdl/backend/connection.go @@ -3,9 +3,8 @@ package backend import ( + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" - "github.com/mendixlabs/mxcli/sdk/mpr/version" ) // ConnectionBackend manages the lifecycle of a backend connection. @@ -22,9 +21,9 @@ type ConnectionBackend interface { // Path returns the path of the connected project, or "" if not connected. Path() string // Version returns the MPR format version. - Version() mpr.MPRVersion + Version() types.MPRVersion // ProjectVersion returns the Mendix project version. - ProjectVersion() *version.ProjectVersion + ProjectVersion() *types.ProjectVersion // GetMendixVersion returns the Mendix version string. GetMendixVersion() (string, error) } @@ -42,7 +41,7 @@ type ModuleBackend interface { // FolderBackend provides folder operations. type FolderBackend interface { - ListFolders() ([]*mpr.FolderInfo, error) + ListFolders() ([]*types.FolderInfo, error) CreateFolder(folder *model.Folder) error DeleteFolder(id model.ID) error MoveFolder(id model.ID, newContainerID model.ID) error diff --git a/mdl/backend/doc.go b/mdl/backend/doc.go index 7e316b39..475f526a 100644 --- a/mdl/backend/doc.go +++ b/mdl/backend/doc.go @@ -4,8 +4,7 @@ // executor from concrete storage (e.g. .mpr files). Each interface // groups related read/write operations by domain concept. // -// Several method signatures currently reference types from sdk/mpr -// (e.g. NavigationDocument, FolderInfo, ImageCollection, JsonStructure, -// JavaAction, EntityMemberAccess, RenameHit). These should eventually be -// extracted into a shared types package to remove the mpr dependency. +// Shared value types live in mdl/types to keep this package free of +// sdk/mpr dependencies. Conversion between types.* and sdk/mpr.* +// structs is handled inside mdl/backend/mpr (MprBackend). package backend diff --git a/mdl/backend/infrastructure.go b/mdl/backend/infrastructure.go index a5e153f1..779a2b19 100644 --- a/mdl/backend/infrastructure.go +++ b/mdl/backend/infrastructure.go @@ -3,15 +3,15 @@ package backend import ( + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/agenteditor" - "github.com/mendixlabs/mxcli/sdk/mpr" ) // RenameBackend provides cross-cutting rename and reference-update operations. type RenameBackend interface { UpdateQualifiedNameInAllUnits(oldName, newName string) (int, error) - RenameReferences(oldName, newName string, dryRun bool) ([]mpr.RenameHit, error) + RenameReferences(oldName, newName string, dryRun bool) ([]types.RenameHit, error) RenameDocumentByName(moduleName, oldName, newName string) error } @@ -20,9 +20,9 @@ type RenameBackend interface { type RawUnitBackend interface { GetRawUnit(id model.ID) (map[string]any, error) GetRawUnitBytes(id model.ID) ([]byte, error) - ListRawUnitsByType(typePrefix string) ([]*mpr.RawUnit, error) - ListRawUnits(objectType string) ([]*mpr.RawUnitInfo, error) - GetRawUnitByName(objectType, qualifiedName string) (*mpr.RawUnitInfo, error) + ListRawUnitsByType(typePrefix string) ([]*types.RawUnit, error) + ListRawUnits(objectType string) ([]*types.RawUnitInfo, error) + GetRawUnitByName(objectType, qualifiedName string) (*types.RawUnitInfo, error) GetRawMicroflowByName(qualifiedName string) ([]byte, error) UpdateRawUnit(unitID string, contents []byte) error } @@ -30,7 +30,7 @@ type RawUnitBackend interface { // MetadataBackend provides project-level metadata and introspection. type MetadataBackend interface { ListAllUnitIDs() ([]string, error) - ListUnits() ([]*mpr.UnitInfo, error) + ListUnits() ([]*types.UnitInfo, error) GetUnitTypes() (map[string]int, error) GetProjectRootID() (string, error) ContentsDir() string @@ -40,8 +40,8 @@ type MetadataBackend interface { // WidgetBackend provides widget introspection operations. type WidgetBackend interface { - FindCustomWidgetType(widgetID string) (*mpr.RawCustomWidgetType, error) - FindAllCustomWidgetTypes(widgetID string) ([]*mpr.RawCustomWidgetType, error) + FindCustomWidgetType(widgetID string) (*types.RawCustomWidgetType, error) + FindAllCustomWidgetTypes(widgetID string) ([]*types.RawCustomWidgetType, error) } // AgentEditorBackend provides agent editor document operations. diff --git a/mdl/backend/java.go b/mdl/backend/java.go index 4908f2c4..cc4935fd 100644 --- a/mdl/backend/java.go +++ b/mdl/backend/java.go @@ -3,18 +3,18 @@ package backend import ( + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/javaactions" - "github.com/mendixlabs/mxcli/sdk/mpr" ) // JavaBackend provides Java and JavaScript action operations. type JavaBackend interface { - ListJavaActions() ([]*mpr.JavaAction, error) + ListJavaActions() ([]*types.JavaAction, error) ListJavaActionsFull() ([]*javaactions.JavaAction, error) - ListJavaScriptActions() ([]*mpr.JavaScriptAction, error) + ListJavaScriptActions() ([]*types.JavaScriptAction, error) ReadJavaActionByName(qualifiedName string) (*javaactions.JavaAction, error) - ReadJavaScriptActionByName(qualifiedName string) (*mpr.JavaScriptAction, error) + ReadJavaScriptActionByName(qualifiedName string) (*types.JavaScriptAction, error) CreateJavaAction(ja *javaactions.JavaAction) error UpdateJavaAction(ja *javaactions.JavaAction) error DeleteJavaAction(id model.ID) error diff --git a/mdl/backend/mapping.go b/mdl/backend/mapping.go index 50008b2d..9bbf1efe 100644 --- a/mdl/backend/mapping.go +++ b/mdl/backend/mapping.go @@ -3,8 +3,8 @@ package backend import ( + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" ) // MappingBackend provides import/export mapping and JSON structure operations. @@ -23,8 +23,8 @@ type MappingBackend interface { DeleteExportMapping(id model.ID) error MoveExportMapping(em *model.ExportMapping) error - ListJsonStructures() ([]*mpr.JsonStructure, error) - GetJsonStructureByQualifiedName(moduleName, name string) (*mpr.JsonStructure, error) - CreateJsonStructure(js *mpr.JsonStructure) error + ListJsonStructures() ([]*types.JsonStructure, error) + GetJsonStructureByQualifiedName(moduleName, name string) (*types.JsonStructure, error) + CreateJsonStructure(js *types.JsonStructure) error DeleteJsonStructure(id string) error } diff --git a/mdl/backend/mock/backend.go b/mdl/backend/mock/backend.go index b6dc8a39..e2f78a0c 100644 --- a/mdl/backend/mock/backend.go +++ b/mdl/backend/mock/backend.go @@ -12,8 +12,7 @@ import ( "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/javaactions" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/sdk/mpr" - "github.com/mendixlabs/mxcli/sdk/mpr/version" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/pages" "github.com/mendixlabs/mxcli/sdk/security" "github.com/mendixlabs/mxcli/sdk/workflows" @@ -31,8 +30,8 @@ type MockBackend struct { CommitFunc func() error IsConnectedFunc func() bool PathFunc func() string - VersionFunc func() mpr.MPRVersion - ProjectVersionFunc func() *version.ProjectVersion + VersionFunc func() types.MPRVersion + ProjectVersionFunc func() *types.ProjectVersion GetMendixVersionFunc func() (string, error) // ModuleBackend @@ -45,7 +44,7 @@ type MockBackend struct { DeleteModuleWithCleanupFunc func(id model.ID, moduleName string) error // FolderBackend - ListFoldersFunc func() ([]*mpr.FolderInfo, error) + ListFoldersFunc func() ([]*types.FolderInfo, error) CreateFolderFunc func(folder *model.Folder) error DeleteFolderFunc func(id model.ID) error MoveFolderFunc func(id model.ID, newContainerID model.ID) error @@ -144,14 +143,14 @@ type MockBackend struct { RemoveFromAllowedRolesFunc func(unitID model.ID, roleName string) (bool, error) AddEntityAccessRuleFunc func(params backend.EntityAccessRuleParams) error RemoveEntityAccessRuleFunc func(unitID model.ID, entityName string, roleNames []string) (int, error) - RevokeEntityMemberAccessFunc func(unitID model.ID, entityName string, roleNames []string, revocation mpr.EntityAccessRevocation) (int, error) + RevokeEntityMemberAccessFunc func(unitID model.ID, entityName string, roleNames []string, revocation types.EntityAccessRevocation) (int, error) RemoveRoleFromAllEntitiesFunc func(unitID model.ID, roleName string) (int, error) ReconcileMemberAccessesFunc func(unitID model.ID, moduleName string) (int, error) // NavigationBackend - ListNavigationDocumentsFunc func() ([]*mpr.NavigationDocument, error) - GetNavigationFunc func() (*mpr.NavigationDocument, error) - UpdateNavigationProfileFunc func(navDocID model.ID, profileName string, spec mpr.NavigationProfileSpec) error + ListNavigationDocumentsFunc func() ([]*types.NavigationDocument, error) + GetNavigationFunc func() (*types.NavigationDocument, error) + UpdateNavigationProfileFunc func(navDocID model.ID, profileName string, spec types.NavigationProfileSpec) error // ServiceBackend ListConsumedODataServicesFunc func() ([]*model.ConsumedODataService, error) @@ -196,17 +195,17 @@ type MockBackend struct { UpdateExportMappingFunc func(em *model.ExportMapping) error DeleteExportMappingFunc func(id model.ID) error MoveExportMappingFunc func(em *model.ExportMapping) error - ListJsonStructuresFunc func() ([]*mpr.JsonStructure, error) - GetJsonStructureByQualifiedNameFunc func(moduleName, name string) (*mpr.JsonStructure, error) - CreateJsonStructureFunc func(js *mpr.JsonStructure) error + ListJsonStructuresFunc func() ([]*types.JsonStructure, error) + GetJsonStructureByQualifiedNameFunc func(moduleName, name string) (*types.JsonStructure, error) + CreateJsonStructureFunc func(js *types.JsonStructure) error DeleteJsonStructureFunc func(id string) error // JavaBackend - ListJavaActionsFunc func() ([]*mpr.JavaAction, error) + ListJavaActionsFunc func() ([]*types.JavaAction, error) ListJavaActionsFullFunc func() ([]*javaactions.JavaAction, error) - ListJavaScriptActionsFunc func() ([]*mpr.JavaScriptAction, error) + ListJavaScriptActionsFunc func() ([]*types.JavaScriptAction, error) ReadJavaActionByNameFunc func(qualifiedName string) (*javaactions.JavaAction, error) - ReadJavaScriptActionByNameFunc func(qualifiedName string) (*mpr.JavaScriptAction, error) + ReadJavaScriptActionByNameFunc func(qualifiedName string) (*types.JavaScriptAction, error) CreateJavaActionFunc func(ja *javaactions.JavaAction) error UpdateJavaActionFunc func(ja *javaactions.JavaAction) error DeleteJavaActionFunc func(id model.ID) error @@ -224,8 +223,8 @@ type MockBackend struct { UpdateProjectSettingsFunc func(ps *model.ProjectSettings) error // ImageBackend - ListImageCollectionsFunc func() ([]*mpr.ImageCollection, error) - CreateImageCollectionFunc func(ic *mpr.ImageCollection) error + ListImageCollectionsFunc func() ([]*types.ImageCollection, error) + CreateImageCollectionFunc func(ic *types.ImageCollection) error DeleteImageCollectionFunc func(id string) error // ScheduledEventBackend @@ -234,21 +233,21 @@ type MockBackend struct { // RenameBackend UpdateQualifiedNameInAllUnitsFunc func(oldName, newName string) (int, error) - RenameReferencesFunc func(oldName, newName string, dryRun bool) ([]mpr.RenameHit, error) + RenameReferencesFunc func(oldName, newName string, dryRun bool) ([]types.RenameHit, error) RenameDocumentByNameFunc func(moduleName, oldName, newName string) error // RawUnitBackend GetRawUnitFunc func(id model.ID) (map[string]any, error) GetRawUnitBytesFunc func(id model.ID) ([]byte, error) - ListRawUnitsByTypeFunc func(typePrefix string) ([]*mpr.RawUnit, error) - ListRawUnitsFunc func(objectType string) ([]*mpr.RawUnitInfo, error) - GetRawUnitByNameFunc func(objectType, qualifiedName string) (*mpr.RawUnitInfo, error) + ListRawUnitsByTypeFunc func(typePrefix string) ([]*types.RawUnit, error) + ListRawUnitsFunc func(objectType string) ([]*types.RawUnitInfo, error) + GetRawUnitByNameFunc func(objectType, qualifiedName string) (*types.RawUnitInfo, error) GetRawMicroflowByNameFunc func(qualifiedName string) ([]byte, error) UpdateRawUnitFunc func(unitID string, contents []byte) error // MetadataBackend ListAllUnitIDsFunc func() ([]string, error) - ListUnitsFunc func() ([]*mpr.UnitInfo, error) + ListUnitsFunc func() ([]*types.UnitInfo, error) GetUnitTypesFunc func() (map[string]int, error) GetProjectRootIDFunc func() (string, error) ContentsDirFunc func() string @@ -256,8 +255,8 @@ type MockBackend struct { InvalidateCacheFunc func() // WidgetBackend - FindCustomWidgetTypeFunc func(widgetID string) (*mpr.RawCustomWidgetType, error) - FindAllCustomWidgetTypesFunc func(widgetID string) ([]*mpr.RawCustomWidgetType, error) + FindCustomWidgetTypeFunc func(widgetID string) (*types.RawCustomWidgetType, error) + FindAllCustomWidgetTypesFunc func(widgetID string) ([]*types.RawCustomWidgetType, error) // AgentEditorBackend ListAgentEditorModelsFunc func() ([]*agenteditor.Model, error) diff --git a/mdl/backend/mock/mock_connection.go b/mdl/backend/mock/mock_connection.go index 54ebdefd..7c7d6a7f 100644 --- a/mdl/backend/mock/mock_connection.go +++ b/mdl/backend/mock/mock_connection.go @@ -3,8 +3,7 @@ package mock import ( - "github.com/mendixlabs/mxcli/sdk/mpr" - "github.com/mendixlabs/mxcli/sdk/mpr/version" + "github.com/mendixlabs/mxcli/mdl/types" ) func (m *MockBackend) Connect(path string) error { @@ -42,15 +41,15 @@ func (m *MockBackend) Path() string { return "" } -func (m *MockBackend) Version() mpr.MPRVersion { +func (m *MockBackend) Version() types.MPRVersion { if m.VersionFunc != nil { return m.VersionFunc() } - var zero mpr.MPRVersion + var zero types.MPRVersion return zero } -func (m *MockBackend) ProjectVersion() *version.ProjectVersion { +func (m *MockBackend) ProjectVersion() *types.ProjectVersion { if m.ProjectVersionFunc != nil { return m.ProjectVersionFunc() } diff --git a/mdl/backend/mock/mock_infrastructure.go b/mdl/backend/mock/mock_infrastructure.go index f0b6f9bc..0e033f08 100644 --- a/mdl/backend/mock/mock_infrastructure.go +++ b/mdl/backend/mock/mock_infrastructure.go @@ -5,7 +5,7 @@ package mock import ( "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/agenteditor" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" ) // --------------------------------------------------------------------------- @@ -19,7 +19,7 @@ func (m *MockBackend) UpdateQualifiedNameInAllUnits(oldName, newName string) (in return 0, nil } -func (m *MockBackend) RenameReferences(oldName, newName string, dryRun bool) ([]mpr.RenameHit, error) { +func (m *MockBackend) RenameReferences(oldName, newName string, dryRun bool) ([]types.RenameHit, error) { if m.RenameReferencesFunc != nil { return m.RenameReferencesFunc(oldName, newName, dryRun) } @@ -51,21 +51,21 @@ func (m *MockBackend) GetRawUnitBytes(id model.ID) ([]byte, error) { return nil, nil } -func (m *MockBackend) ListRawUnitsByType(typePrefix string) ([]*mpr.RawUnit, error) { +func (m *MockBackend) ListRawUnitsByType(typePrefix string) ([]*types.RawUnit, error) { if m.ListRawUnitsByTypeFunc != nil { return m.ListRawUnitsByTypeFunc(typePrefix) } return nil, nil } -func (m *MockBackend) ListRawUnits(objectType string) ([]*mpr.RawUnitInfo, error) { +func (m *MockBackend) ListRawUnits(objectType string) ([]*types.RawUnitInfo, error) { if m.ListRawUnitsFunc != nil { return m.ListRawUnitsFunc(objectType) } return nil, nil } -func (m *MockBackend) GetRawUnitByName(objectType, qualifiedName string) (*mpr.RawUnitInfo, error) { +func (m *MockBackend) GetRawUnitByName(objectType, qualifiedName string) (*types.RawUnitInfo, error) { if m.GetRawUnitByNameFunc != nil { return m.GetRawUnitByNameFunc(objectType, qualifiedName) } @@ -97,7 +97,7 @@ func (m *MockBackend) ListAllUnitIDs() ([]string, error) { return nil, nil } -func (m *MockBackend) ListUnits() ([]*mpr.UnitInfo, error) { +func (m *MockBackend) ListUnits() ([]*types.UnitInfo, error) { if m.ListUnitsFunc != nil { return m.ListUnitsFunc() } @@ -142,14 +142,14 @@ func (m *MockBackend) InvalidateCache() { // WidgetBackend // --------------------------------------------------------------------------- -func (m *MockBackend) FindCustomWidgetType(widgetID string) (*mpr.RawCustomWidgetType, error) { +func (m *MockBackend) FindCustomWidgetType(widgetID string) (*types.RawCustomWidgetType, error) { if m.FindCustomWidgetTypeFunc != nil { return m.FindCustomWidgetTypeFunc(widgetID) } return nil, nil } -func (m *MockBackend) FindAllCustomWidgetTypes(widgetID string) ([]*mpr.RawCustomWidgetType, error) { +func (m *MockBackend) FindAllCustomWidgetTypes(widgetID string) ([]*types.RawCustomWidgetType, error) { if m.FindAllCustomWidgetTypesFunc != nil { return m.FindAllCustomWidgetTypesFunc(widgetID) } diff --git a/mdl/backend/mock/mock_java.go b/mdl/backend/mock/mock_java.go index 776c6e80..c44ce357 100644 --- a/mdl/backend/mock/mock_java.go +++ b/mdl/backend/mock/mock_java.go @@ -5,10 +5,10 @@ package mock import ( "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/javaactions" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" ) -func (m *MockBackend) ListJavaActions() ([]*mpr.JavaAction, error) { +func (m *MockBackend) ListJavaActions() ([]*types.JavaAction, error) { if m.ListJavaActionsFunc != nil { return m.ListJavaActionsFunc() } @@ -22,7 +22,7 @@ func (m *MockBackend) ListJavaActionsFull() ([]*javaactions.JavaAction, error) { return nil, nil } -func (m *MockBackend) ListJavaScriptActions() ([]*mpr.JavaScriptAction, error) { +func (m *MockBackend) ListJavaScriptActions() ([]*types.JavaScriptAction, error) { if m.ListJavaScriptActionsFunc != nil { return m.ListJavaScriptActionsFunc() } @@ -36,7 +36,7 @@ func (m *MockBackend) ReadJavaActionByName(qualifiedName string) (*javaactions.J return nil, nil } -func (m *MockBackend) ReadJavaScriptActionByName(qualifiedName string) (*mpr.JavaScriptAction, error) { +func (m *MockBackend) ReadJavaScriptActionByName(qualifiedName string) (*types.JavaScriptAction, error) { if m.ReadJavaScriptActionByNameFunc != nil { return m.ReadJavaScriptActionByNameFunc(qualifiedName) } diff --git a/mdl/backend/mock/mock_mapping.go b/mdl/backend/mock/mock_mapping.go index 6bdd1592..27b7b0bc 100644 --- a/mdl/backend/mock/mock_mapping.go +++ b/mdl/backend/mock/mock_mapping.go @@ -4,7 +4,7 @@ package mock import ( "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" ) func (m *MockBackend) ListImportMappings() ([]*model.ImportMapping, error) { @@ -91,21 +91,21 @@ func (m *MockBackend) MoveExportMapping(em *model.ExportMapping) error { return nil } -func (m *MockBackend) ListJsonStructures() ([]*mpr.JsonStructure, error) { +func (m *MockBackend) ListJsonStructures() ([]*types.JsonStructure, error) { if m.ListJsonStructuresFunc != nil { return m.ListJsonStructuresFunc() } return nil, nil } -func (m *MockBackend) GetJsonStructureByQualifiedName(moduleName, name string) (*mpr.JsonStructure, error) { +func (m *MockBackend) GetJsonStructureByQualifiedName(moduleName, name string) (*types.JsonStructure, error) { if m.GetJsonStructureByQualifiedNameFunc != nil { return m.GetJsonStructureByQualifiedNameFunc(moduleName, name) } return nil, nil } -func (m *MockBackend) CreateJsonStructure(js *mpr.JsonStructure) error { +func (m *MockBackend) CreateJsonStructure(js *types.JsonStructure) error { if m.CreateJsonStructureFunc != nil { return m.CreateJsonStructureFunc(js) } diff --git a/mdl/backend/mock/mock_module.go b/mdl/backend/mock/mock_module.go index c8bc4087..ea99426a 100644 --- a/mdl/backend/mock/mock_module.go +++ b/mdl/backend/mock/mock_module.go @@ -3,8 +3,8 @@ package mock import ( + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" ) // --------------------------------------------------------------------------- @@ -64,7 +64,7 @@ func (m *MockBackend) DeleteModuleWithCleanup(id model.ID, moduleName string) er // FolderBackend // --------------------------------------------------------------------------- -func (m *MockBackend) ListFolders() ([]*mpr.FolderInfo, error) { +func (m *MockBackend) ListFolders() ([]*types.FolderInfo, error) { if m.ListFoldersFunc != nil { return m.ListFoldersFunc() } diff --git a/mdl/backend/mock/mock_navigation.go b/mdl/backend/mock/mock_navigation.go index f03c36eb..e993bc80 100644 --- a/mdl/backend/mock/mock_navigation.go +++ b/mdl/backend/mock/mock_navigation.go @@ -4,24 +4,24 @@ package mock import ( "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" ) -func (m *MockBackend) ListNavigationDocuments() ([]*mpr.NavigationDocument, error) { +func (m *MockBackend) ListNavigationDocuments() ([]*types.NavigationDocument, error) { if m.ListNavigationDocumentsFunc != nil { return m.ListNavigationDocumentsFunc() } return nil, nil } -func (m *MockBackend) GetNavigation() (*mpr.NavigationDocument, error) { +func (m *MockBackend) GetNavigation() (*types.NavigationDocument, error) { if m.GetNavigationFunc != nil { return m.GetNavigationFunc() } return nil, nil } -func (m *MockBackend) UpdateNavigationProfile(navDocID model.ID, profileName string, spec mpr.NavigationProfileSpec) error { +func (m *MockBackend) UpdateNavigationProfile(navDocID model.ID, profileName string, spec types.NavigationProfileSpec) error { if m.UpdateNavigationProfileFunc != nil { return m.UpdateNavigationProfileFunc(navDocID, profileName, spec) } diff --git a/mdl/backend/mock/mock_security.go b/mdl/backend/mock/mock_security.go index f8d03bff..214f907e 100644 --- a/mdl/backend/mock/mock_security.go +++ b/mdl/backend/mock/mock_security.go @@ -5,7 +5,7 @@ package mock import ( "github.com/mendixlabs/mxcli/mdl/backend" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/security" ) @@ -135,7 +135,7 @@ func (m *MockBackend) RemoveEntityAccessRule(unitID model.ID, entityName string, return 0, nil } -func (m *MockBackend) RevokeEntityMemberAccess(unitID model.ID, entityName string, roleNames []string, revocation mpr.EntityAccessRevocation) (int, error) { +func (m *MockBackend) RevokeEntityMemberAccess(unitID model.ID, entityName string, roleNames []string, revocation types.EntityAccessRevocation) (int, error) { if m.RevokeEntityMemberAccessFunc != nil { return m.RevokeEntityMemberAccessFunc(unitID, entityName, roleNames, revocation) } diff --git a/mdl/backend/mock/mock_workflow.go b/mdl/backend/mock/mock_workflow.go index 5f2a1298..73ea9fe0 100644 --- a/mdl/backend/mock/mock_workflow.go +++ b/mdl/backend/mock/mock_workflow.go @@ -4,7 +4,7 @@ package mock import ( "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/workflows" ) @@ -62,14 +62,14 @@ func (m *MockBackend) UpdateProjectSettings(ps *model.ProjectSettings) error { // ImageBackend // --------------------------------------------------------------------------- -func (m *MockBackend) ListImageCollections() ([]*mpr.ImageCollection, error) { +func (m *MockBackend) ListImageCollections() ([]*types.ImageCollection, error) { if m.ListImageCollectionsFunc != nil { return m.ListImageCollectionsFunc() } return nil, nil } -func (m *MockBackend) CreateImageCollection(ic *mpr.ImageCollection) error { +func (m *MockBackend) CreateImageCollection(ic *types.ImageCollection) error { if m.CreateImageCollectionFunc != nil { return m.CreateImageCollectionFunc(ic) } diff --git a/mdl/backend/mpr/backend.go b/mdl/backend/mpr/backend.go index ca93c87f..2dc66ecf 100644 --- a/mdl/backend/mpr/backend.go +++ b/mdl/backend/mpr/backend.go @@ -7,13 +7,13 @@ package mprbackend import ( "github.com/mendixlabs/mxcli/mdl/backend" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/agenteditor" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/javaactions" "github.com/mendixlabs/mxcli/sdk/microflows" "github.com/mendixlabs/mxcli/sdk/mpr" - "github.com/mendixlabs/mxcli/sdk/mpr/version" "github.com/mendixlabs/mxcli/sdk/pages" "github.com/mendixlabs/mxcli/sdk/security" "github.com/mendixlabs/mxcli/sdk/workflows" @@ -75,9 +75,9 @@ func (b *MprBackend) Disconnect() error { func (b *MprBackend) IsConnected() bool { return b.writer != nil } func (b *MprBackend) Path() string { return b.path } -func (b *MprBackend) Version() mpr.MPRVersion { return b.reader.Version() } -func (b *MprBackend) ProjectVersion() *version.ProjectVersion { return b.reader.ProjectVersion() } -func (b *MprBackend) GetMendixVersion() (string, error) { return b.reader.GetMendixVersion() } +func (b *MprBackend) Version() types.MPRVersion { return convertMPRVersion(b.reader.Version()) } +func (b *MprBackend) ProjectVersion() *types.ProjectVersion { return convertProjectVersion(b.reader.ProjectVersion()) } +func (b *MprBackend) GetMendixVersion() (string, error) { return b.reader.GetMendixVersion() } // Commit is a no-op — the MPR writer auto-commits on each write operation. func (b *MprBackend) Commit() error { return nil } @@ -102,7 +102,7 @@ func (b *MprBackend) DeleteModuleWithCleanup(id model.ID, moduleName string) err // FolderBackend // --------------------------------------------------------------------------- -func (b *MprBackend) ListFolders() ([]*mpr.FolderInfo, error) { return b.reader.ListFolders() } +func (b *MprBackend) ListFolders() ([]*types.FolderInfo, error) { return convertFolderInfoSlice(b.reader.ListFolders()) } func (b *MprBackend) CreateFolder(folder *model.Folder) error { return b.writer.CreateFolder(folder) } func (b *MprBackend) DeleteFolder(id model.ID) error { return b.writer.DeleteFolder(id) } func (b *MprBackend) MoveFolder(id model.ID, newContainerID model.ID) error { @@ -354,13 +354,13 @@ func (b *MprBackend) RemoveFromAllowedRoles(unitID model.ID, roleName string) (b return b.writer.RemoveFromAllowedRoles(unitID, roleName) } func (b *MprBackend) AddEntityAccessRule(params backend.EntityAccessRuleParams) error { - return b.writer.AddEntityAccessRule(params.UnitID, params.EntityName, params.RoleNames, params.AllowCreate, params.AllowDelete, params.DefaultMemberAccess, params.XPathConstraint, params.MemberAccesses) + return b.writer.AddEntityAccessRule(params.UnitID, params.EntityName, params.RoleNames, params.AllowCreate, params.AllowDelete, params.DefaultMemberAccess, params.XPathConstraint, unconvertEntityMemberAccessSlice(params.MemberAccesses)) } func (b *MprBackend) RemoveEntityAccessRule(unitID model.ID, entityName string, roleNames []string) (int, error) { return b.writer.RemoveEntityAccessRule(unitID, entityName, roleNames) } -func (b *MprBackend) RevokeEntityMemberAccess(unitID model.ID, entityName string, roleNames []string, revocation mpr.EntityAccessRevocation) (int, error) { - return b.writer.RevokeEntityMemberAccess(unitID, entityName, roleNames, revocation) +func (b *MprBackend) RevokeEntityMemberAccess(unitID model.ID, entityName string, roleNames []string, revocation types.EntityAccessRevocation) (int, error) { + return b.writer.RevokeEntityMemberAccess(unitID, entityName, roleNames, unconvertEntityAccessRevocation(revocation)) } func (b *MprBackend) RemoveRoleFromAllEntities(unitID model.ID, roleName string) (int, error) { return b.writer.RemoveRoleFromAllEntities(unitID, roleName) @@ -373,14 +373,14 @@ func (b *MprBackend) ReconcileMemberAccesses(unitID model.ID, moduleName string) // NavigationBackend // --------------------------------------------------------------------------- -func (b *MprBackend) ListNavigationDocuments() ([]*mpr.NavigationDocument, error) { - return b.reader.ListNavigationDocuments() +func (b *MprBackend) ListNavigationDocuments() ([]*types.NavigationDocument, error) { + return convertNavDocSlice(b.reader.ListNavigationDocuments()) } -func (b *MprBackend) GetNavigation() (*mpr.NavigationDocument, error) { - return b.reader.GetNavigation() +func (b *MprBackend) GetNavigation() (*types.NavigationDocument, error) { + return convertNavDocPtr(b.reader.GetNavigation()) } -func (b *MprBackend) UpdateNavigationProfile(navDocID model.ID, profileName string, spec mpr.NavigationProfileSpec) error { - return b.writer.UpdateNavigationProfile(navDocID, profileName, spec) +func (b *MprBackend) UpdateNavigationProfile(navDocID model.ID, profileName string, spec types.NavigationProfileSpec) error { + return b.writer.UpdateNavigationProfile(navDocID, profileName, unconvertNavProfileSpec(spec)) } // --------------------------------------------------------------------------- @@ -518,14 +518,14 @@ func (b *MprBackend) MoveExportMapping(em *model.ExportMapping) error { return b.writer.MoveExportMapping(em) } -func (b *MprBackend) ListJsonStructures() ([]*mpr.JsonStructure, error) { - return b.reader.ListJsonStructures() +func (b *MprBackend) ListJsonStructures() ([]*types.JsonStructure, error) { + return convertJsonStructureSlice(b.reader.ListJsonStructures()) } -func (b *MprBackend) GetJsonStructureByQualifiedName(moduleName, name string) (*mpr.JsonStructure, error) { - return b.reader.GetJsonStructureByQualifiedName(moduleName, name) +func (b *MprBackend) GetJsonStructureByQualifiedName(moduleName, name string) (*types.JsonStructure, error) { + return convertJsonStructurePtr(b.reader.GetJsonStructureByQualifiedName(moduleName, name)) } -func (b *MprBackend) CreateJsonStructure(js *mpr.JsonStructure) error { - return b.writer.CreateJsonStructure(js) +func (b *MprBackend) CreateJsonStructure(js *types.JsonStructure) error { + return b.writer.CreateJsonStructure(unconvertJsonStructure(js)) } func (b *MprBackend) DeleteJsonStructure(id string) error { return b.writer.DeleteJsonStructure(id) @@ -535,20 +535,20 @@ func (b *MprBackend) DeleteJsonStructure(id string) error { // JavaBackend // --------------------------------------------------------------------------- -func (b *MprBackend) ListJavaActions() ([]*mpr.JavaAction, error) { - return b.reader.ListJavaActions() +func (b *MprBackend) ListJavaActions() ([]*types.JavaAction, error) { + return convertJavaActionSlice(b.reader.ListJavaActions()) } func (b *MprBackend) ListJavaActionsFull() ([]*javaactions.JavaAction, error) { return b.reader.ListJavaActionsFull() } -func (b *MprBackend) ListJavaScriptActions() ([]*mpr.JavaScriptAction, error) { - return b.reader.ListJavaScriptActions() +func (b *MprBackend) ListJavaScriptActions() ([]*types.JavaScriptAction, error) { + return convertJavaScriptActionSlice(b.reader.ListJavaScriptActions()) } func (b *MprBackend) ReadJavaActionByName(qualifiedName string) (*javaactions.JavaAction, error) { return b.reader.ReadJavaActionByName(qualifiedName) } -func (b *MprBackend) ReadJavaScriptActionByName(qualifiedName string) (*mpr.JavaScriptAction, error) { - return b.reader.ReadJavaScriptActionByName(qualifiedName) +func (b *MprBackend) ReadJavaScriptActionByName(qualifiedName string) (*types.JavaScriptAction, error) { + return convertJavaScriptActionPtr(b.reader.ReadJavaScriptActionByName(qualifiedName)) } func (b *MprBackend) CreateJavaAction(ja *javaactions.JavaAction) error { return b.writer.CreateJavaAction(ja) @@ -600,11 +600,11 @@ func (b *MprBackend) UpdateProjectSettings(ps *model.ProjectSettings) error { // ImageBackend // --------------------------------------------------------------------------- -func (b *MprBackend) ListImageCollections() ([]*mpr.ImageCollection, error) { - return b.reader.ListImageCollections() +func (b *MprBackend) ListImageCollections() ([]*types.ImageCollection, error) { + return convertImageCollectionSlice(b.reader.ListImageCollections()) } -func (b *MprBackend) CreateImageCollection(ic *mpr.ImageCollection) error { - return b.writer.CreateImageCollection(ic) +func (b *MprBackend) CreateImageCollection(ic *types.ImageCollection) error { + return b.writer.CreateImageCollection(unconvertImageCollection(ic)) } func (b *MprBackend) DeleteImageCollection(id string) error { return b.writer.DeleteImageCollection(id) @@ -628,8 +628,8 @@ func (b *MprBackend) GetScheduledEvent(id model.ID) (*model.ScheduledEvent, erro func (b *MprBackend) UpdateQualifiedNameInAllUnits(oldName, newName string) (int, error) { return b.writer.UpdateQualifiedNameInAllUnits(oldName, newName) } -func (b *MprBackend) RenameReferences(oldName, newName string, dryRun bool) ([]mpr.RenameHit, error) { - return b.writer.RenameReferences(oldName, newName, dryRun) +func (b *MprBackend) RenameReferences(oldName, newName string, dryRun bool) ([]types.RenameHit, error) { + return convertRenameHitSlice(b.writer.RenameReferences(oldName, newName, dryRun)) } func (b *MprBackend) RenameDocumentByName(moduleName, oldName, newName string) error { return b.writer.RenameDocumentByName(moduleName, oldName, newName) @@ -645,14 +645,14 @@ func (b *MprBackend) GetRawUnit(id model.ID) (map[string]any, error) { func (b *MprBackend) GetRawUnitBytes(id model.ID) ([]byte, error) { return b.reader.GetRawUnitBytes(id) } -func (b *MprBackend) ListRawUnitsByType(typePrefix string) ([]*mpr.RawUnit, error) { - return b.reader.ListRawUnitsByType(typePrefix) +func (b *MprBackend) ListRawUnitsByType(typePrefix string) ([]*types.RawUnit, error) { + return convertRawUnitSlice(b.reader.ListRawUnitsByType(typePrefix)) } -func (b *MprBackend) ListRawUnits(objectType string) ([]*mpr.RawUnitInfo, error) { - return b.reader.ListRawUnits(objectType) +func (b *MprBackend) ListRawUnits(objectType string) ([]*types.RawUnitInfo, error) { + return convertRawUnitInfoSlice(b.reader.ListRawUnits(objectType)) } -func (b *MprBackend) GetRawUnitByName(objectType, qualifiedName string) (*mpr.RawUnitInfo, error) { - return b.reader.GetRawUnitByName(objectType, qualifiedName) +func (b *MprBackend) GetRawUnitByName(objectType, qualifiedName string) (*types.RawUnitInfo, error) { + return convertRawUnitInfoPtr(b.reader.GetRawUnitByName(objectType, qualifiedName)) } func (b *MprBackend) GetRawMicroflowByName(qualifiedName string) ([]byte, error) { return b.reader.GetRawMicroflowByName(qualifiedName) @@ -666,7 +666,7 @@ func (b *MprBackend) UpdateRawUnit(unitID string, contents []byte) error { // --------------------------------------------------------------------------- func (b *MprBackend) ListAllUnitIDs() ([]string, error) { return b.reader.ListAllUnitIDs() } -func (b *MprBackend) ListUnits() ([]*mpr.UnitInfo, error) { return b.reader.ListUnits() } +func (b *MprBackend) ListUnits() ([]*types.UnitInfo, error) { return convertUnitInfoSlice(b.reader.ListUnits()) } func (b *MprBackend) GetUnitTypes() (map[string]int, error) { return b.reader.GetUnitTypes() } func (b *MprBackend) GetProjectRootID() (string, error) { return b.reader.GetProjectRootID() } func (b *MprBackend) ContentsDir() string { return b.reader.ContentsDir() } @@ -677,11 +677,11 @@ func (b *MprBackend) InvalidateCache() { b.reader.Invalidat // WidgetBackend // --------------------------------------------------------------------------- -func (b *MprBackend) FindCustomWidgetType(widgetID string) (*mpr.RawCustomWidgetType, error) { - return b.reader.FindCustomWidgetType(widgetID) +func (b *MprBackend) FindCustomWidgetType(widgetID string) (*types.RawCustomWidgetType, error) { + return convertRawCustomWidgetTypePtr(b.reader.FindCustomWidgetType(widgetID)) } -func (b *MprBackend) FindAllCustomWidgetTypes(widgetID string) ([]*mpr.RawCustomWidgetType, error) { - return b.reader.FindAllCustomWidgetTypes(widgetID) +func (b *MprBackend) FindAllCustomWidgetTypes(widgetID string) ([]*types.RawCustomWidgetType, error) { + return convertRawCustomWidgetTypeSlice(b.reader.FindAllCustomWidgetTypes(widgetID)) } // --------------------------------------------------------------------------- diff --git a/mdl/backend/mpr/convert.go b/mdl/backend/mpr/convert.go new file mode 100644 index 00000000..1cfd39ca --- /dev/null +++ b/mdl/backend/mpr/convert.go @@ -0,0 +1,457 @@ +// SPDX-License-Identifier: Apache-2.0 + +package mprbackend + +import ( + "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/sdk/mpr/version" +) + +// --------------------------------------------------------------------------- +// Conversion helpers: sdk/mpr -> mdl/types +// --------------------------------------------------------------------------- + +func convertMPRVersion(v mpr.MPRVersion) types.MPRVersion { return types.MPRVersion(v) } + +func convertProjectVersion(v *version.ProjectVersion) *types.ProjectVersion { + if v == nil { + return nil + } + return &types.ProjectVersion{ + ProductVersion: v.ProductVersion, + BuildVersion: v.BuildVersion, + FormatVersion: v.FormatVersion, + SchemaHash: v.SchemaHash, + MajorVersion: v.MajorVersion, + MinorVersion: v.MinorVersion, + PatchVersion: v.PatchVersion, + } +} + +func convertFolderInfoSlice(in []*types.FolderInfo, err error) ([]*types.FolderInfo, error) { + if err != nil || in == nil { + return nil, err + } + out := make([]*types.FolderInfo, len(in)) + for i, f := range in { + out[i] = &types.FolderInfo{ID: f.ID, ContainerID: f.ContainerID, Name: f.Name} + } + return out, nil +} + +func convertUnitInfoSlice(in []*types.UnitInfo, err error) ([]*types.UnitInfo, error) { + if err != nil || in == nil { + return nil, err + } + out := make([]*types.UnitInfo, len(in)) + for i, u := range in { + out[i] = &types.UnitInfo{ + ID: u.ID, ContainerID: u.ContainerID, + ContainmentName: u.ContainmentName, Type: u.Type, + } + } + return out, nil +} + +func convertRenameHitSlice(in []mpr.RenameHit, err error) ([]types.RenameHit, error) { + if err != nil || in == nil { + return nil, err + } + out := make([]types.RenameHit, len(in)) + for i, h := range in { + out[i] = types.RenameHit{UnitID: h.UnitID, UnitType: h.UnitType, Name: h.Name, Count: h.Count} + } + return out, nil +} + +func convertRawUnitSlice(in []*types.RawUnit, err error) ([]*types.RawUnit, error) { + if err != nil || in == nil { + return nil, err + } + out := make([]*types.RawUnit, len(in)) + for i, r := range in { + out[i] = &types.RawUnit{ + ID: r.ID, ContainerID: r.ContainerID, Type: r.Type, Contents: r.Contents, + } + } + return out, nil +} + +func convertRawUnitInfoSlice(in []*mpr.RawUnitInfo, err error) ([]*types.RawUnitInfo, error) { + if err != nil || in == nil { + return nil, err + } + out := make([]*types.RawUnitInfo, len(in)) + for i, r := range in { + out[i] = &types.RawUnitInfo{ + ID: r.ID, QualifiedName: r.QualifiedName, Type: r.Type, + ModuleName: r.ModuleName, Contents: r.Contents, + } + } + return out, nil +} + +func convertRawUnitInfoPtr(in *mpr.RawUnitInfo, err error) (*types.RawUnitInfo, error) { + if err != nil || in == nil { + return nil, err + } + return &types.RawUnitInfo{ + ID: in.ID, QualifiedName: in.QualifiedName, Type: in.Type, + ModuleName: in.ModuleName, Contents: in.Contents, + }, nil +} + +func convertRawCustomWidgetTypePtr(in *mpr.RawCustomWidgetType, err error) (*types.RawCustomWidgetType, error) { + if err != nil || in == nil { + return nil, err + } + return &types.RawCustomWidgetType{ + WidgetID: in.WidgetID, RawType: in.RawType, RawObject: in.RawObject, + UnitID: in.UnitID, UnitName: in.UnitName, WidgetName: in.WidgetName, + }, nil +} + +func convertRawCustomWidgetTypeSlice(in []*mpr.RawCustomWidgetType, err error) ([]*types.RawCustomWidgetType, error) { + if err != nil || in == nil { + return nil, err + } + out := make([]*types.RawCustomWidgetType, len(in)) + for i, w := range in { + out[i] = &types.RawCustomWidgetType{ + WidgetID: w.WidgetID, RawType: w.RawType, RawObject: w.RawObject, + UnitID: w.UnitID, UnitName: w.UnitName, WidgetName: w.WidgetName, + } + } + return out, nil +} + +func convertJavaActionSlice(in []*types.JavaAction, err error) ([]*types.JavaAction, error) { + if err != nil || in == nil { + return nil, err + } + out := make([]*types.JavaAction, len(in)) + for i, ja := range in { + out[i] = &types.JavaAction{ + BaseElement: ja.BaseElement, + ContainerID: ja.ContainerID, + Name: ja.Name, + Documentation: ja.Documentation, + } + } + return out, nil +} + +func convertJavaScriptActionSlice(in []*types.JavaScriptAction, err error) ([]*types.JavaScriptAction, error) { + if err != nil || in == nil { + return nil, err + } + out := make([]*types.JavaScriptAction, len(in)) + for i, jsa := range in { + out[i] = convertJavaScriptAction(jsa) + } + return out, nil +} + +func convertJavaScriptActionPtr(in *types.JavaScriptAction, err error) (*types.JavaScriptAction, error) { + if err != nil || in == nil { + return nil, err + } + return convertJavaScriptAction(in), nil +} + +func convertJavaScriptAction(in *types.JavaScriptAction) *types.JavaScriptAction { + return &types.JavaScriptAction{ + BaseElement: in.BaseElement, + ContainerID: in.ContainerID, + Name: in.Name, + Documentation: in.Documentation, + Platform: in.Platform, + Excluded: in.Excluded, + ExportLevel: in.ExportLevel, + ActionDefaultReturnName: in.ActionDefaultReturnName, + ReturnType: in.ReturnType, + Parameters: in.Parameters, + TypeParameters: in.TypeParameters, + MicroflowActionInfo: in.MicroflowActionInfo, + } +} + +func convertNavDocSlice(in []*types.NavigationDocument, err error) ([]*types.NavigationDocument, error) { + if err != nil || in == nil { + return nil, err + } + out := make([]*types.NavigationDocument, len(in)) + for i, nd := range in { + out[i] = convertNavDoc(nd) + } + return out, nil +} + +func convertNavDocPtr(in *types.NavigationDocument, err error) (*types.NavigationDocument, error) { + if err != nil || in == nil { + return nil, err + } + return convertNavDoc(in), nil +} + +func convertNavDoc(in *types.NavigationDocument) *types.NavigationDocument { + nd := &types.NavigationDocument{ + BaseElement: in.BaseElement, + ContainerID: in.ContainerID, + Name: in.Name, + } + if in.Profiles != nil { + nd.Profiles = make([]*types.NavigationProfile, len(in.Profiles)) + for i, p := range in.Profiles { + nd.Profiles[i] = convertNavProfile(p) + } + } + return nd +} + +func convertNavProfile(in *types.NavigationProfile) *types.NavigationProfile { + p := &types.NavigationProfile{ + Name: in.Name, + Kind: in.Kind, + IsNative: in.IsNative, + LoginPage: in.LoginPage, + NotFoundPage: in.NotFoundPage, + } + if in.HomePage != nil { + p.HomePage = &types.NavHomePage{Page: in.HomePage.Page, Microflow: in.HomePage.Microflow} + } + if in.RoleBasedHomePages != nil { + p.RoleBasedHomePages = make([]*types.NavRoleBasedHome, len(in.RoleBasedHomePages)) + for i, rbh := range in.RoleBasedHomePages { + p.RoleBasedHomePages[i] = &types.NavRoleBasedHome{ + UserRole: rbh.UserRole, Page: rbh.Page, Microflow: rbh.Microflow, + } + } + } + if in.MenuItems != nil { + p.MenuItems = make([]*types.NavMenuItem, len(in.MenuItems)) + for i, mi := range in.MenuItems { + p.MenuItems[i] = convertNavMenuItem(mi) + } + } + if in.OfflineEntities != nil { + p.OfflineEntities = make([]*types.NavOfflineEntity, len(in.OfflineEntities)) + for i, oe := range in.OfflineEntities { + p.OfflineEntities[i] = &types.NavOfflineEntity{ + Entity: oe.Entity, SyncMode: oe.SyncMode, Constraint: oe.Constraint, + } + } + } + return p +} + +func convertNavMenuItem(in *types.NavMenuItem) *types.NavMenuItem { + mi := &types.NavMenuItem{ + Caption: in.Caption, Page: in.Page, Microflow: in.Microflow, ActionType: in.ActionType, + } + if in.Items != nil { + mi.Items = make([]*types.NavMenuItem, len(in.Items)) + for i, sub := range in.Items { + mi.Items[i] = convertNavMenuItem(sub) + } + } + return mi +} + +// --------------------------------------------------------------------------- +// Conversion helpers: mdl/types -> sdk/mpr (for write methods) +// --------------------------------------------------------------------------- + +func unconvertNavProfileSpec(s types.NavigationProfileSpec) mpr.NavigationProfileSpec { + out := mpr.NavigationProfileSpec{ + LoginPage: s.LoginPage, + NotFoundPage: s.NotFoundPage, + HasMenu: s.HasMenu, + } + if s.HomePages != nil { + out.HomePages = make([]mpr.NavHomePageSpec, len(s.HomePages)) + for i, hp := range s.HomePages { + out.HomePages[i] = mpr.NavHomePageSpec{IsPage: hp.IsPage, Target: hp.Target, ForRole: hp.ForRole} + } + } + if s.MenuItems != nil { + out.MenuItems = make([]mpr.NavMenuItemSpec, len(s.MenuItems)) + for i, mi := range s.MenuItems { + out.MenuItems[i] = unconvertNavMenuItemSpec(mi) + } + } + return out +} + +func unconvertNavMenuItemSpec(in types.NavMenuItemSpec) mpr.NavMenuItemSpec { + out := mpr.NavMenuItemSpec{Caption: in.Caption, Page: in.Page, Microflow: in.Microflow} + if in.Items != nil { + out.Items = make([]mpr.NavMenuItemSpec, len(in.Items)) + for i, sub := range in.Items { + out.Items[i] = unconvertNavMenuItemSpec(sub) + } + } + return out +} + +func unconvertEntityMemberAccessSlice(in []types.EntityMemberAccess) []mpr.EntityMemberAccess { + if in == nil { + return nil + } + out := make([]mpr.EntityMemberAccess, len(in)) + for i, ma := range in { + out[i] = mpr.EntityMemberAccess{ + AttributeRef: ma.AttributeRef, AssociationRef: ma.AssociationRef, AccessRights: ma.AccessRights, + } + } + return out +} + +func unconvertEntityAccessRevocation(in types.EntityAccessRevocation) mpr.EntityAccessRevocation { + return mpr.EntityAccessRevocation{ + RevokeCreate: in.RevokeCreate, + RevokeDelete: in.RevokeDelete, + RevokeReadMembers: in.RevokeReadMembers, + RevokeWriteMembers: in.RevokeWriteMembers, + RevokeReadAll: in.RevokeReadAll, + RevokeWriteAll: in.RevokeWriteAll, + } +} + +func convertJsonStructureSlice(in []*types.JsonStructure, err error) ([]*types.JsonStructure, error) { + if err != nil || in == nil { + return nil, err + } + out := make([]*types.JsonStructure, len(in)) + for i, js := range in { + out[i] = convertJsonStructure(js) + } + return out, nil +} + +func convertJsonStructurePtr(in *types.JsonStructure, err error) (*types.JsonStructure, error) { + if err != nil || in == nil { + return nil, err + } + return convertJsonStructure(in), nil +} + +func convertJsonStructure(in *types.JsonStructure) *types.JsonStructure { + js := &types.JsonStructure{ + BaseElement: in.BaseElement, + ContainerID: in.ContainerID, + Name: in.Name, + Documentation: in.Documentation, + JsonSnippet: in.JsonSnippet, + Excluded: in.Excluded, + ExportLevel: in.ExportLevel, + } + if in.Elements != nil { + js.Elements = make([]*types.JsonElement, len(in.Elements)) + for i, e := range in.Elements { + js.Elements[i] = convertJsonElement(e) + } + } + return js +} + +func convertJsonElement(in *types.JsonElement) *types.JsonElement { + e := &types.JsonElement{ + ExposedName: in.ExposedName, ExposedItemName: in.ExposedItemName, + Path: in.Path, ElementType: in.ElementType, PrimitiveType: in.PrimitiveType, + MinOccurs: in.MinOccurs, MaxOccurs: in.MaxOccurs, Nillable: in.Nillable, + IsDefaultType: in.IsDefaultType, MaxLength: in.MaxLength, + FractionDigits: in.FractionDigits, TotalDigits: in.TotalDigits, + OriginalValue: in.OriginalValue, + } + if in.Children != nil { + e.Children = make([]*types.JsonElement, len(in.Children)) + for i, c := range in.Children { + e.Children[i] = convertJsonElement(c) + } + } + return e +} + +func unconvertJsonStructure(in *types.JsonStructure) *types.JsonStructure { + js := &types.JsonStructure{ + BaseElement: in.BaseElement, + ContainerID: in.ContainerID, + Name: in.Name, + Documentation: in.Documentation, + JsonSnippet: in.JsonSnippet, + Excluded: in.Excluded, + ExportLevel: in.ExportLevel, + } + if in.Elements != nil { + js.Elements = make([]*types.JsonElement, len(in.Elements)) + for i, e := range in.Elements { + js.Elements[i] = unconvertJsonElement(e) + } + } + return js +} + +func unconvertJsonElement(in *types.JsonElement) *types.JsonElement { + e := &types.JsonElement{ + ExposedName: in.ExposedName, ExposedItemName: in.ExposedItemName, + Path: in.Path, ElementType: in.ElementType, PrimitiveType: in.PrimitiveType, + MinOccurs: in.MinOccurs, MaxOccurs: in.MaxOccurs, Nillable: in.Nillable, + IsDefaultType: in.IsDefaultType, MaxLength: in.MaxLength, + FractionDigits: in.FractionDigits, TotalDigits: in.TotalDigits, + OriginalValue: in.OriginalValue, + } + if in.Children != nil { + e.Children = make([]*types.JsonElement, len(in.Children)) + for i, c := range in.Children { + e.Children[i] = unconvertJsonElement(c) + } + } + return e +} + +func convertImageCollectionSlice(in []*types.ImageCollection, err error) ([]*types.ImageCollection, error) { + if err != nil || in == nil { + return nil, err + } + out := make([]*types.ImageCollection, len(in)) + for i, ic := range in { + out[i] = convertImageCollection(ic) + } + return out, nil +} + +func convertImageCollection(in *types.ImageCollection) *types.ImageCollection { + ic := &types.ImageCollection{ + BaseElement: in.BaseElement, + ContainerID: in.ContainerID, + Name: in.Name, + ExportLevel: in.ExportLevel, + Documentation: in.Documentation, + } + if in.Images != nil { + ic.Images = make([]types.Image, len(in.Images)) + for i, img := range in.Images { + ic.Images[i] = types.Image{ID: img.ID, Name: img.Name, Data: img.Data, Format: img.Format} + } + } + return ic +} + +func unconvertImageCollection(in *types.ImageCollection) *types.ImageCollection { + ic := &types.ImageCollection{ + BaseElement: in.BaseElement, + ContainerID: in.ContainerID, + Name: in.Name, + ExportLevel: in.ExportLevel, + Documentation: in.Documentation, + } + if in.Images != nil { + ic.Images = make([]types.Image, len(in.Images)) + for i, img := range in.Images { + ic.Images[i] = types.Image{ID: img.ID, Name: img.Name, Data: img.Data, Format: img.Format} + } + } + return ic +} diff --git a/mdl/backend/navigation.go b/mdl/backend/navigation.go index 185beaad..3c47cc0d 100644 --- a/mdl/backend/navigation.go +++ b/mdl/backend/navigation.go @@ -3,13 +3,13 @@ package backend import ( + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" ) // NavigationBackend provides navigation document operations. type NavigationBackend interface { - ListNavigationDocuments() ([]*mpr.NavigationDocument, error) - GetNavigation() (*mpr.NavigationDocument, error) - UpdateNavigationProfile(navDocID model.ID, profileName string, spec mpr.NavigationProfileSpec) error + ListNavigationDocuments() ([]*types.NavigationDocument, error) + GetNavigation() (*types.NavigationDocument, error) + UpdateNavigationProfile(navDocID model.ID, profileName string, spec types.NavigationProfileSpec) error } diff --git a/mdl/backend/security.go b/mdl/backend/security.go index 14086cc6..1f383249 100644 --- a/mdl/backend/security.go +++ b/mdl/backend/security.go @@ -3,8 +3,8 @@ package backend import ( + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/security" ) @@ -52,7 +52,7 @@ type EntityAccessRuleParams struct { AllowDelete bool DefaultMemberAccess string XPathConstraint string - MemberAccesses []mpr.EntityMemberAccess + MemberAccesses []types.EntityMemberAccess } // EntityAccessBackend manages entity-level access rules and role assignments. @@ -62,7 +62,7 @@ type EntityAccessBackend interface { RemoveFromAllowedRoles(unitID model.ID, roleName string) (bool, error) AddEntityAccessRule(params EntityAccessRuleParams) error RemoveEntityAccessRule(unitID model.ID, entityName string, roleNames []string) (int, error) - RevokeEntityMemberAccess(unitID model.ID, entityName string, roleNames []string, revocation mpr.EntityAccessRevocation) (int, error) + RevokeEntityMemberAccess(unitID model.ID, entityName string, roleNames []string, revocation types.EntityAccessRevocation) (int, error) RemoveRoleFromAllEntities(unitID model.ID, roleName string) (int, error) ReconcileMemberAccesses(unitID model.ID, moduleName string) (int, error) } diff --git a/mdl/backend/workflow.go b/mdl/backend/workflow.go index b985170e..87ce241d 100644 --- a/mdl/backend/workflow.go +++ b/mdl/backend/workflow.go @@ -3,8 +3,8 @@ package backend import ( + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/workflows" ) @@ -24,8 +24,8 @@ type SettingsBackend interface { // ImageBackend provides image collection operations. type ImageBackend interface { - ListImageCollections() ([]*mpr.ImageCollection, error) - CreateImageCollection(ic *mpr.ImageCollection) error + ListImageCollections() ([]*types.ImageCollection, error) + CreateImageCollection(ic *types.ImageCollection) error DeleteImageCollection(id string) error } diff --git a/mdl/catalog/builder.go b/mdl/catalog/builder.go index 56c62dc9..433fba80 100644 --- a/mdl/catalog/builder.go +++ b/mdl/catalog/builder.go @@ -12,7 +12,7 @@ import ( "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/javaactions" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/pages" "github.com/mendixlabs/mxcli/sdk/security" "github.com/mendixlabs/mxcli/sdk/workflows" @@ -23,9 +23,9 @@ import ( type CatalogReader interface { // Infrastructure GetRawUnit(id model.ID) (map[string]any, error) - ListRawUnitsByType(typePrefix string) ([]*mpr.RawUnit, error) - ListUnits() ([]*mpr.UnitInfo, error) - ListFolders() ([]*mpr.FolderInfo, error) + ListRawUnitsByType(typePrefix string) ([]*types.RawUnit, error) + ListUnits() ([]*types.UnitInfo, error) + ListFolders() ([]*types.FolderInfo, error) // Modules ListModules() ([]*model.Module, error) @@ -33,7 +33,7 @@ type CatalogReader interface { // Settings & security GetProjectSettings() (*model.ProjectSettings, error) GetProjectSecurity() (*security.ProjectSecurity, error) - GetNavigation() (*mpr.NavigationDocument, error) + GetNavigation() (*types.NavigationDocument, error) // Domain models & enumerations ListDomainModels() ([]*domainmodel.DomainModel, error) @@ -66,7 +66,7 @@ type CatalogReader interface { // Mappings & JSON structures ListImportMappings() ([]*model.ImportMapping, error) ListExportMappings() ([]*model.ExportMapping, error) - ListJsonStructures() ([]*mpr.JsonStructure, error) + ListJsonStructures() ([]*types.JsonStructure, error) } // DescribeFunc generates MDL source for a given object type and qualified name. diff --git a/mdl/catalog/builder_contract.go b/mdl/catalog/builder_contract.go index 6cce9ea0..f20cfae6 100644 --- a/mdl/catalog/builder_contract.go +++ b/mdl/catalog/builder_contract.go @@ -7,7 +7,7 @@ import ( "fmt" "strings" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" ) // buildContractEntities parses cached $metadata from consumed OData services @@ -55,7 +55,7 @@ func (b *Builder) buildContractEntities() error { moduleName := b.hierarchy.getModuleName(moduleID) svcQN := moduleName + "." + svc.Name - doc, err := mpr.ParseEdmx(svc.Metadata) + doc, err := types.ParseEdmx(svc.Metadata) if err != nil { continue // skip services with unparseable metadata } @@ -161,7 +161,7 @@ func (b *Builder) buildContractMessages() error { moduleName := b.hierarchy.getModuleName(moduleID) svcQN := moduleName + "." + svc.Name - doc, err := mpr.ParseAsyncAPI(svc.Document) + doc, err := types.ParseAsyncAPI(svc.Document) if err != nil { continue } diff --git a/mdl/catalog/builder_navigation.go b/mdl/catalog/builder_navigation.go index a5bceb93..82bde2ee 100644 --- a/mdl/catalog/builder_navigation.go +++ b/mdl/catalog/builder_navigation.go @@ -6,7 +6,7 @@ import ( "database/sql" "fmt" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" ) func (b *Builder) buildNavigation() error { @@ -129,7 +129,7 @@ func (b *Builder) buildNavigation() error { } // countMenuItems recursively counts all menu items. -func countMenuItems(items []*mpr.NavMenuItem) int { +func countMenuItems(items []*types.NavMenuItem) int { count := len(items) for _, item := range items { count += countMenuItems(item.Items) @@ -138,7 +138,7 @@ func countMenuItems(items []*mpr.NavMenuItem) int { } // insertMenuItems recursively inserts menu items with hierarchical path encoding. -func insertMenuItems(stmt *sql.Stmt, profileName string, items []*mpr.NavMenuItem, parentPath string, depth int, projectID, snapshotID string) int { +func insertMenuItems(stmt *sql.Stmt, profileName string, items []*types.NavMenuItem, parentPath string, depth int, projectID, snapshotID string) int { count := 0 for i, item := range items { itemPath := fmt.Sprintf("%d", i) diff --git a/mdl/catalog/builder_references.go b/mdl/catalog/builder_references.go index 2d397db3..3fce000a 100644 --- a/mdl/catalog/builder_references.go +++ b/mdl/catalog/builder_references.go @@ -6,9 +6,9 @@ import ( "database/sql" "strings" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/pages" "github.com/mendixlabs/mxcli/sdk/workflows" ) @@ -310,7 +310,7 @@ func (b *Builder) buildReferences() error { } // extractMenuItemRefs extracts page and microflow references from menu items recursively. -func (b *Builder) extractMenuItemRefs(stmt *sql.Stmt, items []*mpr.NavMenuItem, sourceName, projectID, snapshotID string) int { +func (b *Builder) extractMenuItemRefs(stmt *sql.Stmt, items []*types.NavMenuItem, sourceName, projectID, snapshotID string) int { refCount := 0 for _, item := range items { if item.Page != "" { diff --git a/mdl/executor/cmd_businessevents.go b/mdl/executor/cmd_businessevents.go index 294c8908..28fe7eaa 100644 --- a/mdl/executor/cmd_businessevents.go +++ b/mdl/executor/cmd_businessevents.go @@ -9,7 +9,7 @@ import ( "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/mdl/types" ) // showBusinessEventServices displays a table of all business event service documents. @@ -407,7 +407,7 @@ func dropBusinessEventService(ctx *ExecContext, stmt *ast.DropBusinessEventServi // generateChannelName generates a hex channel name (similar to Mendix Studio Pro). func generateChannelName() string { // Generate a UUID-like hex string - uuid := mpr.GenerateID() + uuid := types.GenerateID() return strings.ReplaceAll(uuid, "-", "") } diff --git a/mdl/executor/cmd_contract.go b/mdl/executor/cmd_contract.go index e8185c49..f94da438 100644 --- a/mdl/executor/cmd_contract.go +++ b/mdl/executor/cmd_contract.go @@ -9,9 +9,9 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" - "github.com/mendixlabs/mxcli/sdk/mpr" ) // showContractEntities handles SHOW CONTRACT ENTITIES FROM Module.Service. @@ -226,7 +226,7 @@ func describeContractAction(ctx *ExecContext, name ast.QualifiedName, format str return err } - var action *mpr.EdmAction + var action *types.EdmAction for _, a := range doc.Actions { if strings.EqualFold(a.Name, actionName) { action = a @@ -263,7 +263,7 @@ func describeContractAction(ctx *ExecContext, name ast.QualifiedName, format str } // outputContractEntityMDL outputs a CREATE EXTERNAL ENTITY statement from contract metadata. -func outputContractEntityMDL(ctx *ExecContext, et *mpr.EdmEntityType, svcQN string, doc *mpr.EdmxDocument) error { +func outputContractEntityMDL(ctx *ExecContext, et *types.EdmEntityType, svcQN string, doc *types.EdmxDocument) error { // Find entity set name entitySetName := et.Name + "s" // fallback for _, es := range doc.EntitySets { @@ -315,7 +315,7 @@ func outputContractEntityMDL(ctx *ExecContext, et *mpr.EdmEntityType, svcQN stri } // parseServiceContract finds a consumed OData service by name and parses its cached $metadata. -func parseServiceContract(ctx *ExecContext, name ast.QualifiedName) (*mpr.EdmxDocument, string, error) { +func parseServiceContract(ctx *ExecContext, name ast.QualifiedName) (*types.EdmxDocument, string, error) { services, err := ctx.Backend.ListConsumedODataServices() if err != nil { return nil, "", mdlerrors.NewBackend("list consumed OData services", err) @@ -340,7 +340,7 @@ func parseServiceContract(ctx *ExecContext, name ast.QualifiedName) (*mpr.EdmxDo 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) + doc, err := types.ParseEdmx(svc.Metadata) if err != nil { return nil, svcQN, mdlerrors.NewBackend(fmt.Sprintf("parse contract metadata for %s", svcQN), err) } @@ -371,7 +371,7 @@ func splitContractRef(name ast.QualifiedName) (ast.QualifiedName, string, error) } // formatEdmType returns a human-readable type string for a property. -func formatEdmType(p *mpr.EdmProperty) string { +func formatEdmType(p *types.EdmProperty) string { t := p.Type if p.MaxLength != "" { t += "(" + p.MaxLength + ")" @@ -398,7 +398,7 @@ func shortenEdmType(t string) string { } // edmToMendixType maps an Edm type to a Mendix attribute type string for MDL output. -func edmToMendixType(p *mpr.EdmProperty) string { +func edmToMendixType(p *types.EdmProperty) string { switch p.Type { case "Edm.String": if p.MaxLength != "" && p.MaxLength != "max" { @@ -457,7 +457,7 @@ func createExternalEntities(ctx *ExecContext, s *ast.CreateExternalEntitiesStmt) // Build entity set lookup: entity type qualified name → entity set name esMap := make(map[string]string) - esByType := make(map[string]*mpr.EdmEntitySet) + esByType := make(map[string]*types.EdmEntitySet) for _, es := range doc.EntitySets { esMap[es.EntityType] = es.Name esByType[es.EntityType] = es @@ -491,7 +491,7 @@ func createExternalEntities(ctx *ExecContext, s *ast.CreateExternalEntitiesStmt) } // Build a global type lookup so we can resolve BaseType references across schemas. - typeByQualified := make(map[string]*mpr.EdmEntityType) + typeByQualified := make(map[string]*types.EdmEntityType) for _, schema := range doc.Schemas { for _, et := range schema.EntityTypes { typeByQualified[schema.Namespace+"."+et.Name] = et @@ -533,7 +533,7 @@ func createExternalEntities(ctx *ExecContext, s *ast.CreateExternalEntitiesStmt) // Build key parts from the resolved key (root entity in the chain) var keyParts []*domainmodel.RemoteKeyPart for _, keyName := range keyProps { - var keyProp *mpr.EdmProperty + var keyProp *types.EdmProperty for _, p := range mergedProps { if p.Name == keyName { keyProp = p @@ -621,7 +621,7 @@ func createExternalEntities(ctx *ExecContext, s *ast.CreateExternalEntitiesStmt) Creatable: creatable, Updatable: updatable, } - attr.ID = model.ID(mpr.GenerateID()) + attr.ID = model.ID(types.GenerateID()) attrs = append(attrs, attr) } @@ -646,7 +646,7 @@ func createExternalEntities(ctx *ExecContext, s *ast.CreateExternalEntitiesStmt) Name: mendixName, Location: location, } - newEntity.ID = model.ID(mpr.GenerateID()) + newEntity.ID = model.ID(types.GenerateID()) applyExternalEntityFields(newEntity, et, isTopLevel, serviceRef, entitySet, keyParts, attrs) if err := ctx.Backend.CreateEntity(dm.ID, newEntity); err != nil { fmt.Fprintf(ctx.Output, " FAILED: %s.%s — %v\n", targetModule, mendixName, err) @@ -699,8 +699,8 @@ type assocKey struct { func createPrimitiveCollectionNPEs( ctx *ExecContext, dm *domainmodel.DomainModel, - doc *mpr.EdmxDocument, - typeByQualified map[string]*mpr.EdmEntityType, + doc *types.EdmxDocument, + typeByQualified map[string]*types.EdmEntityType, esMap map[string]string, serviceRef string, ) int { @@ -759,7 +759,7 @@ func createPrimitiveCollectionNPEs( // Build the inner attribute type from the element type innerType := p.Type[len("Collection(") : len(p.Type)-1] - innerProp := &mpr.EdmProperty{ + innerProp := &types.EdmProperty{ Name: singular(p.Name), Type: innerType, MaxLength: p.MaxLength, @@ -773,7 +773,7 @@ func createPrimitiveCollectionNPEs( RemoteType: primitiveCollectionRemoteType(innerType, p.Nullable), IsPrimitiveCollection: true, } - attr.ID = model.ID(mpr.GenerateID()) + attr.ID = model.ID(types.GenerateID()) npe := &domainmodel.Entity{ Name: npeName, @@ -783,7 +783,7 @@ func createPrimitiveCollectionNPEs( Source: "Rest$ODataPrimitiveCollectionEntitySource", RemoteServiceName: serviceRef, } - npe.ID = model.ID(mpr.GenerateID()) + npe.ID = model.ID(types.GenerateID()) if err := ctx.Backend.CreateEntity(dm.ID, npe); err != nil { fmt.Fprintf(ctx.Output, " NPE FAILED: %s — %v\n", npeName, err) @@ -805,7 +805,7 @@ func createPrimitiveCollectionNPEs( StorageFormat: domainmodel.StorageFormatColumn, Source: "Rest$ODataPrimitiveCollectionAssociationSource", } - assoc.ID = model.ID(mpr.GenerateID()) + assoc.ID = model.ID(types.GenerateID()) if err := ctx.Backend.CreateAssociation(dm.ID, assoc); err != nil { fmt.Fprintf(ctx.Output, " NPE ASSOC FAILED: %s — %v\n", assocName, err) } @@ -817,7 +817,7 @@ func createPrimitiveCollectionNPEs( // isInheritedProperty reports whether a property name comes from one of the // entity type's base types (rather than being defined on the type itself). -func isInheritedProperty(et *mpr.EdmEntityType, propName string, byQN map[string]*mpr.EdmEntityType) bool { +func isInheritedProperty(et *types.EdmEntityType, propName string, byQN map[string]*types.EdmEntityType) bool { for _, p := range et.Properties { if p.Name == propName { return false @@ -882,8 +882,8 @@ func singular(name string) string { func createNavigationAssociations( ctx *ExecContext, dm *domainmodel.DomainModel, - doc *mpr.EdmxDocument, - typeByQualified map[string]*mpr.EdmEntityType, + doc *types.EdmxDocument, + typeByQualified map[string]*types.EdmEntityType, esMap map[string]string, serviceRef string, ) int { @@ -895,7 +895,7 @@ func createNavigationAssociations( nonUpdatable map[string]bool } restrictionsByType := make(map[string]navRestrictions) - esByType := make(map[string]*mpr.EdmEntitySet) + esByType := make(map[string]*types.EdmEntitySet) for _, es := range doc.EntitySets { r := navRestrictions{ nonInsertable: make(map[string]bool), @@ -1027,7 +1027,7 @@ func createNavigationAssociations( CreatableFromParent: creatable, UpdatableFromParent: updatable, } - assoc.ID = model.ID(mpr.GenerateID()) + assoc.ID = model.ID(types.GenerateID()) if err := ctx.Backend.CreateAssociation(dm.ID, assoc); err != nil { fmt.Fprintf(ctx.Output, " ASSOC FAILED: %s.%s — %v\n", parentEnt.Name, assocName, err) @@ -1082,10 +1082,10 @@ func uniqueAssocName(base string, dm *domainmodel.DomainModel, existingAssocs ma // optimistic defaults. func applyExternalEntityFields( ent *domainmodel.Entity, - et *mpr.EdmEntityType, + et *types.EdmEntityType, isTopLevel bool, serviceRef string, - entitySet *mpr.EdmEntitySet, + entitySet *types.EdmEntitySet, keyParts []*domainmodel.RemoteKeyPart, attrs []*domainmodel.Attribute, ) { @@ -1133,20 +1133,20 @@ func applyExternalEntityFields( // mergedPropertiesWithKey walks the BaseType chain of an entity type and // returns the merged property list (base properties first, then derived) along // with the key property names from the root of the chain. -func mergedPropertiesWithKey(et *mpr.EdmEntityType, byQualified map[string]*mpr.EdmEntityType) ([]*mpr.EdmProperty, []string) { +func mergedPropertiesWithKey(et *types.EdmEntityType, byQualified map[string]*types.EdmEntityType) ([]*types.EdmProperty, []string) { // Walk to the root, collecting types in order from base → derived. - chain := []*mpr.EdmEntityType{et} + chain := []*types.EdmEntityType{et} current := et for current.BaseType != "" { parent := byQualified[current.BaseType] if parent == nil { break } - chain = append([]*mpr.EdmEntityType{parent}, chain...) + chain = append([]*types.EdmEntityType{parent}, chain...) current = parent } - var merged []*mpr.EdmProperty + var merged []*types.EdmProperty seen := make(map[string]bool) for _, t := range chain { for _, p := range t.Properties { @@ -1176,7 +1176,7 @@ func attrNameForOData(propName, entityName string) string { // edmToDomainModelAttrType converts an EDM property to a domainmodel attribute type. // isKey forces a non-zero length for string keys: Mendix forbids unlimited // strings as part of an external entity key (CE6121). -func edmToDomainModelAttrType(p *mpr.EdmProperty, isKey bool) domainmodel.AttributeType { +func edmToDomainModelAttrType(p *types.EdmProperty, isKey bool) domainmodel.AttributeType { switch p.Type { case "Edm.String": // Studio Pro stores Length=0 (unlimited) for OData strings without MaxLength. @@ -1208,7 +1208,7 @@ func edmToDomainModelAttrType(p *mpr.EdmProperty, isKey bool) domainmodel.Attrib } // edmToAstDataType converts an Edm property to an AST data type. -func edmToAstDataType(p *mpr.EdmProperty) ast.DataType { +func edmToAstDataType(p *types.EdmProperty) ast.DataType { switch p.Type { case "Edm.String": length := 200 @@ -1380,7 +1380,7 @@ func describeContractMessage(ctx *ExecContext, name ast.QualifiedName) error { } // parseAsyncAPIContract finds a business event service by name and parses its cached AsyncAPI document. -func parseAsyncAPIContract(ctx *ExecContext, name ast.QualifiedName) (*mpr.AsyncAPIDocument, string, error) { +func parseAsyncAPIContract(ctx *ExecContext, name ast.QualifiedName) (*types.AsyncAPIDocument, string, error) { services, err := ctx.Backend.ListBusinessEventServices() if err != nil { return nil, "", mdlerrors.NewBackend("list business event services", err) @@ -1405,7 +1405,7 @@ func parseAsyncAPIContract(ctx *ExecContext, name ast.QualifiedName) (*mpr.Async 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) + doc, err := types.ParseAsyncAPI(svc.Document) if err != nil { return nil, svcQN, mdlerrors.NewBackend(fmt.Sprintf("parse AsyncAPI contract for %s", svcQN), err) } @@ -1417,7 +1417,7 @@ func parseAsyncAPIContract(ctx *ExecContext, name ast.QualifiedName) (*mpr.Async } // asyncTypeString formats an AsyncAPI property type for display. -func asyncTypeString(p *mpr.AsyncAPIProperty) string { +func asyncTypeString(p *types.AsyncAPIProperty) string { if p.Format != "" { return p.Type + " (" + p.Format + ")" } diff --git a/mdl/executor/cmd_entities.go b/mdl/executor/cmd_entities.go index 8a231239..9517c446 100644 --- a/mdl/executor/cmd_entities.go +++ b/mdl/executor/cmd_entities.go @@ -9,9 +9,9 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" - "github.com/mendixlabs/mxcli/sdk/mpr" ) // execCreateEntity handles CREATE ENTITY statements. @@ -31,7 +31,7 @@ func buildEventHandlers(ctx *ExecContext, defs []ast.EventHandlerDef) ([]*domain } handlers = append(handlers, &domainmodel.EventHandler{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "DomainModels$EventHandler", }, Moment: domainmodel.EventMoment(d.Moment), @@ -134,7 +134,7 @@ func execCreateEntity(ctx *ExecContext, s *ast.CreateEntityStmt) error { } // Generate ID for the attribute so we can reference it in validation rules/indexes - attrID := model.ID(mpr.GenerateID()) + attrID := model.ID(types.GenerateID()) attrNameToID[a.Name] = attrID attr := &domainmodel.Attribute{ @@ -188,12 +188,12 @@ func execCreateEntity(ctx *ExecContext, s *ast.CreateEntityStmt) error { AttributeID: attrID, Type: "Required", } - vr.ID = model.ID(mpr.GenerateID()) + vr.ID = model.ID(types.GenerateID()) if a.NotNullError != "" { vr.ErrorMessage = &model.Text{ Translations: map[string]string{"en_US": a.NotNullError}, } - vr.ErrorMessage.ID = model.ID(mpr.GenerateID()) + vr.ErrorMessage.ID = model.ID(types.GenerateID()) } validationRules = append(validationRules, vr) } @@ -204,12 +204,12 @@ func execCreateEntity(ctx *ExecContext, s *ast.CreateEntityStmt) error { AttributeID: attrID, Type: "Unique", } - vr.ID = model.ID(mpr.GenerateID()) + vr.ID = model.ID(types.GenerateID()) if a.UniqueError != "" { vr.ErrorMessage = &model.Text{ Translations: map[string]string{"en_US": a.UniqueError}, } - vr.ErrorMessage.ID = model.ID(mpr.GenerateID()) + vr.ErrorMessage.ID = model.ID(types.GenerateID()) } validationRules = append(validationRules, vr) } @@ -218,11 +218,11 @@ func execCreateEntity(ctx *ExecContext, s *ast.CreateEntityStmt) error { // Create indexes var indexes []*domainmodel.Index for _, idx := range s.Indexes { - idxID := model.ID(mpr.GenerateID()) + idxID := model.ID(types.GenerateID()) var indexAttrs []*domainmodel.IndexAttribute for _, col := range idx.Columns { if attrID, ok := attrNameToID[col.Name]; ok { - iaID := model.ID(mpr.GenerateID()) + iaID := model.ID(types.GenerateID()) ia := &domainmodel.IndexAttribute{ AttributeID: attrID, Ascending: !col.Descending, @@ -529,7 +529,7 @@ func execAlterEntity(ctx *ExecContext, s *ast.AlterEntityStmt) error { } } - attrID := model.ID(mpr.GenerateID()) + attrID := model.ID(types.GenerateID()) attr := &domainmodel.Attribute{ Name: a.Name, Documentation: a.Documentation, @@ -569,12 +569,12 @@ func execAlterEntity(ctx *ExecContext, s *ast.AlterEntityStmt) error { AttributeID: attrID, Type: "Required", } - vr.ID = model.ID(mpr.GenerateID()) + vr.ID = model.ID(types.GenerateID()) if a.NotNullError != "" { vr.ErrorMessage = &model.Text{ Translations: map[string]string{"en_US": a.NotNullError}, } - vr.ErrorMessage.ID = model.ID(mpr.GenerateID()) + vr.ErrorMessage.ID = model.ID(types.GenerateID()) } entity.ValidationRules = append(entity.ValidationRules, vr) } @@ -583,12 +583,12 @@ func execAlterEntity(ctx *ExecContext, s *ast.AlterEntityStmt) error { AttributeID: attrID, Type: "Unique", } - vr.ID = model.ID(mpr.GenerateID()) + vr.ID = model.ID(types.GenerateID()) if a.UniqueError != "" { vr.ErrorMessage = &model.Text{ Translations: map[string]string{"en_US": a.UniqueError}, } - vr.ErrorMessage.ID = model.ID(mpr.GenerateID()) + vr.ErrorMessage.ID = model.ID(types.GenerateID()) } entity.ValidationRules = append(entity.ValidationRules, vr) } @@ -808,7 +808,7 @@ func execAlterEntity(ctx *ExecContext, s *ast.AlterEntityStmt) error { for _, attr := range entity.Attributes { attrNameToID[attr.Name] = attr.ID } - idxID := model.ID(mpr.GenerateID()) + idxID := model.ID(types.GenerateID()) var indexAttrs []*domainmodel.IndexAttribute for _, col := range s.Index.Columns { if attrID, ok := attrNameToID[col.Name]; ok { @@ -816,7 +816,7 @@ func execAlterEntity(ctx *ExecContext, s *ast.AlterEntityStmt) error { AttributeID: attrID, Ascending: !col.Descending, } - ia.ID = model.ID(mpr.GenerateID()) + ia.ID = model.ID(types.GenerateID()) indexAttrs = append(indexAttrs, ia) } else { return mdlerrors.NewNotFoundMsg("attribute", col.Name, fmt.Sprintf("attribute '%s' not found for index on entity %s", col.Name, s.Name)) diff --git a/mdl/executor/cmd_error_mock_test.go b/mdl/executor/cmd_error_mock_test.go index d60fa2db..cdc959af 100644 --- a/mdl/executor/cmd_error_mock_test.go +++ b/mdl/executor/cmd_error_mock_test.go @@ -8,10 +8,10 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/backend/mock" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/agenteditor" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/pages" "github.com/mendixlabs/mxcli/sdk/security" "github.com/mendixlabs/mxcli/sdk/workflows" @@ -131,7 +131,7 @@ func TestShowPublishedRestServices_Mock_BackendError(t *testing.T) { func TestShowJavaActions_Mock_BackendError(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - ListJavaActionsFunc: func() ([]*mpr.JavaAction, error) { return nil, errBackend }, + ListJavaActionsFunc: func() ([]*types.JavaAction, error) { return nil, errBackend }, } ctx, _ := newMockCtx(t, withBackend(mb)) assertError(t, showJavaActions(ctx, "")) @@ -140,7 +140,7 @@ func TestShowJavaActions_Mock_BackendError(t *testing.T) { func TestShowJavaScriptActions_Mock_BackendError(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - ListJavaScriptActionsFunc: func() ([]*mpr.JavaScriptAction, error) { return nil, errBackend }, + ListJavaScriptActionsFunc: func() ([]*types.JavaScriptAction, error) { return nil, errBackend }, } ctx, _ := newMockCtx(t, withBackend(mb)) assertError(t, showJavaScriptActions(ctx, "")) @@ -158,7 +158,7 @@ func TestShowDatabaseConnections_Mock_BackendError(t *testing.T) { func TestShowImageCollections_Mock_BackendError(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - ListImageCollectionsFunc: func() ([]*mpr.ImageCollection, error) { return nil, errBackend }, + ListImageCollectionsFunc: func() ([]*types.ImageCollection, error) { return nil, errBackend }, } ctx, _ := newMockCtx(t, withBackend(mb)) assertError(t, showImageCollections(ctx, "")) @@ -167,7 +167,7 @@ func TestShowImageCollections_Mock_BackendError(t *testing.T) { func TestShowJsonStructures_Mock_BackendError(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - ListJsonStructuresFunc: func() ([]*mpr.JsonStructure, error) { return nil, errBackend }, + ListJsonStructuresFunc: func() ([]*types.JsonStructure, error) { return nil, errBackend }, } ctx, _ := newMockCtx(t, withBackend(mb)) assertError(t, showJsonStructures(ctx, "")) @@ -176,7 +176,7 @@ func TestShowJsonStructures_Mock_BackendError(t *testing.T) { func TestShowNavigation_Mock_BackendError(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - GetNavigationFunc: func() (*mpr.NavigationDocument, error) { return nil, errBackend }, + GetNavigationFunc: func() (*types.NavigationDocument, error) { return nil, errBackend }, } ctx, _ := newMockCtx(t, withBackend(mb)) assertError(t, showNavigation(ctx)) @@ -340,7 +340,7 @@ func TestDescribeWorkflow_Mock_BackendError(t *testing.T) { func TestDescribeNavigation_Mock_BackendError(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - GetNavigationFunc: func() (*mpr.NavigationDocument, error) { return nil, errBackend }, + GetNavigationFunc: func() (*types.NavigationDocument, error) { return nil, errBackend }, } ctx, _ := newMockCtx(t, withBackend(mb)) assertError(t, describeNavigation(ctx, ast.QualifiedName{Module: "M", Name: "N"})) @@ -376,7 +376,7 @@ func TestDescribeRestClient_Mock_BackendError(t *testing.T) { func TestDescribeImageCollection_Mock_BackendError(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - ListImageCollectionsFunc: func() ([]*mpr.ImageCollection, error) { return nil, errBackend }, + ListImageCollectionsFunc: func() ([]*types.ImageCollection, error) { return nil, errBackend }, } ctx, _ := newMockCtx(t, withBackend(mb)) assertError(t, describeImageCollection(ctx, ast.QualifiedName{Module: "M", Name: "I"})) diff --git a/mdl/executor/cmd_export_mappings.go b/mdl/executor/cmd_export_mappings.go index 0745cad6..2633b97c 100644 --- a/mdl/executor/cmd_export_mappings.go +++ b/mdl/executor/cmd_export_mappings.go @@ -11,6 +11,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/mpr" ) @@ -207,7 +208,7 @@ func execCreateExportMapping(ctx *ExecContext, s *ast.CreateExportMappingStmt) e } // Build a path→element info map from the JSON structure for schema alignment. - jsElems := map[string]*mpr.JsonElement{} + jsElems := map[string]*types.JsonElement{} if s.SchemaKind == "JSON_STRUCTURE" && s.SchemaRef.Module != "" { if js, err2 := ctx.Backend.GetJsonStructureByQualifiedName(s.SchemaRef.Module, s.SchemaRef.Name); err2 == nil { buildJsonElementPathMap(js.Elements, jsElems) @@ -232,7 +233,7 @@ func execCreateExportMapping(ctx *ExecContext, s *ast.CreateExportMappingStmt) e // buildExportMappingElementModel converts an AST element definition to a model element. // It clones properties from the matching JSON structure element and adds mapping bindings. -func buildExportMappingElementModel(moduleName string, def *ast.ExportMappingElementDef, parentEntity, parentPath string, jsElems map[string]*mpr.JsonElement, reader *mpr.Reader, isRoot bool) *model.ExportMappingElement { +func buildExportMappingElementModel(moduleName string, def *ast.ExportMappingElementDef, parentEntity, parentPath string, jsElems map[string]*types.JsonElement, reader *mpr.Reader, isRoot bool) *model.ExportMappingElement { elem := &model.ExportMappingElement{ BaseElement: model.BaseElement{ ID: model.ID(mpr.GenerateID()), diff --git a/mdl/executor/cmd_folders.go b/mdl/executor/cmd_folders.go index 5dafe8f8..15e373eb 100644 --- a/mdl/executor/cmd_folders.go +++ b/mdl/executor/cmd_folders.go @@ -10,11 +10,11 @@ import ( "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/mdl/types" ) // findFolderByPath walks a folder path under a module and returns the folder ID. -func findFolderByPath(ctx *ExecContext, moduleID model.ID, folderPath string, folders []*mpr.FolderInfo) (model.ID, error) { +func findFolderByPath(ctx *ExecContext, moduleID model.ID, folderPath string, folders []*types.FolderInfo) (model.ID, error) { parts := strings.Split(folderPath, "/") currentContainerID := moduleID diff --git a/mdl/executor/cmd_imagecollections.go b/mdl/executor/cmd_imagecollections.go index fdafb88a..9521f0b6 100644 --- a/mdl/executor/cmd_imagecollections.go +++ b/mdl/executor/cmd_imagecollections.go @@ -11,7 +11,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" ) // execCreateImageCollection handles CREATE IMAGE COLLECTION statements. @@ -33,7 +33,7 @@ func execCreateImageCollection(ctx *ExecContext, s *ast.CreateImageCollectionStm } // Build ImageCollection - ic := &mpr.ImageCollection{ + ic := &types.ImageCollection{ ContainerID: module.ID, Name: s.Name.Name, ExportLevel: s.ExportLevel, @@ -55,7 +55,7 @@ func execCreateImageCollection(ctx *ExecContext, s *ast.CreateImageCollectionStm return mdlerrors.NewBackend(fmt.Sprintf("read image file %q", item.FilePath), err) } format := extToImageFormat(filepath.Ext(filePath)) - ic.Images = append(ic.Images, mpr.Image{ + ic.Images = append(ic.Images, types.Image{ Name: item.Name, Data: data, Format: format, @@ -232,7 +232,7 @@ func showImageCollections(ctx *ExecContext, moduleName string) error { } // findImageCollection finds an image collection by module and name. -func findImageCollection(ctx *ExecContext, moduleName, collectionName string) *mpr.ImageCollection { +func findImageCollection(ctx *ExecContext, moduleName, collectionName string) *types.ImageCollection { collections, err := ctx.Backend.ListImageCollections() if err != nil { return nil diff --git a/mdl/executor/cmd_imagecollections_mock_test.go b/mdl/executor/cmd_imagecollections_mock_test.go index 9b5f7548..eb7650bd 100644 --- a/mdl/executor/cmd_imagecollections_mock_test.go +++ b/mdl/executor/cmd_imagecollections_mock_test.go @@ -7,13 +7,13 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/backend/mock" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" ) func TestShowImageCollections_Mock(t *testing.T) { mod := mkModule("Icons") - ic := &mpr.ImageCollection{ + ic := &types.ImageCollection{ BaseElement: model.BaseElement{ID: nextID("ic")}, ContainerID: mod.ID, Name: "AppIcons", @@ -25,7 +25,7 @@ func TestShowImageCollections_Mock(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - ListImageCollectionsFunc: func() ([]*mpr.ImageCollection, error) { return []*mpr.ImageCollection{ic}, nil }, + ListImageCollectionsFunc: func() ([]*types.ImageCollection, error) { return []*types.ImageCollection{ic}, nil }, } ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) @@ -38,7 +38,7 @@ func TestShowImageCollections_Mock(t *testing.T) { func TestDescribeImageCollection_Mock(t *testing.T) { mod := mkModule("Icons") - ic := &mpr.ImageCollection{ + ic := &types.ImageCollection{ BaseElement: model.BaseElement{ID: nextID("ic")}, ContainerID: mod.ID, Name: "AppIcons", @@ -50,7 +50,7 @@ func TestDescribeImageCollection_Mock(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - ListImageCollectionsFunc: func() ([]*mpr.ImageCollection, error) { return []*mpr.ImageCollection{ic}, nil }, + ListImageCollectionsFunc: func() ([]*types.ImageCollection, error) { return []*types.ImageCollection{ic}, nil }, } ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) diff --git a/mdl/executor/cmd_import_mappings.go b/mdl/executor/cmd_import_mappings.go index a750fc3f..4837a8c3 100644 --- a/mdl/executor/cmd_import_mappings.go +++ b/mdl/executor/cmd_import_mappings.go @@ -11,6 +11,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/mpr" ) @@ -216,7 +217,7 @@ func execCreateImportMapping(ctx *ExecContext, s *ast.CreateImportMappingStmt) e } // Build path→JsonElement map from JSON structure — mapping elements clone from this - jsElementsByPath := map[string]*mpr.JsonElement{} + jsElementsByPath := map[string]*types.JsonElement{} if s.SchemaKind == "JSON_STRUCTURE" && s.SchemaRef.Module != "" { if js, err2 := ctx.Backend.GetJsonStructureByQualifiedName(s.SchemaRef.Module, s.SchemaRef.Name); err2 == nil { buildJsonElementPathMap(js.Elements, jsElementsByPath) @@ -243,7 +244,7 @@ func execCreateImportMapping(ctx *ExecContext, s *ast.CreateImportMappingStmt) e // It clones properties from the matching JSON structure element (ExposedName, JsonPath, // MaxOccurs, ElementType, etc.) and adds mapping-specific bindings (Entity, Attribute, // Association, ObjectHandling). -func buildImportMappingElementModel(moduleName string, def *ast.ImportMappingElementDef, parentEntity, parentPath string, reader *mpr.Reader, jsElems map[string]*mpr.JsonElement, isRoot bool) *model.ImportMappingElement { +func buildImportMappingElementModel(moduleName string, def *ast.ImportMappingElementDef, parentEntity, parentPath string, reader *mpr.Reader, jsElems map[string]*types.JsonElement, isRoot bool) *model.ImportMappingElement { elem := &model.ImportMappingElement{ BaseElement: model.BaseElement{ ID: model.ID(mpr.GenerateID()), @@ -336,7 +337,7 @@ func buildImportMappingElementModel(moduleName string, def *ast.ImportMappingEle } // buildJsonElementPathMap recursively builds a map from JSON path → JsonElement. -func buildJsonElementPathMap(elems []*mpr.JsonElement, m map[string]*mpr.JsonElement) { +func buildJsonElementPathMap(elems []*types.JsonElement, m map[string]*types.JsonElement) { for _, e := range elems { if e == nil { continue diff --git a/mdl/executor/cmd_javaactions.go b/mdl/executor/cmd_javaactions.go index eb19cbd8..395d1010 100644 --- a/mdl/executor/cmd_javaactions.go +++ b/mdl/executor/cmd_javaactions.go @@ -12,9 +12,9 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/javaactions" - "github.com/mendixlabs/mxcli/sdk/mpr" ) // showJavaActions handles SHOW JAVA ACTIONS command. @@ -325,7 +325,7 @@ func execCreateJavaAction(ctx *ExecContext, s *ast.CreateJavaActionStmt) error { // Create the Java action ja := &javaactions.JavaAction{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "JavaActions$JavaAction", }, ContainerID: containerID, @@ -339,7 +339,7 @@ func execCreateJavaAction(ctx *ExecContext, s *ast.CreateJavaActionStmt) error { for _, tpName := range s.TypeParameters { tpDef := &javaactions.TypeParameterDef{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), }, Name: tpName, } @@ -360,7 +360,7 @@ func execCreateJavaAction(ctx *ExecContext, s *ast.CreateJavaActionStmt) error { for _, param := range s.Parameters { jaParam := &javaactions.JavaActionParameter{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "JavaActions$JavaActionParameter", }, Name: param.Name, @@ -370,7 +370,7 @@ func execCreateJavaAction(ctx *ExecContext, s *ast.CreateJavaActionStmt) error { // Explicit ENTITY → EntityTypeParameterType (entity type selector) tpName := param.Type.TypeParamName jaParam.ParameterType = &javaactions.EntityTypeParameterType{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, TypeParameterID: typeParamNameToID[tpName], TypeParameterName: tpName, } @@ -378,7 +378,7 @@ func execCreateJavaAction(ctx *ExecContext, s *ast.CreateJavaActionStmt) error { // Bare name matching a type parameter → TypeParameter (ParameterizedEntityType) tpName := getTypeParamRefName(param.Type) jaParam.ParameterType = &javaactions.TypeParameter{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, TypeParameterID: typeParamNameToID[tpName], TypeParameter: tpName, } @@ -392,7 +392,7 @@ func execCreateJavaAction(ctx *ExecContext, s *ast.CreateJavaActionStmt) error { if isTypeParamRef(s.ReturnType, typeParamNames) { tpName := getTypeParamRefName(s.ReturnType) ja.ReturnType = &javaactions.TypeParameter{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, TypeParameterID: typeParamNameToID[tpName], TypeParameter: tpName, } @@ -403,7 +403,7 @@ func execCreateJavaAction(ctx *ExecContext, s *ast.CreateJavaActionStmt) error { // Build MicroflowActionInfo if EXPOSED AS clause is present if s.ExposedCaption != "" { ja.MicroflowActionInfo = &javaactions.MicroflowActionInfo{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Caption: s.ExposedCaption, Category: s.ExposedCategory, } @@ -434,42 +434,42 @@ func astDataTypeToJavaActionParamType(dt ast.DataType) javaactions.CodeActionPar case ast.TypeBoolean: return &javaactions.BooleanType{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "CodeActions$BooleanType", }, } case ast.TypeInteger: return &javaactions.IntegerType{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "CodeActions$IntegerType", }, } case ast.TypeLong: return &javaactions.LongType{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "CodeActions$LongType", }, } case ast.TypeDecimal: return &javaactions.DecimalType{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "CodeActions$DecimalType", }, } case ast.TypeString: return &javaactions.StringType{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "CodeActions$StringType", }, } case ast.TypeDateTime, ast.TypeDate: return &javaactions.DateTimeType{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "CodeActions$DateTimeType", }, } @@ -485,7 +485,7 @@ func astDataTypeToJavaActionParamType(dt ast.DataType) javaactions.CodeActionPar } return &javaactions.EntityType{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "CodeActions$EntityType", }, Entity: entityName, @@ -497,7 +497,7 @@ func astDataTypeToJavaActionParamType(dt ast.DataType) javaactions.CodeActionPar } return &javaactions.ListType{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "CodeActions$ListType", }, Entity: entityName, @@ -506,7 +506,7 @@ func astDataTypeToJavaActionParamType(dt ast.DataType) javaactions.CodeActionPar // Default to String type for unknown kinds return &javaactions.StringType{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "CodeActions$StringType", }, } @@ -519,49 +519,49 @@ func astDataTypeToJavaActionReturnType(dt ast.DataType) javaactions.CodeActionRe case ast.TypeVoid: return &javaactions.VoidType{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "CodeActions$VoidType", }, } case ast.TypeBoolean: return &javaactions.BooleanType{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "CodeActions$BooleanType", }, } case ast.TypeInteger: return &javaactions.IntegerType{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "CodeActions$IntegerType", }, } case ast.TypeLong: return &javaactions.LongType{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "CodeActions$LongType", }, } case ast.TypeDecimal: return &javaactions.DecimalType{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "CodeActions$DecimalType", }, } case ast.TypeString: return &javaactions.StringType{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "CodeActions$StringType", }, } case ast.TypeDateTime, ast.TypeDate: return &javaactions.DateTimeType{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "CodeActions$DateTimeType", }, } @@ -576,7 +576,7 @@ func astDataTypeToJavaActionReturnType(dt ast.DataType) javaactions.CodeActionRe } return &javaactions.EntityType{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "CodeActions$EntityType", }, Entity: entityName, @@ -588,7 +588,7 @@ func astDataTypeToJavaActionReturnType(dt ast.DataType) javaactions.CodeActionRe } return &javaactions.ListType{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "CodeActions$ListType", }, Entity: entityName, @@ -597,7 +597,7 @@ func astDataTypeToJavaActionReturnType(dt ast.DataType) javaactions.CodeActionRe // Default to Boolean type (most common for Java actions) return &javaactions.BooleanType{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "CodeActions$BooleanType", }, } diff --git a/mdl/executor/cmd_javaactions_mock_test.go b/mdl/executor/cmd_javaactions_mock_test.go index bd331139..53860c08 100644 --- a/mdl/executor/cmd_javaactions_mock_test.go +++ b/mdl/executor/cmd_javaactions_mock_test.go @@ -7,14 +7,14 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/backend/mock" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/javaactions" - "github.com/mendixlabs/mxcli/sdk/mpr" ) func TestShowJavaActions_Mock(t *testing.T) { mod := mkModule("MyModule") - ja := &mpr.JavaAction{ + ja := &types.JavaAction{ BaseElement: model.BaseElement{ID: nextID("ja")}, ContainerID: mod.ID, Name: "DoSomething", @@ -25,7 +25,7 @@ func TestShowJavaActions_Mock(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - ListJavaActionsFunc: func() ([]*mpr.JavaAction, error) { return []*mpr.JavaAction{ja}, nil }, + ListJavaActionsFunc: func() ([]*types.JavaAction, error) { return []*types.JavaAction{ja}, nil }, } ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) diff --git a/mdl/executor/cmd_javascript_actions_mock_test.go b/mdl/executor/cmd_javascript_actions_mock_test.go index dd47c049..3ec07738 100644 --- a/mdl/executor/cmd_javascript_actions_mock_test.go +++ b/mdl/executor/cmd_javascript_actions_mock_test.go @@ -7,13 +7,13 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/backend/mock" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" ) func TestShowJavaScriptActions_Mock(t *testing.T) { mod := mkModule("WebMod") - jsa := &mpr.JavaScriptAction{ + jsa := &types.JavaScriptAction{ BaseElement: model.BaseElement{ID: nextID("jsa")}, ContainerID: mod.ID, Name: "ShowAlert", @@ -25,7 +25,7 @@ func TestShowJavaScriptActions_Mock(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - ListJavaScriptActionsFunc: func() ([]*mpr.JavaScriptAction, error) { return []*mpr.JavaScriptAction{jsa}, nil }, + ListJavaScriptActionsFunc: func() ([]*types.JavaScriptAction, error) { return []*types.JavaScriptAction{jsa}, nil }, } ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) @@ -41,8 +41,8 @@ func TestDescribeJavaScriptAction_Mock(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - ReadJavaScriptActionByNameFunc: func(qn string) (*mpr.JavaScriptAction, error) { - return &mpr.JavaScriptAction{ + ReadJavaScriptActionByNameFunc: func(qn string) (*types.JavaScriptAction, error) { + return &types.JavaScriptAction{ BaseElement: model.BaseElement{ID: nextID("jsa")}, ContainerID: mod.ID, Name: "ShowAlert", diff --git a/mdl/executor/cmd_json_mock_test.go b/mdl/executor/cmd_json_mock_test.go index 9986fe53..5ee03a8d 100644 --- a/mdl/executor/cmd_json_mock_test.go +++ b/mdl/executor/cmd_json_mock_test.go @@ -6,10 +6,10 @@ import ( "testing" "github.com/mendixlabs/mxcli/mdl/backend/mock" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/agenteditor" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/pages" "github.com/mendixlabs/mxcli/sdk/security" "github.com/mendixlabs/mxcli/sdk/workflows" @@ -238,7 +238,7 @@ func TestShowPublishedRestServices_Mock_JSON(t *testing.T) { func TestShowJavaActions_Mock_JSON(t *testing.T) { mod := mkModule("MyModule") h := mkHierarchy(mod) - ja := &mpr.JavaAction{ + ja := &types.JavaAction{ BaseElement: model.BaseElement{ID: nextID("ja")}, ContainerID: mod.ID, Name: "MyJavaAction", @@ -247,7 +247,7 @@ func TestShowJavaActions_Mock_JSON(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - ListJavaActionsFunc: func() ([]*mpr.JavaAction, error) { return []*mpr.JavaAction{ja}, nil }, + ListJavaActionsFunc: func() ([]*types.JavaAction, error) { return []*types.JavaAction{ja}, nil }, } ctx, buf := newMockCtx(t, withBackend(mb), withFormat(FormatJSON), withHierarchy(h)) @@ -259,7 +259,7 @@ func TestShowJavaActions_Mock_JSON(t *testing.T) { func TestShowJavaScriptActions_Mock_JSON(t *testing.T) { mod := mkModule("MyModule") h := mkHierarchy(mod) - jsa := &mpr.JavaScriptAction{ + jsa := &types.JavaScriptAction{ BaseElement: model.BaseElement{ID: nextID("jsa")}, ContainerID: mod.ID, Name: "MyJSAction", @@ -268,7 +268,7 @@ func TestShowJavaScriptActions_Mock_JSON(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - ListJavaScriptActionsFunc: func() ([]*mpr.JavaScriptAction, error) { return []*mpr.JavaScriptAction{jsa}, nil }, + ListJavaScriptActionsFunc: func() ([]*types.JavaScriptAction, error) { return []*types.JavaScriptAction{jsa}, nil }, } ctx, buf := newMockCtx(t, withBackend(mb), withFormat(FormatJSON), withHierarchy(h)) @@ -301,7 +301,7 @@ func TestShowDatabaseConnections_Mock_JSON(t *testing.T) { func TestShowImageCollections_Mock_JSON(t *testing.T) { mod := mkModule("MyModule") h := mkHierarchy(mod) - ic := &mpr.ImageCollection{ + ic := &types.ImageCollection{ BaseElement: model.BaseElement{ID: nextID("ic")}, ContainerID: mod.ID, Name: "Icons", @@ -310,7 +310,7 @@ func TestShowImageCollections_Mock_JSON(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - ListImageCollectionsFunc: func() ([]*mpr.ImageCollection, error) { return []*mpr.ImageCollection{ic}, nil }, + ListImageCollectionsFunc: func() ([]*types.ImageCollection, error) { return []*types.ImageCollection{ic}, nil }, } ctx, buf := newMockCtx(t, withBackend(mb), withFormat(FormatJSON), withHierarchy(h)) @@ -322,7 +322,7 @@ func TestShowImageCollections_Mock_JSON(t *testing.T) { func TestShowJsonStructures_Mock_JSON(t *testing.T) { mod := mkModule("MyModule") h := mkHierarchy(mod) - js := &mpr.JsonStructure{ + js := &types.JsonStructure{ BaseElement: model.BaseElement{ID: nextID("js")}, ContainerID: mod.ID, Name: "OrderSchema", @@ -331,7 +331,7 @@ func TestShowJsonStructures_Mock_JSON(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - ListJsonStructuresFunc: func() ([]*mpr.JsonStructure, error) { return []*mpr.JsonStructure{js}, nil }, + ListJsonStructuresFunc: func() ([]*types.JsonStructure, error) { return []*types.JsonStructure{js}, nil }, } ctx, buf := newMockCtx(t, withBackend(mb), withFormat(FormatJSON), withHierarchy(h)) diff --git a/mdl/executor/cmd_jsonstructures.go b/mdl/executor/cmd_jsonstructures.go index 57a0d9d3..5c908cda 100644 --- a/mdl/executor/cmd_jsonstructures.go +++ b/mdl/executor/cmd_jsonstructures.go @@ -11,7 +11,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" ) // showJsonStructures handles SHOW JSON STRUCTURES [IN module]. @@ -100,7 +100,7 @@ func describeJsonStructure(ctx *ExecContext, name ast.QualifiedName) error { } if js.JsonSnippet != "" { - snippet := mpr.PrettyPrintJSON(js.JsonSnippet) + snippet := types.PrettyPrintJSON(js.JsonSnippet) if strings.Contains(snippet, "'") || strings.Contains(snippet, "\n") { fmt.Fprintf(ctx.Output, "\n SNIPPET $$%s$$", snippet) } else { @@ -135,7 +135,7 @@ func describeJsonStructure(ctx *ExecContext, name ast.QualifiedName) error { // collectCustomNameMappings walks the element tree and returns JSON key → ExposedName // mappings where the ExposedName differs from the auto-generated default (capitalizeFirst). -func collectCustomNameMappings(elements []*mpr.JsonElement) map[string]string { +func collectCustomNameMappings(elements []*types.JsonElement) map[string]string { mappings := make(map[string]string) for _, elem := range elements { collectCustomNames(elem, mappings) @@ -143,7 +143,7 @@ func collectCustomNameMappings(elements []*mpr.JsonElement) map[string]string { return mappings } -func collectCustomNames(elem *mpr.JsonElement, mappings map[string]string) { +func collectCustomNames(elem *types.JsonElement, mappings map[string]string) { // Extract the JSON key from the last segment of the Path. // Path format: "(Object)|fieldName" or "(Object)|parent|(Object)|child" if parts := strings.Split(elem.Path, "|"); len(parts) > 1 { @@ -207,7 +207,7 @@ func execCreateJsonStructure(ctx *ExecContext, s *ast.CreateJsonStructureStmt) e } // Build element tree from JSON snippet, applying custom name mappings - elements, err := mpr.BuildJsonElementsFromSnippet(s.JsonSnippet, s.CustomNameMap) + elements, err := types.BuildJsonElementsFromSnippet(s.JsonSnippet, s.CustomNameMap) if err != nil { return mdlerrors.NewBackend("build element tree", err) } @@ -217,11 +217,11 @@ func execCreateJsonStructure(ctx *ExecContext, s *ast.CreateJsonStructureStmt) e containerID = existing.ContainerID } - js := &mpr.JsonStructure{ + js := &types.JsonStructure{ ContainerID: containerID, Name: s.Name.Name, Documentation: s.Documentation, - JsonSnippet: mpr.PrettyPrintJSON(s.JsonSnippet), + JsonSnippet: types.PrettyPrintJSON(s.JsonSnippet), Elements: elements, } @@ -260,7 +260,7 @@ func execDropJsonStructure(ctx *ExecContext, s *ast.DropJsonStructureStmt) error } // findJsonStructure finds a JSON structure by module and name. -func findJsonStructure(ctx *ExecContext, moduleName, structName string) *mpr.JsonStructure { +func findJsonStructure(ctx *ExecContext, moduleName, structName string) *types.JsonStructure { structures, err := ctx.Backend.ListJsonStructures() if err != nil { return nil diff --git a/mdl/executor/cmd_jsonstructures_mock_test.go b/mdl/executor/cmd_jsonstructures_mock_test.go index 44409735..57896021 100644 --- a/mdl/executor/cmd_jsonstructures_mock_test.go +++ b/mdl/executor/cmd_jsonstructures_mock_test.go @@ -6,13 +6,13 @@ import ( "testing" "github.com/mendixlabs/mxcli/mdl/backend/mock" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" ) func TestShowJsonStructures_Mock(t *testing.T) { mod := mkModule("API") - js := &mpr.JsonStructure{ + js := &types.JsonStructure{ BaseElement: model.BaseElement{ID: nextID("js")}, ContainerID: mod.ID, Name: "OrderSchema", @@ -23,7 +23,7 @@ func TestShowJsonStructures_Mock(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - ListJsonStructuresFunc: func() ([]*mpr.JsonStructure, error) { return []*mpr.JsonStructure{js}, nil }, + ListJsonStructuresFunc: func() ([]*types.JsonStructure, error) { return []*types.JsonStructure{js}, nil }, } ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h)) diff --git a/mdl/executor/cmd_microflows_builder_actions.go b/mdl/executor/cmd_microflows_builder_actions.go index 633bf0eb..d60a9142 100644 --- a/mdl/executor/cmd_microflows_builder_actions.go +++ b/mdl/executor/cmd_microflows_builder_actions.go @@ -8,10 +8,10 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/sdk/mpr" ) // addCreateVariableAction creates a DECLARE statement as a CreateVariableAction. @@ -29,7 +29,7 @@ func (fb *flowBuilder) addCreateVariableAction(s *ast.DeclareStmt) model.ID { fb.declaredVars[s.Variable] = typeName action := µflows.CreateVariableAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, VariableName: s.Variable, DataType: convertASTToMicroflowDataType(declType, nil), InitialValue: fb.exprToString(s.InitialValue), @@ -38,7 +38,7 @@ func (fb *flowBuilder) addCreateVariableAction(s *ast.DeclareStmt) model.ID { activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -62,7 +62,7 @@ func (fb *flowBuilder) addChangeVariableAction(s *ast.MfSetStmt) model.ID { } action := µflows.ChangeVariableAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, VariableName: s.Target, Value: fb.exprToString(s.Value), } @@ -70,7 +70,7 @@ func (fb *flowBuilder) addChangeVariableAction(s *ast.MfSetStmt) model.ID { activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -87,7 +87,7 @@ func (fb *flowBuilder) addChangeVariableAction(s *ast.MfSetStmt) model.ID { // addCreateObjectAction creates a CREATE OBJECT statement. func (fb *flowBuilder) addCreateObjectAction(s *ast.CreateObjectStmt) model.ID { action := µflows.CreateObjectAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, OutputVariable: s.Variable, Commit: microflows.CommitTypeNo, } @@ -106,7 +106,7 @@ func (fb *flowBuilder) addCreateObjectAction(s *ast.CreateObjectStmt) model.ID { // Build InitialMembers for each SET assignment for _, change := range s.Changes { memberChange := µflows.MemberChange{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Type: microflows.MemberChangeTypeSet, Value: fb.memberExpressionToString(change.Value, entityQN, change.Attribute), } @@ -118,7 +118,7 @@ func (fb *flowBuilder) addCreateObjectAction(s *ast.CreateObjectStmt) model.ID { activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -144,7 +144,7 @@ func (fb *flowBuilder) addCreateObjectAction(s *ast.CreateObjectStmt) model.ID { // addCommitAction creates a COMMIT statement. func (fb *flowBuilder) addCommitAction(s *ast.MfCommitStmt) model.ID { action := µflows.CommitObjectsAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), CommitVariable: s.Variable, WithEvents: s.WithEvents, @@ -155,7 +155,7 @@ func (fb *flowBuilder) addCommitAction(s *ast.MfCommitStmt) model.ID { activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -180,7 +180,7 @@ func (fb *flowBuilder) addCommitAction(s *ast.MfCommitStmt) model.ID { // addDeleteAction creates a DELETE statement. func (fb *flowBuilder) addDeleteAction(s *ast.DeleteObjectStmt) model.ID { action := µflows.DeleteObjectAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, DeleteVariable: s.Variable, } @@ -188,7 +188,7 @@ func (fb *flowBuilder) addDeleteAction(s *ast.DeleteObjectStmt) model.ID { activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -214,7 +214,7 @@ func (fb *flowBuilder) addDeleteAction(s *ast.DeleteObjectStmt) model.ID { // addRollbackAction creates a ROLLBACK statement. func (fb *flowBuilder) addRollbackAction(s *ast.RollbackStmt) model.ID { action := µflows.RollbackObjectAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, RollbackVariable: s.Variable, RefreshInClient: s.RefreshInClient, } @@ -222,7 +222,7 @@ func (fb *flowBuilder) addRollbackAction(s *ast.RollbackStmt) model.ID { activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -240,7 +240,7 @@ func (fb *flowBuilder) addRollbackAction(s *ast.RollbackStmt) model.ID { // addChangeObjectAction creates a CHANGE statement. func (fb *flowBuilder) addChangeObjectAction(s *ast.ChangeObjectStmt) model.ID { action := µflows.ChangeObjectAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ChangeVariable: s.Variable, Commit: microflows.CommitTypeNo, RefreshInClient: false, @@ -255,7 +255,7 @@ func (fb *flowBuilder) addChangeObjectAction(s *ast.ChangeObjectStmt) model.ID { // Build MemberChange items for each SET assignment for _, change := range s.Changes { memberChange := µflows.MemberChange{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Type: microflows.MemberChangeTypeSet, Value: fb.memberExpressionToString(change.Value, entityQN, change.Attribute), } @@ -266,7 +266,7 @@ func (fb *flowBuilder) addChangeObjectAction(s *ast.ChangeObjectStmt) model.ID { activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -304,7 +304,7 @@ func (fb *flowBuilder) addRetrieveAction(s *ast.RetrieveStmt) model.ID { // Reverse traversal on Reference: child → parent (one-to-many) // Use DatabaseRetrieveSource with XPath to get a list of parent entities dbSource := µflows.DatabaseRetrieveSource{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, EntityQualifiedName: assocInfo.parentEntityQN, XPathConstraint: "[" + assocQN + " = $" + s.StartVariable + "]", } @@ -315,7 +315,7 @@ func (fb *flowBuilder) addRetrieveAction(s *ast.RetrieveStmt) model.ID { } else { // Forward traversal or ReferenceSet: use AssociationRetrieveSource source = µflows.AssociationRetrieveSource{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, StartVariable: s.StartVariable, AssociationQualifiedName: assocQN, } @@ -337,7 +337,7 @@ func (fb *flowBuilder) addRetrieveAction(s *ast.RetrieveStmt) model.ID { // Database retrieve: RETRIEVE $List FROM Module.Entity WHERE ... entityQN := s.Source.Module + "." + s.Source.Name dbSource := µflows.DatabaseRetrieveSource{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, EntityQualifiedName: entityQN, } @@ -349,7 +349,7 @@ func (fb *flowBuilder) addRetrieveAction(s *ast.RetrieveStmt) model.ID { rangeType = microflows.RangeTypeFirst } dbSource.Range = µflows.Range{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, RangeType: rangeType, Limit: s.Limit, Offset: s.Offset, @@ -389,7 +389,7 @@ func (fb *flowBuilder) addRetrieveAction(s *ast.RetrieveStmt) model.ID { } dbSource.Sorting = append(dbSource.Sorting, µflows.SortItem{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, AttributeQualifiedName: attrPath, Direction: direction, }) @@ -412,7 +412,7 @@ func (fb *flowBuilder) addRetrieveAction(s *ast.RetrieveStmt) model.ID { } action := µflows.RetrieveAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, OutputVariable: s.Variable, Source: source, } @@ -421,7 +421,7 @@ func (fb *flowBuilder) addRetrieveAction(s *ast.RetrieveStmt) model.ID { activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -451,23 +451,23 @@ func (fb *flowBuilder) addListOperationAction(s *ast.ListOperationStmt) model.ID switch s.Operation { case ast.ListOpHead: operation = µflows.HeadOperation{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ListVariable: s.InputVariable, } case ast.ListOpTail: operation = µflows.TailOperation{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ListVariable: s.InputVariable, } case ast.ListOpFind: operation = µflows.FindOperation{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ListVariable: s.InputVariable, Expression: fb.exprToString(s.Condition), } case ast.ListOpFilter: operation = µflows.FilterOperation{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ListVariable: s.InputVariable, Expression: fb.exprToString(s.Condition), } @@ -494,49 +494,49 @@ func (fb *flowBuilder) addListOperationAction(s *ast.ListOperationStmt) model.ID attrQN = entityType + "." + spec.Attribute } sortItems = append(sortItems, µflows.SortItem{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, AttributeQualifiedName: attrQN, Direction: direction, }) } operation = µflows.SortOperation{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ListVariable: s.InputVariable, Sorting: sortItems, } case ast.ListOpUnion: operation = µflows.UnionOperation{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ListVariable1: s.InputVariable, ListVariable2: s.SecondVariable, } case ast.ListOpIntersect: operation = µflows.IntersectOperation{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ListVariable1: s.InputVariable, ListVariable2: s.SecondVariable, } case ast.ListOpSubtract: operation = µflows.SubtractOperation{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ListVariable1: s.InputVariable, ListVariable2: s.SecondVariable, } case ast.ListOpContains: operation = µflows.ContainsOperation{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ListVariable: s.InputVariable, ObjectVariable: s.SecondVariable, // The item to check } case ast.ListOpEquals: operation = µflows.EqualsOperation{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ListVariable1: s.InputVariable, ListVariable2: s.SecondVariable, } case ast.ListOpRange: rangeOp := µflows.ListRangeOperation{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ListVariable: s.InputVariable, } if s.OffsetExpr != nil { @@ -551,7 +551,7 @@ func (fb *flowBuilder) addListOperationAction(s *ast.ListOperationStmt) model.ID } action := µflows.ListOperationAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Operation: operation, OutputVariable: s.OutputVariable, } @@ -577,7 +577,7 @@ func (fb *flowBuilder) addListOperationAction(s *ast.ListOperationStmt) model.ID activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -610,7 +610,7 @@ func (fb *flowBuilder) addAggregateListAction(s *ast.AggregateListStmt) model.ID } action := µflows.AggregateListAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, InputVariable: s.InputVariable, OutputVariable: s.OutputVariable, Function: function, @@ -631,7 +631,7 @@ func (fb *flowBuilder) addAggregateListAction(s *ast.AggregateListStmt) model.ID activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -653,7 +653,7 @@ func (fb *flowBuilder) addCreateListAction(s *ast.CreateListStmt) model.ID { } action := µflows.CreateListAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, OutputVariable: s.Variable, EntityQualifiedName: entityQN, } @@ -666,7 +666,7 @@ func (fb *flowBuilder) addCreateListAction(s *ast.CreateListStmt) model.ID { activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -683,7 +683,7 @@ func (fb *flowBuilder) addCreateListAction(s *ast.CreateListStmt) model.ID { // addAddToListAction creates an ADD TO list statement. func (fb *flowBuilder) addAddToListAction(s *ast.AddToListStmt) model.ID { action := µflows.ChangeListAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Type: microflows.ChangeListTypeAdd, ChangeVariable: s.List, Value: "$" + s.Item, @@ -692,7 +692,7 @@ func (fb *flowBuilder) addAddToListAction(s *ast.AddToListStmt) model.ID { activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -709,7 +709,7 @@ func (fb *flowBuilder) addAddToListAction(s *ast.AddToListStmt) model.ID { // addRemoveFromListAction creates a REMOVE FROM list statement. func (fb *flowBuilder) addRemoveFromListAction(s *ast.RemoveFromListStmt) model.ID { action := µflows.ChangeListAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Type: microflows.ChangeListTypeRemove, ChangeVariable: s.List, Value: "$" + s.Item, @@ -718,7 +718,7 @@ func (fb *flowBuilder) addRemoveFromListAction(s *ast.RemoveFromListStmt) model. activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, diff --git a/mdl/executor/cmd_microflows_builder_annotations.go b/mdl/executor/cmd_microflows_builder_annotations.go index 668b9511..476863ee 100644 --- a/mdl/executor/cmd_microflows_builder_annotations.go +++ b/mdl/executor/cmd_microflows_builder_annotations.go @@ -7,7 +7,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" ) // getStatementAnnotations extracts the annotations field from any microflow statement. @@ -151,7 +151,7 @@ func (fb *flowBuilder) addEndEventWithReturn(s *ast.ReturnStmt) model.ID { endEvent := µflows.EndEvent{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: EventSize, Height: EventSize}, }, @@ -169,7 +169,7 @@ func (fb *flowBuilder) addEndEventWithReturn(s *ast.ReturnStmt) model.ID { func (fb *flowBuilder) addErrorEvent() model.ID { errorEvent := µflows.ErrorEvent{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: EventSize, Height: EventSize}, }, @@ -197,7 +197,7 @@ func (fb *flowBuilder) attachAnnotation(text string, activityID model.ID) { annotation := µflows.Annotation{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: actX, Y: actY - 100}, Size: model.Size{Width: 200, Height: 50}, }, @@ -206,7 +206,7 @@ func (fb *flowBuilder) attachAnnotation(text string, activityID model.ID) { fb.objects = append(fb.objects, annotation) fb.annotationFlows = append(fb.annotationFlows, µflows.AnnotationFlow{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, OriginID: annotation.ID, DestinationID: activityID, }) @@ -216,7 +216,7 @@ func (fb *flowBuilder) attachAnnotation(text string, activityID model.ID) { func (fb *flowBuilder) attachFreeAnnotation(text string) { annotation := µflows.Annotation{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY - 100}, Size: model.Size{Width: 200, Height: 50}, }, diff --git a/mdl/executor/cmd_microflows_builder_calls.go b/mdl/executor/cmd_microflows_builder_calls.go index 94a8db0a..a03b21e0 100644 --- a/mdl/executor/cmd_microflows_builder_calls.go +++ b/mdl/executor/cmd_microflows_builder_calls.go @@ -11,7 +11,7 @@ import ( "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/javaactions" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" ) // addLogMessageAction creates a LOG statement as a LogMessageAction. @@ -62,11 +62,11 @@ func (fb *flowBuilder) addLogMessageAction(s *ast.LogStmt) model.ID { } action := µflows.LogMessageAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, LogLevel: logLevel, LogNodeName: "'" + s.Node + "'", // Store as expression (e.g., 'TEST') MessageTemplate: &model.Text{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Translations: map[string]string{ "en_US": templateText, }, @@ -77,7 +77,7 @@ func (fb *flowBuilder) addLogMessageAction(s *ast.LogStmt) model.ID { activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -101,7 +101,7 @@ func (fb *flowBuilder) addCallMicroflowAction(s *ast.CallMicroflowStmt) model.ID // Parameter is the full qualified name: Module.Microflow.ParameterName paramQN := mfQN + "." + arg.Name mapping := µflows.MicroflowCallParameterMapping{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Parameter: paramQN, Argument: fb.exprToString(arg.Value), } @@ -110,13 +110,13 @@ func (fb *flowBuilder) addCallMicroflowAction(s *ast.CallMicroflowStmt) model.ID // Create nested MicroflowCall structure mfCall := µflows.MicroflowCall{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Microflow: mfQN, ParameterMappings: mappings, } action := µflows.MicroflowCallAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), MicroflowCall: mfCall, ResultVariableName: s.OutputVariable, @@ -127,7 +127,7 @@ func (fb *flowBuilder) addCallMicroflowAction(s *ast.CallMicroflowStmt) model.ID activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -190,20 +190,20 @@ func (fb *flowBuilder) addCallJavaActionAction(s *ast.CallJavaActionStmt) model. } } value = µflows.EntityTypeCodeActionParameterValue{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Entity: entityName, } } else { // Regular parameter: expression-based value valueExpr := fb.exprToString(arg.Value) value = µflows.BasicCodeActionParameterValue{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Argument: valueExpr, } } mapping := µflows.JavaActionParameterMapping{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Parameter: paramQN, Value: value, } @@ -211,7 +211,7 @@ func (fb *flowBuilder) addCallJavaActionAction(s *ast.CallJavaActionStmt) model. } action := µflows.JavaActionCallAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), JavaAction: actionQN, ParameterMappings: mappings, @@ -223,7 +223,7 @@ func (fb *flowBuilder) addCallJavaActionAction(s *ast.CallJavaActionStmt) model. activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -253,7 +253,7 @@ func (fb *flowBuilder) addCallExternalActionAction(s *ast.CallExternalActionStmt var mappings []*microflows.ExternalActionParameterMapping for _, arg := range s.Arguments { mapping := µflows.ExternalActionParameterMapping{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ParameterName: arg.Name, Argument: fb.exprToString(arg.Value), } @@ -261,7 +261,7 @@ func (fb *flowBuilder) addCallExternalActionAction(s *ast.CallExternalActionStmt } action := µflows.CallExternalAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), ConsumedODataService: serviceQN, Name: s.ActionName, @@ -274,7 +274,7 @@ func (fb *flowBuilder) addCallExternalActionAction(s *ast.CallExternalActionStmt activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -308,7 +308,7 @@ func (fb *flowBuilder) addShowPageAction(s *ast.ShowPageStmt) model.ID { // Parameter qualified name format: Module.Page.ParameterName paramQN := pageQN + "." + arg.ParamName mapping := µflows.PageParameterMapping{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Parameter: paramQN, Argument: fb.exprToString(arg.Value), } @@ -328,7 +328,7 @@ func (fb *flowBuilder) addShowPageAction(s *ast.ShowPageStmt) model.ID { // Create page settings pageSettings := µflows.PageSettings{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Location: location, ModalForm: s.ModalForm, } @@ -337,7 +337,7 @@ func (fb *flowBuilder) addShowPageAction(s *ast.ShowPageStmt) model.ID { // Use PageName (BY_NAME_REFERENCE) instead of PageID (BY_ID_REFERENCE) // The modern Mendix format uses FormSettings.Form as a qualified name string action := µflows.ShowPageAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, PageName: pageQN, // BY_NAME_REFERENCE - qualified name string PageSettings: pageSettings, PageParameterMappings: mappings, @@ -352,7 +352,7 @@ func (fb *flowBuilder) addShowPageAction(s *ast.ShowPageStmt) model.ID { if s.Title != "" { action.OverridePageTitle = &model.Text{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Texts$Text", }, Translations: map[string]string{"en_US": s.Title}, @@ -362,7 +362,7 @@ func (fb *flowBuilder) addShowPageAction(s *ast.ShowPageStmt) model.ID { activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -379,13 +379,13 @@ func (fb *flowBuilder) addShowPageAction(s *ast.ShowPageStmt) model.ID { // addShowHomePageAction creates a SHOW HOME PAGE statement. func (fb *flowBuilder) addShowHomePageAction(s *ast.ShowHomePageStmt) model.ID { action := µflows.ShowHomePageAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, } activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -420,7 +420,7 @@ func (fb *flowBuilder) addShowMessageAction(s *ast.ShowMessageStmt) model.ID { } template := &model.Text{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Translations: map[string]string{"en_US": templateText}, } @@ -430,7 +430,7 @@ func (fb *flowBuilder) addShowMessageAction(s *ast.ShowMessageStmt) model.ID { } action := µflows.ShowMessageAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Template: template, Type: msgType, TemplateParameters: templateParams, @@ -439,7 +439,7 @@ func (fb *flowBuilder) addShowMessageAction(s *ast.ShowMessageStmt) model.ID { activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -461,14 +461,14 @@ func (fb *flowBuilder) addClosePageAction(s *ast.ClosePageStmt) model.ID { } action := µflows.ClosePageAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, NumberOfPages: numPages, } activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -502,7 +502,7 @@ func (fb *flowBuilder) addValidationFeedbackAction(s *ast.ValidationFeedbackStmt // Create template with translations map (default language "en_US") template := &model.Text{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Translations: map[string]string{"en_US": templateText}, } @@ -547,7 +547,7 @@ func (fb *flowBuilder) addValidationFeedbackAction(s *ast.ValidationFeedbackStmt } action := µflows.ValidationFeedbackAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ObjectVariable: varName, AttributeName: attributeName, AssociationName: associationName, @@ -558,7 +558,7 @@ func (fb *flowBuilder) addValidationFeedbackAction(s *ast.ValidationFeedbackStmt activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -576,7 +576,7 @@ func (fb *flowBuilder) addValidationFeedbackAction(s *ast.ValidationFeedbackStmt func (fb *flowBuilder) addRestCallAction(s *ast.RestCallStmt) model.ID { // Build HTTP configuration httpConfig := µflows.HttpConfiguration{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, } // Set HTTP method @@ -610,7 +610,7 @@ func (fb *flowBuilder) addRestCallAction(s *ast.RestCallStmt) model.ID { // Set custom headers for _, header := range s.Headers { h := µflows.HttpHeader{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Name: header.Name, Value: fb.exprToString(header.Value), } @@ -642,7 +642,7 @@ func (fb *flowBuilder) addRestCallAction(s *ast.RestCallStmt) model.ID { templateParams = append(templateParams, fb.exprToString(param.Value)) } requestHandling = µflows.CustomRequestHandling{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Template: template, TemplateParams: templateParams, } @@ -650,21 +650,21 @@ func (fb *flowBuilder) addRestCallAction(s *ast.RestCallStmt) model.ID { // Export mapping mappingQN := s.Body.MappingName.Module + "." + s.Body.MappingName.Name requestHandling = µflows.MappingRequestHandling{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, MappingID: model.ID(mappingQN), // Use qualified name as ID for BY_NAME references ParameterVariable: s.Body.SourceVariable, } default: // No body requestHandling = µflows.CustomRequestHandling{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Template: "", } } } else { // Default: empty custom request handling requestHandling = µflows.CustomRequestHandling{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Template: "", } } @@ -674,11 +674,11 @@ func (fb *flowBuilder) addRestCallAction(s *ast.RestCallStmt) model.ID { switch s.Result.Type { case ast.RestResultString: resultHandling = µflows.ResultHandlingString{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, } case ast.RestResultResponse: resultHandling = µflows.ResultHandlingString{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, } // Note: For HttpResponse, we would need a different result type, but using String for now case ast.RestResultMapping: @@ -702,7 +702,7 @@ func (fb *flowBuilder) addRestCallAction(s *ast.RestCallStmt) model.ID { } } resultHandling = µflows.ResultHandlingMapping{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, MappingID: model.ID(mappingQN), ResultEntityID: model.ID(entityQN), ResultVariable: s.OutputVariable, @@ -710,11 +710,11 @@ func (fb *flowBuilder) addRestCallAction(s *ast.RestCallStmt) model.ID { } case ast.RestResultNone: resultHandling = µflows.ResultHandlingNone{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, } default: resultHandling = µflows.ResultHandlingString{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, } } @@ -727,7 +727,7 @@ func (fb *flowBuilder) addRestCallAction(s *ast.RestCallStmt) model.ID { } action := µflows.RestCallAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, HttpConfiguration: httpConfig, RequestHandling: requestHandling, ResultHandling: resultHandling, @@ -741,7 +741,7 @@ func (fb *flowBuilder) addRestCallAction(s *ast.RestCallStmt) model.ID { activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -783,7 +783,7 @@ func (fb *flowBuilder) addSendRestRequestAction(s *ast.SendRestRequestStmt) mode var outputVar *microflows.RestOutputVar if s.OutputVariable != "" { outputVar = µflows.RestOutputVar{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, VariableName: s.OutputVariable, } } @@ -794,7 +794,7 @@ func (fb *flowBuilder) addSendRestRequestAction(s *ast.SendRestRequestStmt) mode var bodyVar *microflows.RestBodyVar if s.BodyVariable != "" && shouldSetBodyVariable(opDef) { bodyVar = µflows.RestBodyVar{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, VariableName: s.BodyVariable, } } @@ -806,7 +806,7 @@ func (fb *flowBuilder) addSendRestRequestAction(s *ast.SendRestRequestStmt) mode // RestOperationCallAction does not support custom error handling (CE6035). // ON ERROR clauses in the MDL are silently ignored for this action type. action := µflows.RestOperationCallAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Operation: operationQN, OutputVariable: outputVar, BodyVariable: bodyVar, @@ -817,7 +817,7 @@ func (fb *flowBuilder) addSendRestRequestAction(s *ast.SendRestRequestStmt) mode activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -919,7 +919,7 @@ func (fb *flowBuilder) addExecuteDatabaseQueryAction(s *ast.ExecuteDatabaseQuery } action := µflows.ExecuteDatabaseQueryAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), OutputVariableName: s.OutputVariable, Query: s.QueryName, @@ -929,7 +929,7 @@ func (fb *flowBuilder) addExecuteDatabaseQueryAction(s *ast.ExecuteDatabaseQuery // Build parameter mappings from arguments for _, arg := range s.Arguments { pm := µflows.DatabaseQueryParameterMapping{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ParameterName: arg.Name, Value: fb.exprToString(arg.Value), } @@ -939,7 +939,7 @@ func (fb *flowBuilder) addExecuteDatabaseQueryAction(s *ast.ExecuteDatabaseQuery // Build connection parameter mappings (runtime connection override) for _, arg := range s.ConnectionArguments { cm := µflows.DatabaseConnectionParameterMapping{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ParameterName: arg.Name, Value: fb.exprToString(arg.Value), } @@ -950,7 +950,7 @@ func (fb *flowBuilder) addExecuteDatabaseQueryAction(s *ast.ExecuteDatabaseQuery activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -977,13 +977,13 @@ func (fb *flowBuilder) addImportFromMappingAction(s *ast.ImportFromMappingStmt) activityX := fb.posX action := µflows.ImportXmlAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), XmlDocumentVariable: s.SourceVariable, } resultHandling := µflows.ResultHandlingMapping{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, MappingID: model.ID(s.Mapping.String()), ResultVariable: s.OutputVariable, SingleObject: true, @@ -1013,7 +1013,7 @@ func (fb *flowBuilder) addImportFromMappingAction(s *ast.ImportFromMappingStmt) activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -1039,7 +1039,7 @@ func (fb *flowBuilder) addTransformJsonAction(s *ast.TransformJsonStmt) model.ID activityX := fb.posX action := µflows.TransformJsonAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), InputVariableName: s.InputVariable, OutputVariableName: s.OutputVariable, @@ -1049,7 +1049,7 @@ func (fb *flowBuilder) addTransformJsonAction(s *ast.TransformJsonStmt) model.ID activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -1074,11 +1074,11 @@ func (fb *flowBuilder) addExportToMappingAction(s *ast.ExportToMappingStmt) mode activityX := fb.posX action := µflows.ExportXmlAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), OutputVariable: s.OutputVariable, RequestHandling: µflows.MappingRequestHandling{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, MappingID: model.ID(s.Mapping.String()), ParameterVariable: s.SourceVariable, }, @@ -1087,7 +1087,7 @@ func (fb *flowBuilder) addExportToMappingAction(s *ast.ExportToMappingStmt) mode activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, diff --git a/mdl/executor/cmd_microflows_builder_control.go b/mdl/executor/cmd_microflows_builder_control.go index 6191bbd3..98edcf49 100644 --- a/mdl/executor/cmd_microflows_builder_control.go +++ b/mdl/executor/cmd_microflows_builder_control.go @@ -9,7 +9,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" ) // addIfStatement creates an IF/THEN/ELSE statement using ExclusiveSplit and ExclusiveMerge. @@ -45,13 +45,13 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID { // Create ExclusiveSplit with expression condition splitCondition := µflows.ExpressionSplitCondition{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Expression: fb.exprToString(s.Condition), } split := µflows.ExclusiveSplit{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: splitX, Y: centerY}, Size: model.Size{Width: SplitWidth, Height: SplitHeight}, }, @@ -80,7 +80,7 @@ func (fb *flowBuilder) addIfStatement(s *ast.IfStmt) model.ID { if needMerge { merge := µflows.ExclusiveMerge{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: mergeX, Y: centerY}, Size: model.Size{Width: MergeSize, Height: MergeSize}, }, @@ -312,17 +312,17 @@ func (fb *flowBuilder) addLoopStatement(s *ast.LoopStmt) model.ID { // Position is the CENTER point (RelativeMiddlePoint in Mendix) loop := µflows.LoopedActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX + loopWidth/2, Y: fb.posY}, Size: model.Size{Width: loopWidth, Height: loopHeight}, }, LoopSource: µflows.IterableList{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ListVariableName: s.ListVariable, VariableName: s.LoopVariable, }, ObjectCollection: µflows.MicroflowObjectCollection{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Objects: loopBuilder.objects, Flows: nil, // Internal flows go at top-level, not inside the loop's ObjectCollection }, @@ -384,16 +384,16 @@ func (fb *flowBuilder) addWhileStatement(s *ast.WhileStmt) model.ID { loop := µflows.LoopedActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX + loopWidth/2, Y: fb.posY}, Size: model.Size{Width: loopWidth, Height: loopHeight}, }, LoopSource: µflows.WhileLoopCondition{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, WhileExpression: whileExpr, }, ObjectCollection: µflows.MicroflowObjectCollection{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Objects: loopBuilder.objects, Flows: nil, }, diff --git a/mdl/executor/cmd_microflows_builder_flows.go b/mdl/executor/cmd_microflows_builder_flows.go index f0d554a1..78d96239 100644 --- a/mdl/executor/cmd_microflows_builder_flows.go +++ b/mdl/executor/cmd_microflows_builder_flows.go @@ -7,7 +7,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" ) // convertErrorHandlingType converts AST error handling type to SDK error handling type. @@ -33,7 +33,7 @@ func convertErrorHandlingType(eh *ast.ErrorHandlingClause) microflows.ErrorHandl // connecting from the bottom of the source activity to the left of the error handler. func newErrorHandlerFlow(originID, destinationID model.ID) *microflows.SequenceFlow { return µflows.SequenceFlow{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, OriginID: originID, DestinationID: destinationID, OriginConnectionIndex: AnchorBottom, @@ -113,7 +113,7 @@ func (fb *flowBuilder) handleErrorHandlerMerge(lastErrID model.ID, activityID mo // fall back to empty (works for void microflows). endEvent := µflows.EndEvent{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: errorY}, Size: model.Size{Width: EventSize, Height: EventSize}, }, @@ -126,7 +126,7 @@ func (fb *flowBuilder) handleErrorHandlerMerge(lastErrID model.ID, activityID mo // newHorizontalFlow creates a SequenceFlow with anchors for horizontal left-to-right connection func newHorizontalFlow(originID, destinationID model.ID) *microflows.SequenceFlow { return µflows.SequenceFlow{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, OriginID: originID, DestinationID: destinationID, OriginConnectionIndex: AnchorRight, // Connect from right side of origin @@ -138,7 +138,7 @@ func newHorizontalFlow(originID, destinationID model.ID) *microflows.SequenceFlo func newHorizontalFlowWithCase(originID, destinationID model.ID, caseValue string) *microflows.SequenceFlow { flow := newHorizontalFlow(originID, destinationID) flow.CaseValue = microflows.EnumerationCase{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Value: caseValue, // "true" or "false" as string } return flow @@ -148,13 +148,13 @@ func newHorizontalFlowWithCase(originID, destinationID model.ID, caseValue strin // Used when TRUE path goes below the main line func newDownwardFlowWithCase(originID, destinationID model.ID, caseValue string) *microflows.SequenceFlow { return µflows.SequenceFlow{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, OriginID: originID, DestinationID: destinationID, OriginConnectionIndex: AnchorBottom, // Connect from bottom of origin (going down) DestinationConnectionIndex: AnchorLeft, // Connect to left side of destination CaseValue: microflows.EnumerationCase{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Value: caseValue, // "true" or "false" as string }, } @@ -164,7 +164,7 @@ func newDownwardFlowWithCase(originID, destinationID model.ID, caseValue string) // Used when returning from a lower branch to merge func newUpwardFlow(originID, destinationID model.ID) *microflows.SequenceFlow { return µflows.SequenceFlow{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, OriginID: originID, DestinationID: destinationID, OriginConnectionIndex: AnchorRight, // Connect from right side of origin diff --git a/mdl/executor/cmd_microflows_builder_graph.go b/mdl/executor/cmd_microflows_builder_graph.go index 70ce119a..2b0737ba 100644 --- a/mdl/executor/cmd_microflows_builder_graph.go +++ b/mdl/executor/cmd_microflows_builder_graph.go @@ -7,7 +7,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" ) // buildFlowGraph converts AST statements to a Microflow flow graph. @@ -42,7 +42,7 @@ func (fb *flowBuilder) buildFlowGraph(stmts []ast.MicroflowStatement, returns *a // Create StartEvent - Position is the CENTER point (RelativeMiddlePoint in Mendix) startEvent := µflows.StartEvent{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: EventSize, Height: EventSize}, }, @@ -98,7 +98,7 @@ func (fb *flowBuilder) buildFlowGraph(stmts []ast.MicroflowStatement, returns *a fb.posY = fb.baseY // Ensure end event is on the happy path center line endEvent := µflows.EndEvent{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: EventSize, Height: EventSize}, }, @@ -116,7 +116,7 @@ func (fb *flowBuilder) buildFlowGraph(stmts []ast.MicroflowStatement, returns *a } return µflows.MicroflowObjectCollection{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Objects: fb.objects, Flows: fb.flows, AnnotationFlows: fb.annotationFlows, diff --git a/mdl/executor/cmd_microflows_builder_workflow.go b/mdl/executor/cmd_microflows_builder_workflow.go index a14d6ef9..aacf40e0 100644 --- a/mdl/executor/cmd_microflows_builder_workflow.go +++ b/mdl/executor/cmd_microflows_builder_workflow.go @@ -6,7 +6,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" ) // wrapAction wraps a MicroflowAction in an ActionActivity with standard positioning. @@ -15,7 +15,7 @@ func (fb *flowBuilder) wrapAction(action microflows.MicroflowAction, errorHandli activity := µflows.ActionActivity{ BaseActivity: microflows.BaseActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Position: model.Point{X: fb.posX, Y: fb.posY}, Size: model.Size{Width: ActivityWidth, Height: ActivityHeight}, }, @@ -46,7 +46,7 @@ func (fb *flowBuilder) addCallWorkflowAction(s *ast.CallWorkflowStmt) model.ID { } action := µflows.WorkflowCallAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), Workflow: wfQN, WorkflowContextVariable: ctxVar, @@ -58,7 +58,7 @@ func (fb *flowBuilder) addCallWorkflowAction(s *ast.CallWorkflowStmt) model.ID { func (fb *flowBuilder) addGetWorkflowDataAction(s *ast.GetWorkflowDataStmt) model.ID { action := µflows.GetWorkflowDataAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), OutputVariableName: s.OutputVariable, Workflow: s.Workflow.Module + "." + s.Workflow.Name, @@ -69,7 +69,7 @@ func (fb *flowBuilder) addGetWorkflowDataAction(s *ast.GetWorkflowDataStmt) mode func (fb *flowBuilder) addGetWorkflowsAction(s *ast.GetWorkflowsStmt) model.ID { action := µflows.GetWorkflowsAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), OutputVariableName: s.OutputVariable, WorkflowContextVariableName: s.WorkflowContextVariableName, @@ -79,7 +79,7 @@ func (fb *flowBuilder) addGetWorkflowsAction(s *ast.GetWorkflowsStmt) model.ID { func (fb *flowBuilder) addGetWorkflowActivityRecordsAction(s *ast.GetWorkflowActivityRecordsStmt) model.ID { action := µflows.GetWorkflowActivityRecordsAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), OutputVariableName: s.OutputVariable, WorkflowVariable: s.WorkflowVariable, @@ -96,39 +96,39 @@ func (fb *flowBuilder) addWorkflowOperationAction(s *ast.WorkflowOperationStmt) reason = fb.exprToString(s.Reason) } op = µflows.AbortOperation{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, Reason: reason, WorkflowVariable: s.WorkflowVariable, } case "CONTINUE": op = µflows.ContinueOperation{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, WorkflowVariable: s.WorkflowVariable, } case "PAUSE": op = µflows.PauseOperation{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, WorkflowVariable: s.WorkflowVariable, } case "RESTART": op = µflows.RestartOperation{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, WorkflowVariable: s.WorkflowVariable, } case "RETRY": op = µflows.RetryOperation{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, WorkflowVariable: s.WorkflowVariable, } case "UNPAUSE": op = µflows.UnpauseOperation{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, WorkflowVariable: s.WorkflowVariable, } } action := µflows.WorkflowOperationAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), Operation: op, } @@ -137,7 +137,7 @@ func (fb *flowBuilder) addWorkflowOperationAction(s *ast.WorkflowOperationStmt) func (fb *flowBuilder) addSetTaskOutcomeAction(s *ast.SetTaskOutcomeStmt) model.ID { action := µflows.SetTaskOutcomeAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), OutcomeValue: s.OutcomeValue, WorkflowTaskVariable: s.WorkflowTaskVariable, @@ -147,7 +147,7 @@ func (fb *flowBuilder) addSetTaskOutcomeAction(s *ast.SetTaskOutcomeStmt) model. func (fb *flowBuilder) addOpenUserTaskAction(s *ast.OpenUserTaskStmt) model.ID { action := µflows.OpenUserTaskAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), UserTaskVariable: s.UserTaskVariable, } @@ -156,7 +156,7 @@ func (fb *flowBuilder) addOpenUserTaskAction(s *ast.OpenUserTaskStmt) model.ID { func (fb *flowBuilder) addNotifyWorkflowAction(s *ast.NotifyWorkflowStmt) model.ID { action := µflows.NotifyWorkflowAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), OutputVariableName: s.OutputVariable, WorkflowVariable: s.WorkflowVariable, @@ -166,7 +166,7 @@ func (fb *flowBuilder) addNotifyWorkflowAction(s *ast.NotifyWorkflowStmt) model. func (fb *flowBuilder) addOpenWorkflowAction(s *ast.OpenWorkflowStmt) model.ID { action := µflows.OpenWorkflowAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), WorkflowVariable: s.WorkflowVariable, } @@ -175,7 +175,7 @@ func (fb *flowBuilder) addOpenWorkflowAction(s *ast.OpenWorkflowStmt) model.ID { func (fb *flowBuilder) addLockWorkflowAction(s *ast.LockWorkflowStmt) model.ID { action := µflows.LockWorkflowAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), PauseAllWorkflows: s.PauseAllWorkflows, WorkflowVariable: s.WorkflowVariable, @@ -185,7 +185,7 @@ func (fb *flowBuilder) addLockWorkflowAction(s *ast.LockWorkflowStmt) model.ID { func (fb *flowBuilder) addUnlockWorkflowAction(s *ast.UnlockWorkflowStmt) model.ID { action := µflows.UnlockWorkflowAction{ - BaseElement: model.BaseElement{ID: model.ID(mpr.GenerateID())}, + BaseElement: model.BaseElement{ID: model.ID(types.GenerateID())}, ErrorHandlingType: convertErrorHandlingType(s.ErrorHandling), ResumeAllPausedWorkflows: s.ResumeAllPausedWorkflows, WorkflowVariable: s.WorkflowVariable, diff --git a/mdl/executor/cmd_microflows_create.go b/mdl/executor/cmd_microflows_create.go index 350e701e..dedaa545 100644 --- a/mdl/executor/cmd_microflows_create.go +++ b/mdl/executor/cmd_microflows_create.go @@ -9,9 +9,9 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/sdk/mpr" ) // isBuiltinModuleEntity returns true for modules whose entities are defined @@ -73,7 +73,7 @@ func execCreateMicroflow(ctx *ExecContext, s *ast.CreateMicroflowStmt) error { } // For CREATE OR REPLACE/MODIFY, reuse the existing ID to preserve references - microflowID := model.ID(mpr.GenerateID()) + microflowID := model.ID(types.GenerateID()) if existingID != "" { microflowID = existingID // Keep the original folder unless a new folder is explicitly specified @@ -143,7 +143,7 @@ func execCreateMicroflow(ctx *ExecContext, s *ast.CreateMicroflowStmt) error { } param := µflows.MicroflowParameter{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), }, ContainerID: mf.ID, Name: p.Name, diff --git a/mdl/executor/cmd_misc_mock_test.go b/mdl/executor/cmd_misc_mock_test.go index 7ade8376..14dc5c68 100644 --- a/mdl/executor/cmd_misc_mock_test.go +++ b/mdl/executor/cmd_misc_mock_test.go @@ -6,14 +6,14 @@ import ( "testing" "github.com/mendixlabs/mxcli/mdl/backend/mock" - "github.com/mendixlabs/mxcli/sdk/mpr/version" + "github.com/mendixlabs/mxcli/mdl/types" ) func TestShowVersion_Mock(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - ProjectVersionFunc: func() *version.ProjectVersion { - return &version.ProjectVersion{ + ProjectVersionFunc: func() *types.ProjectVersion { + return &types.ProjectVersion{ ProductVersion: "10.18.0", BuildVersion: "10.18.0.12345", FormatVersion: 2, diff --git a/mdl/executor/cmd_modules_mock_test.go b/mdl/executor/cmd_modules_mock_test.go index 39119b1d..e6371b69 100644 --- a/mdl/executor/cmd_modules_mock_test.go +++ b/mdl/executor/cmd_modules_mock_test.go @@ -6,9 +6,9 @@ import ( "testing" "github.com/mendixlabs/mxcli/mdl/backend/mock" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" - "github.com/mendixlabs/mxcli/sdk/mpr" ) func TestShowModules_Mock(t *testing.T) { @@ -18,7 +18,7 @@ func TestShowModules_Mock(t *testing.T) { // showModules uses ListUnits to count documents per module. // Provide a unit belonging to mod1 so the count is non-zero. unitID := nextID("unit") - units := []*mpr.UnitInfo{{ID: unitID, ContainerID: mod1.ID}} + units := []*types.UnitInfo{{ID: unitID, ContainerID: mod1.ID}} // Need a hierarchy for getHierarchy — provide modules + units + folders h := mkHierarchy(mod1, mod2) @@ -31,7 +31,7 @@ func TestShowModules_Mock(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod1, mod2}, nil }, - ListUnitsFunc: func() ([]*mpr.UnitInfo, error) { return units, nil }, + ListUnitsFunc: func() ([]*types.UnitInfo, error) { return units, nil }, ListDomainModelsFunc: func() ([]*domainmodel.DomainModel, error) { return []*domainmodel.DomainModel{dm}, nil }, // All other list functions return nil (zero counts) via MockBackend defaults. } diff --git a/mdl/executor/cmd_navigation.go b/mdl/executor/cmd_navigation.go index 912db18f..6ed878dc 100644 --- a/mdl/executor/cmd_navigation.go +++ b/mdl/executor/cmd_navigation.go @@ -9,7 +9,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" ) // execAlterNavigation handles CREATE [OR REPLACE] NAVIGATION command. @@ -38,12 +38,12 @@ func execAlterNavigation(ctx *ExecContext, s *ast.AlterNavigationStmt) error { } // Convert AST types to writer spec - spec := mpr.NavigationProfileSpec{ + spec := types.NavigationProfileSpec{ HasMenu: s.HasMenuBlock, } for _, hp := range s.HomePages { - hpSpec := mpr.NavHomePageSpec{ + hpSpec := types.NavHomePageSpec{ IsPage: hp.IsPage, Target: hp.Target.String(), } @@ -73,8 +73,8 @@ func execAlterNavigation(ctx *ExecContext, s *ast.AlterNavigationStmt) error { } // convertMenuItemDef converts an AST NavMenuItemDef to a writer NavMenuItemSpec. -func convertMenuItemDef(def ast.NavMenuItemDef) mpr.NavMenuItemSpec { - spec := mpr.NavMenuItemSpec{ +func convertMenuItemDef(def ast.NavMenuItemDef) types.NavMenuItemSpec { + spec := types.NavMenuItemSpec{ Caption: def.Caption, } if def.Page != nil { @@ -90,7 +90,7 @@ func convertMenuItemDef(def ast.NavMenuItemDef) mpr.NavMenuItemSpec { } // profileNames returns a comma-separated list of profile names for error messages. -func profileNames(nav *mpr.NavigationDocument) string { +func profileNames(nav *types.NavigationDocument) string { names := make([]string, len(nav.Profiles)) for i, p := range nav.Profiles { names[i] = p.Name @@ -251,7 +251,7 @@ func describeNavigation(ctx *ExecContext, name ast.QualifiedName) error { } // outputNavigationProfile outputs a single profile in round-trippable CREATE OR REPLACE NAVIGATION format. -func outputNavigationProfile(ctx *ExecContext, p *mpr.NavigationProfile) { +func outputNavigationProfile(ctx *ExecContext, p *types.NavigationProfile) { fmt.Fprintf(ctx.Output, "-- NAVIGATION PROFILE: %s\n", p.Name) fmt.Fprintf(ctx.Output, "-- Kind: %s\n", p.Kind) if p.IsNative { @@ -312,7 +312,7 @@ func outputNavigationProfile(ctx *ExecContext, p *mpr.NavigationProfile) { } // countMenuItems counts the total number of menu items recursively. -func countMenuItems(items []*mpr.NavMenuItem) int { +func countMenuItems(items []*types.NavMenuItem) int { count := len(items) for _, item := range items { count += countMenuItems(item.Items) @@ -321,7 +321,7 @@ func countMenuItems(items []*mpr.NavMenuItem) int { } // printMenuTree prints a menu tree with indentation to an io.Writer. -func printMenuTree(w io.Writer, items []*mpr.NavMenuItem, depth int) { +func printMenuTree(w io.Writer, items []*types.NavMenuItem, depth int) { indent := strings.Repeat(" ", depth+1) for _, item := range items { target := menuItemTarget(item) @@ -333,7 +333,7 @@ func printMenuTree(w io.Writer, items []*mpr.NavMenuItem, depth int) { } // menuItemTarget returns a display string for a menu item's action target. -func menuItemTarget(item *mpr.NavMenuItem) string { +func menuItemTarget(item *types.NavMenuItem) string { if item.Page != "" { return " -> " + item.Page } @@ -344,7 +344,7 @@ func menuItemTarget(item *mpr.NavMenuItem) string { } // printMenuMDL prints menu items in MDL-style format. -func printMenuMDL(w io.Writer, items []*mpr.NavMenuItem, depth int) { +func printMenuMDL(w io.Writer, items []*types.NavMenuItem, depth int) { indent := strings.Repeat(" ", depth) for _, item := range items { if len(item.Items) > 0 { diff --git a/mdl/executor/cmd_navigation_mock_test.go b/mdl/executor/cmd_navigation_mock_test.go index 322e4adc..22feeec0 100644 --- a/mdl/executor/cmd_navigation_mock_test.go +++ b/mdl/executor/cmd_navigation_mock_test.go @@ -7,18 +7,18 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/backend/mock" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" ) func TestShowNavigation_Mock(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - GetNavigationFunc: func() (*mpr.NavigationDocument, error) { - return &mpr.NavigationDocument{ - Profiles: []*mpr.NavigationProfile{{ + GetNavigationFunc: func() (*types.NavigationDocument, error) { + return &types.NavigationDocument{ + Profiles: []*types.NavigationProfile{{ Name: "Responsive", Kind: "Responsive", - MenuItems: []*mpr.NavMenuItem{ + MenuItems: []*types.NavMenuItem{ {Caption: "Home"}, {Caption: "Admin"}, {Caption: "Settings"}, @@ -38,12 +38,12 @@ func TestShowNavigation_Mock(t *testing.T) { func TestShowNavigationMenu_Mock(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - GetNavigationFunc: func() (*mpr.NavigationDocument, error) { - return &mpr.NavigationDocument{ - Profiles: []*mpr.NavigationProfile{{ + GetNavigationFunc: func() (*types.NavigationDocument, error) { + return &types.NavigationDocument{ + Profiles: []*types.NavigationProfile{{ Name: "Responsive", Kind: "Responsive", - MenuItems: []*mpr.NavMenuItem{ + MenuItems: []*types.NavMenuItem{ {Caption: "Dashboard", Page: "MyModule.Dashboard"}, }, }}, @@ -58,12 +58,12 @@ func TestShowNavigationMenu_Mock(t *testing.T) { func TestShowNavigationHomes_Mock(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - GetNavigationFunc: func() (*mpr.NavigationDocument, error) { - return &mpr.NavigationDocument{ - Profiles: []*mpr.NavigationProfile{{ + GetNavigationFunc: func() (*types.NavigationDocument, error) { + return &types.NavigationDocument{ + Profiles: []*types.NavigationProfile{{ Name: "Responsive", Kind: "Responsive", - HomePage: &mpr.NavHomePage{Page: "MyModule.Home"}, + HomePage: &types.NavHomePage{Page: "MyModule.Home"}, }}, }, nil }, @@ -76,12 +76,12 @@ func TestShowNavigationHomes_Mock(t *testing.T) { func TestDescribeNavigation_Mock(t *testing.T) { mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - GetNavigationFunc: func() (*mpr.NavigationDocument, error) { - return &mpr.NavigationDocument{ - Profiles: []*mpr.NavigationProfile{{ + GetNavigationFunc: func() (*types.NavigationDocument, error) { + return &types.NavigationDocument{ + Profiles: []*types.NavigationProfile{{ Name: "Responsive", Kind: "Responsive", - HomePage: &mpr.NavHomePage{Page: "MyModule.Home"}, + HomePage: &types.NavHomePage{Page: "MyModule.Home"}, }}, }, nil }, diff --git a/mdl/executor/cmd_odata.go b/mdl/executor/cmd_odata.go index 69c22c78..384695cf 100644 --- a/mdl/executor/cmd_odata.go +++ b/mdl/executor/cmd_odata.go @@ -13,10 +13,10 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/sdk/mpr" ) // outputJavadoc writes a javadoc-style comment block. @@ -789,7 +789,7 @@ func execCreateExternalEntity(ctx *ExecContext, s *ast.CreateExternalEntityStmt) Name: a.Name, Type: convertDataType(a.Type), } - attr.ID = model.ID(mpr.GenerateID()) + attr.ID = model.ID(types.GenerateID()) attrs = append(attrs, attr) } @@ -837,7 +837,7 @@ func execCreateExternalEntity(ctx *ExecContext, s *ast.CreateExternalEntityStmt) Deletable: s.Deletable, Updatable: s.Updatable, } - newEntity.ID = model.ID(mpr.GenerateID()) + newEntity.ID = model.ID(types.GenerateID()) if err := ctx.Backend.CreateEntity(dm.ID, newEntity); err != nil { return mdlerrors.NewBackend("create external entity", err) @@ -1029,7 +1029,7 @@ func createODataClient(ctx *ExecContext, stmt *ast.CreateODataClientStmt) error fmt.Fprintf(ctx.Output, "Created OData client: %s.%s\n", stmt.Name.Module, stmt.Name.Name) if newSvc.Metadata != "" { // Parse to show summary - if doc, err := mpr.ParseEdmx(newSvc.Metadata); err == nil { + if doc, err := types.ParseEdmx(newSvc.Metadata); err == nil { entityCount := 0 actionCount := 0 for _, s := range doc.Schemas { diff --git a/mdl/executor/cmd_pages_builder.go b/mdl/executor/cmd_pages_builder.go index 3de745ba..ed74a533 100644 --- a/mdl/executor/cmd_pages_builder.go +++ b/mdl/executor/cmd_pages_builder.go @@ -10,6 +10,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/microflows" @@ -44,7 +45,7 @@ type pageBuilder struct { layoutsCache []*pages.Layout pagesCache []*pages.Page microflowsCache []*microflows.Microflow - foldersCache []*mpr.FolderInfo + foldersCache []*types.FolderInfo // Entity context for resolving short attribute names inside DataViews entityContext string // Qualified entity name (e.g., "Module.Entity") @@ -253,7 +254,7 @@ func (pb *pageBuilder) getMainPlaceholderRef(layoutName string) string { } // getFolders returns cached folders or loads them. -func (pb *pageBuilder) getFolders() ([]*mpr.FolderInfo, error) { +func (pb *pageBuilder) getFolders() ([]*types.FolderInfo, error) { if pb.foldersCache == nil { var err error pb.foldersCache, err = pb.reader.ListFolders() @@ -286,7 +287,7 @@ func (pb *pageBuilder) resolveFolder(folderPath string) (model.ID, error) { } // Find folder with this name under current container - var foundFolder *mpr.FolderInfo + var foundFolder *types.FolderInfo for _, f := range folders { if f.ContainerID == currentContainerID && f.Name == part { foundFolder = f @@ -305,7 +306,7 @@ func (pb *pageBuilder) resolveFolder(folderPath string) (model.ID, error) { currentContainerID = newFolderID // Add to cache - pb.foldersCache = append(pb.foldersCache, &mpr.FolderInfo{ + pb.foldersCache = append(pb.foldersCache, &types.FolderInfo{ ID: newFolderID, ContainerID: currentContainerID, Name: part, diff --git a/mdl/executor/cmd_pages_builder_v3.go b/mdl/executor/cmd_pages_builder_v3.go index a5838ac9..e4c0a8e7 100644 --- a/mdl/executor/cmd_pages_builder_v3.go +++ b/mdl/executor/cmd_pages_builder_v3.go @@ -11,7 +11,7 @@ import ( "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/pages" ) @@ -33,7 +33,7 @@ func (pb *pageBuilder) buildPageV3(s *ast.CreatePageStmtV3) (*pages.Page, error) page := &pages.Page{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$Page", }, ContainerID: containerID, @@ -48,7 +48,7 @@ func (pb *pageBuilder) buildPageV3(s *ast.CreatePageStmtV3) (*pages.Page, error) if s.Title != "" { page.Title = &model.Text{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Texts$Text", }, Translations: map[string]string{"en_US": s.Title}, @@ -67,7 +67,7 @@ func (pb *pageBuilder) buildPageV3(s *ast.CreatePageStmtV3) (*pages.Page, error) // Create LayoutCall with arguments for placeholders page.LayoutCall = &pages.LayoutCall{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$LayoutCall", }, LayoutID: layoutID, @@ -80,7 +80,7 @@ func (pb *pageBuilder) buildPageV3(s *ast.CreatePageStmtV3) (*pages.Page, error) for _, param := range s.Parameters { pageParam := &pages.PageParameter{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$PageParameter", }, ContainerID: page.ID, @@ -112,7 +112,7 @@ func (pb *pageBuilder) buildPageV3(s *ast.CreatePageStmtV3) (*pages.Page, error) for _, v := range s.Variables { localVar := &pages.LocalVariable{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$LocalVariable", }, ContainerID: page.ID, @@ -129,7 +129,7 @@ func (pb *pageBuilder) buildPageV3(s *ast.CreatePageStmtV3) (*pages.Page, error) arg := &pages.LayoutCallArgument{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$FormCallArgument", }, ParameterID: model.ID(mainPlaceholderRef), @@ -140,7 +140,7 @@ func (pb *pageBuilder) buildPageV3(s *ast.CreatePageStmtV3) (*pages.Page, error) containerWidget := &pages.Container{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$DivContainer", }, Name: "conditionalVisibilityWidget1", @@ -182,7 +182,7 @@ func (pb *pageBuilder) buildSnippetV3(s *ast.CreateSnippetStmtV3) (*pages.Snippe snippet := &pages.Snippet{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$Snippet", }, ContainerID: containerID, @@ -194,7 +194,7 @@ func (pb *pageBuilder) buildSnippetV3(s *ast.CreateSnippetStmtV3) (*pages.Snippe for _, param := range s.Parameters { snippetParam := &pages.SnippetParameter{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$SnippetParameter", }, ContainerID: snippet.ID, @@ -221,7 +221,7 @@ func (pb *pageBuilder) buildSnippetV3(s *ast.CreateSnippetStmtV3) (*pages.Snippe for _, v := range s.Variables { localVar := &pages.LocalVariable{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$LocalVariable", }, ContainerID: snippet.ID, @@ -382,7 +382,7 @@ func applyConditionalSettings(widget pages.Widget, w *ast.WidgetV3) { if visibleIf := w.GetStringProp("VisibleIf"); visibleIf != "" { bw.ConditionalVisibility = &pages.ConditionalVisibilitySettings{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$ConditionalVisibilitySettings", }, Expression: visibleIf, @@ -392,7 +392,7 @@ func applyConditionalSettings(widget pages.Widget, w *ast.WidgetV3) { if editableIf := w.GetStringProp("EditableIf"); editableIf != "" { bw.ConditionalEditability = &pages.ConditionalEditabilitySettings{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$ConditionalEditabilitySettings", }, Expression: editableIf, @@ -496,7 +496,7 @@ func (pb *pageBuilder) buildDataSourceV3(ds *ast.DataSourceV3) (pages.DataSource // Use DataViewSource with IsSnippetParameter flag return &pages.DataViewSource{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$DataViewSource", }, EntityID: entityID, @@ -517,7 +517,7 @@ func (pb *pageBuilder) buildDataSourceV3(ds *ast.DataSourceV3) (pages.DataSource dbSource := &pages.DatabaseSource{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$DatabaseSource", // Note: actual BSON $Type depends on widget context (grid/listview/dataview) }, EntityID: entityID, @@ -537,7 +537,7 @@ func (pb *pageBuilder) buildDataSourceV3(ds *ast.DataSourceV3) (pages.DataSource } sortItem := &pages.GridSort{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$GridSort", }, AttributePath: pb.resolveAttributePathForEntity(ob.Attribute, ds.Reference), @@ -560,7 +560,7 @@ func (pb *pageBuilder) buildDataSourceV3(ds *ast.DataSourceV3) (pages.DataSource return &pages.MicroflowSource{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$MicroflowSource", }, MicroflowID: mfID, @@ -579,7 +579,7 @@ func (pb *pageBuilder) buildDataSourceV3(ds *ast.DataSourceV3) (pages.DataSource return &pages.NanoflowSource{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$NanoflowSource", }, NanoflowID: nfID, @@ -610,7 +610,7 @@ func (pb *pageBuilder) buildDataSourceV3(ds *ast.DataSourceV3) (pages.DataSource // widget can resolve short attribute names against it. return &pages.AssociationSource{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$AssociationSource", }, EntityPath: path + "/" + destEntity, @@ -630,7 +630,7 @@ func (pb *pageBuilder) buildDataSourceV3(ds *ast.DataSourceV3) (pages.DataSource return &pages.ListenToWidgetSource{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$ListenTargetSource", }, WidgetID: widgetID, @@ -842,7 +842,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc case "save": return &pages.SaveChangesClientAction{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$SaveChangesClientAction", }, ClosePage: action.ClosePage, @@ -851,7 +851,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc case "cancel": return &pages.CancelChangesClientAction{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$CancelChangesClientAction", }, ClosePage: action.ClosePage, @@ -860,7 +860,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc case "close": return &pages.ClosePageClientAction{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$ClosePageClientAction", }, }, nil @@ -868,7 +868,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc case "delete": return &pages.DeleteClientAction{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$DeleteClientAction", }, }, nil @@ -884,7 +884,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc createAct := &pages.CreateObjectClientAction{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$CreateObjectClientAction", }, EntityID: entityID, @@ -911,7 +911,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc pageAction := &pages.PageClientAction{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$PageClientAction", }, PageName: action.Target, @@ -921,7 +921,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc for _, arg := range action.Args { mapping := &pages.PageClientParameterMapping{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$PageParameterMapping", }, ParameterName: arg.Name, @@ -950,7 +950,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc mfAction := &pages.MicroflowClientAction{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$MicroflowAction", }, MicroflowID: mfID, @@ -961,7 +961,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc for _, arg := range action.Args { mapping := &pages.MicroflowParameterMapping{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$MicroflowParameterMapping", }, ParameterName: arg.Name, @@ -990,7 +990,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc nfAction := &pages.NanoflowClientAction{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$NanoflowAction", }, NanoflowID: nfID, @@ -1001,7 +1001,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc for _, arg := range action.Args { mapping := &pages.NanoflowParameterMapping{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$NanoflowParameterMapping", }, ParameterName: arg.Name, @@ -1025,7 +1025,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc case "openLink": return &pages.LinkClientAction{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$LinkClientAction", }, LinkType: pages.LinkTypeWeb, @@ -1035,7 +1035,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc case "signOut": return &pages.SignOutClientAction{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$SignOutClientAction", }, }, nil @@ -1043,7 +1043,7 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc case "completeTask": return &pages.SetTaskOutcomeClientAction{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$SetTaskOutcomeClientAction", }, ClosePage: true, diff --git a/mdl/executor/cmd_pages_builder_v3_layout.go b/mdl/executor/cmd_pages_builder_v3_layout.go index cd26f1b3..a7dca672 100644 --- a/mdl/executor/cmd_pages_builder_v3_layout.go +++ b/mdl/executor/cmd_pages_builder_v3_layout.go @@ -6,7 +6,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/pages" "github.com/mendixlabs/mxcli/mdl/ast" @@ -16,7 +16,7 @@ func (pb *pageBuilder) buildLayoutGridV3(w *ast.WidgetV3) (*pages.LayoutGrid, er lg := &pages.LayoutGrid{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$LayoutGrid", }, Name: w.Name, @@ -40,7 +40,7 @@ func (pb *pageBuilder) buildLayoutGridV3(w *ast.WidgetV3) (*pages.LayoutGrid, er func (pb *pageBuilder) buildLayoutGridRowV3(w *ast.WidgetV3) (*pages.LayoutGridRow, error) { row := &pages.LayoutGridRow{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$LayoutGridRow", }, } @@ -62,7 +62,7 @@ func (pb *pageBuilder) buildLayoutGridRowV3(w *ast.WidgetV3) (*pages.LayoutGridR func (pb *pageBuilder) buildLayoutGridColumnV3(w *ast.WidgetV3) (*pages.LayoutGridColumn, error) { col := &pages.LayoutGridColumn{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$LayoutGridColumn", }, Weight: 1, @@ -121,7 +121,7 @@ func (pb *pageBuilder) buildContainerWithRowV3(w *ast.WidgetV3) (*pages.Containe container := &pages.Container{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$DivContainer", }, Name: w.Name, @@ -131,7 +131,7 @@ func (pb *pageBuilder) buildContainerWithRowV3(w *ast.WidgetV3) (*pages.Containe lg := &pages.LayoutGrid{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$LayoutGrid", }, Name: w.Name + "_grid", @@ -153,7 +153,7 @@ func (pb *pageBuilder) buildContainerWithColumnV3(w *ast.WidgetV3) (*pages.Conta container := &pages.Container{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$DivContainer", }, Name: w.Name, @@ -163,7 +163,7 @@ func (pb *pageBuilder) buildContainerWithColumnV3(w *ast.WidgetV3) (*pages.Conta lg := &pages.LayoutGrid{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$LayoutGrid", }, Name: w.Name + "_grid", @@ -172,7 +172,7 @@ func (pb *pageBuilder) buildContainerWithColumnV3(w *ast.WidgetV3) (*pages.Conta row := &pages.LayoutGridRow{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$LayoutGridRow", }, } @@ -192,7 +192,7 @@ func (pb *pageBuilder) buildContainerV3(w *ast.WidgetV3) (*pages.Container, erro container := &pages.Container{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$DivContainer", }, Name: w.Name, @@ -220,7 +220,7 @@ func (pb *pageBuilder) buildTabContainerV3(w *ast.WidgetV3) (*pages.TabContainer tc := &pages.TabContainer{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$TabControl", }, Name: w.Name, @@ -248,7 +248,7 @@ func (pb *pageBuilder) buildTabContainerV3(w *ast.WidgetV3) (*pages.TabContainer func (pb *pageBuilder) buildTabPageV3(w *ast.WidgetV3) (*pages.TabPage, error) { tp := &pages.TabPage{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$TabPage", }, Name: w.Name, @@ -258,7 +258,7 @@ func (pb *pageBuilder) buildTabPageV3(w *ast.WidgetV3) (*pages.TabPage, error) { if caption := w.GetCaption(); caption != "" { tp.Caption = &model.Text{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Texts$Text", }, Translations: map[string]string{"en_US": caption}, @@ -285,7 +285,7 @@ func (pb *pageBuilder) buildGroupBoxV3(w *ast.WidgetV3) (*pages.GroupBox, error) gb := &pages.GroupBox{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$GroupBox", }, Name: w.Name, @@ -299,7 +299,7 @@ func (pb *pageBuilder) buildGroupBoxV3(w *ast.WidgetV3) (*pages.GroupBox, error) gb.Caption = &pages.ClientTemplate{ Template: &model.Text{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Texts$Text", }, Translations: map[string]string{"en_US": caption}, @@ -347,7 +347,7 @@ func (pb *pageBuilder) buildFooterV3(w *ast.WidgetV3) (*pages.Container, error) footer := &pages.Container{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$DivContainer", }, Name: w.Name, @@ -375,7 +375,7 @@ func (pb *pageBuilder) buildHeaderV3(w *ast.WidgetV3) (*pages.Container, error) header := &pages.Container{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$DivContainer", }, Name: w.Name, @@ -403,7 +403,7 @@ func (pb *pageBuilder) buildControlBarV3(w *ast.WidgetV3) (*pages.Container, err controlBar := &pages.Container{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$DivContainer", }, Name: w.Name, diff --git a/mdl/executor/cmd_pages_builder_v3_widgets.go b/mdl/executor/cmd_pages_builder_v3_widgets.go index 27d98581..52fa365c 100644 --- a/mdl/executor/cmd_pages_builder_v3_widgets.go +++ b/mdl/executor/cmd_pages_builder_v3_widgets.go @@ -11,8 +11,8 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/pages" "github.com/mendixlabs/mxcli/sdk/widgets" ) @@ -21,7 +21,7 @@ func (pb *pageBuilder) buildDataViewV3(w *ast.WidgetV3) (*pages.DataView, error) dv := &pages.DataView{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$DataView", }, Name: w.Name, @@ -91,10 +91,10 @@ func (pb *pageBuilder) buildDataViewV3(w *ast.WidgetV3) (*pages.DataView, error) func (pb *pageBuilder) buildDataGridV3(w *ast.WidgetV3) (*pages.CustomWidget, error) { // Build DataGrid2 as a CustomWidget (pluggable widget) like V2 does. // The built-in DataGrid (Forms$DataGrid) has serialization issues. - widgetID := model.ID(mpr.GenerateID()) + widgetID := model.ID(types.GenerateID()) // Load embedded template (required for pluggable widgets to work) - embeddedType, embeddedObject, embeddedIDs, embeddedObjectTypeID, err := widgets.GetTemplateFullBSON(pages.WidgetIDDataGrid2, mpr.GenerateID, pb.reader.Path()) + embeddedType, embeddedObject, embeddedIDs, embeddedObjectTypeID, err := widgets.GetTemplateFullBSON(pages.WidgetIDDataGrid2, types.GenerateID, pb.reader.Path()) if err != nil { return nil, mdlerrors.NewBackend("load DataGrid2 template", err) } @@ -198,7 +198,7 @@ func (pb *pageBuilder) buildDataGridV3(w *ast.WidgetV3) (*pages.CustomWidget, er func (pb *pageBuilder) buildDataGridColumnV3(w *ast.WidgetV3) (*pages.DataGridColumn, error) { col := &pages.DataGridColumn{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$DataGridColumn", }, Name: w.Name, @@ -214,7 +214,7 @@ func (pb *pageBuilder) buildDataGridColumnV3(w *ast.WidgetV3) (*pages.DataGridCo if caption := w.GetCaption(); caption != "" { col.Caption = &model.Text{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Texts$Text", }, Translations: map[string]string{"en_US": caption}, @@ -228,7 +228,7 @@ func (pb *pageBuilder) buildListViewV3(w *ast.WidgetV3) (*pages.ListView, error) lv := &pages.ListView{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$ListView", }, Name: w.Name, @@ -276,7 +276,7 @@ func (pb *pageBuilder) buildTextBoxV3(w *ast.WidgetV3) (*pages.TextBox, error) { tb := &pages.TextBox{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$TextBox", }, Name: w.Name, @@ -304,7 +304,7 @@ func (pb *pageBuilder) buildTextAreaV3(w *ast.WidgetV3) (*pages.TextArea, error) ta := &pages.TextArea{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$TextArea", }, Name: w.Name, @@ -332,7 +332,7 @@ func (pb *pageBuilder) buildDatePickerV3(w *ast.WidgetV3) (*pages.DatePicker, er dp := &pages.DatePicker{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$DatePicker", }, Name: w.Name, @@ -360,7 +360,7 @@ func (pb *pageBuilder) buildDropdownV3(w *ast.WidgetV3) (*pages.DropDown, error) dd := &pages.DropDown{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$DropDown", }, Name: w.Name, @@ -388,7 +388,7 @@ func (pb *pageBuilder) buildCheckBoxV3(w *ast.WidgetV3) (*pages.CheckBox, error) cb := &pages.CheckBox{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$CheckBox", }, Name: w.Name, @@ -417,7 +417,7 @@ func (pb *pageBuilder) buildRadioButtonsV3(w *ast.WidgetV3) (*pages.RadioButtons rb := &pages.RadioButtons{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$RadioButtonGroup", }, Name: w.Name, @@ -441,7 +441,7 @@ func (pb *pageBuilder) buildTextWidgetV3(w *ast.WidgetV3) (*pages.Text, error) { st := &pages.Text{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$Text", }, Name: w.Name, @@ -453,7 +453,7 @@ func (pb *pageBuilder) buildTextWidgetV3(w *ast.WidgetV3) (*pages.Text, error) { if content := w.GetContent(); content != "" { st.Caption = &model.Text{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Texts$Text", }, Translations: map[string]string{"en_US": content}, @@ -476,7 +476,7 @@ func (pb *pageBuilder) buildDynamicTextV3(w *ast.WidgetV3) (*pages.DynamicText, dt := &pages.DynamicText{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$DynamicText", }, Name: w.Name, @@ -524,12 +524,12 @@ func (pb *pageBuilder) buildDynamicTextV3(w *ast.WidgetV3) (*pages.DynamicText, dt.Content = &pages.ClientTemplate{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$ClientTemplate", }, Template: &model.Text{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Texts$Text", }, Translations: map[string]string{"en_US": content}, @@ -540,7 +540,7 @@ func (pb *pageBuilder) buildDynamicTextV3(w *ast.WidgetV3) (*pages.DynamicText, for _, attrRef := range autoGeneratedParams { param := &pages.ClientTemplateParameter{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$ClientTemplateParameter", }, } @@ -553,7 +553,7 @@ func (pb *pageBuilder) buildDynamicTextV3(w *ast.WidgetV3) (*pages.DynamicText, for _, p := range explicitParams { param := &pages.ClientTemplateParameter{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$ClientTemplateParameter", }, } @@ -585,7 +585,7 @@ func (pb *pageBuilder) buildTitleV3(w *ast.WidgetV3) (*pages.Title, error) { title := &pages.Title{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$Title", }, Name: w.Name, @@ -597,7 +597,7 @@ func (pb *pageBuilder) buildTitleV3(w *ast.WidgetV3) (*pages.Title, error) { if content != "" { title.Caption = &model.Text{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Texts$Text", }, Translations: map[string]string{"en_US": content}, @@ -615,7 +615,7 @@ func (pb *pageBuilder) buildButtonV3(w *ast.WidgetV3) (*pages.ActionButton, erro btn := &pages.ActionButton{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$ActionButton", }, Name: w.Name, @@ -627,12 +627,12 @@ func (pb *pageBuilder) buildButtonV3(w *ast.WidgetV3) (*pages.ActionButton, erro if caption := w.GetCaption(); caption != "" { btn.CaptionTemplate = &pages.ClientTemplate{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$ClientTemplate", }, Template: &model.Text{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Texts$Text", }, Translations: map[string]string{"en_US": caption}, @@ -644,7 +644,7 @@ func (pb *pageBuilder) buildButtonV3(w *ast.WidgetV3) (*pages.ActionButton, erro for _, p := range params { param := &pages.ClientTemplateParameter{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$ClientTemplateParameter", }, } @@ -692,7 +692,7 @@ func (pb *pageBuilder) buildNavigationListV3(w *ast.WidgetV3) (*pages.Navigation navList := &pages.NavigationList{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$NavigationList", }, Name: w.Name, @@ -725,7 +725,7 @@ func (pb *pageBuilder) buildNavigationListItemV3(w *ast.WidgetV3) (*pages.Naviga item := &pages.NavigationListItem{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$NavigationListItem", }, Name: w.Name, @@ -739,7 +739,7 @@ func (pb *pageBuilder) buildNavigationListItemV3(w *ast.WidgetV3) (*pages.Naviga if caption := w.GetCaption(); caption != "" { item.Caption = &model.Text{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Texts$Text", }, Translations: map[string]string{"en_US": caption}, @@ -772,7 +772,7 @@ func (pb *pageBuilder) buildSnippetCallV3(w *ast.WidgetV3) (*pages.SnippetCallWi sc := &pages.SnippetCallWidget{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$SnippetCallWidget", }, Name: w.Name, @@ -801,7 +801,7 @@ func (pb *pageBuilder) buildTemplateV3(w *ast.WidgetV3) (*pages.Container, error container := &pages.Container{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$DivContainer", }, Name: w.Name, @@ -825,7 +825,7 @@ func (pb *pageBuilder) buildFilterV3(w *ast.WidgetV3) (*pages.Container, error) container := &pages.Container{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$DivContainer", }, Name: w.Name, @@ -848,7 +848,7 @@ func (pb *pageBuilder) buildStaticImageV3(w *ast.WidgetV3) (*pages.StaticImage, img := &pages.StaticImage{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$StaticImageViewer", }, Name: w.Name, @@ -874,7 +874,7 @@ func (pb *pageBuilder) buildDynamicImageV3(w *ast.WidgetV3) (*pages.DynamicImage img := &pages.DynamicImage{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Forms$ImageViewer", }, Name: w.Name, diff --git a/mdl/executor/cmd_rename.go b/mdl/executor/cmd_rename.go index 05a96443..441a900d 100644 --- a/mdl/executor/cmd_rename.go +++ b/mdl/executor/cmd_rename.go @@ -9,7 +9,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" ) // execRename handles RENAME statements for all document types. @@ -347,7 +347,7 @@ func execRenameAssociation(ctx *ExecContext, s *ast.RenameStmt) error { } // printRenameReport outputs a dry-run report of what would change. -func printRenameReport(ctx *ExecContext, oldName, newName string, hits []mpr.RenameHit) { +func printRenameReport(ctx *ExecContext, oldName, newName string, hits []types.RenameHit) { fmt.Fprintf(ctx.Output, "Would rename: %s → %s\n", oldName, newName) fmt.Fprintf(ctx.Output, "References found: %d in %d document(s)\n", totalRefCount(hits), len(hits)) @@ -364,7 +364,7 @@ func printRenameReport(ctx *ExecContext, oldName, newName string, hits []mpr.Ren } } -func totalRefCount(hits []mpr.RenameHit) int { +func totalRefCount(hits []types.RenameHit) int { total := 0 for _, h := range hits { total += h.Count @@ -372,9 +372,9 @@ func totalRefCount(hits []mpr.RenameHit) int { return total } -func mergeHits(a, b []mpr.RenameHit) []mpr.RenameHit { +func mergeHits(a, b []types.RenameHit) []types.RenameHit { seen := make(map[string]int) // unitID → index in result - result := make([]mpr.RenameHit, len(a)) + result := make([]types.RenameHit, len(a)) copy(result, a) for i := range result { seen[result[i].UnitID] = i diff --git a/mdl/executor/cmd_security_write.go b/mdl/executor/cmd_security_write.go index ffe7cc0e..e97bf593 100644 --- a/mdl/executor/cmd_security_write.go +++ b/mdl/executor/cmd_security_write.go @@ -11,7 +11,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/backend" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/security" ) @@ -332,7 +332,7 @@ func execGrantEntityAccess(ctx *ExecContext, s *ast.GrantEntityAccessStmt) error // Build MemberAccess entries for all entity attributes and associations. // Mendix requires explicit MemberAccess entries for every member — an empty // MemberAccesses array triggers CE0066 "Entity access is out of date". - var memberAccesses []mpr.EntityMemberAccess + var memberAccesses []types.EntityMemberAccess // Build sets for specific member overrides (when READ (Name, Email) syntax is used) writeMemberSet := make(map[string]bool) @@ -357,7 +357,7 @@ func execGrantEntityAccess(ctx *ExecContext, s *ast.GrantEntityAccessStmt) error if isCalculated && (rights == "ReadWrite" || rights == "WriteOnly") { rights = "ReadOnly" } - memberAccesses = append(memberAccesses, mpr.EntityMemberAccess{ + memberAccesses = append(memberAccesses, types.EntityMemberAccess{ AttributeRef: module.Name + "." + s.Entity.Name + "." + attr.Name, AccessRights: rights, }) @@ -374,7 +374,7 @@ func execGrantEntityAccess(ctx *ExecContext, s *ast.GrantEntityAccessStmt) error } else if readMemberSet[assoc.Name] { rights = "ReadOnly" } - memberAccesses = append(memberAccesses, mpr.EntityMemberAccess{ + memberAccesses = append(memberAccesses, types.EntityMemberAccess{ AssociationRef: module.Name + "." + assoc.Name, AccessRights: rights, }) @@ -388,7 +388,7 @@ func execGrantEntityAccess(ctx *ExecContext, s *ast.GrantEntityAccessStmt) error } else if readMemberSet[ca.Name] { rights = "ReadOnly" } - memberAccesses = append(memberAccesses, mpr.EntityMemberAccess{ + memberAccesses = append(memberAccesses, types.EntityMemberAccess{ AssociationRef: module.Name + "." + ca.Name, AccessRights: rights, }) @@ -399,13 +399,13 @@ func execGrantEntityAccess(ctx *ExecContext, s *ast.GrantEntityAccessStmt) error // When an entity has HasOwner/HasChangedBy, Mendix implicitly adds // System.owner/System.changedBy associations that require MemberAccess. if entity.HasOwner { - memberAccesses = append(memberAccesses, mpr.EntityMemberAccess{ + memberAccesses = append(memberAccesses, types.EntityMemberAccess{ AssociationRef: "System.owner", AccessRights: defaultMemberAccess, }) } if entity.HasChangedBy { - memberAccesses = append(memberAccesses, mpr.EntityMemberAccess{ + memberAccesses = append(memberAccesses, types.EntityMemberAccess{ AssociationRef: "System.changedBy", AccessRights: defaultMemberAccess, }) @@ -470,7 +470,7 @@ func execRevokeEntityAccess(ctx *ExecContext, s *ast.RevokeEntityAccessStmt) err if len(s.Rights) > 0 { // Partial revoke — downgrade specific rights - revocation := mpr.EntityAccessRevocation{} + revocation := types.EntityAccessRevocation{} for _, right := range s.Rights { switch right.Type { case ast.EntityAccessCreate: diff --git a/mdl/executor/cmd_workflows_write.go b/mdl/executor/cmd_workflows_write.go index db298405..ce2d116d 100644 --- a/mdl/executor/cmd_workflows_write.go +++ b/mdl/executor/cmd_workflows_write.go @@ -11,7 +11,7 @@ import ( "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/mdl/types" "github.com/mendixlabs/mxcli/sdk/workflows" ) @@ -161,7 +161,7 @@ func execDropWorkflow(ctx *ExecContext, s *ast.DropWorkflowStmt) error { // generateWorkflowUUID generates a UUID for workflow elements. func generateWorkflowUUID() string { - return mpr.GenerateID() + return types.GenerateID() } // buildWorkflowActivities converts AST activity nodes to SDK workflow activities. @@ -334,7 +334,7 @@ func buildCallWorkflowActivity(n *ast.WorkflowCallWorkflowNode) *workflows.CallW Parameter: wfQN + "." + pm.Parameter, Expression: pm.Expression, } - mapping.BaseElement.ID = model.ID(mpr.GenerateID()) + mapping.BaseElement.ID = model.ID(types.GenerateID()) act.ParameterMappings = append(act.ParameterMappings, mapping) } @@ -575,7 +575,7 @@ func uniqueName(name string, nameCount map[string]int) string { func buildAnnotationActivity(n *ast.WorkflowAnnotationActivityNode) *workflows.WorkflowAnnotationActivity { a := &workflows.WorkflowAnnotationActivity{} - a.ID = model.ID(mpr.GenerateID()) + a.ID = model.ID(types.GenerateID()) a.Description = n.Text return a } @@ -702,7 +702,7 @@ func autoBindCallMicroflow(ctx *ExecContext, task *workflows.CallMicroflowTask) Parameter: paramQualifiedName, Expression: "$WorkflowContext", } - mapping.BaseElement.ID = model.ID(mpr.GenerateID()) + mapping.BaseElement.ID = model.ID(types.GenerateID()) task.ParameterMappings = append(task.ParameterMappings, mapping) } @@ -711,8 +711,8 @@ func autoBindCallMicroflow(ctx *ExecContext, task *workflows.CallMicroflowTask) outcome := &workflows.VoidConditionOutcome{ Flow: &workflows.Flow{}, } - outcome.BaseElement.ID = model.ID(mpr.GenerateID()) - outcome.Flow.BaseElement.ID = model.ID(mpr.GenerateID()) + outcome.BaseElement.ID = model.ID(types.GenerateID()) + outcome.Flow.BaseElement.ID = model.ID(types.GenerateID()) task.Outcomes = append(task.Outcomes, outcome) } break @@ -757,7 +757,7 @@ func autoBindCallWorkflow(ctx *ExecContext, act *workflows.CallWorkflowActivity) Parameter: paramName, Expression: "$WorkflowContext", } - mapping.BaseElement.ID = model.ID(mpr.GenerateID()) + mapping.BaseElement.ID = model.ID(types.GenerateID()) act.ParameterMappings = append(act.ParameterMappings, mapping) } break diff --git a/mdl/executor/cmd_write_handlers_mock_test.go b/mdl/executor/cmd_write_handlers_mock_test.go index 6d1130a9..5d442758 100644 --- a/mdl/executor/cmd_write_handlers_mock_test.go +++ b/mdl/executor/cmd_write_handlers_mock_test.go @@ -7,10 +7,10 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/backend/mock" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/pages" ) @@ -257,7 +257,7 @@ func TestExecDropAssociation_Mock(t *testing.T) { func TestExecDropJavaAction_Mock(t *testing.T) { mod := mkModule("MyModule") jaID := nextID("ja") - ja := &mpr.JavaAction{ + ja := &types.JavaAction{ BaseElement: model.BaseElement{ID: jaID}, ContainerID: mod.ID, Name: "MyAction", @@ -268,8 +268,8 @@ func TestExecDropJavaAction_Mock(t *testing.T) { called := false mb := &mock.MockBackend{ IsConnectedFunc: func() bool { return true }, - ListJavaActionsFunc: func() ([]*mpr.JavaAction, error) { - return []*mpr.JavaAction{ja}, nil + ListJavaActionsFunc: func() ([]*types.JavaAction, error) { + return []*types.JavaAction{ja}, nil }, DeleteJavaActionFunc: func(id model.ID) error { called = true @@ -291,7 +291,7 @@ func TestExecDropJavaAction_Mock(t *testing.T) { func TestExecDropFolder_Mock(t *testing.T) { mod := mkModule("MyModule") folderID := nextID("folder") - folder := &mpr.FolderInfo{ + folder := &types.FolderInfo{ ID: folderID, ContainerID: mod.ID, Name: "Resources", @@ -306,8 +306,8 @@ func TestExecDropFolder_Mock(t *testing.T) { ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, - ListFoldersFunc: func() ([]*mpr.FolderInfo, error) { - return []*mpr.FolderInfo{folder}, nil + ListFoldersFunc: func() ([]*types.FolderInfo, error) { + return []*types.FolderInfo{folder}, nil }, DeleteFolderFunc: func(id model.ID) error { called = true diff --git a/mdl/executor/executor.go b/mdl/executor/executor.go index b858fd2c..e68fa89a 100644 --- a/mdl/executor/executor.go +++ b/mdl/executor/executor.go @@ -14,6 +14,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/catalog" "github.com/mendixlabs/mxcli/mdl/diaglog" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/mpr" @@ -23,8 +24,8 @@ import ( // executorCache holds cached data for performance across multiple operations. type executorCache struct { modules []*model.Module - units []*mpr.UnitInfo - folders []*mpr.FolderInfo + units []*types.UnitInfo + folders []*types.FolderInfo domainModels []*domainmodel.DomainModel hierarchy *ContainerHierarchy // pages, layouts, microflows are cached separately as they may change during execution diff --git a/mdl/executor/helpers.go b/mdl/executor/helpers.go index d8e34102..87bf68fe 100644 --- a/mdl/executor/helpers.go +++ b/mdl/executor/helpers.go @@ -10,9 +10,9 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" - "github.com/mendixlabs/mxcli/sdk/mpr" ) // ---------------------------------------------------------------------------- @@ -118,7 +118,7 @@ func resolveFolder(ctx *ExecContext, moduleID model.ID, folderPath string) (mode } // Find folder with this name under current container - var foundFolder *mpr.FolderInfo + var foundFolder *types.FolderInfo for _, f := range folders { if f.ContainerID == currentContainerID && f.Name == part { foundFolder = f @@ -138,7 +138,7 @@ func resolveFolder(ctx *ExecContext, moduleID model.ID, folderPath string) (mode currentContainerID = newFolderID // Add to the list so subsequent lookups find it - folders = append(folders, &mpr.FolderInfo{ + folders = append(folders, &types.FolderInfo{ ID: newFolderID, ContainerID: parentID, Name: part, @@ -153,7 +153,7 @@ func resolveFolder(ctx *ExecContext, moduleID model.ID, folderPath string) (mode func createFolder(ctx *ExecContext, name string, containerID model.ID) (model.ID, error) { folder := &model.Folder{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Projects$Folder", }, ContainerID: containerID, diff --git a/mdl/executor/widget_engine.go b/mdl/executor/widget_engine.go index ad11effa..548fa01b 100644 --- a/mdl/executor/widget_engine.go +++ b/mdl/executor/widget_engine.go @@ -8,6 +8,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/pages" @@ -94,7 +95,7 @@ func (e *PluggableWidgetEngine) Build(def *WidgetDefinition, w *ast.WidgetV3) (* // 1. Load template embeddedType, embeddedObject, embeddedIDs, embeddedObjectTypeID, err := - widgets.GetTemplateFullBSON(def.WidgetID, mpr.GenerateID, e.pageBuilder.reader.Path()) + widgets.GetTemplateFullBSON(def.WidgetID, types.GenerateID, e.pageBuilder.reader.Path()) if err != nil { return nil, mdlerrors.NewBackend("load "+def.MDLName+" template", err) } @@ -328,7 +329,7 @@ func (e *PluggableWidgetEngine) Build(def *WidgetDefinition, w *ast.WidgetV3) (* updatedObject = ensureRequiredObjectLists(updatedObject, propertyTypeIDs) // 5. Build CustomWidget - widgetID := model.ID(mpr.GenerateID()) + widgetID := model.ID(types.GenerateID()) cw := &pages.CustomWidget{ BaseWidget: pages.BaseWidget{ BaseElement: model.BaseElement{ diff --git a/mdl/executor/widget_operations.go b/mdl/executor/widget_operations.go index 8d869633..6d183314 100644 --- a/mdl/executor/widget_operations.go +++ b/mdl/executor/widget_operations.go @@ -5,6 +5,7 @@ package executor import ( "log" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/pages" "go.mongodb.org/mongo-driver/bson" @@ -217,7 +218,7 @@ func updateTemplateText(tmpl bson.D, text string) bson.D { updated = append(updated, bson.E{Key: "Items", Value: bson.A{ int32(3), bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: mpr.IDToBsonBinary(types.GenerateID())}, {Key: "$Type", Value: "Texts$Translation"}, {Key: "LanguageCode", Value: "en_US"}, {Key: "Text", Value: text}, diff --git a/mdl/executor/widget_templates.go b/mdl/executor/widget_templates.go index d25459e9..d5d5e9c9 100644 --- a/mdl/executor/widget_templates.go +++ b/mdl/executor/widget_templates.go @@ -8,7 +8,7 @@ import ( "regexp" "strings" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" "go.mongodb.org/mongo-driver/bson" ) @@ -144,7 +144,7 @@ func createDefaultClientTemplateBSON(text string) bson.D { // generateBinaryID creates a new random 16-byte UUID in Microsoft GUID binary format. func generateBinaryID() []byte { - return hexIDToBlob(mpr.GenerateID()) + return hexIDToBlob(types.GenerateID()) } // hexIDToBlob converts a hex UUID string to a 16-byte binary blob in Microsoft GUID format. diff --git a/mdl/linter/rules/page_navigation_security.go b/mdl/linter/rules/page_navigation_security.go index a8e0f01d..33a73f56 100644 --- a/mdl/linter/rules/page_navigation_security.go +++ b/mdl/linter/rules/page_navigation_security.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/linter" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/mpr" ) @@ -122,7 +123,7 @@ func (r *PageNavigationSecurityRule) Check(ctx *linter.LintContext) []linter.Vio } // collectMenuPages recursively collects pages from navigation menu items. -func collectMenuPages(items []*mpr.NavMenuItem, profileName string, navPages map[string][]navUsage) { +func collectMenuPages(items []*types.NavMenuItem, profileName string, navPages map[string][]navUsage) { for _, item := range items { if item.Page != "" { navPages[item.Page] = append(navPages[item.Page], diff --git a/mdl/types/asyncapi.go b/mdl/types/asyncapi.go new file mode 100644 index 00000000..692eaae6 --- /dev/null +++ b/mdl/types/asyncapi.go @@ -0,0 +1,205 @@ +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "fmt" + "strings" + + "gopkg.in/yaml.v3" +) + +// AsyncAPIDocument represents a parsed AsyncAPI 2.x document. +type AsyncAPIDocument struct { + Version string // AsyncAPI version (e.g. "2.2.0") + Title string // Service title + DocVersion string // Document version + Description string + Channels []*AsyncAPIChannel // Resolved channels + Messages []*AsyncAPIMessage // Resolved messages (from components) +} + +// AsyncAPIChannel represents a channel in the AsyncAPI document. +type AsyncAPIChannel struct { + Name string // Channel ID/name + OperationType string // "subscribe" or "publish" + OperationID string // e.g. "receiveOrderChangedEventEvents" + MessageRef string // Resolved message name +} + +// AsyncAPIMessage represents a message type. +type AsyncAPIMessage struct { + Name string + Title string + Description string + ContentType string + Properties []*AsyncAPIProperty // Resolved from payload schema +} + +// AsyncAPIProperty represents a property in a message payload schema. +type AsyncAPIProperty struct { + Name string + Type string // "string", "integer", "number", "boolean", "array", "object" + Format string // "int64", "int32", "date-time", "uri-reference", etc. +} + +// ParseAsyncAPI parses an AsyncAPI YAML string into an AsyncAPIDocument. +func ParseAsyncAPI(yamlStr string) (*AsyncAPIDocument, error) { + if yamlStr == "" { + return nil, fmt.Errorf("empty AsyncAPI document") + } + + var raw yamlAsyncAPI + if err := yaml.Unmarshal([]byte(yamlStr), &raw); err != nil { + return nil, fmt.Errorf("failed to parse AsyncAPI YAML: %w", err) + } + + doc := &AsyncAPIDocument{ + Version: raw.AsyncAPI, + Title: raw.Info.Title, + DocVersion: raw.Info.Version, + Description: raw.Info.Description, + } + + // Resolve messages from components + for name, msg := range raw.Components.Messages { + resolved := &AsyncAPIMessage{ + Name: name, + Title: msg.Title, + Description: msg.Description, + ContentType: msg.ContentType, + } + + // Resolve payload schema (follow $ref if present) + schemaName := "" + if msg.Payload.Ref != "" { + schemaName = asyncRefName(msg.Payload.Ref) + } + + if schemaName != "" { + if schema, ok := raw.Components.Schemas[schemaName]; ok { + resolved.Properties = resolveAsyncSchemaProperties(schema) + } + } else if msg.Payload.Properties != nil { + // Inline schema + resolved.Properties = resolveAsyncSchemaProperties(msg.Payload) + } + + doc.Messages = append(doc.Messages, resolved) + } + + // Resolve channels + for channelName, channel := range raw.Channels { + if channel.Subscribe != nil { + msgName := "" + if channel.Subscribe.Message.Ref != "" { + msgName = asyncRefName(channel.Subscribe.Message.Ref) + } + doc.Channels = append(doc.Channels, &AsyncAPIChannel{ + Name: channelName, + OperationType: "subscribe", + OperationID: channel.Subscribe.OperationID, + MessageRef: msgName, + }) + } + if channel.Publish != nil { + msgName := "" + if channel.Publish.Message.Ref != "" { + msgName = asyncRefName(channel.Publish.Message.Ref) + } + doc.Channels = append(doc.Channels, &AsyncAPIChannel{ + Name: channelName, + OperationType: "publish", + OperationID: channel.Publish.OperationID, + MessageRef: msgName, + }) + } + } + + return doc, nil +} + +// FindMessage looks up a message by name. +func (d *AsyncAPIDocument) FindMessage(name string) *AsyncAPIMessage { + for _, m := range d.Messages { + if strings.EqualFold(m.Name, name) { + return m + } + } + return nil +} + +// asyncRefName extracts the last segment from a $ref like "#/components/messages/OrderChangedEvent". +func asyncRefName(ref string) string { + if idx := strings.LastIndex(ref, "/"); idx >= 0 { + return ref[idx+1:] + } + return ref +} + +func resolveAsyncSchemaProperties(schema yamlAsyncSchema) []*AsyncAPIProperty { + var props []*AsyncAPIProperty + for name, prop := range schema.Properties { + props = append(props, &AsyncAPIProperty{ + Name: name, + Type: prop.Type, + Format: prop.Format, + }) + } + return props +} + +// ============================================================================ +// YAML deserialization types (internal) +// ============================================================================ + +type yamlAsyncAPI struct { + AsyncAPI string `yaml:"asyncapi"` + Info yamlAsyncInfo `yaml:"info"` + Channels map[string]yamlAsyncChannel `yaml:"channels"` + Components yamlAsyncComponents `yaml:"components"` +} + +type yamlAsyncInfo struct { + Title string `yaml:"title"` + Version string `yaml:"version"` + Description string `yaml:"description"` +} + +type yamlAsyncChannel struct { + Subscribe *yamlAsyncOperation `yaml:"subscribe"` + Publish *yamlAsyncOperation `yaml:"publish"` +} + +type yamlAsyncOperation struct { + OperationID string `yaml:"operationId"` + Message yamlAsyncRef `yaml:"message"` +} + +type yamlAsyncRef struct { + Ref string `yaml:"$ref"` +} + +type yamlAsyncComponents struct { + Messages map[string]yamlAsyncMessage `yaml:"messages"` + Schemas map[string]yamlAsyncSchema `yaml:"schemas"` +} + +type yamlAsyncMessage struct { + Name string `yaml:"name"` + Title string `yaml:"title"` + Description string `yaml:"description"` + ContentType string `yaml:"contentType"` + Payload yamlAsyncSchema `yaml:"payload"` +} + +type yamlAsyncSchema struct { + Ref string `yaml:"$ref"` + Type string `yaml:"type"` + Properties map[string]yamlAsyncSchemaProperty `yaml:"properties"` +} + +type yamlAsyncSchemaProperty struct { + Type string `yaml:"type"` + Format string `yaml:"format"` +} diff --git a/mdl/types/doc.go b/mdl/types/doc.go new file mode 100644 index 00000000..fe45c6ad --- /dev/null +++ b/mdl/types/doc.go @@ -0,0 +1,7 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Package types defines shared value types used in backend interfaces. +// These types are decoupled from sdk/mpr to enable WASM compilation of +// the mdl/ subtree. Conversion functions between these types and their +// sdk/mpr counterparts live in mdl/backend/mpr/. +package types diff --git a/mdl/types/edmx.go b/mdl/types/edmx.go new file mode 100644 index 00000000..17367b86 --- /dev/null +++ b/mdl/types/edmx.go @@ -0,0 +1,541 @@ +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "encoding/xml" + "fmt" + "strings" +) + +// EdmxDocument represents a parsed OData $metadata document (EDMX/CSDL). +// Supports both OData v3 (CSDL 2.0/3.0) and OData v4 (CSDL 4.0). +type EdmxDocument struct { + Version string // "1.0" (OData3) or "4.0" (OData4) + Schemas []*EdmSchema // Schema definitions + EntitySets []*EdmEntitySet // Entity sets from EntityContainer + Actions []*EdmAction // OData4 actions / OData3 function imports +} + +// EdmSchema represents an EDM schema namespace. +type EdmSchema struct { + Namespace string + EntityTypes []*EdmEntityType + EnumTypes []*EdmEnumType +} + +// EdmEntityType represents an entity type definition. +type EdmEntityType struct { + Name string + BaseType string // Qualified name of base type (e.g. "Microsoft...PlanItem"), empty if none + IsAbstract bool // True if + IsOpen bool // True if + KeyProperties []string + Properties []*EdmProperty + NavigationProperties []*EdmNavigationProperty + Summary string + Description string +} + +// EdmProperty represents a property on an entity type. +type EdmProperty struct { + Name string + Type string // e.g. "Edm.String", "Edm.Int64" + Nullable *bool // nil = not specified (default true) + MaxLength string // e.g. "200", "max" + Scale string // e.g. "variable" + + // Capability annotations (OData Core V1). When true, the property is not + // settable by the client: + // Computed = server-computed, not settable on create or update. + // Immutable = settable on create, but not on update. + Computed bool + Immutable bool +} + +// EdmNavigationProperty represents a navigation property (association). +type EdmNavigationProperty struct { + Name string + Type string // OData4: "DefaultNamespace.Customer" or "Collection(DefaultNamespace.Part)" + Partner string // OData4 partner property name + TargetType string // Resolved target entity type name (without namespace/Collection) + IsMany bool // true if Collection() + ContainsTarget bool // true if + // OData3 fields (from Association) + Relationship string + FromRole string + ToRole string +} + +// EdmEntitySet represents an entity set in the entity container. +type EdmEntitySet struct { + Name string + EntityType string // Qualified name of entity type + + // Capabilities derived from Org.OData.Capabilities.V1 annotations. + // nil = not specified (treat as default true). + Insertable *bool // InsertRestrictions/Insertable + Updatable *bool // UpdateRestrictions/Updatable + Deletable *bool // DeleteRestrictions/Deletable + + // Navigation property names listed under + // Org.OData.Capabilities.V1.{Insert,Update}Restrictions/Non*NavigationProperties. + NonInsertableNavigationProperties []string + NonUpdatableNavigationProperties []string + + // Property names listed under + // Org.OData.Capabilities.V1.{Insert,Update}Restrictions/Non*Properties. + // Structural properties named here cannot be set on insert / update. + NonInsertableProperties []string + NonUpdatableProperties []string +} + +// EdmAction represents an OData4 action or OData3 function import. +type EdmAction struct { + Name string + IsBound bool + Parameters []*EdmActionParameter + ReturnType string +} + +// EdmActionParameter represents a parameter of an action. +type EdmActionParameter struct { + Name string + Type string + Nullable *bool +} + +// EdmEnumType represents an enumeration type. +type EdmEnumType struct { + Name string + Members []*EdmEnumMember +} + +// EdmEnumMember represents a member of an enum type. +type EdmEnumMember struct { + Name string + Value string +} + +// FindEntityType looks up an entity type by name (with or without namespace prefix). +func (d *EdmxDocument) FindEntityType(name string) *EdmEntityType { + // Strip namespace prefix if present + shortName := name + if idx := strings.LastIndex(name, "."); idx >= 0 { + shortName = name[idx+1:] + } + for _, s := range d.Schemas { + for _, et := range s.EntityTypes { + if et.Name == shortName { + return et + } + } + } + return nil +} + +// ParseEdmx parses an OData $metadata XML string into an EdmxDocument. +func ParseEdmx(metadataXML string) (*EdmxDocument, error) { + if metadataXML == "" { + return nil, fmt.Errorf("empty metadata XML") + } + + var edmx xmlEdmx + if err := xml.Unmarshal([]byte(metadataXML), &edmx); err != nil { + return nil, fmt.Errorf("failed to parse EDMX XML: %w", err) + } + + doc := &EdmxDocument{ + Version: edmx.Version, + } + + for _, ds := range edmx.DataServices { + for _, s := range ds.Schemas { + schema := &EdmSchema{ + Namespace: s.Namespace, + } + + // Parse entity types + for _, et := range s.EntityTypes { + entityType := parseXmlEntityType(&et) + schema.EntityTypes = append(schema.EntityTypes, entityType) + } + + // Parse enum types + for _, en := range s.EnumTypes { + enumType := &EdmEnumType{Name: en.Name} + for _, m := range en.Members { + enumType.Members = append(enumType.Members, &EdmEnumMember{ + Name: m.Name, + Value: m.Value, + }) + } + schema.EnumTypes = append(schema.EnumTypes, enumType) + } + + doc.Schemas = append(doc.Schemas, schema) + + // Parse entity container + for _, ec := range s.EntityContainers { + for _, es := range ec.EntitySets { + entitySet := &EdmEntitySet{ + Name: es.Name, + EntityType: es.EntityType, + } + applyCapabilityAnnotations(entitySet, es.Annotations) + doc.EntitySets = append(doc.EntitySets, entitySet) + } + + // OData3 function imports + for _, fi := range ec.FunctionImports { + action := &EdmAction{ + Name: fi.Name, + ReturnType: fi.ReturnType, + } + for _, p := range fi.Parameters { + action.Parameters = append(action.Parameters, &EdmActionParameter{ + Name: p.Name, + Type: p.Type, + }) + } + doc.Actions = append(doc.Actions, action) + } + } + + // OData4 actions + for _, a := range s.Actions { + action := &EdmAction{ + Name: a.Name, + IsBound: a.IsBound == "true", + } + if a.ReturnType != nil { + action.ReturnType = a.ReturnType.Type + } + for _, p := range a.Parameters { + param := &EdmActionParameter{ + Name: p.Name, + Type: p.Type, + } + if p.Nullable != "" { + v := p.Nullable == "true" + param.Nullable = &v + } + action.Parameters = append(action.Parameters, param) + } + doc.Actions = append(doc.Actions, action) + } + + // OData4 functions (treated same as actions for discovery) + for _, f := range s.Functions { + action := &EdmAction{ + Name: f.Name, + IsBound: f.IsBound == "true", + } + if f.ReturnType != nil { + action.ReturnType = f.ReturnType.Type + } + for _, p := range f.Parameters { + param := &EdmActionParameter{ + Name: p.Name, + Type: p.Type, + } + action.Parameters = append(action.Parameters, param) + } + doc.Actions = append(doc.Actions, action) + } + } + } + + return doc, nil +} + +func parseXmlEntityType(et *xmlEntityType) *EdmEntityType { + entityType := &EdmEntityType{ + Name: et.Name, + BaseType: et.BaseType, + IsAbstract: et.Abstract == "true", + IsOpen: et.OpenType == "true", + } + + // Parse key + if et.Key != nil { + for _, pr := range et.Key.PropertyRefs { + entityType.KeyProperties = append(entityType.KeyProperties, pr.Name) + } + } + + // Parse documentation (OData3 style) + if et.Documentation != nil { + entityType.Summary = et.Documentation.Summary + entityType.Description = et.Documentation.LongDescription + } + + // Parse annotations (OData4 style) + for _, ann := range et.Annotations { + switch ann.Term { + case "Org.OData.Core.V1.Description": + entityType.Summary = ann.String + case "Org.OData.Core.V1.LongDescription": + entityType.Description = ann.String + } + } + + // Parse properties + for _, p := range et.Properties { + prop := &EdmProperty{ + Name: p.Name, + Type: p.Type, + MaxLength: p.MaxLength, + Scale: p.Scale, + } + if p.Nullable != "" { + v := p.Nullable != "false" + prop.Nullable = &v + } + for _, ann := range p.Annotations { + switch ann.Term { + case "Org.OData.Core.V1.Computed": + prop.Computed = ann.Bool == "" || ann.Bool == "true" + case "Org.OData.Core.V1.Immutable": + prop.Immutable = ann.Bool == "" || ann.Bool == "true" + } + } + entityType.Properties = append(entityType.Properties, prop) + } + + // Parse navigation properties + for _, np := range et.NavigationProperties { + nav := &EdmNavigationProperty{ + Name: np.Name, + Type: np.Type, + Partner: np.Partner, + ContainsTarget: np.ContainsTarget == "true", + Relationship: np.Relationship, + FromRole: np.FromRole, + ToRole: np.ToRole, + } + + // Resolve target type from OData4 Type field + if np.Type != "" { + nav.TargetType, nav.IsMany = ResolveNavType(np.Type) + } + + entityType.NavigationProperties = append(entityType.NavigationProperties, nav) + } + + return entityType +} + +// applyCapabilityAnnotations reads Org.OData.Capabilities.V1.{Insert,Update, +// Delete}Restrictions annotations on an entity set and stores the relevant +// flags on the EdmEntitySet. +func applyCapabilityAnnotations(es *EdmEntitySet, annotations []xmlCapabilitiesAnnotation) { + for _, ann := range annotations { + if ann.Record == nil { + continue + } + switch ann.Term { + case "Org.OData.Capabilities.V1.InsertRestrictions": + for _, pv := range ann.Record.PropertyValues { + switch pv.Property { + case "Insertable": + if pv.Bool != "" { + v := pv.Bool == "true" + es.Insertable = &v + } + case "NonInsertableNavigationProperties": + if pv.Collection != nil { + es.NonInsertableNavigationProperties = pv.Collection.NavigationPropertyPaths + } + case "NonInsertableProperties": + if pv.Collection != nil { + es.NonInsertableProperties = pv.Collection.PropertyPaths + } + } + } + case "Org.OData.Capabilities.V1.UpdateRestrictions": + for _, pv := range ann.Record.PropertyValues { + switch pv.Property { + case "Updatable": + if pv.Bool != "" { + v := pv.Bool == "true" + es.Updatable = &v + } + case "NonUpdatableNavigationProperties": + if pv.Collection != nil { + es.NonUpdatableNavigationProperties = pv.Collection.NavigationPropertyPaths + } + case "NonUpdatableProperties": + if pv.Collection != nil { + es.NonUpdatableProperties = pv.Collection.PropertyPaths + } + } + } + case "Org.OData.Capabilities.V1.DeleteRestrictions": + for _, pv := range ann.Record.PropertyValues { + if pv.Property == "Deletable" && pv.Bool != "" { + v := pv.Bool == "true" + es.Deletable = &v + } + } + } + } +} + +// ResolveNavType parses "Collection(Namespace.Type)" or "Namespace.Type" into the short type name. +func ResolveNavType(t string) (typeName string, isMany bool) { + if strings.HasPrefix(t, "Collection(") && strings.HasSuffix(t, ")") { + isMany = true + t = t[len("Collection(") : len(t)-1] + } + if idx := strings.LastIndex(t, "."); idx >= 0 { + typeName = t[idx+1:] + } else { + typeName = t + } + return +} + +// ============================================================================ +// XML deserialization types (internal) +// ============================================================================ + +type xmlEdmx struct { + XMLName xml.Name `xml:"Edmx"` + Version string `xml:"Version,attr"` + DataServices []xmlDataServices `xml:"DataServices"` +} + +type xmlDataServices struct { + Schemas []xmlSchema `xml:"Schema"` +} + +type xmlSchema struct { + Namespace string `xml:"Namespace,attr"` + EntityTypes []xmlEntityType `xml:"EntityType"` + EnumTypes []xmlEnumType `xml:"EnumType"` + EntityContainers []xmlEntityContainer `xml:"EntityContainer"` + Actions []xmlAction `xml:"Action"` + Functions []xmlAction `xml:"Function"` +} + +type xmlEntityType struct { + Name string `xml:"Name,attr"` + BaseType string `xml:"BaseType,attr"` + Abstract string `xml:"Abstract,attr"` + OpenType string `xml:"OpenType,attr"` + Key *xmlKey `xml:"Key"` + Properties []xmlProperty `xml:"Property"` + NavigationProperties []xmlNavigationProperty `xml:"NavigationProperty"` + Documentation *xmlDocumentation `xml:"Documentation"` + Annotations []xmlAnnotation `xml:"Annotation"` +} + +type xmlKey struct { + PropertyRefs []xmlPropertyRef `xml:"PropertyRef"` +} + +type xmlPropertyRef struct { + Name string `xml:"Name,attr"` +} + +type xmlProperty struct { + Name string `xml:"Name,attr"` + Type string `xml:"Type,attr"` + Nullable string `xml:"Nullable,attr"` + MaxLength string `xml:"MaxLength,attr"` + Scale string `xml:"Scale,attr"` + Annotations []xmlAnnotation `xml:"Annotation"` +} + +type xmlNavigationProperty struct { + Name string `xml:"Name,attr"` + Type string `xml:"Type,attr"` // OData4 + Partner string `xml:"Partner,attr"` // OData4 + ContainsTarget string `xml:"ContainsTarget,attr"` // OData4: contained nav target (e.g. Person.Trips) + Relationship string `xml:"Relationship,attr"` // OData3 + FromRole string `xml:"FromRole,attr"` // OData3 + ToRole string `xml:"ToRole,attr"` // OData3 +} + +type xmlDocumentation struct { + Summary string `xml:"Summary"` + LongDescription string `xml:"LongDescription"` +} + +type xmlAnnotation struct { + Term string `xml:"Term,attr"` + String string `xml:"String,attr"` + Bool string `xml:"Bool,attr"` +} + +type xmlEntityContainer struct { + Name string `xml:"Name,attr"` + EntitySets []xmlEntitySet `xml:"EntitySet"` + FunctionImports []xmlFunctionImport `xml:"FunctionImport"` +} + +type xmlEntitySet struct { + Name string `xml:"Name,attr"` + EntityType string `xml:"EntityType,attr"` + Annotations []xmlCapabilitiesAnnotation `xml:"Annotation"` +} + +// xmlCapabilitiesAnnotation captures the bits of OData V1 Capabilities +// annotations we care about. The wrapping contains +// and (sometimes) +// +// Trips. +type xmlCapabilitiesAnnotation struct { + Term string `xml:"Term,attr"` + Record *xmlCapabilitiesRecord `xml:"Record"` +} + +type xmlCapabilitiesRecord struct { + PropertyValues []xmlCapabilitiesPropertyValue `xml:"PropertyValue"` +} + +type xmlCapabilitiesPropertyValue struct { + Property string `xml:"Property,attr"` + Bool string `xml:"Bool,attr"` + Collection *xmlCapabilitiesCollection `xml:"Collection"` +} + +type xmlCapabilitiesCollection struct { + NavigationPropertyPaths []string `xml:"NavigationPropertyPath"` + PropertyPaths []string `xml:"PropertyPath"` +} + +type xmlFunctionImport struct { + Name string `xml:"Name,attr"` + ReturnType string `xml:"ReturnType,attr"` + Parameters []xmlActionParam `xml:"Parameter"` +} + +type xmlAction struct { + Name string `xml:"Name,attr"` + IsBound string `xml:"IsBound,attr"` + ReturnType *xmlReturnType `xml:"ReturnType"` + Parameters []xmlActionParam `xml:"Parameter"` +} + +type xmlReturnType struct { + Type string `xml:"Type,attr"` + Nullable string `xml:"Nullable,attr"` +} + +type xmlActionParam struct { + Name string `xml:"Name,attr"` + Type string `xml:"Type,attr"` + Nullable string `xml:"Nullable,attr"` +} + +type xmlEnumType struct { + Name string `xml:"Name,attr"` + Members []xmlEnumMember `xml:"Member"` +} + +type xmlEnumMember struct { + Name string `xml:"Name,attr"` + Value string `xml:"Value,attr"` +} diff --git a/mdl/types/id.go b/mdl/types/id.go new file mode 100644 index 00000000..528f145a --- /dev/null +++ b/mdl/types/id.go @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "crypto/rand" + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" +) + +// GenerateID generates a new unique UUID v4 for model elements. +func GenerateID() string { + b := make([]byte, 16) + _, _ = rand.Read(b) + b[6] = (b[6] & 0x0f) | 0x40 // Version 4 + b[8] = (b[8] & 0x3f) | 0x80 // Variant is 10 + + return fmt.Sprintf("%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + b[0], b[1], b[2], b[3], + b[4], b[5], + b[6], b[7], + b[8], b[9], + b[10], b[11], b[12], b[13], b[14], b[15]) +} + +// GenerateDeterministicID generates a stable UUID from a seed string. +// Used for System module entities that aren't in the MPR but need consistent IDs. +func GenerateDeterministicID(seed string) string { + h := sha256.Sum256([]byte(seed)) + return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", + h[0:4], h[4:6], h[6:8], h[8:10], h[10:16]) +} + +// BlobToUUID converts a 16-byte binary ID blob to a UUID string. +func BlobToUUID(data []byte) string { + if len(data) != 16 { + return hex.EncodeToString(data) + } + return fmt.Sprintf("%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", + data[3], data[2], data[1], data[0], + data[5], data[4], + data[7], data[6], + data[8], data[9], + data[10], data[11], data[12], data[13], data[14], data[15]) +} + +// UUIDToBlob converts a UUID string to a 16-byte blob in Microsoft GUID format. +func UUIDToBlob(uuid string) []byte { + if uuid == "" { + return nil + } + var clean strings.Builder + for _, c := range uuid { + if c != '-' { + clean.WriteString(string(c)) + } + } + decoded, err := hex.DecodeString(clean.String()) + if err != nil || len(decoded) != 16 { + return nil + } + blob := make([]byte, 16) + blob[0] = decoded[3] + blob[1] = decoded[2] + blob[2] = decoded[1] + blob[3] = decoded[0] + blob[4] = decoded[5] + blob[5] = decoded[4] + blob[6] = decoded[7] + blob[7] = decoded[6] + copy(blob[8:], decoded[8:]) + return blob +} + +// ValidateID checks if an ID is a valid UUID format. +func ValidateID(id string) bool { + if len(id) != 36 { + return false + } + for i, c := range id { + if i == 8 || i == 13 || i == 18 || i == 23 { + if c != '-' { + return false + } + } else { + if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { + return false + } + } + } + return true +} + +// Hash computes a hash for content (used for content deduplication). +func Hash(content []byte) string { + var sum uint64 + for i, b := range content { + sum += uint64(b) * uint64(i+1) + } + return fmt.Sprintf("%016x", sum) +} diff --git a/mdl/types/infrastructure.go b/mdl/types/infrastructure.go new file mode 100644 index 00000000..75b5bf6b --- /dev/null +++ b/mdl/types/infrastructure.go @@ -0,0 +1,93 @@ +// SPDX-License-Identifier: Apache-2.0 + +package types + +import "github.com/mendixlabs/mxcli/model" + +// MPRVersion identifies the MPR file format version. +type MPRVersion int + +const ( + MPRVersionV1 MPRVersion = 1 // single-file format + MPRVersionV2 MPRVersion = 2 // mprcontents folder (Mendix 10.18+) +) + +// ProjectVersion holds the parsed Mendix project version. +type ProjectVersion struct { + ProductVersion string + BuildVersion string + FormatVersion int + SchemaHash string + MajorVersion int + MinorVersion int + PatchVersion int +} + +// FolderInfo is a lightweight folder descriptor. +type FolderInfo struct { + ID model.ID + ContainerID model.ID + Name string +} + +// UnitInfo is a lightweight unit descriptor. +type UnitInfo struct { + ID model.ID + ContainerID model.ID + ContainmentName string + Type string +} + +// RenameHit records a single rename reference replacement. +type RenameHit struct { + UnitID string + UnitType string + Name string + Count int +} + +// RawUnit holds a unit's raw BSON contents. +type RawUnit struct { + ID model.ID + ContainerID model.ID + Type string + Contents []byte +} + +// RawUnitInfo holds a unit's raw contents with metadata. +type RawUnitInfo struct { + ID string + QualifiedName string + Type string + ModuleName string + Contents []byte +} + +// RawCustomWidgetType holds a custom widget's raw type/object data. +// RawType and RawObject are bson.D in sdk/mpr; here they are any to +// avoid a BSON driver dependency. +type RawCustomWidgetType struct { + WidgetID string + RawType any + RawObject any + UnitID string + UnitName string + WidgetName string +} + +// EntityMemberAccess describes access rights for a single entity member. +type EntityMemberAccess struct { + AttributeRef string + AssociationRef string + AccessRights string +} + +// EntityAccessRevocation describes which entity access to revoke. +type EntityAccessRevocation struct { + RevokeCreate bool + RevokeDelete bool + RevokeReadMembers []string + RevokeWriteMembers []string + RevokeReadAll bool + RevokeWriteAll bool +} diff --git a/mdl/types/java.go b/mdl/types/java.go new file mode 100644 index 00000000..f9da1ab8 --- /dev/null +++ b/mdl/types/java.go @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/javaactions" +) + +// JavaAction is a lightweight Java action descriptor. +type JavaAction struct { + model.BaseElement + ContainerID model.ID `json:"containerId"` + Name string `json:"name"` + Documentation string `json:"documentation,omitempty"` +} + +// GetName returns the Java action's name. +func (ja *JavaAction) GetName() string { return ja.Name } + +// GetContainerID returns the container ID. +func (ja *JavaAction) GetContainerID() model.ID { return ja.ContainerID } + +// JavaScriptAction is a JavaScript action descriptor. +type JavaScriptAction struct { + model.BaseElement + ContainerID model.ID `json:"containerId"` + Name string `json:"name"` + Documentation string `json:"documentation,omitempty"` + Platform string `json:"platform,omitempty"` + Excluded bool `json:"excluded"` + ExportLevel string `json:"exportLevel,omitempty"` + ActionDefaultReturnName string `json:"actionDefaultReturnName,omitempty"` + ReturnType javaactions.CodeActionReturnType `json:"returnType,omitempty"` + Parameters []*javaactions.JavaActionParameter `json:"parameters,omitempty"` + TypeParameters []*javaactions.TypeParameterDef `json:"typeParameters,omitempty"` + MicroflowActionInfo *javaactions.MicroflowActionInfo `json:"microflowActionInfo,omitempty"` +} + +// GetName returns the JavaScript action's name. +func (jsa *JavaScriptAction) GetName() string { return jsa.Name } + +// GetContainerID returns the container ID. +func (jsa *JavaScriptAction) GetContainerID() model.ID { return jsa.ContainerID } + +// FindTypeParameterName looks up a type parameter name by its ID. +func (jsa *JavaScriptAction) FindTypeParameterName(id model.ID) string { + for _, tp := range jsa.TypeParameters { + if tp.ID == id { + return tp.Name + } + } + return "" +} diff --git a/mdl/types/json_utils.go b/mdl/types/json_utils.go new file mode 100644 index 00000000..7a9d8da8 --- /dev/null +++ b/mdl/types/json_utils.go @@ -0,0 +1,374 @@ +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "bytes" + "encoding/json" + "fmt" + "math" + "regexp" + "strings" + "unicode" +) + +// iso8601Pattern matches common ISO 8601 datetime strings that Mendix Studio Pro +// recognizes as DateTime primitive types in JSON structures. +var iso8601Pattern = regexp.MustCompile( + `^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?$`, +) + +// PrettyPrintJSON re-formats a JSON string with standard indentation. +// Returns the original string if it is not valid JSON. +func PrettyPrintJSON(s string) string { + var buf bytes.Buffer + if err := json.Indent(&buf, []byte(s), "", " "); err != nil { + return s + } + return buf.String() +} + +// normalizeDateTimeValue pads fractional seconds to 7 digits to match +// Studio Pro's .NET DateTime format (e.g., "2015-05-22T14:56:29.000Z" → "2015-05-22T14:56:29.0000000Z"). +func normalizeDateTimeValue(s string) string { + // Find the decimal point after seconds + dotIdx := strings.Index(s, ".") + if dotIdx == -1 { + // No fractional part — insert .0000000 before timezone suffix + if idx := strings.IndexAny(s, "Z+-"); idx > 0 { + return s[:idx] + ".0000000" + s[idx:] + } + return s + } + // Find where fractional digits end (at Z, +, - or end of string) + fracEnd := len(s) + for i := dotIdx + 1; i < len(s); i++ { + if s[i] < '0' || s[i] > '9' { + fracEnd = i + break + } + } + frac := s[dotIdx+1 : fracEnd] + if len(frac) < 7 { + frac = frac + strings.Repeat("0", 7-len(frac)) + } else { + frac = frac[:7] + } + return s[:dotIdx+1] + frac + s[fracEnd:] +} + +// BuildJsonElementsFromSnippet parses a JSON snippet and builds the element tree +// that Mendix Studio Pro would generate. Returns the root element. +// The optional customNameMap maps JSON keys to custom ExposedNames (as set in +// Studio Pro's "Custom name" column). Unmapped keys use auto-generated names. +func BuildJsonElementsFromSnippet(snippet string, customNameMap map[string]string) ([]*JsonElement, error) { + // Validate JSON + if !json.Valid([]byte(snippet)) { + return nil, fmt.Errorf("invalid JSON snippet") + } + + // Detect root type (object or array) + dec := json.NewDecoder(strings.NewReader(snippet)) + tok, err := dec.Token() + if err != nil { + return nil, fmt.Errorf("failed to parse JSON snippet: %w", err) + } + + b := &snippetBuilder{customNameMap: customNameMap} + tracker := &nameTracker{seen: make(map[string]int)} + + switch tok { + case json.Delim('{'): + root := b.buildElementFromRawObject("Root", "(Object)", snippet, tracker) + root.MinOccurs = 0 + root.MaxOccurs = 0 + root.Nillable = true + return []*JsonElement{root}, nil + + case json.Delim('['): + root := b.buildElementFromRawRootArray("Root", "(Array)", snippet, tracker) + root.MinOccurs = 0 + root.MaxOccurs = 0 + root.Nillable = true + return []*JsonElement{root}, nil + + default: + return nil, fmt.Errorf("JSON snippet must be an object or array at root level") + } +} + +// snippetBuilder holds state for building the element tree from a JSON snippet. +type snippetBuilder struct { + customNameMap map[string]string // JSON key → custom ExposedName +} + +// reservedExposedNames are element names that Mendix rejects as ExposedName values. +// Studio Pro handles these by prefixing with underscore and keeping original case. +var reservedExposedNames = map[string]bool{ + "Id": true, "Type": true, +} + +// resolveExposedName returns the custom name if mapped, otherwise capitalizes the JSON key. +// Reserved names (Id, Type, Name) are prefixed with underscore to match Studio Pro behavior. +func (b *snippetBuilder) resolveExposedName(jsonKey string) string { + if b.customNameMap != nil { + if custom, ok := b.customNameMap[jsonKey]; ok { + return custom + } + } + name := capitalizeFirst(jsonKey) + if reservedExposedNames[name] { + return "_" + jsonKey + } + return name +} + +// nameTracker tracks used ExposedNames at each level to handle duplicates. +type nameTracker struct { + seen map[string]int +} + +func (t *nameTracker) uniqueName(base string) string { + t.seen[base]++ + count := t.seen[base] + if count == 1 { + return base + } + return fmt.Sprintf("%s_%d", base, count) +} + +func (t *nameTracker) child() *nameTracker { + return &nameTracker{seen: make(map[string]int)} +} + +// capitalizeFirst capitalizes the first letter of a string for ExposedName. +func capitalizeFirst(s string) string { + if s == "" { + return s + } + runes := []rune(s) + runes[0] = unicode.ToUpper(runes[0]) + return string(runes) +} + +// buildElementFromRawObject builds an Object element by decoding a raw JSON object string, +// preserving the original key order (Go's map[string]any loses order). +func (b *snippetBuilder) buildElementFromRawObject(exposedName, path, rawJSON string, tracker *nameTracker) *JsonElement { + elem := &JsonElement{ + ExposedName: exposedName, + Path: path, + ElementType: "Object", + PrimitiveType: "Unknown", + MinOccurs: 0, + MaxOccurs: 0, + Nillable: true, + MaxLength: -1, + FractionDigits: -1, + TotalDigits: -1, + } + + childTracker := tracker.child() + + // Decode with key order preserved + dec := json.NewDecoder(strings.NewReader(rawJSON)) + if _, err := dec.Token(); err != nil { // opening { + return elem + } + for dec.More() { + tok, err := dec.Token() + if err != nil { + break + } + key, ok := tok.(string) + if !ok { + continue + } + // Capture the raw value to pass down for nested objects/arrays + var rawVal json.RawMessage + if err := dec.Decode(&rawVal); err != nil { + break + } + + childName := childTracker.uniqueName(b.resolveExposedName(key)) + childPath := path + "|" + key + child := b.buildElementFromRawValue(childName, childPath, key, rawVal, childTracker) + elem.Children = append(elem.Children, child) + } + + return elem +} + +// buildElementFromRawValue inspects a json.RawMessage to determine its type and build the element. +func (b *snippetBuilder) buildElementFromRawValue(exposedName, path, jsonKey string, raw json.RawMessage, tracker *nameTracker) *JsonElement { + trimmed := strings.TrimSpace(string(raw)) + + // Object — recurse with raw JSON to preserve key order + if len(trimmed) > 0 && trimmed[0] == '{' { + return b.buildElementFromRawObject(exposedName, path, trimmed, tracker) + } + + // Array + if len(trimmed) > 0 && trimmed[0] == '[' { + return b.buildElementFromRawArray(exposedName, path, jsonKey, trimmed, tracker) + } + + // Primitive — unmarshal to determine type + var val any + json.Unmarshal(raw, &val) + + switch v := val.(type) { + case string: + primitiveType := "String" + if iso8601Pattern.MatchString(v) { + primitiveType = "DateTime" + v = normalizeDateTimeValue(v) + } + return buildValueElement(exposedName, path, primitiveType, fmt.Sprintf("%q", v)) + case float64: + // Check the raw JSON text for a decimal point — Go's %v drops ".0" from 41850.0 + if v == math.Trunc(v) && !strings.Contains(trimmed, ".") { + return buildValueElement(exposedName, path, "Integer", fmt.Sprintf("%v", int64(v))) + } + return buildValueElement(exposedName, path, "Decimal", fmt.Sprintf("%v", v)) + case bool: + return buildValueElement(exposedName, path, "Boolean", fmt.Sprintf("%v", v)) + case nil: + // JSON null → Unknown primitive type (matches Studio Pro) + return buildValueElement(exposedName, path, "Unknown", "") + default: + return buildValueElement(exposedName, path, "String", "") + } +} + +// buildElementFromRawRootArray builds a root-level Array element. +// Studio Pro names the child object "JsonObject" (not "RootItem") for root arrays. +func (b *snippetBuilder) buildElementFromRawRootArray(exposedName, path, rawJSON string, tracker *nameTracker) *JsonElement { + arrayElem := &JsonElement{ + ExposedName: exposedName, + Path: path, + ElementType: "Array", + PrimitiveType: "Unknown", + MinOccurs: 0, + MaxOccurs: 0, + Nillable: true, + MaxLength: -1, + FractionDigits: -1, + TotalDigits: -1, + } + + dec := json.NewDecoder(strings.NewReader(rawJSON)) + dec.Token() // opening [ + if dec.More() { + var firstItem json.RawMessage + dec.Decode(&firstItem) + + itemPath := path + "|(Object)" + trimmed := strings.TrimSpace(string(firstItem)) + + if len(trimmed) > 0 && trimmed[0] == '{' { + itemElem := b.buildElementFromRawObject("JsonObject", itemPath, trimmed, tracker) + itemElem.MinOccurs = 0 + itemElem.MaxOccurs = 0 + itemElem.Nillable = true + arrayElem.Children = append(arrayElem.Children, itemElem) + } else { + child := b.buildElementFromRawValue("JsonObject", itemPath, "", firstItem, tracker) + child.MinOccurs = 0 + child.MaxOccurs = 0 + arrayElem.Children = append(arrayElem.Children, child) + } + } + + return arrayElem +} + +// buildElementFromRawArray builds an Array element, using the first item's raw JSON for ordering. +// For primitive arrays (strings, numbers), Studio Pro creates a Wrapper element with a Value child. +func (b *snippetBuilder) buildElementFromRawArray(exposedName, path, jsonKey, rawJSON string, tracker *nameTracker) *JsonElement { + arrayElem := &JsonElement{ + ExposedName: exposedName, + Path: path, + ElementType: "Array", + PrimitiveType: "Unknown", + MinOccurs: 0, + MaxOccurs: 0, + Nillable: true, + MaxLength: -1, + FractionDigits: -1, + TotalDigits: -1, + } + + // Decode array and get first element as raw JSON + dec := json.NewDecoder(strings.NewReader(rawJSON)) + dec.Token() // opening [ + if dec.More() { + var firstItem json.RawMessage + dec.Decode(&firstItem) + + trimmed := strings.TrimSpace(string(firstItem)) + + if len(trimmed) > 0 && trimmed[0] == '{' { + // Object array: child is NameItem object + itemName := exposedName + "Item" + itemPath := path + "|(Object)" + itemElem := b.buildElementFromRawObject(itemName, itemPath, trimmed, tracker) + itemElem.MinOccurs = 0 + itemElem.MaxOccurs = -1 + itemElem.Nillable = true + arrayElem.Children = append(arrayElem.Children, itemElem) + } else { + // Primitive array: Studio Pro wraps in a Wrapper element with singular name + // e.g., tags: ["a","b"] → Tag (Wrapper) → Value (String) + wrapperName := singularize(exposedName) + wrapperPath := path + "|(Object)" + wrapper := &JsonElement{ + ExposedName: wrapperName, + Path: wrapperPath, + ElementType: "Wrapper", + PrimitiveType: "Unknown", + MinOccurs: 0, + MaxOccurs: 0, + Nillable: true, + MaxLength: -1, + FractionDigits: -1, + TotalDigits: -1, + } + valueElem := b.buildElementFromRawValue("Value", wrapperPath+"|", jsonKey, firstItem, tracker) + valueElem.MinOccurs = 0 + valueElem.MaxOccurs = 0 + wrapper.Children = append(wrapper.Children, valueElem) + arrayElem.Children = append(arrayElem.Children, wrapper) + } + } + + return arrayElem +} + +// singularize returns a simple singular form by stripping trailing "s". +// Handles common cases: Tags→Tag, Items→Item, Addresses→Addresse. +func singularize(s string) string { + if len(s) > 1 && strings.HasSuffix(s, "s") { + return s[:len(s)-1] + } + return s +} + +func buildValueElement(exposedName, path, primitiveType, originalValue string) *JsonElement { + maxLength := -1 + if primitiveType == "String" { + maxLength = 0 + } + return &JsonElement{ + ExposedName: exposedName, + Path: path, + ElementType: "Value", + PrimitiveType: primitiveType, + MinOccurs: 0, + MaxOccurs: 0, + Nillable: true, + MaxLength: maxLength, + FractionDigits: -1, + TotalDigits: -1, + OriginalValue: originalValue, + } +} diff --git a/mdl/types/mapping.go b/mdl/types/mapping.go new file mode 100644 index 00000000..4e3a193e --- /dev/null +++ b/mdl/types/mapping.go @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: Apache-2.0 + +package types + +import "github.com/mendixlabs/mxcli/model" + +// JsonStructure represents a JSON structure document. +type JsonStructure struct { + model.BaseElement + ContainerID model.ID `json:"containerId"` + Name string `json:"name"` + Documentation string `json:"documentation,omitempty"` + JsonSnippet string `json:"jsonSnippet,omitempty"` + Elements []*JsonElement `json:"elements,omitempty"` + Excluded bool `json:"excluded,omitempty"` + ExportLevel string `json:"exportLevel,omitempty"` +} + +// GetName returns the JSON structure's name. +func (js *JsonStructure) GetName() string { return js.Name } + +// GetContainerID returns the container ID. +func (js *JsonStructure) GetContainerID() model.ID { return js.ContainerID } + +// JsonElement represents a single element in a JSON structure (recursive). +type JsonElement struct { + ExposedName string `json:"exposedName"` + ExposedItemName string `json:"exposedItemName,omitempty"` + Path string `json:"path"` + ElementType string `json:"elementType"` + PrimitiveType string `json:"primitiveType"` + MinOccurs int `json:"minOccurs"` + MaxOccurs int `json:"maxOccurs"` + Nillable bool `json:"nillable,omitempty"` + IsDefaultType bool `json:"isDefaultType,omitempty"` + MaxLength int `json:"maxLength"` + FractionDigits int `json:"fractionDigits"` + TotalDigits int `json:"totalDigits"` + OriginalValue string `json:"originalValue,omitempty"` + Children []*JsonElement `json:"children,omitempty"` +} + +// ImageCollection represents an image collection document. +type ImageCollection struct { + model.BaseElement + ContainerID model.ID `json:"containerId"` + Name string `json:"name"` + ExportLevel string `json:"exportLevel,omitempty"` + Documentation string `json:"documentation,omitempty"` + Images []Image `json:"images,omitempty"` +} + +// GetName returns the image collection's name. +func (ic *ImageCollection) GetName() string { return ic.Name } + +// GetContainerID returns the container ID. +func (ic *ImageCollection) GetContainerID() model.ID { return ic.ContainerID } + +// Image represents a single image in an image collection. +type Image struct { + ID model.ID `json:"id"` + Name string `json:"name"` + Data []byte `json:"data,omitempty"` + Format string `json:"format,omitempty"` +} diff --git a/mdl/types/navigation.go b/mdl/types/navigation.go new file mode 100644 index 00000000..b3a14dac --- /dev/null +++ b/mdl/types/navigation.go @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 + +package types + +import "github.com/mendixlabs/mxcli/model" + +// NavigationDocument represents a parsed navigation document. +type NavigationDocument struct { + model.BaseElement + ContainerID model.ID `json:"containerId"` + Name string `json:"name"` + Profiles []*NavigationProfile `json:"profiles,omitempty"` +} + +// GetName returns the navigation document's name. +func (nd *NavigationDocument) GetName() string { return nd.Name } + +// GetContainerID returns the container ID. +func (nd *NavigationDocument) GetContainerID() model.ID { return nd.ContainerID } + +// NavigationProfile represents a single navigation profile. +type NavigationProfile struct { + Name string `json:"name"` + Kind string `json:"kind"` + IsNative bool `json:"isNative"` + HomePage *NavHomePage `json:"homePage,omitempty"` + RoleBasedHomePages []*NavRoleBasedHome `json:"roleBasedHomePages,omitempty"` + LoginPage string `json:"loginPage,omitempty"` + NotFoundPage string `json:"notFoundPage,omitempty"` + MenuItems []*NavMenuItem `json:"menuItems,omitempty"` + OfflineEntities []*NavOfflineEntity `json:"offlineEntities,omitempty"` +} + +// NavHomePage holds a profile's default home page. +type NavHomePage struct { + Page string `json:"page,omitempty"` + Microflow string `json:"microflow,omitempty"` +} + +// NavRoleBasedHome maps a user role to a home page. +type NavRoleBasedHome struct { + UserRole string `json:"userRole"` + Page string `json:"page,omitempty"` + Microflow string `json:"microflow,omitempty"` +} + +// NavMenuItem is a recursive navigation menu entry. +type NavMenuItem struct { + Caption string `json:"caption"` + Page string `json:"page,omitempty"` + Microflow string `json:"microflow,omitempty"` + ActionType string `json:"actionType,omitempty"` + Items []*NavMenuItem `json:"items,omitempty"` +} + +// NavOfflineEntity declares offline sync rules for an entity. +type NavOfflineEntity struct { + Entity string `json:"entity"` + SyncMode string `json:"syncMode"` + Constraint string `json:"constraint,omitempty"` +} + +// NavigationProfileSpec specifies changes to a navigation profile. +type NavigationProfileSpec struct { + HomePages []NavHomePageSpec + LoginPage string + NotFoundPage string + MenuItems []NavMenuItemSpec + HasMenu bool +} + +// NavHomePageSpec specifies a home page assignment. +type NavHomePageSpec struct { + IsPage bool + Target string + ForRole string +} + +// NavMenuItemSpec specifies a menu item (recursive). +type NavMenuItemSpec struct { + Caption string + Page string + Microflow string + Items []NavMenuItemSpec +} diff --git a/sdk/mpr/asyncapi.go b/sdk/mpr/asyncapi.go index c0444b63..2bedd790 100644 --- a/sdk/mpr/asyncapi.go +++ b/sdk/mpr/asyncapi.go @@ -3,203 +3,16 @@ package mpr import ( - "fmt" - "strings" - - "gopkg.in/yaml.v3" + "github.com/mendixlabs/mxcli/mdl/types" ) -// AsyncAPIDocument represents a parsed AsyncAPI 2.x document. -type AsyncAPIDocument struct { - Version string // AsyncAPI version (e.g. "2.2.0") - Title string // Service title - DocVersion string // Document version - Description string - Channels []*AsyncAPIChannel // Resolved channels - Messages []*AsyncAPIMessage // Resolved messages (from components) -} - -// AsyncAPIChannel represents a channel in the AsyncAPI document. -type AsyncAPIChannel struct { - Name string // Channel ID/name - OperationType string // "subscribe" or "publish" - OperationID string // e.g. "receiveOrderChangedEventEvents" - MessageRef string // Resolved message name -} - -// AsyncAPIMessage represents a message type. -type AsyncAPIMessage struct { - Name string - Title string - Description string - ContentType string - Properties []*AsyncAPIProperty // Resolved from payload schema -} +// Type aliases — all AsyncAPI types now live in mdl/types. +type AsyncAPIDocument = types.AsyncAPIDocument +type AsyncAPIChannel = types.AsyncAPIChannel +type AsyncAPIMessage = types.AsyncAPIMessage +type AsyncAPIProperty = types.AsyncAPIProperty -// AsyncAPIProperty represents a property in a message payload schema. -type AsyncAPIProperty struct { - Name string - Type string // "string", "integer", "number", "boolean", "array", "object" - Format string // "int64", "int32", "date-time", "uri-reference", etc. -} - -// ParseAsyncAPI parses an AsyncAPI YAML string into an AsyncAPIDocument. +// ParseAsyncAPI delegates to types.ParseAsyncAPI. func ParseAsyncAPI(yamlStr string) (*AsyncAPIDocument, error) { - if yamlStr == "" { - return nil, fmt.Errorf("empty AsyncAPI document") - } - - var raw yamlAsyncAPI - if err := yaml.Unmarshal([]byte(yamlStr), &raw); err != nil { - return nil, fmt.Errorf("failed to parse AsyncAPI YAML: %w", err) - } - - doc := &AsyncAPIDocument{ - Version: raw.AsyncAPI, - Title: raw.Info.Title, - DocVersion: raw.Info.Version, - Description: raw.Info.Description, - } - - // Resolve messages from components - for name, msg := range raw.Components.Messages { - resolved := &AsyncAPIMessage{ - Name: name, - Title: msg.Title, - Description: msg.Description, - ContentType: msg.ContentType, - } - - // Resolve payload schema (follow $ref if present) - schemaName := "" - if msg.Payload.Ref != "" { - schemaName = refName(msg.Payload.Ref) - } - - if schemaName != "" { - if schema, ok := raw.Components.Schemas[schemaName]; ok { - resolved.Properties = resolveSchemaProperties(schema) - } - } else if msg.Payload.Properties != nil { - // Inline schema - resolved.Properties = resolveSchemaProperties(msg.Payload) - } - - doc.Messages = append(doc.Messages, resolved) - } - - // Resolve channels - for channelName, channel := range raw.Channels { - if channel.Subscribe != nil { - msgName := "" - if channel.Subscribe.Message.Ref != "" { - msgName = refName(channel.Subscribe.Message.Ref) - } - doc.Channels = append(doc.Channels, &AsyncAPIChannel{ - Name: channelName, - OperationType: "subscribe", - OperationID: channel.Subscribe.OperationID, - MessageRef: msgName, - }) - } - if channel.Publish != nil { - msgName := "" - if channel.Publish.Message.Ref != "" { - msgName = refName(channel.Publish.Message.Ref) - } - doc.Channels = append(doc.Channels, &AsyncAPIChannel{ - Name: channelName, - OperationType: "publish", - OperationID: channel.Publish.OperationID, - MessageRef: msgName, - }) - } - } - - return doc, nil -} - -// FindMessage looks up a message by name. -func (d *AsyncAPIDocument) FindMessage(name string) *AsyncAPIMessage { - for _, m := range d.Messages { - if strings.EqualFold(m.Name, name) { - return m - } - } - return nil -} - -// refName extracts the last segment from a $ref like "#/components/messages/OrderChangedEvent". -func refName(ref string) string { - if idx := strings.LastIndex(ref, "/"); idx >= 0 { - return ref[idx+1:] - } - return ref -} - -func resolveSchemaProperties(schema yamlSchema) []*AsyncAPIProperty { - var props []*AsyncAPIProperty - for name, prop := range schema.Properties { - props = append(props, &AsyncAPIProperty{ - Name: name, - Type: prop.Type, - Format: prop.Format, - }) - } - return props -} - -// ============================================================================ -// YAML deserialization types (internal) -// ============================================================================ - -type yamlAsyncAPI struct { - AsyncAPI string `yaml:"asyncapi"` - Info yamlInfo `yaml:"info"` - Channels map[string]yamlChannel `yaml:"channels"` - Components yamlComponents `yaml:"components"` -} - -type yamlInfo struct { - Title string `yaml:"title"` - Version string `yaml:"version"` - Description string `yaml:"description"` -} - -type yamlChannel struct { - Subscribe *yamlOperation `yaml:"subscribe"` - Publish *yamlOperation `yaml:"publish"` -} - -type yamlOperation struct { - OperationID string `yaml:"operationId"` - Message yamlRef `yaml:"message"` -} - -type yamlRef struct { - Ref string `yaml:"$ref"` -} - -type yamlComponents struct { - Messages map[string]yamlMessage `yaml:"messages"` - Schemas map[string]yamlSchema `yaml:"schemas"` -} - -type yamlMessage struct { - Name string `yaml:"name"` - Title string `yaml:"title"` - Description string `yaml:"description"` - ContentType string `yaml:"contentType"` - Payload yamlSchema `yaml:"payload"` -} - -type yamlSchema struct { - Ref string `yaml:"$ref"` - Type string `yaml:"type"` - Properties map[string]yamlSchemaProperty `yaml:"properties"` -} - -type yamlSchemaProperty struct { - Type string `yaml:"type"` - Format string `yaml:"format"` + return types.ParseAsyncAPI(yamlStr) } diff --git a/sdk/mpr/edmx.go b/sdk/mpr/edmx.go index 3538d26a..d407b08b 100644 --- a/sdk/mpr/edmx.go +++ b/sdk/mpr/edmx.go @@ -3,539 +3,27 @@ package mpr import ( - "encoding/xml" - "fmt" - "strings" + "github.com/mendixlabs/mxcli/mdl/types" ) -// EdmxDocument represents a parsed OData $metadata document (EDMX/CSDL). -// Supports both OData v3 (CSDL 2.0/3.0) and OData v4 (CSDL 4.0). -type EdmxDocument struct { - Version string // "1.0" (OData3) or "4.0" (OData4) - Schemas []*EdmSchema // Schema definitions - EntitySets []*EdmEntitySet // Entity sets from EntityContainer - Actions []*EdmAction // OData4 actions / OData3 function imports -} - -// EdmSchema represents an EDM schema namespace. -type EdmSchema struct { - Namespace string - EntityTypes []*EdmEntityType - EnumTypes []*EdmEnumType -} - -// EdmEntityType represents an entity type definition. -type EdmEntityType struct { - Name string - BaseType string // Qualified name of base type (e.g. "Microsoft...PlanItem"), empty if none - IsAbstract bool // True if - IsOpen bool // True if - KeyProperties []string - Properties []*EdmProperty - NavigationProperties []*EdmNavigationProperty - Summary string - Description string -} - -// EdmProperty represents a property on an entity type. -type EdmProperty struct { - Name string - Type string // e.g. "Edm.String", "Edm.Int64" - Nullable *bool // nil = not specified (default true) - MaxLength string // e.g. "200", "max" - Scale string // e.g. "variable" - - // Capability annotations (OData Core V1). When true, the property is not - // settable by the client: - // Computed = server-computed, not settable on create or update. - // Immutable = settable on create, but not on update. - Computed bool - Immutable bool -} - -// EdmNavigationProperty represents a navigation property (association). -type EdmNavigationProperty struct { - Name string - Type string // OData4: "DefaultNamespace.Customer" or "Collection(DefaultNamespace.Part)" - Partner string // OData4 partner property name - TargetType string // Resolved target entity type name (without namespace/Collection) - IsMany bool // true if Collection() - ContainsTarget bool // true if - // OData3 fields (from Association) - Relationship string - FromRole string - ToRole string -} - -// EdmEntitySet represents an entity set in the entity container. -type EdmEntitySet struct { - Name string - EntityType string // Qualified name of entity type - - // Capabilities derived from Org.OData.Capabilities.V1 annotations. - // nil = not specified (treat as default true). - Insertable *bool // InsertRestrictions/Insertable - Updatable *bool // UpdateRestrictions/Updatable - Deletable *bool // DeleteRestrictions/Deletable - - // Navigation property names listed under - // Org.OData.Capabilities.V1.{Insert,Update}Restrictions/Non*NavigationProperties. - NonInsertableNavigationProperties []string - NonUpdatableNavigationProperties []string - - // Property names listed under - // Org.OData.Capabilities.V1.{Insert,Update}Restrictions/Non*Properties. - // Structural properties named here cannot be set on insert / update. - NonInsertableProperties []string - NonUpdatableProperties []string -} - -// EdmAction represents an OData4 action or OData3 function import. -type EdmAction struct { - Name string - IsBound bool - Parameters []*EdmActionParameter - ReturnType string -} - -// EdmActionParameter represents a parameter of an action. -type EdmActionParameter struct { - Name string - Type string - Nullable *bool -} - -// EdmEnumType represents an enumeration type. -type EdmEnumType struct { - Name string - Members []*EdmEnumMember -} - -// EdmEnumMember represents a member of an enum type. -type EdmEnumMember struct { - Name string - Value string -} - -// ParseEdmx parses an OData $metadata XML string into an EdmxDocument. +// Type aliases — all EDMX types now live in mdl/types. +type EdmxDocument = types.EdmxDocument +type EdmSchema = types.EdmSchema +type EdmEntityType = types.EdmEntityType +type EdmProperty = types.EdmProperty +type EdmNavigationProperty = types.EdmNavigationProperty +type EdmEntitySet = types.EdmEntitySet +type EdmAction = types.EdmAction +type EdmActionParameter = types.EdmActionParameter +type EdmEnumType = types.EdmEnumType +type EdmEnumMember = types.EdmEnumMember + +// ParseEdmx delegates to types.ParseEdmx. func ParseEdmx(metadataXML string) (*EdmxDocument, error) { - if metadataXML == "" { - return nil, fmt.Errorf("empty metadata XML") - } - - var edmx xmlEdmx - if err := xml.Unmarshal([]byte(metadataXML), &edmx); err != nil { - return nil, fmt.Errorf("failed to parse EDMX XML: %w", err) - } - - doc := &EdmxDocument{ - Version: edmx.Version, - } - - for _, ds := range edmx.DataServices { - for _, s := range ds.Schemas { - schema := &EdmSchema{ - Namespace: s.Namespace, - } - - // Parse entity types - for _, et := range s.EntityTypes { - entityType := parseXmlEntityType(&et) - schema.EntityTypes = append(schema.EntityTypes, entityType) - } - - // Parse enum types - for _, en := range s.EnumTypes { - enumType := &EdmEnumType{Name: en.Name} - for _, m := range en.Members { - enumType.Members = append(enumType.Members, &EdmEnumMember{ - Name: m.Name, - Value: m.Value, - }) - } - schema.EnumTypes = append(schema.EnumTypes, enumType) - } - - doc.Schemas = append(doc.Schemas, schema) - - // Parse entity container - for _, ec := range s.EntityContainers { - for _, es := range ec.EntitySets { - entitySet := &EdmEntitySet{ - Name: es.Name, - EntityType: es.EntityType, - } - applyCapabilityAnnotations(entitySet, es.Annotations) - doc.EntitySets = append(doc.EntitySets, entitySet) - } - - // OData3 function imports - for _, fi := range ec.FunctionImports { - action := &EdmAction{ - Name: fi.Name, - ReturnType: fi.ReturnType, - } - for _, p := range fi.Parameters { - action.Parameters = append(action.Parameters, &EdmActionParameter{ - Name: p.Name, - Type: p.Type, - }) - } - doc.Actions = append(doc.Actions, action) - } - } - - // OData4 actions - for _, a := range s.Actions { - action := &EdmAction{ - Name: a.Name, - IsBound: a.IsBound == "true", - } - if a.ReturnType != nil { - action.ReturnType = a.ReturnType.Type - } - for _, p := range a.Parameters { - param := &EdmActionParameter{ - Name: p.Name, - Type: p.Type, - } - if p.Nullable != "" { - v := p.Nullable == "true" - param.Nullable = &v - } - action.Parameters = append(action.Parameters, param) - } - doc.Actions = append(doc.Actions, action) - } - - // OData4 functions (treated same as actions for discovery) - for _, f := range s.Functions { - action := &EdmAction{ - Name: f.Name, - IsBound: f.IsBound == "true", - } - if f.ReturnType != nil { - action.ReturnType = f.ReturnType.Type - } - for _, p := range f.Parameters { - param := &EdmActionParameter{ - Name: p.Name, - Type: p.Type, - } - action.Parameters = append(action.Parameters, param) - } - doc.Actions = append(doc.Actions, action) - } - } - } - - return doc, nil -} - -// FindEntityType looks up an entity type by name (with or without namespace prefix). -func (d *EdmxDocument) FindEntityType(name string) *EdmEntityType { - // Strip namespace prefix if present - shortName := name - if idx := strings.LastIndex(name, "."); idx >= 0 { - shortName = name[idx+1:] - } - for _, s := range d.Schemas { - for _, et := range s.EntityTypes { - if et.Name == shortName { - return et - } - } - } - return nil -} - -func parseXmlEntityType(et *xmlEntityType) *EdmEntityType { - entityType := &EdmEntityType{ - Name: et.Name, - BaseType: et.BaseType, - IsAbstract: et.Abstract == "true", - IsOpen: et.OpenType == "true", - } - - // Parse key - if et.Key != nil { - for _, pr := range et.Key.PropertyRefs { - entityType.KeyProperties = append(entityType.KeyProperties, pr.Name) - } - } - - // Parse documentation (OData3 style) - if et.Documentation != nil { - entityType.Summary = et.Documentation.Summary - entityType.Description = et.Documentation.LongDescription - } - - // Parse annotations (OData4 style) - for _, ann := range et.Annotations { - switch ann.Term { - case "Org.OData.Core.V1.Description": - entityType.Summary = ann.String - case "Org.OData.Core.V1.LongDescription": - entityType.Description = ann.String - } - } - - // Parse properties - for _, p := range et.Properties { - prop := &EdmProperty{ - Name: p.Name, - Type: p.Type, - MaxLength: p.MaxLength, - Scale: p.Scale, - } - if p.Nullable != "" { - v := p.Nullable != "false" - prop.Nullable = &v - } - for _, ann := range p.Annotations { - switch ann.Term { - case "Org.OData.Core.V1.Computed": - prop.Computed = ann.Bool == "" || ann.Bool == "true" - case "Org.OData.Core.V1.Immutable": - prop.Immutable = ann.Bool == "" || ann.Bool == "true" - } - } - entityType.Properties = append(entityType.Properties, prop) - } - - // Parse navigation properties - for _, np := range et.NavigationProperties { - nav := &EdmNavigationProperty{ - Name: np.Name, - Type: np.Type, - Partner: np.Partner, - ContainsTarget: np.ContainsTarget == "true", - Relationship: np.Relationship, - FromRole: np.FromRole, - ToRole: np.ToRole, - } - - // Resolve target type from OData4 Type field - if np.Type != "" { - nav.TargetType, nav.IsMany = resolveNavType(np.Type) - } - - entityType.NavigationProperties = append(entityType.NavigationProperties, nav) - } - - return entityType -} - -// applyCapabilityAnnotations reads Org.OData.Capabilities.V1.{Insert,Update, -// Delete}Restrictions annotations on an entity set and stores the relevant -// flags on the EdmEntitySet. -func applyCapabilityAnnotations(es *EdmEntitySet, annotations []xmlCapabilitiesAnnotation) { - for _, ann := range annotations { - if ann.Record == nil { - continue - } - switch ann.Term { - case "Org.OData.Capabilities.V1.InsertRestrictions": - for _, pv := range ann.Record.PropertyValues { - switch pv.Property { - case "Insertable": - if pv.Bool != "" { - v := pv.Bool == "true" - es.Insertable = &v - } - case "NonInsertableNavigationProperties": - if pv.Collection != nil { - es.NonInsertableNavigationProperties = pv.Collection.NavigationPropertyPaths - } - case "NonInsertableProperties": - if pv.Collection != nil { - es.NonInsertableProperties = pv.Collection.PropertyPaths - } - } - } - case "Org.OData.Capabilities.V1.UpdateRestrictions": - for _, pv := range ann.Record.PropertyValues { - switch pv.Property { - case "Updatable": - if pv.Bool != "" { - v := pv.Bool == "true" - es.Updatable = &v - } - case "NonUpdatableNavigationProperties": - if pv.Collection != nil { - es.NonUpdatableNavigationProperties = pv.Collection.NavigationPropertyPaths - } - case "NonUpdatableProperties": - if pv.Collection != nil { - es.NonUpdatableProperties = pv.Collection.PropertyPaths - } - } - } - case "Org.OData.Capabilities.V1.DeleteRestrictions": - for _, pv := range ann.Record.PropertyValues { - if pv.Property == "Deletable" && pv.Bool != "" { - v := pv.Bool == "true" - es.Deletable = &v - } - } - } - } -} - -// resolveNavType parses "Collection(Namespace.Type)" or "Namespace.Type" into the short type name. -func resolveNavType(t string) (typeName string, isMany bool) { - if strings.HasPrefix(t, "Collection(") && strings.HasSuffix(t, ")") { - isMany = true - t = t[len("Collection(") : len(t)-1] - } - if idx := strings.LastIndex(t, "."); idx >= 0 { - typeName = t[idx+1:] - } else { - typeName = t - } - return -} - -// ============================================================================ -// XML deserialization types (internal) -// ============================================================================ - -type xmlEdmx struct { - XMLName xml.Name `xml:"Edmx"` - Version string `xml:"Version,attr"` - DataServices []xmlDataServices `xml:"DataServices"` -} - -type xmlDataServices struct { - Schemas []xmlSchema `xml:"Schema"` -} - -type xmlSchema struct { - Namespace string `xml:"Namespace,attr"` - EntityTypes []xmlEntityType `xml:"EntityType"` - EnumTypes []xmlEnumType `xml:"EnumType"` - EntityContainers []xmlEntityContainer `xml:"EntityContainer"` - Actions []xmlAction `xml:"Action"` - Functions []xmlAction `xml:"Function"` -} - -type xmlEntityType struct { - Name string `xml:"Name,attr"` - BaseType string `xml:"BaseType,attr"` - Abstract string `xml:"Abstract,attr"` - OpenType string `xml:"OpenType,attr"` - Key *xmlKey `xml:"Key"` - Properties []xmlProperty `xml:"Property"` - NavigationProperties []xmlNavigationProperty `xml:"NavigationProperty"` - Documentation *xmlDocumentation `xml:"Documentation"` - Annotations []xmlAnnotation `xml:"Annotation"` -} - -type xmlKey struct { - PropertyRefs []xmlPropertyRef `xml:"PropertyRef"` -} - -type xmlPropertyRef struct { - Name string `xml:"Name,attr"` -} - -type xmlProperty struct { - Name string `xml:"Name,attr"` - Type string `xml:"Type,attr"` - Nullable string `xml:"Nullable,attr"` - MaxLength string `xml:"MaxLength,attr"` - Scale string `xml:"Scale,attr"` - Annotations []xmlAnnotation `xml:"Annotation"` -} - -type xmlNavigationProperty struct { - Name string `xml:"Name,attr"` - Type string `xml:"Type,attr"` // OData4 - Partner string `xml:"Partner,attr"` // OData4 - ContainsTarget string `xml:"ContainsTarget,attr"` // OData4: contained nav target (e.g. Person.Trips) - Relationship string `xml:"Relationship,attr"` // OData3 - FromRole string `xml:"FromRole,attr"` // OData3 - ToRole string `xml:"ToRole,attr"` // OData3 -} - -type xmlDocumentation struct { - Summary string `xml:"Summary"` - LongDescription string `xml:"LongDescription"` -} - -type xmlAnnotation struct { - Term string `xml:"Term,attr"` - String string `xml:"String,attr"` - Bool string `xml:"Bool,attr"` -} - -type xmlEntityContainer struct { - Name string `xml:"Name,attr"` - EntitySets []xmlEntitySet `xml:"EntitySet"` - FunctionImports []xmlFunctionImport `xml:"FunctionImport"` -} - -type xmlEntitySet struct { - Name string `xml:"Name,attr"` - EntityType string `xml:"EntityType,attr"` - Annotations []xmlCapabilitiesAnnotation `xml:"Annotation"` -} - -// xmlCapabilitiesAnnotation captures the bits of OData V1 Capabilities -// annotations we care about. The wrapping contains -// and (sometimes) -// -// Trips. -type xmlCapabilitiesAnnotation struct { - Term string `xml:"Term,attr"` - Record *xmlCapabilitiesRecord `xml:"Record"` -} - -type xmlCapabilitiesRecord struct { - PropertyValues []xmlCapabilitiesPropertyValue `xml:"PropertyValue"` -} - -type xmlCapabilitiesPropertyValue struct { - Property string `xml:"Property,attr"` - Bool string `xml:"Bool,attr"` - Collection *xmlCapabilitiesCollection `xml:"Collection"` -} - -type xmlCapabilitiesCollection struct { - NavigationPropertyPaths []string `xml:"NavigationPropertyPath"` - PropertyPaths []string `xml:"PropertyPath"` -} - -type xmlFunctionImport struct { - Name string `xml:"Name,attr"` - ReturnType string `xml:"ReturnType,attr"` - Parameters []xmlActionParam `xml:"Parameter"` -} - -type xmlAction struct { - Name string `xml:"Name,attr"` - IsBound string `xml:"IsBound,attr"` - ReturnType *xmlReturnType `xml:"ReturnType"` - Parameters []xmlActionParam `xml:"Parameter"` -} - -type xmlReturnType struct { - Type string `xml:"Type,attr"` - Nullable string `xml:"Nullable,attr"` -} - -type xmlActionParam struct { - Name string `xml:"Name,attr"` - Type string `xml:"Type,attr"` - Nullable string `xml:"Nullable,attr"` -} - -type xmlEnumType struct { - Name string `xml:"Name,attr"` - Members []xmlEnumMember `xml:"Member"` + return types.ParseEdmx(metadataXML) } -type xmlEnumMember struct { - Name string `xml:"Name,attr"` - Value string `xml:"Value,attr"` +// resolveNavType delegates to types.ResolveNavType (kept for test compatibility). +func resolveNavType(t string) (string, bool) { + return types.ResolveNavType(t) } diff --git a/sdk/mpr/parser_misc.go b/sdk/mpr/parser_misc.go index 6e4cdb30..662aa404 100644 --- a/sdk/mpr/parser_misc.go +++ b/sdk/mpr/parser_misc.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strings" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/javaactions" "github.com/mendixlabs/mxcli/sdk/pages" @@ -80,7 +81,7 @@ func (r *Reader) parseSnippet(unitID, containerID string, contents []byte) (*pag } // parseJavaAction parses Java action contents from BSON. -func (r *Reader) parseJavaAction(unitID, containerID string, contents []byte) (*JavaAction, error) { +func (r *Reader) parseJavaAction(unitID, containerID string, contents []byte) (*types.JavaAction, error) { contents, err := r.resolveContents(unitID, contents) if err != nil { return nil, err @@ -91,7 +92,7 @@ func (r *Reader) parseJavaAction(unitID, containerID string, contents []byte) (* return nil, fmt.Errorf("failed to unmarshal BSON: %w", err) } - ja := &JavaAction{} + ja := &types.JavaAction{} ja.ID = model.ID(unitID) ja.TypeName = "JavaActions$JavaAction" ja.ContainerID = model.ID(containerID) @@ -137,7 +138,7 @@ func WriteJSON(element any) ([]byte, error) { } // parseJavaScriptAction parses JavaScript action contents from BSON. -func (r *Reader) parseJavaScriptAction(unitID, containerID string, contents []byte) (*JavaScriptAction, error) { +func (r *Reader) parseJavaScriptAction(unitID, containerID string, contents []byte) (*types.JavaScriptAction, error) { contents, err := r.resolveContents(unitID, contents) if err != nil { return nil, err @@ -148,7 +149,7 @@ func (r *Reader) parseJavaScriptAction(unitID, containerID string, contents []by return nil, fmt.Errorf("failed to unmarshal BSON: %w", err) } - jsa := &JavaScriptAction{} + jsa := &types.JavaScriptAction{} jsa.ID = model.ID(unitID) jsa.TypeName = "JavaScriptActions$JavaScriptAction" jsa.ContainerID = model.ID(containerID) @@ -249,7 +250,7 @@ func (r *Reader) parseJavaScriptAction(unitID, containerID string, contents []by } // ReadJavaScriptActionByName reads a JavaScript action by qualified name (Module.ActionName). -func (r *Reader) ReadJavaScriptActionByName(qualifiedName string) (*JavaScriptAction, error) { +func (r *Reader) ReadJavaScriptActionByName(qualifiedName string) (*types.JavaScriptAction, error) { units, err := r.listUnitsByType("JavaScriptActions$JavaScriptAction") if err != nil { return nil, err @@ -363,7 +364,7 @@ func (r *Reader) parsePageTemplate(unitID, containerID string, contents []byte) } // parseNavigationDocument parses navigation document contents from BSON. -func (r *Reader) parseNavigationDocument(unitID, containerID string, contents []byte) (*NavigationDocument, error) { +func (r *Reader) parseNavigationDocument(unitID, containerID string, contents []byte) (*types.NavigationDocument, error) { contents, err := r.resolveContents(unitID, contents) if err != nil { return nil, err @@ -374,7 +375,7 @@ func (r *Reader) parseNavigationDocument(unitID, containerID string, contents [] return nil, fmt.Errorf("failed to unmarshal BSON: %w", err) } - nav := &NavigationDocument{} + nav := &types.NavigationDocument{} nav.ID = model.ID(unitID) nav.TypeName = "Navigation$NavigationDocument" nav.ContainerID = model.ID(containerID) @@ -399,9 +400,9 @@ func (r *Reader) parseNavigationDocument(unitID, containerID string, contents [] } // parseNavigationProfile parses a single navigation profile from BSON. -func parseNavigationProfile(raw map[string]any) *NavigationProfile { +func parseNavigationProfile(raw map[string]any) *types.NavigationProfile { typeName := extractString(raw["$Type"]) - profile := &NavigationProfile{ + profile := &types.NavigationProfile{ Name: extractString(raw["Name"]), Kind: extractString(raw["Kind"]), } @@ -413,13 +414,13 @@ func parseNavigationProfile(raw map[string]any) *NavigationProfile { page := extractString(hp["HomePagePage"]) nanoflow := extractString(hp["HomePageNanoflow"]) if page != "" || nanoflow != "" { - profile.HomePage = &NavHomePage{Page: page, Microflow: nanoflow} + profile.HomePage = &types.NavHomePage{Page: page, Microflow: nanoflow} } } // Native role-based home pages for _, item := range extractBsonArray(raw["RoleBasedNativeHomePages"]) { if rbMap, ok := item.(map[string]any); ok { - rbh := &NavRoleBasedHome{ + rbh := &types.NavRoleBasedHome{ UserRole: extractString(rbMap["UserRole"]), Page: extractString(rbMap["HomePagePage"]), Microflow: extractString(rbMap["HomePageNanoflow"]), @@ -445,13 +446,13 @@ func parseNavigationProfile(raw map[string]any) *NavigationProfile { page := extractString(hp["Page"]) mf := extractString(hp["Microflow"]) if page != "" || mf != "" { - profile.HomePage = &NavHomePage{Page: page, Microflow: mf} + profile.HomePage = &types.NavHomePage{Page: page, Microflow: mf} } } // Role-based home pages (stored as "HomeItems") for _, item := range extractBsonArray(raw["HomeItems"]) { if rbMap, ok := item.(map[string]any); ok { - rbh := &NavRoleBasedHome{ + rbh := &types.NavRoleBasedHome{ UserRole: extractString(rbMap["UserRole"]), Page: extractString(rbMap["Page"]), Microflow: extractString(rbMap["Microflow"]), @@ -488,7 +489,7 @@ func parseNavigationProfile(raw map[string]any) *NavigationProfile { // Offline entity configs (both web and native) for _, item := range extractBsonArray(raw["OfflineEntityConfigs"]) { if oeMap, ok := item.(map[string]any); ok { - oe := &NavOfflineEntity{ + oe := &types.NavOfflineEntity{ Entity: extractString(oeMap["Entity"]), SyncMode: extractString(oeMap["SyncMode"]), Constraint: extractString(oeMap["Constraint"]), @@ -503,8 +504,8 @@ func parseNavigationProfile(raw map[string]any) *NavigationProfile { } // parseNavMenuItem parses a Menus$MenuItem from BSON. -func parseNavMenuItem(raw map[string]any) *NavMenuItem { - mi := &NavMenuItem{} +func parseNavMenuItem(raw map[string]any) *types.NavMenuItem { + mi := &types.NavMenuItem{} // Extract caption text (Caption → Items → first Translation → Text) if caption, ok := raw["Caption"].(map[string]any); ok { @@ -552,8 +553,8 @@ func parseNavMenuItem(raw map[string]any) *NavMenuItem { } // parseNavMenuItemFromBottomBar parses a NativePages$BottomBarItem as a NavMenuItem. -func parseNavMenuItemFromBottomBar(raw map[string]any) *NavMenuItem { - mi := &NavMenuItem{} +func parseNavMenuItemFromBottomBar(raw map[string]any) *types.NavMenuItem { + mi := &types.NavMenuItem{} if caption, ok := raw["Caption"].(map[string]any); ok { mi.Caption = extractTextFromBson(caption) } @@ -589,7 +590,7 @@ func extractTextFromBson(raw map[string]any) string { } // parseImageCollection parses image collection contents from BSON. -func (r *Reader) parseImageCollection(unitID, containerID string, contents []byte) (*ImageCollection, error) { +func (r *Reader) parseImageCollection(unitID, containerID string, contents []byte) (*types.ImageCollection, error) { contents, err := r.resolveContents(unitID, contents) if err != nil { return nil, err @@ -600,7 +601,7 @@ func (r *Reader) parseImageCollection(unitID, containerID string, contents []byt return nil, fmt.Errorf("failed to unmarshal BSON: %w", err) } - ic := &ImageCollection{} + ic := &types.ImageCollection{} ic.ID = model.ID(unitID) ic.TypeName = "Images$ImageCollection" ic.ContainerID = model.ID(containerID) @@ -619,7 +620,7 @@ func (r *Reader) parseImageCollection(unitID, containerID string, contents []byt if images, ok := raw["Images"].(bson.A); ok { for _, img := range images { if imgMap, ok := img.(map[string]any); ok { - image := Image{} + image := types.Image{} if id := extractID(imgMap["$ID"]); id != "" { image.ID = model.ID(id) } @@ -643,7 +644,7 @@ func (r *Reader) parseImageCollection(unitID, containerID string, contents []byt } // parseJsonStructure parses JSON structure contents from BSON. -func (r *Reader) parseJsonStructure(unitID, containerID string, contents []byte) (*JsonStructure, error) { +func (r *Reader) parseJsonStructure(unitID, containerID string, contents []byte) (*types.JsonStructure, error) { contents, err := r.resolveContents(unitID, contents) if err != nil { return nil, err @@ -654,7 +655,7 @@ func (r *Reader) parseJsonStructure(unitID, containerID string, contents []byte) return nil, fmt.Errorf("failed to unmarshal BSON: %w", err) } - js := &JsonStructure{} + js := &types.JsonStructure{} js.ID = model.ID(unitID) js.TypeName = "JsonStructures$JsonStructure" js.ContainerID = model.ID(containerID) @@ -688,8 +689,8 @@ func (r *Reader) parseJsonStructure(unitID, containerID string, contents []byte) } // parseJsonElement recursively parses a JsonStructures$JsonElement from BSON. -func parseJsonElement(raw map[string]any) *JsonElement { - elem := &JsonElement{ +func parseJsonElement(raw map[string]any) *types.JsonElement { + elem := &types.JsonElement{ MaxLength: -1, FractionDigits: -1, TotalDigits: -1, diff --git a/sdk/mpr/reader.go b/sdk/mpr/reader.go index b9e7f9dd..50db5fae 100644 --- a/sdk/mpr/reader.go +++ b/sdk/mpr/reader.go @@ -11,6 +11,7 @@ import ( "os" "path/filepath" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/mpr/version" _ "modernc.org/sqlite" @@ -241,19 +242,9 @@ func (r *Reader) GetMendixVersion() (string, error) { return version, nil } -// blobToUUID converts a 16-byte blob to a UUID string using Microsoft GUID format. -// The first 3 groups are little-endian (byte-swapped), last 2 groups are big-endian. -// This is the standard format used by Mendix for all UUID representations. +// blobToUUID delegates to types.BlobToUUID. func blobToUUID(blob []byte) string { - if len(blob) != 16 { - return hex.EncodeToString(blob) - } - return fmt.Sprintf("%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", - blob[3], blob[2], blob[1], blob[0], - blob[5], blob[4], - blob[7], blob[6], - blob[8], blob[9], - blob[10], blob[11], blob[12], blob[13], blob[14], blob[15]) + return types.BlobToUUID(blob) } // blobToUUIDSwapped converts a 16-byte blob to a UUID string using Microsoft GUID format. diff --git a/sdk/mpr/reader_types.go b/sdk/mpr/reader_types.go index 485fc67c..6b9099a9 100644 --- a/sdk/mpr/reader_types.go +++ b/sdk/mpr/reader_types.go @@ -7,39 +7,21 @@ import ( "encoding/json" "fmt" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/javaactions" "github.com/mendixlabs/mxcli/sdk/pages" "go.mongodb.org/mongo-driver/bson" ) -// JavaAction represents a Java action. -type JavaAction struct { - model.BaseElement - ContainerID model.ID `json:"containerId"` - Name string `json:"name"` - Documentation string `json:"documentation,omitempty"` -} - -// GetName returns the Java action's name. -func (ja *JavaAction) GetName() string { - return ja.Name -} - -// GetContainerID returns the container ID. -func (ja *JavaAction) GetContainerID() model.ID { - return ja.ContainerID -} - // ListJavaActions returns all Java actions in the project. -func (r *Reader) ListJavaActions() ([]*JavaAction, error) { +func (r *Reader) ListJavaActions() ([]*types.JavaAction, error) { units, err := r.listUnitsByType("JavaActions$JavaAction") if err != nil { return nil, err } - var result []*JavaAction + var result []*types.JavaAction for _, u := range units { ja, err := r.parseJavaAction(u.ID, u.ContainerID, u.Contents) if err != nil { @@ -51,50 +33,14 @@ func (r *Reader) ListJavaActions() ([]*JavaAction, error) { return result, nil } -// JavaScriptAction represents a JavaScript action. -type JavaScriptAction struct { - model.BaseElement - ContainerID model.ID `json:"containerId"` - Name string `json:"name"` - Documentation string `json:"documentation,omitempty"` - Platform string `json:"platform,omitempty"` - Excluded bool `json:"excluded"` - ExportLevel string `json:"exportLevel,omitempty"` - ActionDefaultReturnName string `json:"actionDefaultReturnName,omitempty"` - ReturnType javaactions.CodeActionReturnType `json:"returnType,omitempty"` - Parameters []*javaactions.JavaActionParameter `json:"parameters,omitempty"` - TypeParameters []*javaactions.TypeParameterDef `json:"typeParameters,omitempty"` - MicroflowActionInfo *javaactions.MicroflowActionInfo `json:"microflowActionInfo,omitempty"` -} - -// GetName returns the JavaScript action's name. -func (jsa *JavaScriptAction) GetName() string { - return jsa.Name -} - -// GetContainerID returns the container ID. -func (jsa *JavaScriptAction) GetContainerID() model.ID { - return jsa.ContainerID -} - -// FindTypeParameterName looks up a type parameter name by its ID. -func (jsa *JavaScriptAction) FindTypeParameterName(id model.ID) string { - for _, tp := range jsa.TypeParameters { - if tp.ID == id { - return tp.Name - } - } - return "" -} - // ListJavaScriptActions returns all JavaScript actions in the project. -func (r *Reader) ListJavaScriptActions() ([]*JavaScriptAction, error) { +func (r *Reader) ListJavaScriptActions() ([]*types.JavaScriptAction, error) { units, err := r.listUnitsByType("JavaScriptActions$JavaScriptAction") if err != nil { return nil, err } - var result []*JavaScriptAction + var result []*types.JavaScriptAction for _, u := range units { jsa, err := r.parseJavaScriptAction(u.ID, u.ContainerID, u.Contents) if err != nil { @@ -144,74 +90,14 @@ func (r *Reader) ListPageTemplates() ([]*pages.PageTemplate, error) { return result, nil } -// NavigationDocument represents a navigation document. -type NavigationDocument struct { - model.BaseElement - ContainerID model.ID `json:"containerId"` - Name string `json:"name"` - Profiles []*NavigationProfile `json:"profiles,omitempty"` -} - -// GetName returns the navigation document's name. -func (nd *NavigationDocument) GetName() string { - return nd.Name -} - -// GetContainerID returns the container ID. -func (nd *NavigationDocument) GetContainerID() model.ID { - return nd.ContainerID -} - -// NavigationProfile represents a navigation profile (web or native). -type NavigationProfile struct { - Name string `json:"name"` - Kind string `json:"kind"` // Responsive, Phone, Tablet, etc. - IsNative bool `json:"isNative"` - HomePage *NavHomePage `json:"homePage,omitempty"` - RoleBasedHomePages []*NavRoleBasedHome `json:"roleBasedHomePages,omitempty"` - LoginPage string `json:"loginPage,omitempty"` // qualified page name - NotFoundPage string `json:"notFoundPage,omitempty"` // qualified page name - MenuItems []*NavMenuItem `json:"menuItems,omitempty"` - OfflineEntities []*NavOfflineEntity `json:"offlineEntities,omitempty"` -} - -// NavHomePage represents a default home page (page or microflow). -type NavHomePage struct { - Page string `json:"page,omitempty"` // qualified page name - Microflow string `json:"microflow,omitempty"` // qualified microflow name -} - -// NavRoleBasedHome represents a role-specific home page override. -type NavRoleBasedHome struct { - UserRole string `json:"userRole"` // qualified user role name - Page string `json:"page,omitempty"` // qualified page name - Microflow string `json:"microflow,omitempty"` // qualified microflow name -} - -// NavMenuItem represents a menu item (recursive for sub-menus). -type NavMenuItem struct { - Caption string `json:"caption"` - Page string `json:"page,omitempty"` // target page qualified name - Microflow string `json:"microflow,omitempty"` // target microflow qualified name - ActionType string `json:"actionType,omitempty"` // PageAction, MicroflowAction, NoAction, OpenLinkAction - Items []*NavMenuItem `json:"items,omitempty"` -} - -// NavOfflineEntity represents an offline entity sync configuration. -type NavOfflineEntity struct { - Entity string `json:"entity"` // qualified entity name - SyncMode string `json:"syncMode"` // All, Constrained, Never, etc. - Constraint string `json:"constraint,omitempty"` // XPath -} - // ListNavigationDocuments returns all navigation documents in the project. -func (r *Reader) ListNavigationDocuments() ([]*NavigationDocument, error) { +func (r *Reader) ListNavigationDocuments() ([]*types.NavigationDocument, error) { units, err := r.listUnitsByType("Navigation$NavigationDocument") if err != nil { return nil, err } - var result []*NavigationDocument + var result []*types.NavigationDocument for _, u := range units { nav, err := r.parseNavigationDocument(u.ID, u.ContainerID, u.Contents) if err != nil { @@ -224,7 +110,7 @@ func (r *Reader) ListNavigationDocuments() ([]*NavigationDocument, error) { } // GetNavigation returns the project's navigation document (singleton). -func (r *Reader) GetNavigation() (*NavigationDocument, error) { +func (r *Reader) GetNavigation() (*types.NavigationDocument, error) { docs, err := r.ListNavigationDocuments() if err != nil { return nil, err @@ -235,42 +121,14 @@ func (r *Reader) GetNavigation() (*NavigationDocument, error) { return docs[0], nil } -// ImageCollection represents an image collection. -type ImageCollection struct { - model.BaseElement - ContainerID model.ID `json:"containerId"` - Name string `json:"name"` - ExportLevel string `json:"exportLevel,omitempty"` - Documentation string `json:"documentation,omitempty"` - Images []Image `json:"images,omitempty"` -} - -// Image represents an image in a collection. -type Image struct { - ID model.ID `json:"id"` - Name string `json:"name"` - Data []byte `json:"data,omitempty"` // raw image bytes - Format string `json:"format,omitempty"` // "Png", "Svg", "Gif", "Jpeg", "Bmp" -} - -// GetName returns the image collection's name. -func (ic *ImageCollection) GetName() string { - return ic.Name -} - -// GetContainerID returns the container ID. -func (ic *ImageCollection) GetContainerID() model.ID { - return ic.ContainerID -} - // ListImageCollections returns all image collections in the project. -func (r *Reader) ListImageCollections() ([]*ImageCollection, error) { +func (r *Reader) ListImageCollections() ([]*types.ImageCollection, error) { units, err := r.listUnitsByType("Images$ImageCollection") if err != nil { return nil, err } - var result []*ImageCollection + var result []*types.ImageCollection for _, u := range units { ic, err := r.parseImageCollection(u.ID, u.ContainerID, u.Contents) if err != nil { @@ -282,54 +140,14 @@ func (r *Reader) ListImageCollections() ([]*ImageCollection, error) { return result, nil } -// JsonStructure represents a JSON structure document. -type JsonStructure struct { - model.BaseElement - ContainerID model.ID `json:"containerId"` - Name string `json:"name"` - Documentation string `json:"documentation,omitempty"` - JsonSnippet string `json:"jsonSnippet,omitempty"` - Elements []*JsonElement `json:"elements,omitempty"` - Excluded bool `json:"excluded,omitempty"` - ExportLevel string `json:"exportLevel,omitempty"` -} - -// JsonElement represents an element in a JSON structure's element tree. -type JsonElement struct { - ExposedName string `json:"exposedName"` - ExposedItemName string `json:"exposedItemName,omitempty"` - Path string `json:"path"` - ElementType string `json:"elementType"` // "Object", "Array", "Value", "Choice" - PrimitiveType string `json:"primitiveType"` // "String", "Integer", "Boolean", "Decimal", "Unknown" - MinOccurs int `json:"minOccurs"` - MaxOccurs int `json:"maxOccurs"` // -1 = unbounded - Nillable bool `json:"nillable,omitempty"` - IsDefaultType bool `json:"isDefaultType,omitempty"` - MaxLength int `json:"maxLength"` // -1 = unset - FractionDigits int `json:"fractionDigits"` // -1 = unset - TotalDigits int `json:"totalDigits"` // -1 = unset - OriginalValue string `json:"originalValue,omitempty"` - Children []*JsonElement `json:"children,omitempty"` -} - -// GetName returns the JSON structure's name. -func (js *JsonStructure) GetName() string { - return js.Name -} - -// GetContainerID returns the container ID. -func (js *JsonStructure) GetContainerID() model.ID { - return js.ContainerID -} - // ListJsonStructures returns all JSON structures in the project. -func (r *Reader) ListJsonStructures() ([]*JsonStructure, error) { +func (r *Reader) ListJsonStructures() ([]*types.JsonStructure, error) { units, err := r.listUnitsByType("JsonStructures$JsonStructure") if err != nil { return nil, err } - var result []*JsonStructure + var result []*types.JsonStructure for _, u := range units { js, err := r.parseJsonStructure(u.ID, u.ContainerID, u.Contents) if err != nil { @@ -342,7 +160,7 @@ func (r *Reader) ListJsonStructures() ([]*JsonStructure, error) { } // GetJsonStructureByQualifiedName retrieves a JSON structure by its qualified name (Module.Name). -func (r *Reader) GetJsonStructureByQualifiedName(moduleName, name string) (*JsonStructure, error) { +func (r *Reader) GetJsonStructureByQualifiedName(moduleName, name string) (*types.JsonStructure, error) { all, err := r.ListJsonStructures() if err != nil { return nil, err @@ -369,38 +187,22 @@ func (r *Reader) GetJsonStructureByQualifiedName(moduleName, name string) (*Json return nil, fmt.Errorf("JSON structure %s.%s not found", moduleName, name) } -// UnitInfo contains basic information about a unit. -type UnitInfo struct { - ID model.ID - ContainerID model.ID - ContainmentName string - Type string -} - -// RawUnit holds raw unit data with BSON contents. -type RawUnit struct { - ID model.ID - ContainerID model.ID - Type string - Contents []byte -} - // ListRawUnitsByType returns all raw units matching the given type prefix, // including their BSON contents. This is useful for scanning BSON directly // without full parsing. -func (r *Reader) ListRawUnitsByType(typePrefix string) ([]*RawUnit, error) { +func (r *Reader) ListRawUnitsByType(typePrefix string) ([]*types.RawUnit, error) { units, err := r.listUnitsByType(typePrefix) if err != nil { return nil, err } - var result []*RawUnit + var result []*types.RawUnit for _, u := range units { contents, err := r.resolveContents(u.ID, u.Contents) if err != nil { continue } - result = append(result, &RawUnit{ + result = append(result, &types.RawUnit{ ID: model.ID(u.ID), ContainerID: model.ID(u.ContainerID), Type: u.Type, @@ -411,15 +213,15 @@ func (r *Reader) ListRawUnitsByType(typePrefix string) ([]*RawUnit, error) { } // ListUnits returns all units with their IDs and types. -func (r *Reader) ListUnits() ([]*UnitInfo, error) { +func (r *Reader) ListUnits() ([]*types.UnitInfo, error) { units, err := r.listUnitsByType("") if err != nil { return nil, err } - var result []*UnitInfo + var result []*types.UnitInfo for _, u := range units { - result = append(result, &UnitInfo{ + result = append(result, &types.UnitInfo{ ID: model.ID(u.ID), ContainerID: model.ID(u.ContainerID), ContainmentName: u.ContainmentName, @@ -430,21 +232,14 @@ func (r *Reader) ListUnits() ([]*UnitInfo, error) { return result, nil } -// FolderInfo contains information about a project folder. -type FolderInfo struct { - ID model.ID - ContainerID model.ID - Name string -} - // ListFolders returns all project folders with their names. -func (r *Reader) ListFolders() ([]*FolderInfo, error) { +func (r *Reader) ListFolders() ([]*types.FolderInfo, error) { units, err := r.listUnitsByType("Projects$Folder") if err != nil { return nil, err } - var result []*FolderInfo + var result []*types.FolderInfo for _, u := range units { name := "" if len(u.Contents) > 0 { @@ -455,7 +250,7 @@ func (r *Reader) ListFolders() ([]*FolderInfo, error) { } } } - result = append(result, &FolderInfo{ + result = append(result, &types.FolderInfo{ ID: model.ID(u.ID), ContainerID: model.ID(u.ContainerID), Name: name, diff --git a/sdk/mpr/utils.go b/sdk/mpr/utils.go index ffca1a2a..1acc6739 100644 --- a/sdk/mpr/utils.go +++ b/sdk/mpr/utils.go @@ -3,66 +3,48 @@ package mpr import ( - "crypto/sha256" - "fmt" - + "github.com/mendixlabs/mxcli/mdl/types" "go.mongodb.org/mongo-driver/bson/primitive" ) // GenerateID generates a new unique ID for model elements. func GenerateID() string { - return generateUUID() + return types.GenerateID() } // GenerateDeterministicID generates a stable UUID from a seed string. -// Used for System module entities that aren't in the MPR but need consistent IDs. func GenerateDeterministicID(seed string) string { - h := sha256.Sum256([]byte(seed)) - return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", - h[0:4], h[4:6], h[6:8], h[8:10], h[10:16]) + return types.GenerateDeterministicID(seed) } // BlobToUUID converts a binary ID blob to a UUID string. func BlobToUUID(data []byte) string { - return blobToUUID(data) + return types.BlobToUUID(data) } // IDToBsonBinary converts a UUID string to a BSON binary value. func IDToBsonBinary(id string) primitive.Binary { - return idToBsonBinary(id) + blob := types.UUIDToBlob(id) + if blob == nil || len(blob) != 16 { + blob = types.UUIDToBlob(types.GenerateID()) + } + return primitive.Binary{ + Subtype: 0x00, + Data: blob, + } } // BsonBinaryToID converts a BSON binary value to a UUID string. func BsonBinaryToID(bin primitive.Binary) string { - return BlobToUUID(bin.Data) + return types.BlobToUUID(bin.Data) } // Hash computes a hash for content (used for content deduplication). func Hash(content []byte) string { - // Simple hash for now - could use crypto/sha256 for better hashing - var sum uint64 - for i, b := range content { - sum += uint64(b) * uint64(i+1) - } - return fmt.Sprintf("%016x", sum) + return types.Hash(content) } // ValidateID checks if an ID is valid. func ValidateID(id string) bool { - if len(id) != 36 { - return false - } - // Check UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx - for i, c := range id { - if i == 8 || i == 13 || i == 18 || i == 23 { - if c != '-' { - return false - } - } else { - if !((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')) { - return false - } - } - } - return true + return types.ValidateID(id) } diff --git a/sdk/mpr/writer_core.go b/sdk/mpr/writer_core.go index 62e1fada..2f191150 100644 --- a/sdk/mpr/writer_core.go +++ b/sdk/mpr/writer_core.go @@ -3,26 +3,23 @@ package mpr import ( - "crypto/rand" "crypto/sha256" "database/sql" "encoding/base64" - "encoding/hex" "fmt" "os" "path/filepath" - "strings" + "github.com/mendixlabs/mxcli/mdl/types" "go.mongodb.org/mongo-driver/bson/primitive" ) // idToBsonBinary converts a UUID string to BSON Binary format. // Mendix stores IDs as Binary with Subtype 0. func idToBsonBinary(id string) primitive.Binary { - blob := uuidToBlob(id) + blob := types.UUIDToBlob(id) if blob == nil || len(blob) != 16 { - // Generate a new UUID if the provided one is invalid - blob = uuidToBlob(generateUUID()) + blob = types.UUIDToBlob(types.GenerateID()) } return primitive.Binary{ Subtype: 0x00, @@ -198,59 +195,12 @@ func (wt *WriteTransaction) cleanupTempFiles() { } } -// generateUUID generates a new UUID v4 for model elements. -// Returns format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx +// generateUUID delegates to types.GenerateID. func generateUUID() string { - b := make([]byte, 16) - _, _ = rand.Read(b) - b[6] = (b[6] & 0x0f) | 0x40 // Version 4 - b[8] = (b[8] & 0x3f) | 0x80 // Variant is 10 - - return fmt.Sprintf("%02x%02x%02x%02x-%02x%02x-%02x%02x-%02x%02x-%02x%02x%02x%02x%02x%02x", - b[0], b[1], b[2], b[3], - b[4], b[5], - b[6], b[7], - b[8], b[9], - b[10], b[11], b[12], b[13], b[14], b[15]) + return types.GenerateID() } -// uuidToBlob converts a UUID string to a 16-byte blob in Microsoft GUID format. -// UUID format: xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx -// Microsoft GUID format byte-swaps the first 3 groups (little-endian): -// - First 4 bytes: reversed -// - Next 2 bytes: reversed -// - Next 2 bytes: reversed -// - Last 8 bytes: unchanged +// uuidToBlob delegates to types.UUIDToBlob. func uuidToBlob(uuid string) []byte { - if uuid == "" { - return nil - } - // Remove dashes - var clean strings.Builder - for _, c := range uuid { - if c != '-' { - clean.WriteString(string(c)) - } - } - // Decode hex to bytes - decoded, err := hex.DecodeString(clean.String()) - if err != nil || len(decoded) != 16 { - return nil - } - // Swap bytes to Microsoft GUID format - blob := make([]byte, 16) - // First 4 bytes: reversed - blob[0] = decoded[3] - blob[1] = decoded[2] - blob[2] = decoded[1] - blob[3] = decoded[0] - // Next 2 bytes: reversed - blob[4] = decoded[5] - blob[5] = decoded[4] - // Next 2 bytes: reversed - blob[6] = decoded[7] - blob[7] = decoded[6] - // Last 8 bytes: unchanged - copy(blob[8:], decoded[8:]) - return blob + return types.UUIDToBlob(uuid) } diff --git a/sdk/mpr/writer_imagecollection.go b/sdk/mpr/writer_imagecollection.go index 88a777af..42389328 100644 --- a/sdk/mpr/writer_imagecollection.go +++ b/sdk/mpr/writer_imagecollection.go @@ -3,13 +3,14 @@ package mpr import ( + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" ) // CreateImageCollection creates a new empty image collection unit in the MPR. -func (w *Writer) CreateImageCollection(ic *ImageCollection) error { +func (w *Writer) CreateImageCollection(ic *types.ImageCollection) error { if ic.ID == "" { ic.ID = model.ID(generateUUID()) } @@ -31,7 +32,7 @@ func (w *Writer) DeleteImageCollection(id string) error { return w.deleteUnit(id) } -func serializeImageCollection(ic *ImageCollection) ([]byte, error) { +func serializeImageCollection(ic *types.ImageCollection) ([]byte, error) { // Images array always starts with the array marker int32(3) images := bson.A{int32(3)} for i := range ic.Images { diff --git a/sdk/mpr/writer_imagecollection_test.go b/sdk/mpr/writer_imagecollection_test.go index d7e0442e..22d89490 100644 --- a/sdk/mpr/writer_imagecollection_test.go +++ b/sdk/mpr/writer_imagecollection_test.go @@ -5,12 +5,13 @@ package mpr import ( "testing" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "go.mongodb.org/mongo-driver/bson" ) func TestSerializeImageCollection_EmptyImages(t *testing.T) { - ic := &ImageCollection{ + ic := &types.ImageCollection{ BaseElement: model.BaseElement{ID: "ic-test-1"}, ContainerID: model.ID("module-id-1"), Name: "TestIcons", @@ -58,7 +59,7 @@ func TestSerializeImageCollection_EmptyImages(t *testing.T) { } func TestSerializeImageCollection_DefaultExportLevel(t *testing.T) { - ic := &ImageCollection{ + ic := &types.ImageCollection{ BaseElement: model.BaseElement{ID: "ic-test-2"}, ContainerID: model.ID("module-id-1"), Name: "Icons", diff --git a/sdk/mpr/writer_jsonstructure.go b/sdk/mpr/writer_jsonstructure.go index 47a89006..d21e6b47 100644 --- a/sdk/mpr/writer_jsonstructure.go +++ b/sdk/mpr/writer_jsonstructure.go @@ -3,65 +3,21 @@ package mpr import ( - "bytes" - "encoding/json" - "fmt" - "math" - "regexp" - "strings" - "unicode" - + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "go.mongodb.org/mongo-driver/bson" ) -// iso8601Pattern matches common ISO 8601 datetime strings that Mendix Studio Pro -// recognizes as DateTime primitive types in JSON structures. -var iso8601Pattern = regexp.MustCompile( - `^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?$`, -) - -// PrettyPrintJSON re-formats a JSON string with standard indentation. -// Returns the original string if it is not valid JSON. -func PrettyPrintJSON(s string) string { - var buf bytes.Buffer - if err := json.Indent(&buf, []byte(s), "", " "); err != nil { - return s - } - return buf.String() -} +// PrettyPrintJSON delegates to types.PrettyPrintJSON. +func PrettyPrintJSON(s string) string { return types.PrettyPrintJSON(s) } -// normalizeDateTimeValue pads fractional seconds to 7 digits to match -// Studio Pro's .NET DateTime format (e.g., "2015-05-22T14:56:29.000Z" → "2015-05-22T14:56:29.0000000Z"). -func normalizeDateTimeValue(s string) string { - // Find the decimal point after seconds - dotIdx := strings.Index(s, ".") - if dotIdx == -1 { - // No fractional part — insert .0000000 before timezone suffix - if idx := strings.IndexAny(s, "Z+-"); idx > 0 { - return s[:idx] + ".0000000" + s[idx:] - } - return s - } - // Find where fractional digits end (at Z, +, - or end of string) - fracEnd := len(s) - for i := dotIdx + 1; i < len(s); i++ { - if s[i] < '0' || s[i] > '9' { - fracEnd = i - break - } - } - frac := s[dotIdx+1 : fracEnd] - if len(frac) < 7 { - frac = frac + strings.Repeat("0", 7-len(frac)) - } else { - frac = frac[:7] - } - return s[:dotIdx+1] + frac + s[fracEnd:] +// BuildJsonElementsFromSnippet delegates to types.BuildJsonElementsFromSnippet. +func BuildJsonElementsFromSnippet(snippet string, customNameMap map[string]string) ([]*types.JsonElement, error) { + return types.BuildJsonElementsFromSnippet(snippet, customNameMap) } // CreateJsonStructure creates a new JSON structure unit in the MPR. -func (w *Writer) CreateJsonStructure(js *JsonStructure) error { +func (w *Writer) CreateJsonStructure(js *types.JsonStructure) error { if js.ID == "" { js.ID = model.ID(generateUUID()) } @@ -83,7 +39,7 @@ func (w *Writer) DeleteJsonStructure(id string) error { return w.deleteUnit(id) } -func serializeJsonStructure(js *JsonStructure) ([]byte, error) { +func serializeJsonStructure(js *types.JsonStructure) ([]byte, error) { elements := bson.A{int32(2)} for _, elem := range js.Elements { elements = append(elements, serializeJsonElement(elem)) @@ -106,7 +62,7 @@ func serializeJsonStructure(js *JsonStructure) ([]byte, error) { // serializeJsonElement serializes a single JsonElement to BSON. // Note: JsonStructures$JsonElement uses int32 for numeric properties (MinOccurs, MaxOccurs, etc.), // unlike most other Mendix document types which use int64. Verified against Studio Pro-generated BSON. -func serializeJsonElement(elem *JsonElement) bson.D { +func serializeJsonElement(elem *types.JsonElement) bson.D { children := bson.A{int32(2)} for _, child := range elem.Children { children = append(children, serializeJsonElement(child)) @@ -134,318 +90,4 @@ func serializeJsonElement(elem *JsonElement) bson.D { } } -// BuildJsonElementsFromSnippet parses a JSON snippet and builds the element tree -// that Mendix Studio Pro would generate. Returns the root element. -// The optional customNameMap maps JSON keys to custom ExposedNames (as set in -// Studio Pro's "Custom name" column). Unmapped keys use auto-generated names. -func BuildJsonElementsFromSnippet(snippet string, customNameMap map[string]string) ([]*JsonElement, error) { - // Validate JSON - if !json.Valid([]byte(snippet)) { - return nil, fmt.Errorf("invalid JSON snippet") - } - - // Detect root type (object or array) - dec := json.NewDecoder(strings.NewReader(snippet)) - tok, err := dec.Token() - if err != nil { - return nil, fmt.Errorf("failed to parse JSON snippet: %w", err) - } - - b := &snippetBuilder{customNameMap: customNameMap} - tracker := &nameTracker{seen: make(map[string]int)} - - switch tok { - case json.Delim('{'): - root := b.buildElementFromRawObject("Root", "(Object)", snippet, tracker) - root.MinOccurs = 0 - root.MaxOccurs = 0 - root.Nillable = true - return []*JsonElement{root}, nil - - case json.Delim('['): - root := b.buildElementFromRawRootArray("Root", "(Array)", snippet, tracker) - root.MinOccurs = 0 - root.MaxOccurs = 0 - root.Nillable = true - return []*JsonElement{root}, nil - - default: - return nil, fmt.Errorf("JSON snippet must be an object or array at root level") - } -} - -// snippetBuilder holds state for building the element tree from a JSON snippet. -type snippetBuilder struct { - customNameMap map[string]string // JSON key → custom ExposedName -} - -// reservedExposedNames are element names that Mendix rejects as ExposedName values. -// Studio Pro handles these by prefixing with underscore and keeping original case. -var reservedExposedNames = map[string]bool{ - "Id": true, "Type": true, -} - -// resolveExposedName returns the custom name if mapped, otherwise capitalizes the JSON key. -// Reserved names (Id, Type, Name) are prefixed with underscore to match Studio Pro behavior. -func (b *snippetBuilder) resolveExposedName(jsonKey string) string { - if b.customNameMap != nil { - if custom, ok := b.customNameMap[jsonKey]; ok { - return custom - } - } - name := capitalizeFirst(jsonKey) - if reservedExposedNames[name] { - return "_" + jsonKey - } - return name -} - -// nameTracker tracks used ExposedNames at each level to handle duplicates. -type nameTracker struct { - seen map[string]int -} - -func (t *nameTracker) uniqueName(base string) string { - t.seen[base]++ - count := t.seen[base] - if count == 1 { - return base - } - return fmt.Sprintf("%s_%d", base, count) -} - -func (t *nameTracker) child() *nameTracker { - return &nameTracker{seen: make(map[string]int)} -} - -// capitalizeFirst capitalizes the first letter of a string for ExposedName. -func capitalizeFirst(s string) string { - if s == "" { - return s - } - runes := []rune(s) - runes[0] = unicode.ToUpper(runes[0]) - return string(runes) -} - -// buildElementFromRawObject builds an Object element by decoding a raw JSON object string, -// preserving the original key order (Go's map[string]any loses order). -func (b *snippetBuilder) buildElementFromRawObject(exposedName, path, rawJSON string, tracker *nameTracker) *JsonElement { - elem := &JsonElement{ - ExposedName: exposedName, - Path: path, - ElementType: "Object", - PrimitiveType: "Unknown", - MinOccurs: 0, - MaxOccurs: 0, - Nillable: true, - MaxLength: -1, - FractionDigits: -1, - TotalDigits: -1, - } - - childTracker := tracker.child() - - // Decode with key order preserved - dec := json.NewDecoder(strings.NewReader(rawJSON)) - if _, err := dec.Token(); err != nil { // opening { - return elem - } - for dec.More() { - tok, err := dec.Token() - if err != nil { - break - } - key, ok := tok.(string) - if !ok { - continue - } - // Capture the raw value to pass down for nested objects/arrays - var rawVal json.RawMessage - if err := dec.Decode(&rawVal); err != nil { - break - } - - childName := childTracker.uniqueName(b.resolveExposedName(key)) - childPath := path + "|" + key - child := b.buildElementFromRawValue(childName, childPath, key, rawVal, childTracker) - elem.Children = append(elem.Children, child) - } - - return elem -} - -// buildElementFromRawValue inspects a json.RawMessage to determine its type and build the element. -func (b *snippetBuilder) buildElementFromRawValue(exposedName, path, jsonKey string, raw json.RawMessage, tracker *nameTracker) *JsonElement { - trimmed := strings.TrimSpace(string(raw)) - - // Object — recurse with raw JSON to preserve key order - if len(trimmed) > 0 && trimmed[0] == '{' { - return b.buildElementFromRawObject(exposedName, path, trimmed, tracker) - } - - // Array - if len(trimmed) > 0 && trimmed[0] == '[' { - return b.buildElementFromRawArray(exposedName, path, jsonKey, trimmed, tracker) - } - - // Primitive — unmarshal to determine type - var val any - json.Unmarshal(raw, &val) - - switch v := val.(type) { - case string: - primitiveType := "String" - if iso8601Pattern.MatchString(v) { - primitiveType = "DateTime" - v = normalizeDateTimeValue(v) - } - return buildValueElement(exposedName, path, primitiveType, fmt.Sprintf("%q", v)) - case float64: - // Check the raw JSON text for a decimal point — Go's %v drops ".0" from 41850.0 - if v == math.Trunc(v) && !strings.Contains(trimmed, ".") { - return buildValueElement(exposedName, path, "Integer", fmt.Sprintf("%v", int64(v))) - } - return buildValueElement(exposedName, path, "Decimal", fmt.Sprintf("%v", v)) - case bool: - return buildValueElement(exposedName, path, "Boolean", fmt.Sprintf("%v", v)) - case nil: - // JSON null → Unknown primitive type (matches Studio Pro) - return buildValueElement(exposedName, path, "Unknown", "") - default: - return buildValueElement(exposedName, path, "String", "") - } -} - -// buildElementFromRawRootArray builds a root-level Array element. -// Studio Pro names the child object "JsonObject" (not "RootItem") for root arrays. -func (b *snippetBuilder) buildElementFromRawRootArray(exposedName, path, rawJSON string, tracker *nameTracker) *JsonElement { - arrayElem := &JsonElement{ - ExposedName: exposedName, - Path: path, - ElementType: "Array", - PrimitiveType: "Unknown", - MinOccurs: 0, - MaxOccurs: 0, - Nillable: true, - MaxLength: -1, - FractionDigits: -1, - TotalDigits: -1, - } - - dec := json.NewDecoder(strings.NewReader(rawJSON)) - dec.Token() // opening [ - if dec.More() { - var firstItem json.RawMessage - dec.Decode(&firstItem) - - itemPath := path + "|(Object)" - trimmed := strings.TrimSpace(string(firstItem)) - - if len(trimmed) > 0 && trimmed[0] == '{' { - itemElem := b.buildElementFromRawObject("JsonObject", itemPath, trimmed, tracker) - itemElem.MinOccurs = 0 - itemElem.MaxOccurs = 0 - itemElem.Nillable = true - arrayElem.Children = append(arrayElem.Children, itemElem) - } else { - child := b.buildElementFromRawValue("JsonObject", itemPath, "", firstItem, tracker) - child.MinOccurs = 0 - child.MaxOccurs = 0 - arrayElem.Children = append(arrayElem.Children, child) - } - } - - return arrayElem -} - -// buildElementFromRawArray builds an Array element, using the first item's raw JSON for ordering. -// For primitive arrays (strings, numbers), Studio Pro creates a Wrapper element with a Value child. -func (b *snippetBuilder) buildElementFromRawArray(exposedName, path, jsonKey, rawJSON string, tracker *nameTracker) *JsonElement { - arrayElem := &JsonElement{ - ExposedName: exposedName, - Path: path, - ElementType: "Array", - PrimitiveType: "Unknown", - MinOccurs: 0, - MaxOccurs: 0, - Nillable: true, - MaxLength: -1, - FractionDigits: -1, - TotalDigits: -1, - } - - // Decode array and get first element as raw JSON - dec := json.NewDecoder(strings.NewReader(rawJSON)) - dec.Token() // opening [ - if dec.More() { - var firstItem json.RawMessage - dec.Decode(&firstItem) - trimmed := strings.TrimSpace(string(firstItem)) - - if len(trimmed) > 0 && trimmed[0] == '{' { - // Object array: child is NameItem object - itemName := exposedName + "Item" - itemPath := path + "|(Object)" - itemElem := b.buildElementFromRawObject(itemName, itemPath, trimmed, tracker) - itemElem.MinOccurs = 0 - itemElem.MaxOccurs = -1 - itemElem.Nillable = true - arrayElem.Children = append(arrayElem.Children, itemElem) - } else { - // Primitive array: Studio Pro wraps in a Wrapper element with singular name - // e.g., tags: ["a","b"] → Tag (Wrapper) → Value (String) - wrapperName := singularize(exposedName) - wrapperPath := path + "|(Object)" - wrapper := &JsonElement{ - ExposedName: wrapperName, - Path: wrapperPath, - ElementType: "Wrapper", - PrimitiveType: "Unknown", - MinOccurs: 0, - MaxOccurs: 0, - Nillable: true, - MaxLength: -1, - FractionDigits: -1, - TotalDigits: -1, - } - valueElem := b.buildElementFromRawValue("Value", wrapperPath+"|", jsonKey, firstItem, tracker) - valueElem.MinOccurs = 0 - valueElem.MaxOccurs = 0 - wrapper.Children = append(wrapper.Children, valueElem) - arrayElem.Children = append(arrayElem.Children, wrapper) - } - } - - return arrayElem -} - -// singularize returns a simple singular form by stripping trailing "s". -// Handles common cases: Tags→Tag, Items→Item, Addresses→Addresse. -func singularize(s string) string { - if len(s) > 1 && strings.HasSuffix(s, "s") { - return s[:len(s)-1] - } - return s -} - -func buildValueElement(exposedName, path, primitiveType, originalValue string) *JsonElement { - maxLength := -1 - if primitiveType == "String" { - maxLength = 0 - } - return &JsonElement{ - ExposedName: exposedName, - Path: path, - ElementType: "Value", - PrimitiveType: primitiveType, - MinOccurs: 0, - MaxOccurs: 0, - Nillable: true, - MaxLength: maxLength, - FractionDigits: -1, - TotalDigits: -1, - OriginalValue: originalValue, - } -} From 4aa40e92a87a8a8a76e248019319e56547fba806 Mon Sep 17 00:00:00 2001 From: Andrew Vasilyev Date: Sun, 19 Apr 2026 16:15:39 +0200 Subject: [PATCH 2/5] refactor: define mutation backend interfaces with stub implementations Add PageMutator, WorkflowMutator, WidgetSerializationBackend interfaces to mdl/backend/mutation.go for BSON-free handler decoupling. Extract BSON ID helpers (IDToBsonBinary, BsonBinaryToID, NewIDBsonBinary) to mdl/bsonutil/ package. Add panic stubs to MprBackend and mock function fields to MockBackend for all new interface methods. - Create mdl/bsonutil/bsonutil.go with BSON ID conversion utilities - Migrate 10 handler files from mpr.IDToBsonBinary to bsonutil.* - Define PageMutationBackend, WorkflowMutationBackend interfaces - Define WidgetSerializationBackend with opaque return types - Add PluggablePropertyContext for domain-typed widget property input --- mdl/backend/backend.go | 3 + mdl/backend/mock/backend.go | 12 ++ mdl/backend/mock/mock_mutation.go | 64 +++++++ mdl/backend/mpr/backend.go | 33 ++++ mdl/backend/mutation.go | 181 ++++++++++++++++++ mdl/bsonutil/bsonutil.go | 33 ++++ mdl/executor/cmd_alter_page.go | 28 +-- mdl/executor/cmd_alter_workflow.go | 37 ++-- mdl/executor/cmd_pages_builder_input.go | 12 +- .../cmd_pages_builder_input_cloning.go | 25 +-- .../cmd_pages_builder_input_cloning_test.go | 14 +- .../cmd_pages_builder_input_datagrid.go | 135 ++++++------- .../cmd_pages_builder_input_filters.go | 23 +-- .../cmd_pages_builder_v3_pluggable.go | 18 +- mdl/executor/widget_engine_test.go | 4 +- mdl/executor/widget_operations.go | 4 +- 16 files changed, 481 insertions(+), 145 deletions(-) create mode 100644 mdl/backend/mock/mock_mutation.go create mode 100644 mdl/backend/mutation.go create mode 100644 mdl/bsonutil/bsonutil.go diff --git a/mdl/backend/backend.go b/mdl/backend/backend.go index 7776ea4b..18fa681e 100644 --- a/mdl/backend/backend.go +++ b/mdl/backend/backend.go @@ -31,4 +31,7 @@ type FullBackend interface { MetadataBackend WidgetBackend AgentEditorBackend + PageMutationBackend + WorkflowMutationBackend + WidgetSerializationBackend } diff --git a/mdl/backend/mock/backend.go b/mdl/backend/mock/backend.go index e2f78a0c..1a4ff937 100644 --- a/mdl/backend/mock/backend.go +++ b/mdl/backend/mock/backend.go @@ -258,6 +258,18 @@ type MockBackend struct { FindCustomWidgetTypeFunc func(widgetID string) (*types.RawCustomWidgetType, error) FindAllCustomWidgetTypesFunc func(widgetID string) ([]*types.RawCustomWidgetType, error) + // PageMutationBackend + OpenPageForMutationFunc func(unitID model.ID) (backend.PageMutator, error) + + // WorkflowMutationBackend + OpenWorkflowForMutationFunc func(unitID model.ID) (backend.WorkflowMutator, error) + + // WidgetSerializationBackend + SerializeWidgetFunc func(w pages.Widget) (any, error) + SerializeClientActionFunc func(a pages.ClientAction) (any, error) + SerializeDataSourceFunc func(ds pages.DataSource) (any, error) + SerializeWorkflowActivityFunc func(a workflows.WorkflowActivity) (any, error) + // AgentEditorBackend ListAgentEditorModelsFunc func() ([]*agenteditor.Model, error) ListAgentEditorKnowledgeBasesFunc func() ([]*agenteditor.KnowledgeBase, error) diff --git a/mdl/backend/mock/mock_mutation.go b/mdl/backend/mock/mock_mutation.go new file mode 100644 index 00000000..89b91122 --- /dev/null +++ b/mdl/backend/mock/mock_mutation.go @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 + +package mock + +import ( + "github.com/mendixlabs/mxcli/mdl/backend" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/pages" + "github.com/mendixlabs/mxcli/sdk/workflows" +) + +// --------------------------------------------------------------------------- +// PageMutationBackend +// --------------------------------------------------------------------------- + +func (m *MockBackend) OpenPageForMutation(unitID model.ID) (backend.PageMutator, error) { + if m.OpenPageForMutationFunc != nil { + return m.OpenPageForMutationFunc(unitID) + } + return nil, nil +} + +// --------------------------------------------------------------------------- +// WorkflowMutationBackend +// --------------------------------------------------------------------------- + +func (m *MockBackend) OpenWorkflowForMutation(unitID model.ID) (backend.WorkflowMutator, error) { + if m.OpenWorkflowForMutationFunc != nil { + return m.OpenWorkflowForMutationFunc(unitID) + } + return nil, nil +} + +// --------------------------------------------------------------------------- +// WidgetSerializationBackend +// --------------------------------------------------------------------------- + +func (m *MockBackend) SerializeWidget(w pages.Widget) (any, error) { + if m.SerializeWidgetFunc != nil { + return m.SerializeWidgetFunc(w) + } + return nil, nil +} + +func (m *MockBackend) SerializeClientAction(a pages.ClientAction) (any, error) { + if m.SerializeClientActionFunc != nil { + return m.SerializeClientActionFunc(a) + } + return nil, nil +} + +func (m *MockBackend) SerializeDataSource(ds pages.DataSource) (any, error) { + if m.SerializeDataSourceFunc != nil { + return m.SerializeDataSourceFunc(ds) + } + return nil, nil +} + +func (m *MockBackend) SerializeWorkflowActivity(a workflows.WorkflowActivity) (any, error) { + if m.SerializeWorkflowActivityFunc != nil { + return m.SerializeWorkflowActivityFunc(a) + } + return nil, nil +} diff --git a/mdl/backend/mpr/backend.go b/mdl/backend/mpr/backend.go index 2dc66ecf..54bce183 100644 --- a/mdl/backend/mpr/backend.go +++ b/mdl/backend/mpr/backend.go @@ -724,3 +724,36 @@ func (b *MprBackend) CreateAgentEditorAgent(a *agenteditor.Agent) error { func (b *MprBackend) DeleteAgentEditorAgent(id string) error { return b.writer.DeleteAgentEditorAgent(id) } + +// --------------------------------------------------------------------------- +// PageMutationBackend + +func (b *MprBackend) OpenPageForMutation(unitID model.ID) (backend.PageMutator, error) { + panic("MprBackend.OpenPageForMutation not yet implemented") +} + +// --------------------------------------------------------------------------- +// WorkflowMutationBackend + +func (b *MprBackend) OpenWorkflowForMutation(unitID model.ID) (backend.WorkflowMutator, error) { + panic("MprBackend.OpenWorkflowForMutation not yet implemented") +} + +// --------------------------------------------------------------------------- +// WidgetSerializationBackend + +func (b *MprBackend) SerializeWidget(w pages.Widget) (any, error) { + panic("MprBackend.SerializeWidget not yet implemented") +} + +func (b *MprBackend) SerializeClientAction(a pages.ClientAction) (any, error) { + panic("MprBackend.SerializeClientAction not yet implemented") +} + +func (b *MprBackend) SerializeDataSource(ds pages.DataSource) (any, error) { + panic("MprBackend.SerializeDataSource not yet implemented") +} + +func (b *MprBackend) SerializeWorkflowActivity(a workflows.WorkflowActivity) (any, error) { + panic("MprBackend.SerializeWorkflowActivity not yet implemented") +} diff --git a/mdl/backend/mutation.go b/mdl/backend/mutation.go new file mode 100644 index 00000000..cb0846df --- /dev/null +++ b/mdl/backend/mutation.go @@ -0,0 +1,181 @@ +// SPDX-License-Identifier: Apache-2.0 + +package backend + +import ( + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/pages" + "github.com/mendixlabs/mxcli/sdk/workflows" +) + +// PageMutator provides fine-grained mutation operations on a single +// page, layout, or snippet unit. Obtain one via PageMutationBackend.OpenPageForMutation. +// All methods operate on the in-memory representation; call Save to persist. +type PageMutator interface { + // ContainerType returns "page", "layout", or "snippet". + ContainerType() string + + // --- Widget property operations --- + + // SetWidgetProperty sets a simple property on the named widget. + // For pluggable widget properties, prop is the Mendix property key + // and value is the string representation. + SetWidgetProperty(widgetRef string, prop string, value any) error + + // SetWidgetDataSource sets the DataSource on the named widget. + SetWidgetDataSource(widgetRef string, ds pages.DataSource) error + + // SetColumnProperty sets a property on a column within a grid widget. + SetColumnProperty(gridRef string, columnRef string, prop string, value any) error + + // --- Widget tree operations --- + + // InsertWidget inserts serialized widgets at the given position + // relative to the target widget. Position is "before" or "after". + InsertWidget(targetWidget string, position string, widgets []pages.Widget) error + + // DropWidget removes widgets by name from the tree. + DropWidget(widgetRefs []string) error + + // ReplaceWidget replaces the target widget with the given widgets. + ReplaceWidget(targetWidget string, widgets []pages.Widget) error + + // --- Variable operations --- + + // AddVariable adds a local variable to the page/snippet. + AddVariable(name, dataType, defaultValue string) error + + // DropVariable removes a local variable by name. + DropVariable(name string) error + + // --- Layout operations --- + + // SetLayout changes the layout reference and remaps placeholder parameters. + SetLayout(newLayout string, paramMappings map[string]string) error + + // --- Pluggable widget operations --- + + // SetPluggableProperty sets a typed property on a pluggable widget's object. + // propKey is the Mendix property key, opName is the operation type + // ("attribute", "association", "primitive", "selection", "datasource", + // "widgets", "texttemplate", "action", "attributeObjects"). + // ctx carries the operation-specific values. + SetPluggableProperty(widgetRef string, propKey string, opName string, ctx PluggablePropertyContext) error + + // --- Introspection --- + + // EnclosingEntity returns the qualified entity name for the given widget's + // data context, or "" if none. + EnclosingEntity(widgetRef string) string + + // WidgetScope returns a map of widget name → unit ID for all widgets in the tree. + WidgetScope() map[string]model.ID + + // Save persists the mutations to the backend. + Save() error +} + +// PluggablePropertyContext carries operation-specific values for +// SetPluggableProperty. Only fields relevant to the operation are used. +type PluggablePropertyContext struct { + AttributePath string // "attribute", "association" + AttributePaths []string // "attributeObjects" + AssocPath string // "association" + EntityName string // "association" + PrimitiveVal string // "primitive" + DataSource pages.DataSource // "datasource" + ChildWidgets []pages.Widget // "widgets" + Action pages.ClientAction // "action" + TextTemplate string // "texttemplate" + Selection string // "selection" +} + +// WorkflowMutator provides fine-grained mutation operations on a single +// workflow unit. Obtain one via WorkflowMutationBackend.OpenWorkflowForMutation. +// All methods operate on the in-memory representation; call Save to persist. +type WorkflowMutator interface { + // --- Top-level property operations --- + + // SetProperty sets a workflow-level property (DisplayName, Description, + // ExportLevel, DueDate, Parameter, OverviewPage). + SetProperty(prop string, value string) error + + // SetPropertyWithEntity sets a workflow-level property that references + // an entity (e.g. Parameter). + SetPropertyWithEntity(prop string, value string, entity string) error + + // --- Activity operations --- + + // SetActivityProperty sets a property on an activity identified by + // caption and optional position index. + SetActivityProperty(activityRef string, atPos int, prop string, value string) error + + // InsertAfterActivity inserts new activities after the referenced activity. + InsertAfterActivity(activityRef string, atPos int, activities []workflows.WorkflowActivity) error + + // DropActivity removes the referenced activity. + DropActivity(activityRef string, atPos int) error + + // ReplaceActivity replaces the referenced activity with new ones. + ReplaceActivity(activityRef string, atPos int, activities []workflows.WorkflowActivity) error + + // --- Outcome operations --- + + // InsertOutcome adds a new outcome to the referenced activity. + InsertOutcome(activityRef string, atPos int, outcomeName string, activities []workflows.WorkflowActivity) error + + // DropOutcome removes an outcome by name from the referenced activity. + DropOutcome(activityRef string, atPos int, outcomeName string) error + + // --- Path operations (parallel split) --- + + InsertPath(activityRef string, atPos int, pathCaption string, activities []workflows.WorkflowActivity) error + DropPath(activityRef string, atPos int, pathCaption string) error + + // --- Branch operations (exclusive split) --- + + InsertBranch(activityRef string, atPos int, condition string, activities []workflows.WorkflowActivity) error + DropBranch(activityRef string, atPos int, branchName string) error + + // --- Boundary event operations --- + + InsertBoundaryEvent(activityRef string, atPos int, eventType string, delay string, activities []workflows.WorkflowActivity) error + DropBoundaryEvent(activityRef string, atPos int) error + + // Save persists the mutations to the backend. + Save() error +} + +// PageMutationBackend provides page/layout/snippet mutation capabilities. +type PageMutationBackend interface { + // OpenPageForMutation loads a page, layout, or snippet unit and returns + // a mutator for applying changes. Call Save() on the returned mutator + // to persist. + OpenPageForMutation(unitID model.ID) (PageMutator, error) +} + +// WorkflowMutationBackend provides workflow mutation capabilities. +type WorkflowMutationBackend interface { + // OpenWorkflowForMutation loads a workflow unit and returns a mutator + // for applying changes. Call Save() on the returned mutator to persist. + OpenWorkflowForMutation(unitID model.ID) (WorkflowMutator, error) +} + +// WidgetSerializationBackend provides widget and activity serialization +// for CREATE paths where the executor builds domain objects that need +// to be converted to the storage format. +type WidgetSerializationBackend interface { + // SerializeWidget converts a domain Widget to its storage representation. + // The returned value is opaque to the caller; it is only used as input + // to mutation operations or passed to the backend for persistence. + SerializeWidget(w pages.Widget) (any, error) + + // SerializeClientAction converts a domain ClientAction to storage format. + SerializeClientAction(a pages.ClientAction) (any, error) + + // SerializeDataSource converts a domain DataSource to storage format. + SerializeDataSource(ds pages.DataSource) (any, error) + + // SerializeWorkflowActivity converts a domain WorkflowActivity to storage format. + SerializeWorkflowActivity(a workflows.WorkflowActivity) (any, error) +} diff --git a/mdl/bsonutil/bsonutil.go b/mdl/bsonutil/bsonutil.go new file mode 100644 index 00000000..558497c3 --- /dev/null +++ b/mdl/bsonutil/bsonutil.go @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 + +// Package bsonutil provides BSON-aware ID conversion utilities for model elements. +// It depends on mdl/types (WASM-safe) and the BSON driver (also WASM-safe), +// but does NOT depend on sdk/mpr (which pulls in SQLite/CGO). +package bsonutil + +import ( + "github.com/mendixlabs/mxcli/mdl/types" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// IDToBsonBinary converts a hex UUID string to a BSON binary value. +func IDToBsonBinary(id string) primitive.Binary { + blob := types.UUIDToBlob(id) + if blob == nil || len(blob) != 16 { + blob = types.UUIDToBlob(types.GenerateID()) + } + return primitive.Binary{ + Subtype: 0x00, + Data: blob, + } +} + +// BsonBinaryToID converts a BSON binary value to a hex UUID string. +func BsonBinaryToID(bin primitive.Binary) string { + return types.BlobToUUID(bin.Data) +} + +// NewIDBsonBinary generates a new unique ID and returns it as a BSON binary value. +func NewIDBsonBinary() primitive.Binary { + return IDToBsonBinary(types.GenerateID()) +} diff --git a/mdl/executor/cmd_alter_page.go b/mdl/executor/cmd_alter_page.go index 900be082..cfb5b04b 100644 --- a/mdl/executor/cmd_alter_page.go +++ b/mdl/executor/cmd_alter_page.go @@ -10,7 +10,9 @@ import ( "go.mongodb.org/mongo-driver/bson/primitive" "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/bsonutil" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/mpr" ) @@ -357,7 +359,7 @@ func dSetArray(doc bson.D, key string, elements []any) { // extractBinaryIDFromDoc extracts a binary ID string from a bson.D field. func extractBinaryIDFromDoc(val any) string { if bin, ok := val.(primitive.Binary); ok { - return mpr.BlobToUUID(bin.Data) + return types.BlobToUUID(bin.Data) } return "" } @@ -936,7 +938,7 @@ func setWidgetAttributeRef(widget bson.D, value interface{}) error { var attrRefValue interface{} if strings.Count(attrPath, ".") >= 2 { attrRefValue = bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "DomainModels$AttributeRef"}, {Key: "Attribute", Value: attrPath}, {Key: "EntityRef", Value: nil}, @@ -971,7 +973,7 @@ func setWidgetDataSource(widget bson.D, value interface{}) error { case "selection": // SELECTION widgetName → Forms$ListenTargetSource serialized = bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Forms$ListenTargetSource"}, {Key: "ListenTarget", Value: ds.Reference}, } @@ -980,13 +982,13 @@ func setWidgetDataSource(widget bson.D, value interface{}) error { var entityRef interface{} if ds.Reference != "" { entityRef = bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "DomainModels$DirectEntityRef"}, {Key: "Entity", Value: ds.Reference}, } } serialized = bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Forms$DataViewSource"}, {Key: "EntityRef", Value: entityRef}, {Key: "ForceFullObjects", Value: false}, @@ -994,10 +996,10 @@ func setWidgetDataSource(widget bson.D, value interface{}) error { } case "microflow": serialized = bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Forms$MicroflowSource"}, {Key: "MicroflowSettings", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Forms$MicroflowSettings"}, {Key: "Asynchronous", Value: false}, {Key: "ConfirmationInfo", Value: nil}, @@ -1010,10 +1012,10 @@ func setWidgetDataSource(widget bson.D, value interface{}) error { } case "nanoflow": serialized = bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Forms$NanoflowSource"}, {Key: "NanoflowSettings", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Forms$NanoflowSettings"}, {Key: "Nanoflow", Value: ds.Reference}, {Key: "ParameterMappings", Value: bson.A{int32(3)}}, @@ -1462,10 +1464,10 @@ func applyAddVariable(rawData *bson.D, op *ast.AddVariableOp) error { } // Build VariableType BSON - varTypeID := mpr.GenerateID() + varTypeID := types.GenerateID() bsonTypeName := mdlTypeToBsonType(op.Variable.DataType) varType := bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(varTypeID)}, + {Key: "$ID", Value: bsonutil.IDToBsonBinary(varTypeID)}, {Key: "$Type", Value: bsonTypeName}, } if bsonTypeName == "DataTypes$ObjectType" { @@ -1473,9 +1475,9 @@ func applyAddVariable(rawData *bson.D, op *ast.AddVariableOp) error { } // Build LocalVariable BSON document - varID := mpr.GenerateID() + varID := types.GenerateID() varDoc := bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(varID)}, + {Key: "$ID", Value: bsonutil.IDToBsonBinary(varID)}, {Key: "$Type", Value: "Forms$LocalVariable"}, {Key: "DefaultValue", Value: op.Variable.DefaultValue}, {Key: "Name", Value: op.Variable.Name}, diff --git a/mdl/executor/cmd_alter_workflow.go b/mdl/executor/cmd_alter_workflow.go index df403ca5..e7552e10 100644 --- a/mdl/executor/cmd_alter_workflow.go +++ b/mdl/executor/cmd_alter_workflow.go @@ -10,6 +10,7 @@ import ( "go.mongodb.org/mongo-driver/bson" "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/bsonutil" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/mpr" @@ -155,7 +156,7 @@ func applySetWorkflowProperty(doc *bson.D, op *ast.SetWorkflowPropertyOp) error if wfName == nil { // Auto-create the WorkflowName sub-document newName := bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Texts$Text"}, {Key: "Text", Value: op.Value}, } @@ -173,7 +174,7 @@ func applySetWorkflowProperty(doc *bson.D, op *ast.SetWorkflowPropertyOp) error if wfDesc == nil { // Auto-create the WorkflowDescription sub-document newDesc := bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Texts$Text"}, {Key: "Text", Value: op.Value}, } @@ -198,7 +199,7 @@ func applySetWorkflowProperty(doc *bson.D, op *ast.SetWorkflowPropertyOp) error dSet(*doc, "AdminPage", nil) } else { pageRef := bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Workflows$PageReference"}, {Key: "Page", Value: qn}, } @@ -225,7 +226,7 @@ func applySetWorkflowProperty(doc *bson.D, op *ast.SetWorkflowPropertyOp) error } else { // Create new Parameter newParam := bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Workflows$Parameter"}, {Key: "Entity", Value: qn}, {Key: "Name", Value: "WorkflowContext"}, @@ -264,7 +265,7 @@ func applySetActivityProperty(doc bson.D, op *ast.SetActivityPropertyOp) error { } else { // Create TaskPage pageRef := bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Workflows$PageReference"}, {Key: "Page", Value: qn}, } @@ -283,7 +284,7 @@ func applySetActivityProperty(doc bson.D, op *ast.SetActivityPropertyOp) error { case "TARGETING_MICROFLOW": qn := op.Microflow.Module + "." + op.Microflow.Name userTargeting := bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Workflows$MicroflowUserTargeting"}, {Key: "Microflow", Value: qn}, } @@ -292,7 +293,7 @@ func applySetActivityProperty(doc bson.D, op *ast.SetActivityPropertyOp) error { case "TARGETING_XPATH": userTargeting := bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Workflows$XPathUserTargeting"}, {Key: "XPathConstraint", Value: op.Value}, } @@ -500,7 +501,7 @@ func buildSubFlowBson(ctx *ExecContext, doc bson.D, activities []ast.WorkflowAct } } return bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Workflows$Flow"}, {Key: "Activities", Value: subActsBson}, } @@ -602,7 +603,7 @@ func applyInsertOutcome(ctx *ExecContext, doc bson.D, op *ast.InsertOutcomeOp) e // Build outcome BSON outcomeDoc := bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Workflows$UserTaskOutcome"}, } @@ -612,7 +613,7 @@ func applyInsertOutcome(ctx *ExecContext, doc bson.D, op *ast.InsertOutcomeOp) e } outcomeDoc = append(outcomeDoc, - bson.E{Key: "PersistentId", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + bson.E{Key: "PersistentId", Value: bsonutil.NewIDBsonBinary()}, bson.E{Key: "Value", Value: op.OutcomeName}, ) @@ -667,7 +668,7 @@ func applyInsertPath(ctx *ExecContext, doc bson.D, op *ast.InsertPathOp) error { } pathDoc := bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Workflows$ParallelSplitOutcome"}, } @@ -675,7 +676,7 @@ func applyInsertPath(ctx *ExecContext, doc bson.D, op *ast.InsertPathOp) error { pathDoc = append(pathDoc, bson.E{Key: "Flow", Value: buildSubFlowBson(ctx, doc, op.Activities)}) } - pathDoc = append(pathDoc, bson.E{Key: "PersistentId", Value: mpr.IDToBsonBinary(mpr.GenerateID())}) + pathDoc = append(pathDoc, bson.E{Key: "PersistentId", Value: bsonutil.NewIDBsonBinary()}) outcomes := dGetArrayElements(dGet(actDoc, "Outcomes")) outcomes = append(outcomes, pathDoc) @@ -730,24 +731,24 @@ func applyInsertBranch(ctx *ExecContext, doc bson.D, op *ast.InsertBranchOp) err switch strings.ToLower(op.Condition) { case "true": outcomeDoc = bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Workflows$BooleanConditionOutcome"}, {Key: "Value", Value: true}, } case "false": outcomeDoc = bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Workflows$BooleanConditionOutcome"}, {Key: "Value", Value: false}, } case "default": outcomeDoc = bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Workflows$VoidConditionOutcome"}, } default: outcomeDoc = bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Workflows$EnumerationValueConditionOutcome"}, {Key: "Value", Value: op.Condition}, } @@ -835,7 +836,7 @@ func applyInsertBoundaryEvent(ctx *ExecContext, doc bson.D, op *ast.InsertBounda } eventDoc := bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: typeName}, {Key: "Caption", Value: ""}, } @@ -848,7 +849,7 @@ func applyInsertBoundaryEvent(ctx *ExecContext, doc bson.D, op *ast.InsertBounda eventDoc = append(eventDoc, bson.E{Key: "Flow", Value: buildSubFlowBson(ctx, doc, op.Activities)}) } - eventDoc = append(eventDoc, bson.E{Key: "PersistentId", Value: mpr.IDToBsonBinary(mpr.GenerateID())}) + eventDoc = append(eventDoc, bson.E{Key: "PersistentId", Value: bsonutil.NewIDBsonBinary()}) if typeName == "Workflows$NonInterruptingTimerBoundaryEvent" { eventDoc = append(eventDoc, bson.E{Key: "Recurrence", Value: nil}) diff --git a/mdl/executor/cmd_pages_builder_input.go b/mdl/executor/cmd_pages_builder_input.go index 16bb6fcb..4a8cfed1 100644 --- a/mdl/executor/cmd_pages_builder_input.go +++ b/mdl/executor/cmd_pages_builder_input.go @@ -8,6 +8,8 @@ import ( "strings" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/bsonutil" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/pages" @@ -128,10 +130,10 @@ func matchesTypePointer(prop bson.D, propertyTypeID string) bool { // Handle both primitive.Binary (from MPR) and []byte (from JSON templates) switch v := elem.Value.(type) { case primitive.Binary: - propID := strings.ReplaceAll(mpr.BlobToUUID(v.Data), "-", "") + propID := strings.ReplaceAll(types.BlobToUUID(v.Data), "-", "") return propID == normalizedTarget case []byte: - propID := strings.ReplaceAll(mpr.BlobToUUID(v), "-", "") + propID := strings.ReplaceAll(types.BlobToUUID(v), "-", "") if propID == normalizedTarget { return true } @@ -196,12 +198,12 @@ func setAssociationRef(val bson.D, assocPath string, entityName string) bson.D { for _, elem := range val { if elem.Key == "EntityRef" && entityName != "" { result = append(result, bson.E{Key: "EntityRef", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "DomainModels$IndirectEntityRef"}, {Key: "Steps", Value: bson.A{ int32(2), // version marker bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "DomainModels$EntityRefStep"}, {Key: "Association", Value: assocPath}, {Key: "DestinationEntity", Value: entityName}, @@ -224,7 +226,7 @@ func setAttributeRef(val bson.D, attrPath string) bson.D { if elem.Key == "AttributeRef" { if strings.Count(attrPath, ".") >= 2 { result = append(result, bson.E{Key: "AttributeRef", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "DomainModels$AttributeRef"}, {Key: "Attribute", Value: attrPath}, {Key: "EntityRef", Value: nil}, diff --git a/mdl/executor/cmd_pages_builder_input_cloning.go b/mdl/executor/cmd_pages_builder_input_cloning.go index 5a8fb0d0..af8681e2 100644 --- a/mdl/executor/cmd_pages_builder_input_cloning.go +++ b/mdl/executor/cmd_pages_builder_input_cloning.go @@ -3,7 +3,8 @@ package executor import ( - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/bsonutil" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/pages" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" @@ -17,7 +18,7 @@ func (pb *pageBuilder) cloneDataGrid2ObjectWithDatasourceOnly(templateObject bso for _, elem := range templateObject { if elem.Key == "$ID" { // Generate new ID for the object - result = append(result, bson.E{Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}) + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) } else if elem.Key == "Properties" { // Update only datasource property if propsArr, ok := elem.Value.(bson.A); ok { @@ -67,10 +68,10 @@ func (pb *pageBuilder) getTypePointerFromProperty(prop bson.D) string { if elem.Key == "TypePointer" { switch v := elem.Value.(type) { case primitive.Binary: - return mpr.BsonBinaryToID(v) + return bsonutil.BsonBinaryToID(v) case []byte: // When loaded from JSON template, binary is []byte instead of primitive.Binary - return mpr.BlobToUUID(v) + return types.BlobToUUID(v) } } } @@ -82,7 +83,7 @@ func (pb *pageBuilder) clonePropertyWithNewIDs(prop bson.D) bson.D { result := make(bson.D, 0, len(prop)) for _, elem := range prop { if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}) + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) } else if elem.Key == "Value" { if valMap, ok := elem.Value.(bson.D); ok { result = append(result, bson.E{Key: "Value", Value: pb.cloneValueWithNewIDs(valMap)}) @@ -109,7 +110,7 @@ func (pb *pageBuilder) clonePropertyWithPrimitiveValue(prop bson.D, newValue str result := make(bson.D, 0, len(prop)) for _, elem := range prop { if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}) + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) } else if elem.Key == "Value" { if valMap, ok := elem.Value.(bson.D); ok { result = append(result, bson.E{Key: "Value", Value: pb.cloneValueWithUpdatedPrimitive(valMap, newValue)}) @@ -128,7 +129,7 @@ func (pb *pageBuilder) cloneValueWithUpdatedPrimitive(val bson.D, newValue strin result := make(bson.D, 0, len(val)) for _, elem := range val { if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}) + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) } else if elem.Key == "PrimitiveValue" { result = append(result, bson.E{Key: "PrimitiveValue", Value: newValue}) } else { @@ -144,7 +145,7 @@ func (pb *pageBuilder) clonePropertyClearingTextTemplate(prop bson.D) bson.D { result := make(bson.D, 0, len(prop)) for _, elem := range prop { if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}) + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) } else if elem.Key == "Value" { if valMap, ok := elem.Value.(bson.D); ok { result = append(result, bson.E{Key: "Value", Value: pb.cloneValueClearingTextTemplate(valMap)}) @@ -163,7 +164,7 @@ func (pb *pageBuilder) cloneValueClearingTextTemplate(val bson.D) bson.D { result := make(bson.D, 0, len(val)) for _, elem := range val { if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}) + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) } else if elem.Key == "TextTemplate" { result = append(result, bson.E{Key: "TextTemplate", Value: nil}) } else { @@ -189,7 +190,7 @@ func (pb *pageBuilder) clonePropertyWithExpression(prop bson.D, newExpr string) result := make(bson.D, 0, len(prop)) for _, elem := range prop { if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}) + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) } else if elem.Key == "Value" { if valMap, ok := elem.Value.(bson.D); ok { result = append(result, bson.E{Key: "Value", Value: pb.cloneValueWithUpdatedExpression(valMap, newExpr)}) @@ -208,7 +209,7 @@ func (pb *pageBuilder) cloneValueWithUpdatedExpression(val bson.D, newExpr strin result := make(bson.D, 0, len(val)) for _, elem := range val { if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}) + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) } else if elem.Key == "Expression" { result = append(result, bson.E{Key: "Expression", Value: newExpr}) } else { @@ -229,7 +230,7 @@ func deepCloneWithNewIDs(doc bson.D) bson.D { result := make(bson.D, 0, len(doc)) for _, elem := range doc { if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}) + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) } else { result = append(result, bson.E{Key: elem.Key, Value: deepCloneValue(elem.Value)}) } diff --git a/mdl/executor/cmd_pages_builder_input_cloning_test.go b/mdl/executor/cmd_pages_builder_input_cloning_test.go index 830b77b0..0e11bf8b 100644 --- a/mdl/executor/cmd_pages_builder_input_cloning_test.go +++ b/mdl/executor/cmd_pages_builder_input_cloning_test.go @@ -5,15 +5,15 @@ package executor import ( "testing" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/bsonutil" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" ) func TestDeepCloneWithNewIDs_RegeneratesAllIDs(t *testing.T) { - origID1 := mpr.IDToBsonBinary(mpr.GenerateID()) - origID2 := mpr.IDToBsonBinary(mpr.GenerateID()) - origID3 := mpr.IDToBsonBinary(mpr.GenerateID()) + origID1 := bsonutil.NewIDBsonBinary() + origID2 := bsonutil.NewIDBsonBinary() + origID3 := bsonutil.NewIDBsonBinary() doc := bson.D{ {Key: "$ID", Value: origID1}, @@ -75,8 +75,8 @@ func TestDeepCloneWithNewIDs_RegeneratesAllIDs(t *testing.T) { } func TestDeepCloneWithNewIDs_HandlesArrays(t *testing.T) { - origID := mpr.IDToBsonBinary(mpr.GenerateID()) - innerID := mpr.IDToBsonBinary(mpr.GenerateID()) + origID := bsonutil.NewIDBsonBinary() + innerID := bsonutil.NewIDBsonBinary() doc := bson.D{ {Key: "$ID", Value: origID}, @@ -107,7 +107,7 @@ func TestDeepCloneWithNewIDs_HandlesArrays(t *testing.T) { } func TestDeepCloneWithNewIDs_PreservesNil(t *testing.T) { - origID := mpr.IDToBsonBinary(mpr.GenerateID()) + origID := bsonutil.NewIDBsonBinary() doc := bson.D{ {Key: "$ID", Value: origID}, diff --git a/mdl/executor/cmd_pages_builder_input_datagrid.go b/mdl/executor/cmd_pages_builder_input_datagrid.go index e7d2a290..b86a98dc 100644 --- a/mdl/executor/cmd_pages_builder_input_datagrid.go +++ b/mdl/executor/cmd_pages_builder_input_datagrid.go @@ -7,6 +7,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/bsonutil" "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/pages" "go.mongodb.org/mongo-driver/bson" @@ -91,7 +92,7 @@ func (pb *pageBuilder) buildDataGrid2Property(entry pages.PropertyTypeIDEntry, d var attrRefBSON any if attrRef != "" { attrRefBSON = bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "DomainModels$AttributeRef"}, {Key: "Attribute", Value: attrRef}, {Key: "EntityRef", Value: nil}, @@ -99,14 +100,14 @@ func (pb *pageBuilder) buildDataGrid2Property(entry pages.PropertyTypeIDEntry, d } return bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.PropertyTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Forms$NoAction"}, {Key: "DisabledDuringExecution", Value: true}, }}, @@ -125,7 +126,7 @@ func (pb *pageBuilder) buildDataGrid2Property(entry pages.PropertyTypeIDEntry, d {Key: "SourceVariable", Value: nil}, {Key: "TextTemplate", Value: nil}, {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.ValueTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, {Key: "Widgets", Value: bson.A{int32(2)}}, {Key: "XPathConstraint", Value: ""}, }}, @@ -139,7 +140,7 @@ func (pb *pageBuilder) updateDataGrid2Object(templateObject bson.D, propertyType for _, elem := range templateObject { if elem.Key == "$ID" { // Generate new ID for the object - result = append(result, bson.E{Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}) + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) } else if elem.Key == "Properties" { // Update properties if propsArr, ok := elem.Value.(bson.A); ok { @@ -237,13 +238,13 @@ func (pb *pageBuilder) cloneAndUpdateColumnsProperty(templateProp bson.D, column result := make(bson.D, 0, len(templateProp)) for _, elem := range templateProp { if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}) + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) } else if elem.Key == "Value" { if valMap, ok := elem.Value.(bson.D); ok { newVal := make(bson.D, 0, len(valMap)) for _, ve := range valMap { if ve.Key == "$ID" { - newVal = append(newVal, bson.E{Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}) + newVal = append(newVal, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) } else if ve.Key == "Objects" { newVal = append(newVal, bson.E{Key: "Objects", Value: columnObjects}) } else if ve.Key == "Action" { @@ -292,7 +293,7 @@ func (pb *pageBuilder) cloneAndUpdateColumnObject(templateCol bson.D, col *ast.D result := make(bson.D, 0, len(templateCol)) for _, elem := range templateCol { if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}) + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) } else if elem.Key == "Properties" { // Update properties if propsArr, ok := elem.Value.(bson.A); ok { @@ -529,11 +530,11 @@ func (pb *pageBuilder) buildDataGrid2Object(propertyTypeIDs map[string]pages.Pro // Build TypePointer - references the WidgetObjectType var typePointer any if objectTypeID != "" { - typePointer = mpr.IDToBsonBinary(objectTypeID) + typePointer = bsonutil.IDToBsonBinary(objectTypeID) } return bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetObject"}, {Key: "Properties", Value: properties}, {Key: "TypePointer", Value: typePointer}, @@ -556,14 +557,14 @@ func (pb *pageBuilder) buildDataGrid2DefaultProperty(entry pages.PropertyTypeIDE } return bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.PropertyTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Forms$NoAction"}, {Key: "DisabledDuringExecution", Value: true}, }}, @@ -582,7 +583,7 @@ func (pb *pageBuilder) buildDataGrid2DefaultProperty(entry pages.PropertyTypeIDE {Key: "SourceVariable", Value: nil}, {Key: "TextTemplate", Value: textTemplate}, {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.ValueTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, {Key: "Widgets", Value: bson.A{int32(2)}}, {Key: "XPathConstraint", Value: ""}, }}, @@ -591,16 +592,16 @@ func (pb *pageBuilder) buildDataGrid2DefaultProperty(entry pages.PropertyTypeIDE func (pb *pageBuilder) buildEmptyClientTemplate() bson.D { return bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Forms$ClientTemplate"}, {Key: "Fallback", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Texts$Text"}, {Key: "Items", Value: bson.A{int32(3)}}, // Empty items with version marker }}, {Key: "Parameters", Value: bson.A{int32(2)}}, // Empty parameters {Key: "Template", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Texts$Text"}, {Key: "Items", Value: bson.A{int32(3)}}, // Empty items with version marker }}, @@ -609,21 +610,21 @@ func (pb *pageBuilder) buildEmptyClientTemplate() bson.D { func (pb *pageBuilder) buildClientTemplateWithText(text string) bson.D { return bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Forms$ClientTemplate"}, {Key: "Fallback", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Texts$Text"}, {Key: "Items", Value: bson.A{int32(3)}}, }}, {Key: "Parameters", Value: bson.A{int32(2)}}, {Key: "Template", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Texts$Text"}, {Key: "Items", Value: bson.A{ int32(3), bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Texts$Translation"}, {Key: "LanguageCode", Value: "en_US"}, {Key: "Text", Value: text}, @@ -641,14 +642,14 @@ func (pb *pageBuilder) buildFiltersPlaceholderProperty(entry pages.PropertyTypeI } return bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.PropertyTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Forms$NoAction"}, {Key: "DisabledDuringExecution", Value: true}, }}, @@ -667,7 +668,7 @@ func (pb *pageBuilder) buildFiltersPlaceholderProperty(entry pages.PropertyTypeI {Key: "SourceVariable", Value: nil}, {Key: "TextTemplate", Value: nil}, {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.ValueTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, {Key: "Widgets", Value: widgetsArray}, {Key: "XPathConstraint", Value: ""}, }}, @@ -682,14 +683,14 @@ func (pb *pageBuilder) buildDataGrid2ColumnsProperty(entry pages.PropertyTypeIDE } return bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.PropertyTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Forms$NoAction"}, {Key: "DisabledDuringExecution", Value: true}, }}, @@ -708,7 +709,7 @@ func (pb *pageBuilder) buildDataGrid2ColumnsProperty(entry pages.PropertyTypeIDE {Key: "SourceVariable", Value: nil}, {Key: "TextTemplate", Value: nil}, {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.ValueTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, {Key: "Widgets", Value: bson.A{int32(2)}}, {Key: "XPathConstraint", Value: ""}, }}, @@ -878,11 +879,11 @@ func (pb *pageBuilder) buildDataGrid2ColumnObject(col *ast.DataGridColumnDef, co // Column ObjectType pointer var typePointer any if columnObjectTypeID != "" { - typePointer = mpr.IDToBsonBinary(columnObjectTypeID) + typePointer = bsonutil.IDToBsonBinary(columnObjectTypeID) } return bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetObject"}, {Key: "Properties", Value: properties}, {Key: "TypePointer", Value: typePointer}, @@ -897,14 +898,14 @@ func (pb *pageBuilder) buildColumnDefaultProperty(entry pages.PropertyTypeIDEntr } return bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.PropertyTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Forms$NoAction"}, {Key: "DisabledDuringExecution", Value: true}, }}, @@ -923,7 +924,7 @@ func (pb *pageBuilder) buildColumnDefaultProperty(entry pages.PropertyTypeIDEntr {Key: "SourceVariable", Value: nil}, {Key: "TextTemplate", Value: textTemplate}, {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.ValueTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, {Key: "Widgets", Value: bson.A{int32(2)}}, {Key: "XPathConstraint", Value: ""}, }}, @@ -932,14 +933,14 @@ func (pb *pageBuilder) buildColumnDefaultProperty(entry pages.PropertyTypeIDEntr func (pb *pageBuilder) buildColumnPrimitiveProperty(entry pages.PropertyTypeIDEntry, value string) bson.D { return bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.PropertyTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Forms$NoAction"}, {Key: "DisabledDuringExecution", Value: true}, }}, @@ -958,7 +959,7 @@ func (pb *pageBuilder) buildColumnPrimitiveProperty(entry pages.PropertyTypeIDEn {Key: "SourceVariable", Value: nil}, {Key: "TextTemplate", Value: nil}, {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.ValueTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, {Key: "Widgets", Value: bson.A{int32(2)}}, {Key: "XPathConstraint", Value: ""}, }}, @@ -967,14 +968,14 @@ func (pb *pageBuilder) buildColumnPrimitiveProperty(entry pages.PropertyTypeIDEn func (pb *pageBuilder) buildColumnExpressionProperty(entry pages.PropertyTypeIDEntry, expression string) bson.D { return bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.PropertyTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Forms$NoAction"}, {Key: "DisabledDuringExecution", Value: true}, }}, @@ -993,7 +994,7 @@ func (pb *pageBuilder) buildColumnExpressionProperty(entry pages.PropertyTypeIDE {Key: "SourceVariable", Value: nil}, {Key: "TextTemplate", Value: nil}, {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.ValueTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, {Key: "Widgets", Value: bson.A{int32(2)}}, {Key: "XPathConstraint", Value: ""}, }}, @@ -1006,21 +1007,21 @@ func (pb *pageBuilder) buildColumnAttributeProperty(entry pages.PropertyTypeIDEn var attributeRef any if strings.Count(attrPath, ".") >= 2 { attributeRef = bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "DomainModels$AttributeRef"}, {Key: "Attribute", Value: attrPath}, {Key: "EntityRef", Value: nil}, } } return bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.PropertyTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Forms$NoAction"}, {Key: "DisabledDuringExecution", Value: true}, }}, @@ -1039,7 +1040,7 @@ func (pb *pageBuilder) buildColumnAttributeProperty(entry pages.PropertyTypeIDEn {Key: "SourceVariable", Value: nil}, {Key: "TextTemplate", Value: nil}, {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.ValueTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, {Key: "Widgets", Value: bson.A{int32(2)}}, {Key: "XPathConstraint", Value: ""}, }}, @@ -1051,14 +1052,14 @@ func (pb *pageBuilder) buildColumnHeaderProperty(entry pages.PropertyTypeIDEntry textTemplate := pb.buildClientTemplateWithText(caption) return bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.PropertyTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Forms$NoAction"}, {Key: "DisabledDuringExecution", Value: true}, }}, @@ -1077,7 +1078,7 @@ func (pb *pageBuilder) buildColumnHeaderProperty(entry pages.PropertyTypeIDEntry {Key: "SourceVariable", Value: nil}, {Key: "TextTemplate", Value: textTemplate}, {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.ValueTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, {Key: "Widgets", Value: bson.A{int32(2)}}, {Key: "XPathConstraint", Value: ""}, }}, @@ -1099,14 +1100,14 @@ func (pb *pageBuilder) buildColumnContentProperty(entry pages.PropertyTypeIDEntr } return bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.PropertyTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "Forms$NoAction"}, {Key: "DisabledDuringExecution", Value: true}, }}, @@ -1125,7 +1126,7 @@ func (pb *pageBuilder) buildColumnContentProperty(entry pages.PropertyTypeIDEntr {Key: "SourceVariable", Value: nil}, {Key: "TextTemplate", Value: nil}, {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(entry.ValueTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, {Key: "Widgets", Value: widgetsArray}, {Key: "XPathConstraint", Value: ""}, }}, diff --git a/mdl/executor/cmd_pages_builder_input_filters.go b/mdl/executor/cmd_pages_builder_input_filters.go index 58b6cee7..eadf5943 100644 --- a/mdl/executor/cmd_pages_builder_input_filters.go +++ b/mdl/executor/cmd_pages_builder_input_filters.go @@ -5,8 +5,9 @@ package executor import ( "strings" + "github.com/mendixlabs/mxcli/mdl/bsonutil" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/domainmodel" - "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/pages" "github.com/mendixlabs/mxcli/sdk/widgets" "go.mongodb.org/mongo-driver/bson" @@ -106,7 +107,7 @@ func (pb *pageBuilder) findAttributeType(attrPath string) domainmodel.AttributeT func (pb *pageBuilder) buildFilterWidgetBSON(widgetID, filterName string) bson.D { // Load the filter widget template - rawType, rawObject, propertyTypeIDs, objectTypeID, err := widgets.GetTemplateFullBSON(widgetID, mpr.GenerateID, pb.reader.Path()) + rawType, rawObject, propertyTypeIDs, objectTypeID, err := widgets.GetTemplateFullBSON(widgetID, types.GenerateID, pb.reader.Path()) if err != nil || rawType == nil { // Fallback: create minimal filter widget structure return pb.buildMinimalFilterWidgetBSON(widgetID, filterName) @@ -114,7 +115,7 @@ func (pb *pageBuilder) buildFilterWidgetBSON(widgetID, filterName string) bson.D // The widget structure is: CustomWidgets$CustomWidget with Type and Object widgetBSON := bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$CustomWidget"}, {Key: "Editable", Value: "Inherited"}, {Key: "Name", Value: filterName}, @@ -138,9 +139,9 @@ func (pb *pageBuilder) setFilterWidgetLinkedDsAuto(widget bson.D, propertyTypeID } func (pb *pageBuilder) buildMinimalFilterWidgetBSON(widgetID, filterName string) bson.D { - typeID := mpr.GenerateID() - objectTypeID := mpr.GenerateID() - objectID := mpr.GenerateID() + typeID := types.GenerateID() + objectTypeID := types.GenerateID() + objectID := types.GenerateID() // Get widget type name based on ID var widgetTypeName string @@ -158,23 +159,23 @@ func (pb *pageBuilder) buildMinimalFilterWidgetBSON(widgetID, filterName string) } return bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}, + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, {Key: "$Type", Value: "CustomWidgets$CustomWidget"}, {Key: "Editable", Value: "Inherited"}, {Key: "Name", Value: filterName}, {Key: "Object", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(objectID)}, + {Key: "$ID", Value: bsonutil.IDToBsonBinary(objectID)}, {Key: "$Type", Value: "CustomWidgets$WidgetObject"}, {Key: "Properties", Value: bson.A{int32(2)}}, - {Key: "TypePointer", Value: mpr.IDToBsonBinary(objectTypeID)}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(objectTypeID)}, }}, {Key: "TabIndex", Value: int32(0)}, {Key: "Type", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(typeID)}, + {Key: "$ID", Value: bsonutil.IDToBsonBinary(typeID)}, {Key: "$Type", Value: "CustomWidgets$CustomWidgetType"}, {Key: "HelpUrl", Value: ""}, {Key: "ObjectType", Value: bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(objectTypeID)}, + {Key: "$ID", Value: bsonutil.IDToBsonBinary(objectTypeID)}, {Key: "$Type", Value: "CustomWidgets$WidgetObjectType"}, {Key: "PropertyTypes", Value: bson.A{int32(2)}}, }}, diff --git a/mdl/executor/cmd_pages_builder_v3_pluggable.go b/mdl/executor/cmd_pages_builder_v3_pluggable.go index 072959f7..b79db2b9 100644 --- a/mdl/executor/cmd_pages_builder_v3_pluggable.go +++ b/mdl/executor/cmd_pages_builder_v3_pluggable.go @@ -9,7 +9,9 @@ import ( "go.mongodb.org/mongo-driver/bson" "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/bsonutil" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/mpr" ) @@ -24,7 +26,7 @@ func (pb *pageBuilder) buildGallerySelectionProperty(propMap bson.D, selectionMo for _, elem := range propMap { if elem.Key == "$ID" { // Generate new ID - result = append(result, bson.E{Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}) + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) } else if elem.Key == "Value" { // Clone Value and update Selection if valueMap, ok := elem.Value.(bson.D); ok { @@ -46,7 +48,7 @@ func (pb *pageBuilder) cloneGallerySelectionValue(valueMap bson.D, selectionMode for _, elem := range valueMap { if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}) + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) } else if elem.Key == "Selection" { // Update selection mode result = append(result, bson.E{Key: "Selection", Value: selectionMode}) @@ -71,7 +73,7 @@ func (pb *pageBuilder) cloneActionWithNewID(actionMap bson.D) bson.D { for _, elem := range actionMap { if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: mpr.IDToBsonBinary(mpr.GenerateID())}) + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) } else { result = append(result, elem) } @@ -101,24 +103,24 @@ func (pb *pageBuilder) createAttributeObject(attributePath string, objectTypeID, return nil, mdlerrors.NewValidationf("invalid attribute path %q: expected Module.Entity.Attribute format", attributePath) } return bson.D{ - {Key: "$ID", Value: hexToBytes(mpr.GenerateID())}, + {Key: "$ID", Value: hexToBytes(types.GenerateID())}, {Key: "$Type", Value: "CustomWidgets$WidgetObject"}, {Key: "Properties", Value: []any{ int32(2), bson.D{ - {Key: "$ID", Value: hexToBytes(mpr.GenerateID())}, + {Key: "$ID", Value: hexToBytes(types.GenerateID())}, {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, {Key: "TypePointer", Value: hexToBytes(propertyTypeID)}, {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: hexToBytes(mpr.GenerateID())}, + {Key: "$ID", Value: hexToBytes(types.GenerateID())}, {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: hexToBytes(mpr.GenerateID())}, + {Key: "$ID", Value: hexToBytes(types.GenerateID())}, {Key: "$Type", Value: "Forms$NoAction"}, {Key: "DisabledDuringExecution", Value: true}, }}, {Key: "AttributeRef", Value: bson.D{ - {Key: "$ID", Value: hexToBytes(mpr.GenerateID())}, + {Key: "$ID", Value: hexToBytes(types.GenerateID())}, {Key: "$Type", Value: "DomainModels$AttributeRef"}, {Key: "Attribute", Value: attributePath}, {Key: "EntityRef", Value: nil}, diff --git a/mdl/executor/widget_engine_test.go b/mdl/executor/widget_engine_test.go index 23069ed6..bb4502de 100644 --- a/mdl/executor/widget_engine_test.go +++ b/mdl/executor/widget_engine_test.go @@ -8,7 +8,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/pages" "go.mongodb.org/mongo-driver/bson" ) @@ -540,7 +540,7 @@ func TestSetChildWidgets(t *testing.T) { func TestOpSelection(t *testing.T) { // Call the real opSelection function with a properly structured widget BSON. typePointerBytes := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} - typePointerUUID := mpr.BlobToUUID(typePointerBytes) + typePointerUUID := types.BlobToUUID(typePointerBytes) widgetObj := bson.D{ {Key: "Properties", Value: bson.A{ diff --git a/mdl/executor/widget_operations.go b/mdl/executor/widget_operations.go index 6d183314..183d57c9 100644 --- a/mdl/executor/widget_operations.go +++ b/mdl/executor/widget_operations.go @@ -5,8 +5,8 @@ package executor import ( "log" + "github.com/mendixlabs/mxcli/mdl/bsonutil" "github.com/mendixlabs/mxcli/mdl/types" - "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/pages" "go.mongodb.org/mongo-driver/bson" ) @@ -218,7 +218,7 @@ func updateTemplateText(tmpl bson.D, text string) bson.D { updated = append(updated, bson.E{Key: "Items", Value: bson.A{ int32(3), bson.D{ - {Key: "$ID", Value: mpr.IDToBsonBinary(types.GenerateID())}, + {Key: "$ID", Value: bsonutil.IDToBsonBinary(types.GenerateID())}, {Key: "$Type", Value: "Texts$Translation"}, {Key: "LanguageCode", Value: "en_US"}, {Key: "Text", Value: text}, From 3439677443f9a222034d43d4e6de524eecd12dc2 Mon Sep 17 00:00:00 2001 From: Andrew Vasilyev Date: Sun, 19 Apr 2026 16:57:15 +0200 Subject: [PATCH 3/5] refactor: implement mutation backends and migrate handlers off sdk/mpr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implement PageMutator, WorkflowMutator, and WidgetBuilderBackend in mdl/backend/mpr/. Rewrite ALTER PAGE (1721→256 lines) and ALTER WORKFLOW (887→178 lines) as thin orchestrators using mutator sessions. Implement PluggableWidgetEngine with WidgetObjectBuilder interface, eliminating all BSON from widget_engine.go. - Create mdl/backend/mpr/page_mutator.go (1554 lines) - Create mdl/backend/mpr/workflow_mutator.go (771 lines) - Create mdl/backend/mpr/widget_builder.go (1007 lines) - Migrate SerializeWidget/ClientAction/DataSource to backend interface - Add ParseMicroflowFromRaw to MicroflowBackend interface - Delete widget_operations.go, widget_templates.go, widget_defaults.go - Move ALTER PAGE/WORKFLOW tests to backend/mpr/ package --- mdl/backend/backend.go | 1 + mdl/backend/microflow.go | 4 + mdl/backend/mock/backend.go | 9 +- mdl/backend/mock/mock_microflow.go | 7 + mdl/backend/mock/mock_mutation.go | 32 + mdl/backend/mpr/backend.go | 15 +- mdl/backend/mpr/page_mutator.go | 1554 ++++++++++++++++ .../mpr/page_mutator_test.go} | 174 +- mdl/backend/mpr/widget_builder.go | 1007 +++++++++++ mdl/backend/mpr/workflow_mutator.go | 771 ++++++++ .../mpr/workflow_mutator_test.go} | 317 ++-- mdl/backend/mutation.go | 102 +- mdl/executor/bson_helpers.go | 481 +++++ mdl/executor/cmd_alter_page.go | 1598 +---------------- mdl/executor/cmd_alter_workflow.go | 841 +-------- mdl/executor/cmd_diff_local.go | 3 +- mdl/executor/cmd_pages_builder.go | 12 +- mdl/executor/cmd_pages_builder_input.go | 14 - .../cmd_pages_builder_input_datagrid.go | 3 +- .../cmd_pages_builder_v3_pluggable.go | 13 +- mdl/executor/cmd_pages_create_v3.go | 2 + mdl/executor/widget_defaults.go | 151 -- mdl/executor/widget_engine.go | 229 ++- mdl/executor/widget_engine_test.go | 173 +- mdl/executor/widget_operations.go | 301 ---- mdl/executor/widget_registry.go | 44 +- mdl/executor/widget_registry_test.go | 14 +- mdl/executor/widget_templates.go | 162 -- 28 files changed, 4511 insertions(+), 3523 deletions(-) create mode 100644 mdl/backend/mpr/page_mutator.go rename mdl/{executor/alter_page_test.go => backend/mpr/page_mutator_test.go} (80%) create mode 100644 mdl/backend/mpr/widget_builder.go create mode 100644 mdl/backend/mpr/workflow_mutator.go rename mdl/{executor/cmd_alter_workflow_test.go => backend/mpr/workflow_mutator_test.go} (50%) create mode 100644 mdl/executor/bson_helpers.go delete mode 100644 mdl/executor/widget_defaults.go delete mode 100644 mdl/executor/widget_operations.go delete mode 100644 mdl/executor/widget_templates.go diff --git a/mdl/backend/backend.go b/mdl/backend/backend.go index 18fa681e..f9fe9ad0 100644 --- a/mdl/backend/backend.go +++ b/mdl/backend/backend.go @@ -34,4 +34,5 @@ type FullBackend interface { PageMutationBackend WorkflowMutationBackend WidgetSerializationBackend + WidgetBuilderBackend } diff --git a/mdl/backend/microflow.go b/mdl/backend/microflow.go index 06f9edcf..65596ce4 100644 --- a/mdl/backend/microflow.go +++ b/mdl/backend/microflow.go @@ -16,6 +16,10 @@ type MicroflowBackend interface { DeleteMicroflow(id model.ID) error MoveMicroflow(mf *microflows.Microflow) error + // ParseMicroflowFromRaw builds a Microflow from an already-unmarshalled + // BSON map. Used by diff-local and other callers that have raw map data. + ParseMicroflowFromRaw(raw map[string]any, unitID, containerID model.ID) *microflows.Microflow + ListNanoflows() ([]*microflows.Nanoflow, error) GetNanoflow(id model.ID) (*microflows.Nanoflow, error) CreateNanoflow(nf *microflows.Nanoflow) error diff --git a/mdl/backend/mock/backend.go b/mdl/backend/mock/backend.go index 1a4ff937..01a4f1b9 100644 --- a/mdl/backend/mock/backend.go +++ b/mdl/backend/mock/backend.go @@ -80,7 +80,8 @@ type MockBackend struct { CreateMicroflowFunc func(mf *microflows.Microflow) error UpdateMicroflowFunc func(mf *microflows.Microflow) error DeleteMicroflowFunc func(id model.ID) error - MoveMicroflowFunc func(mf *microflows.Microflow) error + MoveMicroflowFunc func(mf *microflows.Microflow) error + ParseMicroflowFromRawFunc func(raw map[string]any, unitID, containerID model.ID) *microflows.Microflow ListNanoflowsFunc func() ([]*microflows.Nanoflow, error) GetNanoflowFunc func(id model.ID) (*microflows.Nanoflow, error) CreateNanoflowFunc func(nf *microflows.Nanoflow) error @@ -270,6 +271,12 @@ type MockBackend struct { SerializeDataSourceFunc func(ds pages.DataSource) (any, error) SerializeWorkflowActivityFunc func(a workflows.WorkflowActivity) (any, error) + // WidgetBuilderBackend + LoadWidgetTemplateFunc func(widgetID string, projectPath string) (backend.WidgetObjectBuilder, error) + SerializeWidgetToOpaqueFunc func(w pages.Widget) any + SerializeDataSourceToOpaqueFunc func(ds pages.DataSource) any + BuildCreateAttributeObjectFunc func(attributePath string, objectTypeID, propertyTypeID, valueTypeID string) (any, error) + // AgentEditorBackend ListAgentEditorModelsFunc func() ([]*agenteditor.Model, error) ListAgentEditorKnowledgeBasesFunc func() ([]*agenteditor.KnowledgeBase, error) diff --git a/mdl/backend/mock/mock_microflow.go b/mdl/backend/mock/mock_microflow.go index ed02e9ed..567bf859 100644 --- a/mdl/backend/mock/mock_microflow.go +++ b/mdl/backend/mock/mock_microflow.go @@ -49,6 +49,13 @@ func (m *MockBackend) MoveMicroflow(mf *microflows.Microflow) error { return nil } +func (m *MockBackend) ParseMicroflowFromRaw(raw map[string]any, unitID, containerID model.ID) *microflows.Microflow { + if m.ParseMicroflowFromRawFunc != nil { + return m.ParseMicroflowFromRawFunc(raw, unitID, containerID) + } + return nil +} + func (m *MockBackend) ListNanoflows() ([]*microflows.Nanoflow, error) { if m.ListNanoflowsFunc != nil { return m.ListNanoflowsFunc() diff --git a/mdl/backend/mock/mock_mutation.go b/mdl/backend/mock/mock_mutation.go index 89b91122..82fabe96 100644 --- a/mdl/backend/mock/mock_mutation.go +++ b/mdl/backend/mock/mock_mutation.go @@ -62,3 +62,35 @@ func (m *MockBackend) SerializeWorkflowActivity(a workflows.WorkflowActivity) (a } return nil, nil } + +// --------------------------------------------------------------------------- +// WidgetBuilderBackend +// --------------------------------------------------------------------------- + +func (m *MockBackend) LoadWidgetTemplate(widgetID string, projectPath string) (backend.WidgetObjectBuilder, error) { + if m.LoadWidgetTemplateFunc != nil { + return m.LoadWidgetTemplateFunc(widgetID, projectPath) + } + return nil, nil +} + +func (m *MockBackend) SerializeWidgetToOpaque(w pages.Widget) any { + if m.SerializeWidgetToOpaqueFunc != nil { + return m.SerializeWidgetToOpaqueFunc(w) + } + return nil +} + +func (m *MockBackend) SerializeDataSourceToOpaque(ds pages.DataSource) any { + if m.SerializeDataSourceToOpaqueFunc != nil { + return m.SerializeDataSourceToOpaqueFunc(ds) + } + return nil +} + +func (m *MockBackend) BuildCreateAttributeObject(attributePath string, objectTypeID, propertyTypeID, valueTypeID string) (any, error) { + if m.BuildCreateAttributeObjectFunc != nil { + return m.BuildCreateAttributeObjectFunc(attributePath, objectTypeID, propertyTypeID, valueTypeID) + } + return nil, nil +} diff --git a/mdl/backend/mpr/backend.go b/mdl/backend/mpr/backend.go index 54bce183..4c5613c7 100644 --- a/mdl/backend/mpr/backend.go +++ b/mdl/backend/mpr/backend.go @@ -211,6 +211,9 @@ func (b *MprBackend) MoveMicroflow(mf *microflows.Microflow) error { func (b *MprBackend) ListNanoflows() ([]*microflows.Nanoflow, error) { return b.reader.ListNanoflows() } +func (b *MprBackend) ParseMicroflowFromRaw(raw map[string]any, unitID, containerID model.ID) *microflows.Microflow { + return mpr.ParseMicroflowFromRaw(raw, unitID, containerID) +} func (b *MprBackend) GetNanoflow(id model.ID) (*microflows.Nanoflow, error) { return b.reader.GetNanoflow(id) } @@ -726,17 +729,17 @@ func (b *MprBackend) DeleteAgentEditorAgent(id string) error { } // --------------------------------------------------------------------------- -// PageMutationBackend +// PageMutationBackend — implemented in page_mutator.go +// --------------------------------------------------------------------------- -func (b *MprBackend) OpenPageForMutation(unitID model.ID) (backend.PageMutator, error) { - panic("MprBackend.OpenPageForMutation not yet implemented") -} +// OpenPageForMutation is implemented in page_mutator.go. // --------------------------------------------------------------------------- // WorkflowMutationBackend +// OpenWorkflowForMutation is implemented in workflow_mutator.go. func (b *MprBackend) OpenWorkflowForMutation(unitID model.ID) (backend.WorkflowMutator, error) { - panic("MprBackend.OpenWorkflowForMutation not yet implemented") + return b.openWorkflowForMutation(unitID) } // --------------------------------------------------------------------------- @@ -751,7 +754,7 @@ func (b *MprBackend) SerializeClientAction(a pages.ClientAction) (any, error) { } func (b *MprBackend) SerializeDataSource(ds pages.DataSource) (any, error) { - panic("MprBackend.SerializeDataSource not yet implemented") + return mpr.SerializeCustomWidgetDataSource(ds), nil } func (b *MprBackend) SerializeWorkflowActivity(a workflows.WorkflowActivity) (any, error) { diff --git a/mdl/backend/mpr/page_mutator.go b/mdl/backend/mpr/page_mutator.go new file mode 100644 index 00000000..c1d0b08f --- /dev/null +++ b/mdl/backend/mpr/page_mutator.go @@ -0,0 +1,1554 @@ +// SPDX-License-Identifier: Apache-2.0 + +package mprbackend + +import ( + "fmt" + "strings" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + + "github.com/mendixlabs/mxcli/mdl/backend" + "github.com/mendixlabs/mxcli/mdl/bsonutil" + "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/sdk/pages" +) + +// Compile-time check. +var _ backend.PageMutator = (*mprPageMutator)(nil) + +// mprPageMutator implements backend.PageMutator for the MPR backend. +type mprPageMutator struct { + rawData bson.D + containerType string // "page", "snippet", or "layout" + unitID model.ID + backend *MprBackend + widgetFinder widgetFinder +} + +// --------------------------------------------------------------------------- +// OpenPageForMutation +// --------------------------------------------------------------------------- + +// OpenPageForMutation loads a page/snippet/layout unit and returns a PageMutator. +func (b *MprBackend) OpenPageForMutation(unitID model.ID) (backend.PageMutator, error) { + rawBytes, err := b.reader.GetRawUnitBytes(unitID) + if err != nil { + return nil, fmt.Errorf("load raw unit bytes: %w", err) + } + var rawData bson.D + if err := bson.Unmarshal(rawBytes, &rawData); err != nil { + return nil, fmt.Errorf("unmarshal unit BSON: %w", err) + } + + // Determine container type from $Type field. + typeName := dGetString(rawData, "$Type") + containerType := "page" + switch { + case strings.Contains(typeName, "Snippet"): + containerType = "snippet" + case strings.Contains(typeName, "Layout"): + containerType = "layout" + } + + finder := findBsonWidget + if containerType == "snippet" { + finder = findBsonWidgetInSnippet + } + + return &mprPageMutator{ + rawData: rawData, + containerType: containerType, + unitID: unitID, + backend: b, + widgetFinder: finder, + }, nil +} + +// --------------------------------------------------------------------------- +// PageMutator interface implementation +// --------------------------------------------------------------------------- + +func (m *mprPageMutator) ContainerType() string { return m.containerType } + +func (m *mprPageMutator) SetWidgetProperty(widgetRef string, prop string, value any) error { + if widgetRef == "" { + // Page-level property + return applyPageLevelSetMut(m.rawData, prop, value) + } + result := m.widgetFinder(m.rawData, widgetRef) + if result == nil { + return fmt.Errorf("widget %q not found", widgetRef) + } + return setRawWidgetPropertyMut(result.widget, prop, value) +} + +func (m *mprPageMutator) SetWidgetDataSource(widgetRef string, ds pages.DataSource) error { + result := m.widgetFinder(m.rawData, widgetRef) + if result == nil { + return fmt.Errorf("widget %q not found", widgetRef) + } + serialized := serializeDataSourceBson(ds) + if serialized == nil { + return fmt.Errorf("unsupported DataSource type %T", ds) + } + dSet(result.widget, "DataSource", serialized) + return nil +} + +func (m *mprPageMutator) SetColumnProperty(gridRef string, columnRef string, prop string, value any) error { + result := findBsonColumn(m.rawData, gridRef, columnRef, m.widgetFinder) + if result == nil { + return fmt.Errorf("column %q on grid %q not found", columnRef, gridRef) + } + return setColumnPropertyMut(result.widget, result.colPropKeys, prop, value) +} + +func (m *mprPageMutator) InsertWidget(widgetRef string, columnRef string, position string, widgets []pages.Widget) error { + var result *bsonWidgetResult + if columnRef != "" { + result = findBsonColumn(m.rawData, widgetRef, columnRef, m.widgetFinder) + } else { + result = m.widgetFinder(m.rawData, widgetRef) + } + if result == nil { + if columnRef != "" { + return fmt.Errorf("column %q on widget %q not found", columnRef, widgetRef) + } + return fmt.Errorf("widget %q not found", widgetRef) + } + + // Serialize widgets + newBsonWidgets, err := serializeWidgets(widgets) + if err != nil { + return fmt.Errorf("serialize widgets: %w", err) + } + + insertIdx := result.index + if strings.EqualFold(position, "after") { + insertIdx = result.index + 1 + } + + newArr := make([]any, 0, len(result.parentArr)+len(newBsonWidgets)) + newArr = append(newArr, result.parentArr[:insertIdx]...) + newArr = append(newArr, newBsonWidgets...) + newArr = append(newArr, result.parentArr[insertIdx:]...) + + dSetArray(result.parentDoc, result.parentKey, newArr) + return nil +} + +func (m *mprPageMutator) DropWidget(refs []backend.WidgetRef) error { + for _, ref := range refs { + var result *bsonWidgetResult + if ref.IsColumn() { + result = findBsonColumn(m.rawData, ref.Widget, ref.Column, m.widgetFinder) + } else { + result = m.widgetFinder(m.rawData, ref.Widget) + } + if result == nil { + return fmt.Errorf("widget %q not found", ref.Name()) + } + newArr := make([]any, 0, len(result.parentArr)-1) + newArr = append(newArr, result.parentArr[:result.index]...) + newArr = append(newArr, result.parentArr[result.index+1:]...) + dSetArray(result.parentDoc, result.parentKey, newArr) + } + return nil +} + +func (m *mprPageMutator) ReplaceWidget(widgetRef string, columnRef string, widgets []pages.Widget) error { + var result *bsonWidgetResult + if columnRef != "" { + result = findBsonColumn(m.rawData, widgetRef, columnRef, m.widgetFinder) + } else { + result = m.widgetFinder(m.rawData, widgetRef) + } + if result == nil { + if columnRef != "" { + return fmt.Errorf("column %q on widget %q not found", columnRef, widgetRef) + } + return fmt.Errorf("widget %q not found", widgetRef) + } + + newBsonWidgets, err := serializeWidgets(widgets) + if err != nil { + return fmt.Errorf("serialize widgets: %w", err) + } + + newArr := make([]any, 0, len(result.parentArr)-1+len(newBsonWidgets)) + newArr = append(newArr, result.parentArr[:result.index]...) + newArr = append(newArr, newBsonWidgets...) + newArr = append(newArr, result.parentArr[result.index+1:]...) + + dSetArray(result.parentDoc, result.parentKey, newArr) + return nil +} + +func (m *mprPageMutator) AddVariable(name, dataType, defaultValue string) error { + // Check for duplicate variable name + existingVars := dGetArrayElements(dGet(m.rawData, "Variables")) + for _, ev := range existingVars { + if evDoc, ok := ev.(bson.D); ok { + if dGetString(evDoc, "Name") == name { + return fmt.Errorf("variable $%s already exists", name) + } + } + } + + varTypeID := types.GenerateID() + bsonTypeName := mdlTypeToBsonType(dataType) + varType := bson.D{ + {Key: "$ID", Value: bsonutil.IDToBsonBinary(varTypeID)}, + {Key: "$Type", Value: bsonTypeName}, + } + if bsonTypeName == "DataTypes$ObjectType" { + varType = append(varType, bson.E{Key: "Entity", Value: dataType}) + } + + varID := types.GenerateID() + varDoc := bson.D{ + {Key: "$ID", Value: bsonutil.IDToBsonBinary(varID)}, + {Key: "$Type", Value: "Forms$LocalVariable"}, + {Key: "DefaultValue", Value: defaultValue}, + {Key: "Name", Value: name}, + {Key: "VariableType", Value: varType}, + } + + existing := toBsonA(dGet(m.rawData, "Variables")) + if existing != nil { + elements := dGetArrayElements(dGet(m.rawData, "Variables")) + elements = append(elements, varDoc) + dSetArray(m.rawData, "Variables", elements) + } else { + m.rawData = append(m.rawData, bson.E{Key: "Variables", Value: bson.A{int32(3), varDoc}}) + } + return nil +} + +func (m *mprPageMutator) DropVariable(name string) error { + elements := dGetArrayElements(dGet(m.rawData, "Variables")) + if elements == nil { + return fmt.Errorf("variable $%s not found", name) + } + + found := false + var kept []any + for _, elem := range elements { + if doc, ok := elem.(bson.D); ok { + if dGetString(doc, "Name") == name { + found = true + continue + } + } + kept = append(kept, elem) + } + if !found { + return fmt.Errorf("variable $%s not found", name) + } + dSetArray(m.rawData, "Variables", kept) + return nil +} + +func (m *mprPageMutator) SetLayout(newLayout string, paramMappings map[string]string) error { + if m.containerType == "snippet" { + return fmt.Errorf("SET Layout is not supported for snippets") + } + + formCall := dGetDoc(m.rawData, "FormCall") + if formCall == nil { + return fmt.Errorf("page has no FormCall (layout reference)") + } + + // Detect old layout name + oldLayoutQN := "" + for _, elem := range formCall { + if elem.Key == "Form" { + if s, ok := elem.Value.(string); ok && s != "" { + oldLayoutQN = s + } + } + if elem.Key == "Arguments" { + if arr, ok := elem.Value.(bson.A); ok { + for _, item := range arr { + if doc, ok := item.(bson.D); ok { + for _, field := range doc { + if field.Key == "Parameter" { + if s, ok := field.Value.(string); ok && oldLayoutQN == "" { + if lastDot := strings.LastIndex(s, "."); lastDot > 0 { + oldLayoutQN = s[:lastDot] + } + } + } + } + } + } + } + } + } + + if oldLayoutQN == "" { + return fmt.Errorf("cannot determine current layout from FormCall") + } + if oldLayoutQN == newLayout { + return nil + } + + // Update Form field + for i, elem := range formCall { + if elem.Key == "Form" { + formCall[i].Value = newLayout + } + } + + // Remap Parameter strings + for _, elem := range formCall { + if elem.Key != "Arguments" { + continue + } + arr, ok := elem.Value.(bson.A) + if !ok { + continue + } + for _, item := range arr { + doc, ok := item.(bson.D) + if !ok { + continue + } + for j, field := range doc { + if field.Key != "Parameter" { + continue + } + paramStr, ok := field.Value.(string) + if !ok { + continue + } + placeholder := paramStr + if strings.HasPrefix(paramStr, oldLayoutQN+".") { + placeholder = paramStr[len(oldLayoutQN)+1:] + } + if paramMappings != nil { + if mapped, ok := paramMappings[placeholder]; ok { + placeholder = mapped + } + } + doc[j].Value = newLayout + "." + placeholder + } + } + } + + // Write FormCall back + for i, elem := range m.rawData { + if elem.Key == "FormCall" { + m.rawData[i].Value = formCall + break + } + } + return nil +} + +func (m *mprPageMutator) SetPluggableProperty(widgetRef string, propKey string, opName string, ctx backend.PluggablePropertyContext) error { + result := m.widgetFinder(m.rawData, widgetRef) + if result == nil { + return fmt.Errorf("widget %q not found", widgetRef) + } + + obj := dGetDoc(result.widget, "Object") + if obj == nil { + return fmt.Errorf("widget %q has no pluggable Object", widgetRef) + } + + propTypeKeyMap := buildPropKeyMap(result.widget) + + props := dGetArrayElements(dGet(obj, "Properties")) + for _, prop := range props { + propDoc, ok := prop.(bson.D) + if !ok { + continue + } + typePointerID := extractBinaryIDFromDoc(dGet(propDoc, "TypePointer")) + resolvedKey := propTypeKeyMap[typePointerID] + if resolvedKey != propKey { + continue + } + valDoc := dGetDoc(propDoc, "Value") + if valDoc == nil { + return fmt.Errorf("property %q has no Value", propKey) + } + + switch opName { + case "primitive": + dSet(valDoc, "PrimitiveValue", ctx.PrimitiveVal) + case "attribute": + if attrDoc := dGetDoc(valDoc, "AttributeRef"); attrDoc != nil { + dSet(attrDoc, "Attribute", ctx.AttributePath) + } else { + dSet(valDoc, "AttributeRef", bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "DomainModels$AttributeRef"}, + {Key: "Attribute", Value: ctx.AttributePath}, + {Key: "EntityRef", Value: nil}, + }) + } + case "association": + dSet(valDoc, "AssociationRef", ctx.AssocPath) + if ctx.EntityName != "" { + dSet(valDoc, "EntityRef", bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "DomainModels$DirectEntityRef"}, + {Key: "Entity", Value: ctx.EntityName}, + }) + } + case "datasource": + serialized := mpr.SerializeCustomWidgetDataSource(ctx.DataSource) + dSet(valDoc, "DataSource", serialized) + case "widgets": + serialized, err := serializeWidgets(ctx.ChildWidgets) + if err != nil { + return fmt.Errorf("serialize child widgets: %w", err) + } + var bsonArr bson.A + bsonArr = append(bsonArr, int32(2)) + for _, w := range serialized { + bsonArr = append(bsonArr, w) + } + dSet(valDoc, "Widgets", bsonArr) + case "texttemplate": + if tmpl := dGetDoc(valDoc, "TextTemplate"); tmpl != nil { + items := dGetArrayElements(dGet(tmpl, "Items")) + if len(items) > 0 { + if itemDoc, ok := items[0].(bson.D); ok { + dSet(itemDoc, "Text", ctx.TextTemplate) + } + } + } + case "action": + serialized := mpr.SerializeClientAction(ctx.Action) + dSet(valDoc, "Action", serialized) + case "selection": + dSet(valDoc, "PrimitiveValue", ctx.Selection) + case "attributeObjects": + // Set multiple attribute paths on sub-objects + objects := dGetArrayElements(dGet(valDoc, "Objects")) + for i, attrPath := range ctx.AttributePaths { + if i >= len(objects) { + break + } + if objDoc, ok := objects[i].(bson.D); ok { + objProps := dGetArrayElements(dGet(objDoc, "Properties")) + for _, op := range objProps { + opDoc, ok := op.(bson.D) + if !ok { + continue + } + if opVal := dGetDoc(opDoc, "Value"); opVal != nil { + if attrRef := dGetDoc(opVal, "AttributeRef"); attrRef != nil { + dSet(attrRef, "Attribute", attrPath) + } + } + } + } + } + default: + return fmt.Errorf("unsupported pluggable property operation: %s", opName) + } + return nil + } + return fmt.Errorf("pluggable property %q not found on widget %q", propKey, widgetRef) +} + +func (m *mprPageMutator) EnclosingEntity(widgetRef string) string { + return findEnclosingEntityContext(m.rawData, widgetRef) +} + +func (m *mprPageMutator) WidgetScope() map[string]model.ID { + return extractWidgetScopeFromBSON(m.rawData) +} + +func (m *mprPageMutator) ParamScope() (map[string]model.ID, map[string]string) { + return extractPageParamsFromBSON(m.rawData) +} + +func (m *mprPageMutator) FindWidget(name string) bool { + return m.widgetFinder(m.rawData, name) != nil +} + +func (m *mprPageMutator) Save() error { + outBytes, err := bson.Marshal(m.rawData) + if err != nil { + return fmt.Errorf("marshal modified %s: %w", m.containerType, err) + } + return m.backend.writer.UpdateRawUnit(string(m.unitID), outBytes) +} + +// --------------------------------------------------------------------------- +// BSON helpers (moved from executor/cmd_alter_page.go) +// --------------------------------------------------------------------------- + +// dGet returns the value for a key in a bson.D, or nil if not found. +func dGet(doc bson.D, key string) any { + for _, elem := range doc { + if elem.Key == key { + return elem.Value + } + } + return nil +} + +// dGetDoc returns a nested bson.D field value, or nil. +func dGetDoc(doc bson.D, key string) bson.D { + v := dGet(doc, key) + if d, ok := v.(bson.D); ok { + return d + } + return nil +} + +// dGetString returns a string field value, or "". +func dGetString(doc bson.D, key string) string { + v := dGet(doc, key) + if s, ok := v.(string); ok { + return s + } + return "" +} + +// dSet sets a field value in a bson.D in place. Returns true if found. +func dSet(doc bson.D, key string, value any) bool { + for i := range doc { + if doc[i].Key == key { + doc[i].Value = value + return true + } + } + return false +} + +// dGetArrayElements extracts Mendix array elements from a bson.D field value. +// Strips the int32 type marker at index 0. +func dGetArrayElements(val any) []any { + arr := toBsonA(val) + if len(arr) == 0 { + return nil + } + if _, ok := arr[0].(int32); ok { + return arr[1:] + } + if _, ok := arr[0].(int); ok { + return arr[1:] + } + return arr +} + +// toBsonA converts various BSON array types to []any. +func toBsonA(v any) []any { + switch arr := v.(type) { + case bson.A: + return []any(arr) + case []any: + return arr + default: + return nil + } +} + +// dSetArray sets a Mendix-style BSON array field, preserving the int32 marker. +func dSetArray(doc bson.D, key string, elements []any) { + existing := toBsonA(dGet(doc, key)) + var marker any + if len(existing) > 0 { + if _, ok := existing[0].(int32); ok { + marker = existing[0] + } else if _, ok := existing[0].(int); ok { + marker = existing[0] + } + } + var result bson.A + if marker != nil { + result = make(bson.A, 0, len(elements)+1) + result = append(result, marker) + result = append(result, elements...) + } else { + result = make(bson.A, len(elements)) + copy(result, elements) + } + dSet(doc, key, result) +} + +// extractBinaryIDFromDoc extracts a binary ID string from a bson.D field. +func extractBinaryIDFromDoc(val any) string { + if bin, ok := val.(primitive.Binary); ok { + return types.BlobToUUID(bin.Data) + } + return "" +} + +// --------------------------------------------------------------------------- +// BSON widget tree walking +// --------------------------------------------------------------------------- + +// bsonWidgetResult holds a found widget and its parent context. +type bsonWidgetResult struct { + widget bson.D + parentArr []any + parentKey string + parentDoc bson.D + index int + colPropKeys map[string]string +} + +// widgetFinder is a function type for locating widgets in a raw BSON tree. +type widgetFinder func(rawData bson.D, widgetName string) *bsonWidgetResult + +// findBsonWidget searches the raw BSON page tree for a widget by name. +func findBsonWidget(rawData bson.D, widgetName string) *bsonWidgetResult { + formCall := dGetDoc(rawData, "FormCall") + if formCall == nil { + return nil + } + args := dGetArrayElements(dGet(formCall, "Arguments")) + for _, arg := range args { + argDoc, ok := arg.(bson.D) + if !ok { + continue + } + if result := findInWidgetArray(argDoc, "Widgets", widgetName); result != nil { + return result + } + } + return nil +} + +// findBsonWidgetInSnippet searches the raw BSON snippet tree for a widget by name. +func findBsonWidgetInSnippet(rawData bson.D, widgetName string) *bsonWidgetResult { + if result := findInWidgetArray(rawData, "Widgets", widgetName); result != nil { + return result + } + if widgetContainer := dGetDoc(rawData, "Widget"); widgetContainer != nil { + if result := findInWidgetArray(widgetContainer, "Widgets", widgetName); result != nil { + return result + } + } + return nil +} + +// findInWidgetArray searches a widget array for a named widget. +func findInWidgetArray(parentDoc bson.D, key string, widgetName string) *bsonWidgetResult { + elements := dGetArrayElements(dGet(parentDoc, key)) + for i, elem := range elements { + wDoc, ok := elem.(bson.D) + if !ok { + continue + } + if dGetString(wDoc, "Name") == widgetName { + return &bsonWidgetResult{ + widget: wDoc, + parentArr: elements, + parentKey: key, + parentDoc: parentDoc, + index: i, + } + } + if result := findInWidgetChildren(wDoc, widgetName); result != nil { + return result + } + } + return nil +} + +// findInWidgetChildren recursively searches widget children for a named widget. +func findInWidgetChildren(wDoc bson.D, widgetName string) *bsonWidgetResult { + typeName := dGetString(wDoc, "$Type") + + if result := findInWidgetArray(wDoc, "Widgets", widgetName); result != nil { + return result + } + if result := findInWidgetArray(wDoc, "FooterWidgets", widgetName); result != nil { + return result + } + + // LayoutGrid: Rows[].Columns[].Widgets[] + if strings.Contains(typeName, "LayoutGrid") { + rows := dGetArrayElements(dGet(wDoc, "Rows")) + for _, row := range rows { + rowDoc, ok := row.(bson.D) + if !ok { + continue + } + cols := dGetArrayElements(dGet(rowDoc, "Columns")) + for _, col := range cols { + colDoc, ok := col.(bson.D) + if !ok { + continue + } + if result := findInWidgetArray(colDoc, "Widgets", widgetName); result != nil { + return result + } + } + } + } + + // TabContainer: TabPages[].Widgets[] + tabPages := dGetArrayElements(dGet(wDoc, "TabPages")) + for _, tp := range tabPages { + tpDoc, ok := tp.(bson.D) + if !ok { + continue + } + if result := findInWidgetArray(tpDoc, "Widgets", widgetName); result != nil { + return result + } + } + + // ControlBar + if controlBar := dGetDoc(wDoc, "ControlBar"); controlBar != nil { + if result := findInWidgetArray(controlBar, "Items", widgetName); result != nil { + return result + } + } + + // CustomWidget (pluggable): Object.Properties[].Value.Widgets[] + if strings.Contains(typeName, "CustomWidget") { + if obj := dGetDoc(wDoc, "Object"); obj != nil { + props := dGetArrayElements(dGet(obj, "Properties")) + for _, prop := range props { + propDoc, ok := prop.(bson.D) + if !ok { + continue + } + if valDoc := dGetDoc(propDoc, "Value"); valDoc != nil { + if result := findInWidgetArray(valDoc, "Widgets", widgetName); result != nil { + return result + } + } + } + } + } + + return nil +} + +// --------------------------------------------------------------------------- +// DataGrid2 column finder +// --------------------------------------------------------------------------- + +// findBsonColumn finds a column inside a DataGrid2 widget by derived name. +func findBsonColumn(rawData bson.D, gridName, columnName string, find widgetFinder) *bsonWidgetResult { + gridResult := find(rawData, gridName) + if gridResult == nil { + return nil + } + + gridPropKeyMap := buildPropKeyMap(gridResult.widget) + + obj := dGetDoc(gridResult.widget, "Object") + if obj == nil { + return nil + } + + props := dGetArrayElements(dGet(obj, "Properties")) + for _, prop := range props { + propDoc, ok := prop.(bson.D) + if !ok { + continue + } + typePointerID := extractBinaryIDFromDoc(dGet(propDoc, "TypePointer")) + propKey := gridPropKeyMap[typePointerID] + if propKey != "columns" { + continue + } + + valDoc := dGetDoc(propDoc, "Value") + if valDoc == nil { + return nil + } + + colPropKeyMap := buildColumnPropKeyMap(gridResult.widget, typePointerID) + + columns := dGetArrayElements(dGet(valDoc, "Objects")) + for i, colItem := range columns { + colDoc, ok := colItem.(bson.D) + if !ok { + continue + } + derived := deriveColumnNameBson(colDoc, colPropKeyMap, i) + if derived == columnName { + return &bsonWidgetResult{ + widget: colDoc, + parentArr: columns, + parentKey: "Objects", + parentDoc: valDoc, + index: i, + colPropKeys: colPropKeyMap, + } + } + } + return nil + } + return nil +} + +// buildPropKeyMap builds a TypePointer ID -> PropertyKey map. +func buildPropKeyMap(widgetDoc bson.D) map[string]string { + m := make(map[string]string) + widgetType := dGetDoc(widgetDoc, "Type") + if widgetType == nil { + return m + } + objType := dGetDoc(widgetType, "ObjectType") + if objType == nil { + return m + } + for _, pt := range dGetArrayElements(dGet(objType, "PropertyTypes")) { + ptDoc, ok := pt.(bson.D) + if !ok { + continue + } + key := dGetString(ptDoc, "PropertyKey") + id := extractBinaryIDFromDoc(dGet(ptDoc, "$ID")) + if key != "" && id != "" { + m[id] = key + } + } + return m +} + +// buildColumnPropKeyMap builds a TypePointer ID -> PropertyKey map for column properties. +func buildColumnPropKeyMap(widgetDoc bson.D, columnsTypePointerID string) map[string]string { + m := make(map[string]string) + widgetType := dGetDoc(widgetDoc, "Type") + if widgetType == nil { + return m + } + objType := dGetDoc(widgetType, "ObjectType") + if objType == nil { + return m + } + for _, pt := range dGetArrayElements(dGet(objType, "PropertyTypes")) { + ptDoc, ok := pt.(bson.D) + if !ok { + continue + } + id := extractBinaryIDFromDoc(dGet(ptDoc, "$ID")) + if id != columnsTypePointerID { + continue + } + valType := dGetDoc(ptDoc, "ValueType") + if valType == nil { + return m + } + colObjType := dGetDoc(valType, "ObjectType") + if colObjType == nil { + return m + } + for _, cpt := range dGetArrayElements(dGet(colObjType, "PropertyTypes")) { + cptDoc, ok := cpt.(bson.D) + if !ok { + continue + } + key := dGetString(cptDoc, "PropertyKey") + cid := extractBinaryIDFromDoc(dGet(cptDoc, "$ID")) + if key != "" && cid != "" { + m[cid] = key + } + } + return m + } + return m +} + +// deriveColumnNameBson derives a column name from its BSON WidgetObject. +func deriveColumnNameBson(colDoc bson.D, propKeyMap map[string]string, index int) string { + var attribute, caption string + + props := dGetArrayElements(dGet(colDoc, "Properties")) + for _, prop := range props { + propDoc, ok := prop.(bson.D) + if !ok { + continue + } + typePointerID := extractBinaryIDFromDoc(dGet(propDoc, "TypePointer")) + propKey := propKeyMap[typePointerID] + + valDoc := dGetDoc(propDoc, "Value") + if valDoc == nil { + continue + } + + switch propKey { + case "attribute": + if attrRef := dGetString(valDoc, "AttributeRef"); attrRef != "" { + attribute = attrRef + } else if attrDoc := dGetDoc(valDoc, "AttributeRef"); attrDoc != nil { + attribute = dGetString(attrDoc, "Attribute") + } + case "header": + if tmpl := dGetDoc(valDoc, "TextTemplate"); tmpl != nil { + items := dGetArrayElements(dGet(tmpl, "Items")) + for _, item := range items { + if itemDoc, ok := item.(bson.D); ok { + if text := dGetString(itemDoc, "Text"); text != "" { + caption = text + } + } + } + } + } + } + + if attribute != "" { + parts := strings.Split(attribute, ".") + return parts[len(parts)-1] + } + if caption != "" { + return sanitizeColumnName(caption) + } + return fmt.Sprintf("col%d", index+1) +} + +// sanitizeColumnName converts a caption string into a valid column identifier. +func sanitizeColumnName(caption string) string { + var result []rune + for _, r := range caption { + if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' { + result = append(result, r) + } else { + result = append(result, '_') + } + } + return string(result) +} + +// --------------------------------------------------------------------------- +// Entity context extraction +// --------------------------------------------------------------------------- + +// findEnclosingEntityContext walks the raw BSON tree to find the entity context. +func findEnclosingEntityContext(rawData bson.D, widgetName string) string { + if formCall := dGetDoc(rawData, "FormCall"); formCall != nil { + args := dGetArrayElements(dGet(formCall, "Arguments")) + for _, arg := range args { + argDoc, ok := arg.(bson.D) + if !ok { + continue + } + if ctx := findEntityContextInWidgets(argDoc, "Widgets", widgetName, ""); ctx != "" { + return ctx + } + } + } + if ctx := findEntityContextInWidgets(rawData, "Widgets", widgetName, ""); ctx != "" { + return ctx + } + if widgetContainer := dGetDoc(rawData, "Widget"); widgetContainer != nil { + if ctx := findEntityContextInWidgets(widgetContainer, "Widgets", widgetName, ""); ctx != "" { + return ctx + } + } + return "" +} + +func findEntityContextInWidgets(parentDoc bson.D, key string, widgetName string, currentEntity string) string { + elements := dGetArrayElements(dGet(parentDoc, key)) + for _, elem := range elements { + wDoc, ok := elem.(bson.D) + if !ok { + continue + } + if dGetString(wDoc, "Name") == widgetName { + return currentEntity + } + entityCtx := currentEntity + if ent := extractEntityFromDataSource(wDoc); ent != "" { + entityCtx = ent + } + if ctx := findEntityContextInChildren(wDoc, widgetName, entityCtx); ctx != "" { + return ctx + } + } + return "" +} + +func findEntityContextInChildren(wDoc bson.D, widgetName string, currentEntity string) string { + typeName := dGetString(wDoc, "$Type") + + if ctx := findEntityContextInWidgets(wDoc, "Widgets", widgetName, currentEntity); ctx != "" { + return ctx + } + if ctx := findEntityContextInWidgets(wDoc, "FooterWidgets", widgetName, currentEntity); ctx != "" { + return ctx + } + if strings.Contains(typeName, "LayoutGrid") { + rows := dGetArrayElements(dGet(wDoc, "Rows")) + for _, row := range rows { + rowDoc, ok := row.(bson.D) + if !ok { + continue + } + cols := dGetArrayElements(dGet(rowDoc, "Columns")) + for _, col := range cols { + colDoc, ok := col.(bson.D) + if !ok { + continue + } + if ctx := findEntityContextInWidgets(colDoc, "Widgets", widgetName, currentEntity); ctx != "" { + return ctx + } + } + } + } + tabPages := dGetArrayElements(dGet(wDoc, "TabPages")) + for _, tp := range tabPages { + tpDoc, ok := tp.(bson.D) + if !ok { + continue + } + if ctx := findEntityContextInWidgets(tpDoc, "Widgets", widgetName, currentEntity); ctx != "" { + return ctx + } + } + if controlBar := dGetDoc(wDoc, "ControlBar"); controlBar != nil { + if ctx := findEntityContextInWidgets(controlBar, "Items", widgetName, currentEntity); ctx != "" { + return ctx + } + } + if strings.Contains(typeName, "CustomWidget") { + if obj := dGetDoc(wDoc, "Object"); obj != nil { + props := dGetArrayElements(dGet(obj, "Properties")) + for _, prop := range props { + propDoc, ok := prop.(bson.D) + if !ok { + continue + } + if valDoc := dGetDoc(propDoc, "Value"); valDoc != nil { + if ctx := findEntityContextInWidgets(valDoc, "Widgets", widgetName, currentEntity); ctx != "" { + return ctx + } + } + } + } + } + return "" +} + +func extractEntityFromDataSource(wDoc bson.D) string { + ds := dGetDoc(wDoc, "DataSource") + if ds == nil { + return "" + } + if entityRef := dGetDoc(ds, "EntityRef"); entityRef != nil { + if entity := dGetString(entityRef, "Entity"); entity != "" { + return entity + } + } + return "" +} + +// --------------------------------------------------------------------------- +// Widget scope extraction +// --------------------------------------------------------------------------- + +func extractWidgetScopeFromBSON(rawData bson.D) map[string]model.ID { + scope := make(map[string]model.ID) + if rawData == nil { + return scope + } + if formCall := dGetDoc(rawData, "FormCall"); formCall != nil { + args := dGetArrayElements(dGet(formCall, "Arguments")) + for _, arg := range args { + argDoc, ok := arg.(bson.D) + if !ok { + continue + } + collectWidgetScope(argDoc, "Widgets", scope) + } + } + collectWidgetScope(rawData, "Widgets", scope) + if widgetContainer := dGetDoc(rawData, "Widget"); widgetContainer != nil { + collectWidgetScope(widgetContainer, "Widgets", scope) + } + return scope +} + +// extractPageParamsFromBSON extracts page/snippet parameter names and entity +// IDs from the raw BSON document. +func extractPageParamsFromBSON(rawData bson.D) (map[string]model.ID, map[string]string) { + paramScope := make(map[string]model.ID) + paramEntityNames := make(map[string]string) + if rawData == nil { + return paramScope, paramEntityNames + } + + params := dGetArrayElements(dGet(rawData, "Parameters")) + for _, p := range params { + pDoc, ok := p.(bson.D) + if !ok { + continue + } + name := dGetString(pDoc, "Name") + if name == "" { + continue + } + paramType := dGetDoc(pDoc, "ParameterType") + if paramType == nil { + continue + } + typeName := dGetString(paramType, "$Type") + if typeName != "DataTypes$ObjectType" { + continue + } + entityName := dGetString(paramType, "Entity") + if entityName == "" { + continue + } + idVal := dGet(pDoc, "$ID") + paramID := model.ID(extractBinaryIDFromDoc(idVal)) + paramScope[name] = paramID + paramEntityNames[name] = entityName + } + return paramScope, paramEntityNames +} + +func collectWidgetScope(parentDoc bson.D, key string, scope map[string]model.ID) { + elements := dGetArrayElements(dGet(parentDoc, key)) + for _, elem := range elements { + wDoc, ok := elem.(bson.D) + if !ok { + continue + } + name := dGetString(wDoc, "Name") + if name != "" { + idVal := dGet(wDoc, "$ID") + if wID := extractBinaryIDFromDoc(idVal); wID != "" { + scope[name] = model.ID(wID) + } + } + collectWidgetScopeInChildren(wDoc, scope) + } +} + +func collectWidgetScopeInChildren(wDoc bson.D, scope map[string]model.ID) { + typeName := dGetString(wDoc, "$Type") + + collectWidgetScope(wDoc, "Widgets", scope) + collectWidgetScope(wDoc, "FooterWidgets", scope) + + if strings.Contains(typeName, "LayoutGrid") { + rows := dGetArrayElements(dGet(wDoc, "Rows")) + for _, row := range rows { + rowDoc, ok := row.(bson.D) + if !ok { + continue + } + cols := dGetArrayElements(dGet(rowDoc, "Columns")) + for _, col := range cols { + colDoc, ok := col.(bson.D) + if !ok { + continue + } + collectWidgetScope(colDoc, "Widgets", scope) + } + } + } + tabPages := dGetArrayElements(dGet(wDoc, "TabPages")) + for _, tp := range tabPages { + tpDoc, ok := tp.(bson.D) + if !ok { + continue + } + collectWidgetScope(tpDoc, "Widgets", scope) + } + if controlBar := dGetDoc(wDoc, "ControlBar"); controlBar != nil { + collectWidgetScope(controlBar, "Items", scope) + } + if strings.Contains(typeName, "CustomWidget") { + if obj := dGetDoc(wDoc, "Object"); obj != nil { + props := dGetArrayElements(dGet(obj, "Properties")) + for _, prop := range props { + propDoc, ok := prop.(bson.D) + if !ok { + continue + } + if valDoc := dGetDoc(propDoc, "Value"); valDoc != nil { + collectWidgetScope(valDoc, "Widgets", scope) + } + } + } + } +} + +// --------------------------------------------------------------------------- +// Property setting helpers +// --------------------------------------------------------------------------- + +// columnPropertyAliases maps user-facing property names to internal column property keys. +var columnPropertyAliases = map[string]string{ + "Caption": "header", + "Attribute": "attribute", + "Visible": "visible", + "Alignment": "alignment", + "WrapText": "wrapText", + "Sortable": "sortable", + "Resizable": "resizable", + "Draggable": "draggable", + "Hidable": "hidable", + "ColumnWidth": "width", + "Size": "size", + "ShowContentAs": "showContentAs", + "ColumnClass": "columnClass", + "Tooltip": "tooltip", +} + +func setColumnPropertyMut(colDoc bson.D, propKeyMap map[string]string, propName string, value any) error { + internalKey := columnPropertyAliases[propName] + if internalKey == "" { + internalKey = propName + } + + props := dGetArrayElements(dGet(colDoc, "Properties")) + for _, prop := range props { + propDoc, ok := prop.(bson.D) + if !ok { + continue + } + typePointerID := extractBinaryIDFromDoc(dGet(propDoc, "TypePointer")) + propKey := propKeyMap[typePointerID] + if propKey != internalKey { + continue + } + if valDoc := dGetDoc(propDoc, "Value"); valDoc != nil { + strVal := fmt.Sprintf("%v", value) + dSet(valDoc, "PrimitiveValue", strVal) + return nil + } + return fmt.Errorf("column property %q has no Value", propName) + } + return fmt.Errorf("column property %q not found", propName) +} + +func applyPageLevelSetMut(rawData bson.D, prop string, value any) error { + switch prop { + case "Title": + if formCall := dGetDoc(rawData, "FormCall"); formCall != nil { + setTranslatableText(formCall, "Title", value) + } else { + setTranslatableText(rawData, "Title", value) + } + case "Url": + strVal, _ := value.(string) + dSet(rawData, "Url", strVal) + default: + return fmt.Errorf("unsupported page-level property: %s", prop) + } + return nil +} + +func setRawWidgetPropertyMut(widget bson.D, propName string, value any) error { + switch propName { + case "Caption": + return setWidgetCaptionMut(widget, value) + case "Content": + return setWidgetContentMut(widget, value) + case "Label": + return setWidgetLabelMut(widget, value) + case "ButtonStyle": + if s, ok := value.(string); ok { + dSet(widget, "ButtonStyle", s) + } + return nil + case "Class": + if appearance := dGetDoc(widget, "Appearance"); appearance != nil { + if s, ok := value.(string); ok { + dSet(appearance, "Class", s) + } + } + return nil + case "Style": + if appearance := dGetDoc(widget, "Appearance"); appearance != nil { + if s, ok := value.(string); ok { + dSet(appearance, "Style", s) + } + } + return nil + case "Editable": + if s, ok := value.(string); ok { + dSet(widget, "Editable", s) + } + return nil + case "Visible": + if s, ok := value.(string); ok { + dSet(widget, "Visible", s) + } else if b, ok := value.(bool); ok { + if b { + dSet(widget, "Visible", "True") + } else { + dSet(widget, "Visible", "False") + } + } + return nil + case "Name": + if s, ok := value.(string); ok { + dSet(widget, "Name", s) + } + return nil + case "Attribute": + return setWidgetAttributeRefMut(widget, value) + default: + // Try as pluggable widget property + return setPluggableWidgetPropertyMut(widget, propName, value) + } +} + +func setWidgetCaptionMut(widget bson.D, value any) error { + caption := dGetDoc(widget, "Caption") + if caption == nil { + setTranslatableText(widget, "Caption", value) + return nil + } + setTranslatableText(caption, "", value) + return nil +} + +func setWidgetContentMut(widget bson.D, value any) error { + strVal, ok := value.(string) + if !ok { + return fmt.Errorf("Content value must be a string") + } + content := dGetDoc(widget, "Content") + if content == nil { + return fmt.Errorf("widget has no Content property") + } + template := dGetDoc(content, "Template") + if template == nil { + return fmt.Errorf("Content has no Template") + } + items := dGetArrayElements(dGet(template, "Items")) + if len(items) > 0 { + if itemDoc, ok := items[0].(bson.D); ok { + dSet(itemDoc, "Text", strVal) + return nil + } + } + return fmt.Errorf("Content.Template has no Items with Text") +} + +func setWidgetLabelMut(widget bson.D, value any) error { + label := dGetDoc(widget, "Label") + if label == nil { + return nil + } + setTranslatableText(label, "Caption", value) + return nil +} + +func setWidgetAttributeRefMut(widget bson.D, value any) error { + attrPath, ok := value.(string) + if !ok { + return fmt.Errorf("Attribute value must be a string") + } + + var attrRefValue any + if strings.Count(attrPath, ".") >= 2 { + attrRefValue = bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "DomainModels$AttributeRef"}, + {Key: "Attribute", Value: attrPath}, + {Key: "EntityRef", Value: nil}, + } + } else { + attrRefValue = nil + } + + for i, elem := range widget { + if elem.Key == "AttributeRef" { + widget[i].Value = attrRefValue + return nil + } + } + return fmt.Errorf("widget does not have an AttributeRef property") +} + +func setPluggableWidgetPropertyMut(widget bson.D, propName string, value any) error { + obj := dGetDoc(widget, "Object") + if obj == nil { + return fmt.Errorf("property %q not found (widget has no pluggable Object)", propName) + } + + propTypeKeyMap := make(map[string]string) + if widgetType := dGetDoc(widget, "Type"); widgetType != nil { + if objType := dGetDoc(widgetType, "ObjectType"); objType != nil { + propTypes := dGetArrayElements(dGet(objType, "PropertyTypes")) + for _, pt := range propTypes { + ptDoc, ok := pt.(bson.D) + if !ok { + continue + } + key := dGetString(ptDoc, "PropertyKey") + if key == "" { + continue + } + id := extractBinaryIDFromDoc(dGet(ptDoc, "$ID")) + if id != "" { + propTypeKeyMap[id] = key + } + } + } + } + + props := dGetArrayElements(dGet(obj, "Properties")) + for _, prop := range props { + propDoc, ok := prop.(bson.D) + if !ok { + continue + } + typePointerID := extractBinaryIDFromDoc(dGet(propDoc, "TypePointer")) + propKey := propTypeKeyMap[typePointerID] + if propKey != propName { + continue + } + if valDoc := dGetDoc(propDoc, "Value"); valDoc != nil { + switch v := value.(type) { + case string: + dSet(valDoc, "PrimitiveValue", v) + case bool: + if v { + dSet(valDoc, "PrimitiveValue", "yes") + } else { + dSet(valDoc, "PrimitiveValue", "no") + } + case int: + dSet(valDoc, "PrimitiveValue", fmt.Sprintf("%d", v)) + case float64: + dSet(valDoc, "PrimitiveValue", fmt.Sprintf("%g", v)) + default: + dSet(valDoc, "PrimitiveValue", fmt.Sprintf("%v", v)) + } + return nil + } + return fmt.Errorf("property %q has no Value map", propName) + } + return fmt.Errorf("pluggable property %q not found", propName) +} + +// setTranslatableText sets a translatable text value in BSON. +func setTranslatableText(parent bson.D, key string, value any) { + strVal, ok := value.(string) + if !ok { + return + } + + target := parent + if key != "" { + if nested := dGetDoc(parent, key); nested != nil { + target = nested + } else { + dSet(parent, key, strVal) + return + } + } + + translations := dGetArrayElements(dGet(target, "Translations")) + if len(translations) > 0 { + if tDoc, ok := translations[0].(bson.D); ok { + dSet(tDoc, "Text", strVal) + return + } + } + dSet(target, "Text", strVal) +} + +// --------------------------------------------------------------------------- +// Widget serialization helpers +// --------------------------------------------------------------------------- + +func serializeWidgets(widgets []pages.Widget) ([]any, error) { + var result []any + for _, w := range widgets { + bsonDoc := mpr.SerializeWidget(w) + if bsonDoc == nil { + continue + } + result = append(result, bsonDoc) + } + return result, nil +} + +// serializeDataSourceBson converts a pages.DataSource to a BSON document for widget-level DataSource fields. +func serializeDataSourceBson(ds pages.DataSource) bson.D { + switch d := ds.(type) { + case *pages.ListenToWidgetSource: + return bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Forms$ListenTargetSource"}, + {Key: "ListenTarget", Value: d.WidgetName}, + } + case *pages.DatabaseSource: + var entityRef any + if d.EntityName != "" { + entityRef = bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "DomainModels$DirectEntityRef"}, + {Key: "Entity", Value: d.EntityName}, + } + } + return bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Forms$DataViewSource"}, + {Key: "EntityRef", Value: entityRef}, + {Key: "ForceFullObjects", Value: false}, + {Key: "SourceVariable", Value: nil}, + } + case *pages.MicroflowSource: + return bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Forms$MicroflowSource"}, + {Key: "MicroflowSettings", Value: bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Forms$MicroflowSettings"}, + {Key: "Asynchronous", Value: false}, + {Key: "ConfirmationInfo", Value: nil}, + {Key: "FormValidations", Value: "All"}, + {Key: "Microflow", Value: d.Microflow}, + {Key: "ParameterMappings", Value: bson.A{int32(3)}}, + {Key: "ProgressBar", Value: "None"}, + {Key: "ProgressMessage", Value: nil}, + }}, + } + case *pages.NanoflowSource: + return bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Forms$NanoflowSource"}, + {Key: "NanoflowSettings", Value: bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Forms$NanoflowSettings"}, + {Key: "Nanoflow", Value: d.Nanoflow}, + {Key: "ParameterMappings", Value: bson.A{int32(3)}}, + }}, + } + default: + return nil + } +} + +// mdlTypeToBsonType converts an MDL type name to a BSON DataTypes$* type string. +func mdlTypeToBsonType(mdlType string) string { + switch strings.ToLower(mdlType) { + case "boolean": + return "DataTypes$BooleanType" + case "string": + return "DataTypes$StringType" + case "integer": + return "DataTypes$IntegerType" + case "long": + return "DataTypes$LongType" + case "decimal": + return "DataTypes$DecimalType" + case "datetime", "date": + return "DataTypes$DateTimeType" + default: + return "DataTypes$ObjectType" + } +} diff --git a/mdl/executor/alter_page_test.go b/mdl/backend/mpr/page_mutator_test.go similarity index 80% rename from mdl/executor/alter_page_test.go rename to mdl/backend/mpr/page_mutator_test.go index 0bf81196..a7629be5 100644 --- a/mdl/executor/alter_page_test.go +++ b/mdl/backend/mpr/page_mutator_test.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -package executor +package mprbackend import ( "testing" @@ -8,7 +8,8 @@ import ( "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" - "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend" + "github.com/mendixlabs/mxcli/model" ) // Helper to build a minimal raw BSON page structure for testing. @@ -89,15 +90,16 @@ func TestFindBsonWidget_NotFound(t *testing.T) { } } -func TestApplyDropWidget_Single(t *testing.T) { +func TestDropWidget_Single(t *testing.T) { w1 := makeWidget("txtName", "Pages$TextBox") w2 := makeWidget("txtEmail", "Pages$TextBox") w3 := makeWidget("txtPhone", "Pages$TextBox") rawData := makeRawPage(w1, w2, w3) - op := &ast.DropWidgetOp{Targets: []ast.WidgetRef{{Widget: "txtEmail"}}} - if err := applyDropWidget(rawData, op); err != nil { - t.Fatalf("applyDropWidget failed: %v", err) + m := &mprPageMutator{rawData: rawData, widgetFinder: findBsonWidget} + refs := []backend.WidgetRef{{Widget: "txtEmail"}} + if err := m.DropWidget(refs); err != nil { + t.Fatalf("DropWidget failed: %v", err) } // Verify txtEmail was removed @@ -120,15 +122,16 @@ func TestApplyDropWidget_Single(t *testing.T) { } } -func TestApplyDropWidget_Multiple(t *testing.T) { +func TestDropWidget_Multiple(t *testing.T) { w1 := makeWidget("a", "Pages$TextBox") w2 := makeWidget("b", "Pages$TextBox") w3 := makeWidget("c", "Pages$TextBox") rawData := makeRawPage(w1, w2, w3) - op := &ast.DropWidgetOp{Targets: []ast.WidgetRef{{Widget: "a"}, {Widget: "c"}}} - if err := applyDropWidget(rawData, op); err != nil { - t.Fatalf("applyDropWidget failed: %v", err) + m := &mprPageMutator{rawData: rawData, widgetFinder: findBsonWidget} + refs := []backend.WidgetRef{{Widget: "a"}, {Widget: "c"}} + if err := m.DropWidget(refs); err != nil { + t.Fatalf("DropWidget failed: %v", err) } formCall := dGetDoc(rawData, "FormCall") @@ -146,29 +149,31 @@ func TestApplyDropWidget_Multiple(t *testing.T) { } } -func TestApplyDropWidget_NotFound(t *testing.T) { +func TestDropWidget_NotFound(t *testing.T) { w1 := makeWidget("txtName", "Pages$TextBox") rawData := makeRawPage(w1) - op := &ast.DropWidgetOp{Targets: []ast.WidgetRef{{Widget: "nonexistent"}}} - err := applyDropWidget(rawData, op) + m := &mprPageMutator{rawData: rawData, widgetFinder: findBsonWidget} + refs := []backend.WidgetRef{{Widget: "nonexistent"}} + err := m.DropWidget(refs) if err == nil { t.Fatal("Expected error for nonexistent widget") } } -func TestApplyDropWidget_Nested(t *testing.T) { +func TestDropWidget_Nested(t *testing.T) { inner1 := makeWidget("txtInner1", "Pages$TextBox") inner2 := makeWidget("txtInner2", "Pages$TextBox") container := makeContainerWidget("ctn1", inner1, inner2) rawData := makeRawPage(container) - op := &ast.DropWidgetOp{Targets: []ast.WidgetRef{{Widget: "txtInner1"}}} - if err := applyDropWidget(rawData, op); err != nil { - t.Fatalf("applyDropWidget failed: %v", err) + m := &mprPageMutator{rawData: rawData, widgetFinder: findBsonWidget} + refs := []backend.WidgetRef{{Widget: "txtInner1"}} + if err := m.DropWidget(refs); err != nil { + t.Fatalf("DropWidget failed: %v", err) } - // Verify txtInner1 was removed from container + // Verify txtInner1 was removed result := findBsonWidget(rawData, "txtInner1") if result != nil { t.Error("txtInner1 should have been removed") @@ -181,18 +186,13 @@ func TestApplyDropWidget_Nested(t *testing.T) { } } -func TestApplySetProperty_Name(t *testing.T) { +func TestSetWidgetProperty_Name(t *testing.T) { w1 := makeWidget("txtOld", "Pages$TextBox") rawData := makeRawPage(w1) - op := &ast.SetPropertyOp{ - Target: ast.WidgetRef{Widget: "txtOld"}, - Properties: map[string]interface{}{ - "Name": "txtNew", - }, - } - if err := applySetProperty(rawData, op); err != nil { - t.Fatalf("applySetProperty failed: %v", err) + m := &mprPageMutator{rawData: rawData, widgetFinder: findBsonWidget} + if err := m.SetWidgetProperty("txtOld", "Name", "txtNew"); err != nil { + t.Fatalf("SetWidgetProperty failed: %v", err) } // Verify name was changed @@ -202,7 +202,7 @@ func TestApplySetProperty_Name(t *testing.T) { } } -func TestApplySetProperty_ButtonStyle(t *testing.T) { +func TestSetWidgetProperty_ButtonStyle(t *testing.T) { w1 := bson.D{ {Key: "$Type", Value: "Pages$ActionButton"}, {Key: "Name", Value: "btnSave"}, @@ -210,14 +210,9 @@ func TestApplySetProperty_ButtonStyle(t *testing.T) { } rawData := makeRawPage(w1) - op := &ast.SetPropertyOp{ - Target: ast.WidgetRef{Widget: "btnSave"}, - Properties: map[string]interface{}{ - "ButtonStyle": "Success", - }, - } - if err := applySetProperty(rawData, op); err != nil { - t.Fatalf("applySetProperty failed: %v", err) + m := &mprPageMutator{rawData: rawData, widgetFinder: findBsonWidget} + if err := m.SetWidgetProperty("btnSave", "ButtonStyle", "Success"); err != nil { + t.Fatalf("SetWidgetProperty failed: %v", err) } result := findBsonWidget(rawData, "btnSave") @@ -229,25 +224,18 @@ func TestApplySetProperty_ButtonStyle(t *testing.T) { } } -func TestApplySetProperty_WidgetNotFound(t *testing.T) { +func TestSetWidgetProperty_WidgetNotFound(t *testing.T) { w1 := makeWidget("txtName", "Pages$TextBox") rawData := makeRawPage(w1) - op := &ast.SetPropertyOp{ - Target: ast.WidgetRef{Widget: "nonexistent"}, - Properties: map[string]interface{}{ - "Name": "new", - }, - } - err := applySetProperty(rawData, op) + m := &mprPageMutator{rawData: rawData, widgetFinder: findBsonWidget} + err := m.SetWidgetProperty("nonexistent", "Name", "new") if err == nil { t.Fatal("Expected error for nonexistent widget") } } -func TestApplySetProperty_PluggableWidget(t *testing.T) { - // Pluggable widget properties are identified by TypePointer referencing - // a PropertyType entry in Type.ObjectType.PropertyTypes, NOT by a "Key" field. +func TestSetWidgetProperty_PluggableWidget(t *testing.T) { propTypeID := primitive.Binary{Subtype: 0x04, Data: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}} w1 := bson.D{ {Key: "$Type", Value: "CustomWidgets$CustomWidget"}, @@ -256,7 +244,7 @@ func TestApplySetProperty_PluggableWidget(t *testing.T) { {Key: "$Type", Value: "CustomWidgets$CustomWidgetType"}, {Key: "ObjectType", Value: bson.D{ {Key: "PropertyTypes", Value: bson.A{ - int32(2), // type marker + int32(2), bson.D{ {Key: "$ID", Value: propTypeID}, {Key: "PropertyKey", Value: "showLabel"}, @@ -266,7 +254,7 @@ func TestApplySetProperty_PluggableWidget(t *testing.T) { }}, {Key: "Object", Value: bson.D{ {Key: "Properties", Value: bson.A{ - int32(2), // type marker + int32(2), bson.D{ {Key: "TypePointer", Value: propTypeID}, {Key: "Value", Value: bson.D{ @@ -278,14 +266,9 @@ func TestApplySetProperty_PluggableWidget(t *testing.T) { } rawData := makeRawPage(w1) - op := &ast.SetPropertyOp{ - Target: ast.WidgetRef{Widget: "cb1"}, - Properties: map[string]interface{}{ - "showLabel": false, - }, - } - if err := applySetProperty(rawData, op); err != nil { - t.Fatalf("applySetProperty failed: %v", err) + m := &mprPageMutator{rawData: rawData, widgetFinder: findBsonWidget} + if err := m.SetWidgetProperty("cb1", "showLabel", false); err != nil { + t.Fatalf("SetWidgetProperty failed: %v", err) } result := findBsonWidget(rawData, "cb1") @@ -374,9 +357,8 @@ func TestFindBsonWidget_LayoutGrid(t *testing.T) { // Snippet BSON tests // ============================================================================ -// Helper to build a minimal raw BSON snippet structure (Studio Pro format). func makeRawSnippet(widgets ...bson.D) bson.D { - widgetArr := bson.A{int32(2)} // type marker + widgetArr := bson.A{int32(2)} for _, w := range widgets { widgetArr = append(widgetArr, w) } @@ -385,9 +367,8 @@ func makeRawSnippet(widgets ...bson.D) bson.D { } } -// Helper to build a minimal raw BSON snippet structure (mxcli format). func makeRawSnippetMxcli(widgets ...bson.D) bson.D { - widgetArr := bson.A{int32(2)} // type marker + widgetArr := bson.A{int32(2)} for _, w := range widgets { widgetArr = append(widgetArr, w) } @@ -443,14 +424,15 @@ func TestFindBsonWidgetInSnippet_NotFound(t *testing.T) { } } -func TestApplyDropWidget_Snippet(t *testing.T) { +func TestDropWidget_Snippet(t *testing.T) { w1 := makeWidget("txtName", "Pages$TextBox") w2 := makeWidget("txtEmail", "Pages$TextBox") rawData := makeRawSnippet(w1, w2) - op := &ast.DropWidgetOp{Targets: []ast.WidgetRef{{Widget: "txtEmail"}}} - if err := applyDropWidgetWith(rawData, op, findBsonWidgetInSnippet); err != nil { - t.Fatalf("applyDropWidgetWith failed: %v", err) + m := &mprPageMutator{rawData: rawData, widgetFinder: findBsonWidgetInSnippet} + refs := []backend.WidgetRef{{Widget: "txtEmail"}} + if err := m.DropWidget(refs); err != nil { + t.Fatalf("DropWidget failed: %v", err) } // Verify txtEmail was removed @@ -464,7 +446,7 @@ func TestApplyDropWidget_Snippet(t *testing.T) { } } -func TestApplySetProperty_Snippet(t *testing.T) { +func TestSetWidgetProperty_Snippet(t *testing.T) { w1 := bson.D{ {Key: "$Type", Value: "Pages$ActionButton"}, {Key: "Name", Value: "btnAction"}, @@ -472,14 +454,9 @@ func TestApplySetProperty_Snippet(t *testing.T) { } rawData := makeRawSnippet(w1) - op := &ast.SetPropertyOp{ - Target: ast.WidgetRef{Widget: "btnAction"}, - Properties: map[string]interface{}{ - "ButtonStyle": "Danger", - }, - } - if err := applySetPropertyWith(rawData, op, findBsonWidgetInSnippet); err != nil { - t.Fatalf("applySetPropertyWith failed: %v", err) + m := &mprPageMutator{rawData: rawData, widgetFinder: findBsonWidgetInSnippet} + if err := m.SetWidgetProperty("btnAction", "ButtonStyle", "Danger"); err != nil { + t.Fatalf("SetWidgetProperty failed: %v", err) } result := findBsonWidgetInSnippet(rawData, "btnAction") @@ -519,7 +496,7 @@ func TestFindBsonWidget_DataViewFooter(t *testing.T) { } // ============================================================================ -// Page context tree tests (#157) +// Page context tree tests // ============================================================================ func makeWidgetWithID(name string, typeName string, id primitive.Binary) bson.D { @@ -539,7 +516,7 @@ func makeBsonID(b byte) primitive.Binary { func TestExtractPageParamsFromBSON_EntityParams(t *testing.T) { rawData := bson.D{ {Key: "Parameters", Value: bson.A{ - int32(2), // type marker + int32(2), bson.D{ {Key: "$ID", Value: makeBsonID(0x01)}, {Key: "$Type", Value: "Forms$PageParameter"}, @@ -700,3 +677,48 @@ func TestExtractWidgetScopeFromBSON_Nil(t *testing.T) { t.Error("Expected empty scope for nil input") } } + +func TestFindWidget(t *testing.T) { + w1 := makeWidget("txtName", "Pages$TextBox") + rawData := makeRawPage(w1) + + m := &mprPageMutator{rawData: rawData, widgetFinder: findBsonWidget} + if !m.FindWidget("txtName") { + t.Error("Expected FindWidget to return true for existing widget") + } + if m.FindWidget("nonexistent") { + t.Error("Expected FindWidget to return false for nonexistent widget") + } +} + +func TestParamScope(t *testing.T) { + rawData := bson.D{ + {Key: "Parameters", Value: bson.A{ + int32(2), + bson.D{ + {Key: "$ID", Value: makeBsonID(0x01)}, + {Key: "$Type", Value: "Forms$PageParameter"}, + {Key: "Name", Value: "Customer"}, + {Key: "ParameterType", Value: bson.D{ + {Key: "$ID", Value: makeBsonID(0x02)}, + {Key: "$Type", Value: "DataTypes$ObjectType"}, + {Key: "Entity", Value: "MyModule.Customer"}, + }}, + }, + }}, + } + + m := &mprPageMutator{rawData: rawData, widgetFinder: findBsonWidget} + ids, names := m.ParamScope() + + if len(ids) != 1 { + t.Fatalf("Expected 1 param, got %d", len(ids)) + } + if names["Customer"] != "MyModule.Customer" { + t.Errorf("Expected MyModule.Customer, got %q", names["Customer"]) + } + // Verify ID is a valid model.ID (non-empty) + if ids["Customer"] == model.ID("") { + t.Error("Expected non-empty ID") + } +} diff --git a/mdl/backend/mpr/widget_builder.go b/mdl/backend/mpr/widget_builder.go new file mode 100644 index 00000000..f9950c72 --- /dev/null +++ b/mdl/backend/mpr/widget_builder.go @@ -0,0 +1,1007 @@ +// SPDX-License-Identifier: Apache-2.0 + +package mprbackend + +import ( + "encoding/hex" + "fmt" + "log" + "regexp" + "strings" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + + "github.com/mendixlabs/mxcli/mdl/backend" + "github.com/mendixlabs/mxcli/mdl/bsonutil" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/sdk/pages" + "github.com/mendixlabs/mxcli/sdk/widgets" +) + +// --------------------------------------------------------------------------- +// mprWidgetObjectBuilder — implements backend.WidgetObjectBuilder +// --------------------------------------------------------------------------- + +type mprWidgetObjectBuilder struct { + embeddedType bson.D + object bson.D // the mutable widget object BSON + propertyTypeIDs map[string]pages.PropertyTypeIDEntry + objectTypeID string +} + +var _ backend.WidgetObjectBuilder = (*mprWidgetObjectBuilder)(nil) + +// --------------------------------------------------------------------------- +// WidgetBuilderBackend — MprBackend methods +// --------------------------------------------------------------------------- + +// LoadWidgetTemplate loads a widget template by ID and returns a builder. +func (b *MprBackend) LoadWidgetTemplate(widgetID string, projectPath string) (backend.WidgetObjectBuilder, error) { + embeddedType, embeddedObject, embeddedIDs, objectTypeID, err := + widgets.GetTemplateFullBSON(widgetID, types.GenerateID, projectPath) + if err != nil { + return nil, err + } + if embeddedType == nil || embeddedObject == nil { + return nil, nil + } + + propertyTypeIDs := convertPropertyTypeIDs(embeddedIDs) + + return &mprWidgetObjectBuilder{ + embeddedType: embeddedType, + object: embeddedObject, + propertyTypeIDs: propertyTypeIDs, + objectTypeID: objectTypeID, + }, nil +} + +// SerializeWidgetToOpaque converts a domain Widget to opaque BSON form. +func (b *MprBackend) SerializeWidgetToOpaque(w pages.Widget) any { + return mpr.SerializeWidget(w) +} + +// SerializeDataSourceToOpaque converts a domain DataSource to opaque BSON form. +func (b *MprBackend) SerializeDataSourceToOpaque(ds pages.DataSource) any { + return mpr.SerializeCustomWidgetDataSource(ds) +} + +// BuildCreateAttributeObject creates an attribute object for filter widgets. +func (b *MprBackend) BuildCreateAttributeObject(attributePath string, objectTypeID, propertyTypeID, valueTypeID string) (any, error) { + return createAttributeObject(attributePath, objectTypeID, propertyTypeID, valueTypeID) +} + +// --------------------------------------------------------------------------- +// WidgetObjectBuilder — property operations +// --------------------------------------------------------------------------- + +func (ob *mprWidgetObjectBuilder) SetAttribute(propertyKey string, attributePath string) { + if attributePath == "" { + return + } + ob.object = updateWidgetPropertyValue(ob.object, ob.propertyTypeIDs, propertyKey, func(val bson.D) bson.D { + return setAttributeRef(val, attributePath) + }) +} + +func (ob *mprWidgetObjectBuilder) SetAssociation(propertyKey string, assocPath string, entityName string) { + if assocPath == "" { + return + } + ob.object = updateWidgetPropertyValue(ob.object, ob.propertyTypeIDs, propertyKey, func(val bson.D) bson.D { + return setAssociationRef(val, assocPath, entityName) + }) +} + +func (ob *mprWidgetObjectBuilder) SetPrimitive(propertyKey string, value string) { + if value == "" { + return + } + ob.object = updateWidgetPropertyValue(ob.object, ob.propertyTypeIDs, propertyKey, func(val bson.D) bson.D { + return setPrimitiveValue(val, value) + }) +} + +func (ob *mprWidgetObjectBuilder) SetSelection(propertyKey string, value string) { + if value == "" { + return + } + ob.object = updateWidgetPropertyValue(ob.object, ob.propertyTypeIDs, propertyKey, func(val bson.D) bson.D { + result := make(bson.D, 0, len(val)) + for _, elem := range val { + if elem.Key == "Selection" { + result = append(result, bson.E{Key: "Selection", Value: value}) + } else { + result = append(result, elem) + } + } + return result + }) +} + +func (ob *mprWidgetObjectBuilder) SetExpression(propertyKey string, value string) { + if value == "" { + return + } + ob.object = updateWidgetPropertyValue(ob.object, ob.propertyTypeIDs, propertyKey, func(val bson.D) bson.D { + result := make(bson.D, 0, len(val)) + for _, elem := range val { + if elem.Key == "Expression" { + result = append(result, bson.E{Key: "Expression", Value: value}) + } else { + result = append(result, elem) + } + } + return result + }) +} + +func (ob *mprWidgetObjectBuilder) SetDataSource(propertyKey string, ds pages.DataSource) { + if ds == nil { + return + } + ob.object = updateWidgetPropertyValue(ob.object, ob.propertyTypeIDs, propertyKey, func(val bson.D) bson.D { + return setDataSource(val, ds) + }) +} + +func (ob *mprWidgetObjectBuilder) SetChildWidgets(propertyKey string, children []pages.Widget) { + if len(children) == 0 { + return + } + ob.object = updateWidgetPropertyValue(ob.object, ob.propertyTypeIDs, propertyKey, func(val bson.D) bson.D { + return setChildWidgets(val, children) + }) +} + +func (ob *mprWidgetObjectBuilder) SetTextTemplate(propertyKey string, text string) { + if text == "" { + return + } + ob.object = updateWidgetPropertyValue(ob.object, ob.propertyTypeIDs, propertyKey, func(val bson.D) bson.D { + return setTextTemplateValue(val, text) + }) +} + +func (ob *mprWidgetObjectBuilder) SetTextTemplateWithParams(propertyKey string, text string, entityContext string) { + if text == "" { + return + } + tmpl := createClientTemplateBSONWithParams(text, entityContext) + ob.object = updateWidgetPropertyValue(ob.object, ob.propertyTypeIDs, propertyKey, func(val bson.D) bson.D { + result := make(bson.D, 0, len(val)) + for _, elem := range val { + if elem.Key == "TextTemplate" { + result = append(result, bson.E{Key: "TextTemplate", Value: tmpl}) + } else { + result = append(result, elem) + } + } + return result + }) +} + +func (ob *mprWidgetObjectBuilder) SetAction(propertyKey string, action pages.ClientAction) { + if action == nil { + return + } + actionBSON := mpr.SerializeClientAction(action) + ob.object = updateWidgetPropertyValue(ob.object, ob.propertyTypeIDs, propertyKey, func(val bson.D) bson.D { + result := make(bson.D, 0, len(val)) + for _, elem := range val { + if elem.Key == "Action" { + result = append(result, bson.E{Key: "Action", Value: actionBSON}) + } else { + result = append(result, elem) + } + } + return result + }) +} + +func (ob *mprWidgetObjectBuilder) SetAttributeObjects(propertyKey string, attributePaths []string) { + if len(attributePaths) == 0 { + return + } + + entry, ok := ob.propertyTypeIDs[propertyKey] + if !ok || entry.ObjectTypeID == "" { + return + } + + nestedEntry, ok := entry.NestedPropertyIDs["attribute"] + if !ok { + return + } + + ob.object = updateWidgetPropertyValue(ob.object, ob.propertyTypeIDs, propertyKey, func(val bson.D) bson.D { + objects := make([]any, 0, len(attributePaths)+1) + objects = append(objects, int32(2)) // BSON array version marker + + for _, attrPath := range attributePaths { + attrObj, err := createAttributeObject(attrPath, entry.ObjectTypeID, nestedEntry.PropertyTypeID, nestedEntry.ValueTypeID) + if err != nil { + log.Printf("warning: skipping attribute %s: %v", attrPath, err) + continue + } + objects = append(objects, attrObj) + } + + result := make(bson.D, 0, len(val)) + for _, elem := range val { + if elem.Key == "Objects" { + result = append(result, bson.E{Key: "Objects", Value: bson.A(objects)}) + } else { + result = append(result, elem) + } + } + return result + }) +} + +// --------------------------------------------------------------------------- +// Template metadata +// --------------------------------------------------------------------------- + +func (ob *mprWidgetObjectBuilder) PropertyTypeIDs() map[string]pages.PropertyTypeIDEntry { + return ob.propertyTypeIDs +} + +// --------------------------------------------------------------------------- +// Object list defaults +// --------------------------------------------------------------------------- + +func (ob *mprWidgetObjectBuilder) EnsureRequiredObjectLists() { + ob.object = ensureRequiredObjectLists(ob.object, ob.propertyTypeIDs) +} + +// --------------------------------------------------------------------------- +// Gallery-specific +// --------------------------------------------------------------------------- + +func (ob *mprWidgetObjectBuilder) CloneGallerySelectionProperty(propertyKey string, selectionMode string) { + propEntry, ok := ob.propertyTypeIDs[propertyKey] + if !ok { + return + } + + ob.object = updateWidgetPropertyValue(ob.object, ob.propertyTypeIDs, propertyKey, func(val bson.D) bson.D { + // The val here is the WidgetValue — but we need the WidgetProperty level. + // CloneGallerySelectionProperty works on the Properties array directly. + return val + }) + + // Actually need to work at the Properties array level: find the property, + // clone it with new IDs and updated Selection, then append. + result := make(bson.D, 0, len(ob.object)) + for _, elem := range ob.object { + if elem.Key == "Properties" { + if arr, ok := elem.Value.(bson.A); ok { + newArr := make(bson.A, len(arr)) + copy(newArr, arr) + // Find the matching property and clone it + for _, item := range arr { + if prop, ok := item.(bson.D); ok { + if matchesTypePointer(prop, propEntry.PropertyTypeID) { + cloned := buildGallerySelectionProperty(prop, selectionMode) + newArr = append(newArr, cloned) + break + } + } + } + result = append(result, bson.E{Key: "Properties", Value: newArr}) + continue + } + } + result = append(result, elem) + } + ob.object = result +} + +// --------------------------------------------------------------------------- +// Finalize +// --------------------------------------------------------------------------- + +func (ob *mprWidgetObjectBuilder) Finalize(id model.ID, name string, label string, editable string) *pages.CustomWidget { + return &pages.CustomWidget{ + BaseWidget: pages.BaseWidget{ + BaseElement: model.BaseElement{ + ID: id, + TypeName: "CustomWidgets$CustomWidget", + }, + Name: name, + }, + Label: label, + Editable: editable, + RawType: ob.embeddedType, + RawObject: ob.object, + PropertyTypeIDMap: ob.propertyTypeIDs, + ObjectTypeID: ob.objectTypeID, + } +} + +// =========================================================================== +// Package-level helpers (moved from executor) +// =========================================================================== + +// --------------------------------------------------------------------------- +// Property update core +// --------------------------------------------------------------------------- + +// updateWidgetPropertyValue finds and updates a specific property value in a WidgetObject. +func updateWidgetPropertyValue(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, updateFn func(bson.D) bson.D) bson.D { + propEntry, ok := propTypeIDs[propertyKey] + if !ok { + return obj + } + + result := make(bson.D, 0, len(obj)) + for _, elem := range obj { + if elem.Key == "Properties" { + if arr, ok := elem.Value.(bson.A); ok { + result = append(result, bson.E{Key: "Properties", Value: updatePropertyInArray(arr, propEntry.PropertyTypeID, updateFn)}) + continue + } + } + result = append(result, elem) + } + return result +} + +// updatePropertyInArray finds a property by TypePointer and updates its value. +func updatePropertyInArray(arr bson.A, propertyTypeID string, updateFn func(bson.D) bson.D) bson.A { + result := make(bson.A, len(arr)) + matched := false + for i, item := range arr { + if prop, ok := item.(bson.D); ok { + if matchesTypePointer(prop, propertyTypeID) { + result[i] = updatePropertyValue(prop, updateFn) + matched = true + } else { + result[i] = item + } + } else { + result[i] = item + } + } + if !matched { + log.Printf("WARNING: updatePropertyInArray: no match for TypePointer %s in %d properties", propertyTypeID, len(arr)-1) + } + return result +} + +// matchesTypePointer checks if a WidgetProperty has the given TypePointer. +func matchesTypePointer(prop bson.D, propertyTypeID string) bool { + normalizedTarget := strings.ReplaceAll(propertyTypeID, "-", "") + for _, elem := range prop { + if elem.Key == "TypePointer" { + switch v := elem.Value.(type) { + case primitive.Binary: + propID := strings.ReplaceAll(types.BlobToUUID(v.Data), "-", "") + return propID == normalizedTarget + case []byte: + propID := strings.ReplaceAll(types.BlobToUUID(v), "-", "") + if propID == normalizedTarget { + return true + } + rawHex := fmt.Sprintf("%x", v) + return rawHex == normalizedTarget + } + } + } + return false +} + +// updatePropertyValue updates the Value field in a WidgetProperty. +func updatePropertyValue(prop bson.D, updateFn func(bson.D) bson.D) bson.D { + result := make(bson.D, 0, len(prop)) + for _, elem := range prop { + if elem.Key == "Value" { + if val, ok := elem.Value.(bson.D); ok { + result = append(result, bson.E{Key: "Value", Value: updateFn(val)}) + } else { + result = append(result, elem) + } + } else { + result = append(result, elem) + } + } + return result +} + +// --------------------------------------------------------------------------- +// Value setters +// --------------------------------------------------------------------------- + +func setPrimitiveValue(val bson.D, value string) bson.D { + result := make(bson.D, 0, len(val)) + for _, elem := range val { + if elem.Key == "PrimitiveValue" { + result = append(result, bson.E{Key: "PrimitiveValue", Value: value}) + } else { + result = append(result, elem) + } + } + return result +} + +func setDataSource(val bson.D, ds pages.DataSource) bson.D { + result := make(bson.D, 0, len(val)) + for _, elem := range val { + if elem.Key == "DataSource" { + result = append(result, bson.E{Key: "DataSource", Value: mpr.SerializeCustomWidgetDataSource(ds)}) + } else { + result = append(result, elem) + } + } + return result +} + +func setAssociationRef(val bson.D, assocPath string, entityName string) bson.D { + result := make(bson.D, 0, len(val)) + for _, elem := range val { + if elem.Key == "EntityRef" && entityName != "" { + result = append(result, bson.E{Key: "EntityRef", Value: bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "DomainModels$IndirectEntityRef"}, + {Key: "Steps", Value: bson.A{ + int32(2), + bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "DomainModels$EntityRefStep"}, + {Key: "Association", Value: assocPath}, + {Key: "DestinationEntity", Value: entityName}, + }, + }}, + }}) + } else { + result = append(result, elem) + } + } + return result +} + +func setAttributeRef(val bson.D, attrPath string) bson.D { + result := make(bson.D, 0, len(val)) + for _, elem := range val { + if elem.Key == "AttributeRef" { + if strings.Count(attrPath, ".") >= 2 { + result = append(result, bson.E{Key: "AttributeRef", Value: bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "DomainModels$AttributeRef"}, + {Key: "Attribute", Value: attrPath}, + {Key: "EntityRef", Value: nil}, + }}) + } else { + result = append(result, bson.E{Key: "AttributeRef", Value: nil}) + } + } else { + result = append(result, elem) + } + } + return result +} + +func setChildWidgets(val bson.D, children []pages.Widget) bson.D { + widgetsArr := bson.A{int32(2)} + for _, w := range children { + widgetsArr = append(widgetsArr, mpr.SerializeWidget(w)) + } + + result := make(bson.D, 0, len(val)) + for _, elem := range val { + if elem.Key == "Widgets" { + result = append(result, bson.E{Key: "Widgets", Value: widgetsArr}) + } else { + result = append(result, elem) + } + } + return result +} + +func setTextTemplateValue(val bson.D, text string) bson.D { + result := make(bson.D, 0, len(val)) + for _, elem := range val { + if elem.Key == "TextTemplate" { + if tmpl, ok := elem.Value.(bson.D); ok && tmpl != nil { + result = append(result, bson.E{Key: "TextTemplate", Value: updateTemplateText(tmpl, text)}) + } else { + result = append(result, elem) + } + } else { + result = append(result, elem) + } + } + return result +} + +func updateTemplateText(tmpl bson.D, text string) bson.D { + result := make(bson.D, 0, len(tmpl)) + for _, elem := range tmpl { + if elem.Key == "Template" { + if template, ok := elem.Value.(bson.D); ok { + updated := make(bson.D, 0, len(template)) + for _, tElem := range template { + if tElem.Key == "Items" { + updated = append(updated, bson.E{Key: "Items", Value: bson.A{ + int32(3), + bson.D{ + {Key: "$ID", Value: bsonutil.IDToBsonBinary(types.GenerateID())}, + {Key: "$Type", Value: "Texts$Translation"}, + {Key: "LanguageCode", Value: "en_US"}, + {Key: "Text", Value: text}, + }, + }}) + } else { + updated = append(updated, tElem) + } + } + result = append(result, bson.E{Key: "Template", Value: updated}) + } else { + result = append(result, elem) + } + } else { + result = append(result, elem) + } + } + return result +} + +// --------------------------------------------------------------------------- +// Template helpers +// --------------------------------------------------------------------------- + +func createClientTemplateBSONWithParams(text string, entityContext string) bson.D { + re := regexp.MustCompile(`\{([A-Za-z][A-Za-z0-9_]*)\}`) + matches := re.FindAllStringSubmatchIndex(text, -1) + + if len(matches) == 0 { + return createDefaultClientTemplateBSON(text) + } + + // Collect attribute names (skip numeric placeholders) + var attrNames []string + for i := 0; i < len(matches); i++ { + match := matches[i] + attrName := text[match[2]:match[3]] + if _, err := fmt.Sscanf(attrName, "%d", new(int)); err == nil { + continue + } + attrNames = append(attrNames, attrName) + } + + paramText := re.ReplaceAllStringFunc(text, func(s string) string { + name := s[1 : len(s)-1] + if _, err := fmt.Sscanf(name, "%d", new(int)); err == nil { + return s + } + for i, an := range attrNames { + if an == name { + return fmt.Sprintf("{%d}", i+1) + } + } + return s + }) + + // Build parameters BSON + params := bson.A{int32(2)} + for _, attrName := range attrNames { + attrPath := attrName + if entityContext != "" && !strings.Contains(attrName, ".") { + attrPath = entityContext + "." + attrName + } + params = append(params, bson.D{ + {Key: "$ID", Value: generateBinaryID()}, + {Key: "$Type", Value: "Forms$ClientTemplateParameter"}, + {Key: "AttributeRef", Value: bson.D{ + {Key: "$ID", Value: generateBinaryID()}, + {Key: "$Type", Value: "DomainModels$AttributeRef"}, + {Key: "Attribute", Value: attrPath}, + {Key: "EntityRef", Value: nil}, + }}, + {Key: "Expression", Value: ""}, + {Key: "FormattingInfo", Value: bson.D{ + {Key: "$ID", Value: generateBinaryID()}, + {Key: "$Type", Value: "Forms$FormattingInfo"}, + {Key: "CustomDateFormat", Value: ""}, + {Key: "DateFormat", Value: "Date"}, + {Key: "DecimalPrecision", Value: int64(2)}, + {Key: "EnumFormat", Value: "Text"}, + {Key: "GroupDigits", Value: false}, + {Key: "TimeFormat", Value: "HoursMinutes"}, + }}, + {Key: "SourceVariable", Value: nil}, + }) + } + + makeText := func(t string) bson.D { + return bson.D{ + {Key: "$ID", Value: generateBinaryID()}, + {Key: "$Type", Value: "Texts$Text"}, + {Key: "Items", Value: bson.A{int32(3), bson.D{ + {Key: "$ID", Value: generateBinaryID()}, + {Key: "$Type", Value: "Texts$Translation"}, + {Key: "LanguageCode", Value: "en_US"}, + {Key: "Text", Value: t}, + }}}, + } + } + + return bson.D{ + {Key: "$ID", Value: generateBinaryID()}, + {Key: "$Type", Value: "Forms$ClientTemplate"}, + {Key: "Fallback", Value: makeText(paramText)}, + {Key: "Parameters", Value: params}, + {Key: "Template", Value: makeText(paramText)}, + } +} + +func createDefaultClientTemplateBSON(text string) bson.D { + makeText := func(t string) bson.D { + return bson.D{ + {Key: "$ID", Value: generateBinaryID()}, + {Key: "$Type", Value: "Texts$Text"}, + {Key: "Items", Value: bson.A{int32(3), bson.D{ + {Key: "$ID", Value: generateBinaryID()}, + {Key: "$Type", Value: "Texts$Translation"}, + {Key: "LanguageCode", Value: "en_US"}, + {Key: "Text", Value: t}, + }}}, + } + } + return bson.D{ + {Key: "$ID", Value: generateBinaryID()}, + {Key: "$Type", Value: "Forms$ClientTemplate"}, + {Key: "Fallback", Value: makeText(text)}, + {Key: "Parameters", Value: bson.A{int32(2)}}, + {Key: "Template", Value: makeText(text)}, + } +} + +// --------------------------------------------------------------------------- +// ID / binary helpers +// --------------------------------------------------------------------------- + +func generateBinaryID() []byte { + return hexIDToBlob(types.GenerateID()) +} + +func hexIDToBlob(hexStr string) []byte { + hexStr = strings.ReplaceAll(hexStr, "-", "") + data, err := hex.DecodeString(hexStr) + if err != nil || len(data) != 16 { + return data + } + data[0], data[1], data[2], data[3] = data[3], data[2], data[1], data[0] + data[4], data[5] = data[5], data[4] + data[6], data[7] = data[7], data[6] + return data +} + +func hexToBytes(hexStr string) []byte { + clean := strings.ReplaceAll(hexStr, "-", "") + if len(clean) != 32 { + return nil + } + + decoded := make([]byte, 16) + for i := range 16 { + decoded[i] = hexByte(clean[i*2])<<4 | hexByte(clean[i*2+1]) + } + + blob := make([]byte, 16) + blob[0] = decoded[3] + blob[1] = decoded[2] + blob[2] = decoded[1] + blob[3] = decoded[0] + blob[4] = decoded[5] + blob[5] = decoded[4] + blob[6] = decoded[7] + blob[7] = decoded[6] + copy(blob[8:], decoded[8:]) + + return blob +} + +func hexByte(c byte) byte { + switch { + case c >= '0' && c <= '9': + return c - '0' + case c >= 'a' && c <= 'f': + return c - 'a' + 10 + case c >= 'A' && c <= 'F': + return c - 'A' + 10 + } + return 0 +} + +func bytesToHex(b []byte) string { + if len(b) != 16 { + if len(b) > 1024 { + return "" + } + const hexChars = "0123456789abcdef" + result := make([]byte, len(b)*2) + for i, v := range b { + result[i*2] = hexChars[v>>4] + result[i*2+1] = hexChars[v&0x0f] + } + return string(result) + } + + return fmt.Sprintf("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", + b[3], b[2], b[1], b[0], + b[5], b[4], + b[7], b[6], + b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]) +} + +// --------------------------------------------------------------------------- +// Property type ID conversion +// --------------------------------------------------------------------------- + +func convertPropertyTypeIDs(src map[string]widgets.PropertyTypeIDEntry) map[string]pages.PropertyTypeIDEntry { + result := make(map[string]pages.PropertyTypeIDEntry) + for k, v := range src { + entry := pages.PropertyTypeIDEntry{ + PropertyTypeID: v.PropertyTypeID, + ValueTypeID: v.ValueTypeID, + DefaultValue: v.DefaultValue, + ValueType: v.ValueType, + Required: v.Required, + ObjectTypeID: v.ObjectTypeID, + } + if len(v.NestedPropertyIDs) > 0 { + entry.NestedPropertyIDs = convertPropertyTypeIDs(v.NestedPropertyIDs) + } + result[k] = entry + } + return result +} + +// --------------------------------------------------------------------------- +// Default object lists +// --------------------------------------------------------------------------- + +func ensureRequiredObjectLists(obj bson.D, propertyTypeIDs map[string]pages.PropertyTypeIDEntry) bson.D { + for propKey, entry := range propertyTypeIDs { + if entry.ObjectTypeID == "" || len(entry.NestedPropertyIDs) == 0 { + continue + } + if !entry.Required { + hasNestedDS := false + for _, nested := range entry.NestedPropertyIDs { + if nested.ValueType == "DataSource" { + hasNestedDS = true + break + } + } + if hasNestedDS { + continue + } + } + hasRequiredAttr := false + for _, nested := range entry.NestedPropertyIDs { + if nested.Required && nested.ValueType == "Attribute" { + hasRequiredAttr = true + break + } + } + if hasRequiredAttr { + continue + } + obj = updateWidgetPropertyValue(obj, propertyTypeIDs, propKey, func(val bson.D) bson.D { + for _, elem := range val { + if elem.Key == "Objects" { + if arr, ok := elem.Value.(bson.A); ok && len(arr) <= 1 { + defaultObj := createDefaultWidgetObject(entry.ObjectTypeID, entry.NestedPropertyIDs) + newArr := bson.A{int32(2), defaultObj} + result := make(bson.D, 0, len(val)) + for _, e := range val { + if e.Key == "Objects" { + result = append(result, bson.E{Key: "Objects", Value: newArr}) + } else { + result = append(result, e) + } + } + return result + } + } + } + return val + }) + } + return obj +} + +func createDefaultWidgetObject(objectTypeID string, nestedProps map[string]pages.PropertyTypeIDEntry) bson.D { + propsArr := bson.A{int32(2)} + for _, entry := range nestedProps { + prop := createDefaultWidgetProperty(entry) + propsArr = append(propsArr, prop) + } + return bson.D{ + {Key: "$ID", Value: generateBinaryID()}, + {Key: "$Type", Value: "CustomWidgets$WidgetObject"}, + {Key: "TypePointer", Value: hexIDToBlob(objectTypeID)}, + {Key: "Properties", Value: propsArr}, + } +} + +func createDefaultWidgetProperty(entry pages.PropertyTypeIDEntry) bson.D { + return bson.D{ + {Key: "$ID", Value: generateBinaryID()}, + {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, + {Key: "TypePointer", Value: hexIDToBlob(entry.PropertyTypeID)}, + {Key: "Value", Value: createDefaultWidgetValue(entry)}, + } +} + +func createDefaultWidgetValue(entry pages.PropertyTypeIDEntry) bson.D { + primitiveVal := entry.DefaultValue + expressionVal := "" + var textTemplate interface{} + + switch entry.ValueType { + case "Expression": + expressionVal = primitiveVal + primitiveVal = "" + case "TextTemplate": + text := primitiveVal + if text == "" { + text = " " + } + textTemplate = createDefaultClientTemplateBSON(text) + case "String": + if primitiveVal == "" { + primitiveVal = " " + } + } + + return bson.D{ + {Key: "$ID", Value: generateBinaryID()}, + {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, + {Key: "Action", Value: bson.D{ + {Key: "$ID", Value: generateBinaryID()}, + {Key: "$Type", Value: "Forms$NoAction"}, + {Key: "DisabledDuringExecution", Value: true}, + }}, + {Key: "AttributeRef", Value: nil}, + {Key: "DataSource", Value: nil}, + {Key: "EntityRef", Value: nil}, + {Key: "Expression", Value: expressionVal}, + {Key: "Form", Value: ""}, + {Key: "Icon", Value: nil}, + {Key: "Image", Value: ""}, + {Key: "Microflow", Value: ""}, + {Key: "Nanoflow", Value: ""}, + {Key: "Objects", Value: bson.A{int32(2)}}, + {Key: "PrimitiveValue", Value: primitiveVal}, + {Key: "Selection", Value: "None"}, + {Key: "SourceVariable", Value: nil}, + {Key: "TextTemplate", Value: textTemplate}, + {Key: "TranslatableValue", Value: nil}, + {Key: "TypePointer", Value: hexIDToBlob(entry.ValueTypeID)}, + {Key: "Widgets", Value: bson.A{int32(2)}}, + {Key: "XPathConstraint", Value: ""}, + } +} + +// --------------------------------------------------------------------------- +// Gallery cloning +// --------------------------------------------------------------------------- + +func buildGallerySelectionProperty(propMap bson.D, selectionMode string) bson.D { + result := make(bson.D, 0, len(propMap)) + + for _, elem := range propMap { + if elem.Key == "$ID" { + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) + } else if elem.Key == "Value" { + if valueMap, ok := elem.Value.(bson.D); ok { + result = append(result, bson.E{Key: "Value", Value: cloneGallerySelectionValue(valueMap, selectionMode)}) + } else { + result = append(result, elem) + } + } else { + result = append(result, elem) + } + } + + return result +} + +func cloneGallerySelectionValue(valueMap bson.D, selectionMode string) bson.D { + result := make(bson.D, 0, len(valueMap)) + + for _, elem := range valueMap { + if elem.Key == "$ID" { + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) + } else if elem.Key == "Selection" { + result = append(result, bson.E{Key: "Selection", Value: selectionMode}) + } else if elem.Key == "Action" { + if actionMap, ok := elem.Value.(bson.D); ok { + result = append(result, bson.E{Key: "Action", Value: cloneActionWithNewID(actionMap)}) + } else { + result = append(result, elem) + } + } else { + result = append(result, elem) + } + } + + return result +} + +func cloneActionWithNewID(actionMap bson.D) bson.D { + result := make(bson.D, 0, len(actionMap)) + + for _, elem := range actionMap { + if elem.Key == "$ID" { + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) + } else { + result = append(result, elem) + } + } + + return result +} + +// --------------------------------------------------------------------------- +// Attribute object creation +// --------------------------------------------------------------------------- + +func createAttributeObject(attributePath string, objectTypeID, propertyTypeID, valueTypeID string) (bson.D, error) { + if strings.Count(attributePath, ".") < 2 { + return nil, mdlerrors.NewValidationf("invalid attribute path %q: expected Module.Entity.Attribute format", attributePath) + } + return bson.D{ + {Key: "$ID", Value: hexToBytes(types.GenerateID())}, + {Key: "$Type", Value: "CustomWidgets$WidgetObject"}, + {Key: "Properties", Value: []any{ + int32(2), + bson.D{ + {Key: "$ID", Value: hexToBytes(types.GenerateID())}, + {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, + {Key: "TypePointer", Value: hexToBytes(propertyTypeID)}, + {Key: "Value", Value: bson.D{ + {Key: "$ID", Value: hexToBytes(types.GenerateID())}, + {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, + {Key: "Action", Value: bson.D{ + {Key: "$ID", Value: hexToBytes(types.GenerateID())}, + {Key: "$Type", Value: "Forms$NoAction"}, + {Key: "DisabledDuringExecution", Value: true}, + }}, + {Key: "AttributeRef", Value: bson.D{ + {Key: "$ID", Value: hexToBytes(types.GenerateID())}, + {Key: "$Type", Value: "DomainModels$AttributeRef"}, + {Key: "Attribute", Value: attributePath}, + {Key: "EntityRef", Value: nil}, + }}, + {Key: "DataSource", Value: nil}, + {Key: "EntityRef", Value: nil}, + {Key: "Expression", Value: ""}, + {Key: "Form", Value: ""}, + {Key: "Icon", Value: nil}, + {Key: "Image", Value: ""}, + {Key: "Microflow", Value: ""}, + {Key: "Nanoflow", Value: ""}, + {Key: "Objects", Value: []any{int32(2)}}, + {Key: "PrimitiveValue", Value: ""}, + {Key: "Selection", Value: "None"}, + {Key: "SourceVariable", Value: nil}, + {Key: "TextTemplate", Value: nil}, + {Key: "TranslatableValue", Value: nil}, + {Key: "TypePointer", Value: hexToBytes(valueTypeID)}, + {Key: "Widgets", Value: []any{int32(2)}}, + {Key: "XPathConstraint", Value: ""}, + }}, + }, + }}, + {Key: "TypePointer", Value: hexToBytes(objectTypeID)}, + }, nil +} diff --git a/mdl/backend/mpr/workflow_mutator.go b/mdl/backend/mpr/workflow_mutator.go new file mode 100644 index 00000000..9309e414 --- /dev/null +++ b/mdl/backend/mpr/workflow_mutator.go @@ -0,0 +1,771 @@ +// SPDX-License-Identifier: Apache-2.0 + +package mprbackend + +import ( + "fmt" + "strings" + + "go.mongodb.org/mongo-driver/bson" + + "github.com/mendixlabs/mxcli/mdl/backend" + "github.com/mendixlabs/mxcli/mdl/bsonutil" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/sdk/workflows" +) + +// bsonArrayMarker is the Mendix BSON array type marker (storageListType 3). +const bsonArrayMarker = int32(3) + +// Compile-time check. +var _ backend.WorkflowMutator = (*mprWorkflowMutator)(nil) + +// mprWorkflowMutator implements backend.WorkflowMutator for the MPR backend. +type mprWorkflowMutator struct { + backend *MprBackend + unitID model.ID + rawData bson.D +} + +// --------------------------------------------------------------------------- +// OpenWorkflowForMutation +// --------------------------------------------------------------------------- + +// OpenWorkflowForMutation loads a workflow unit and returns a WorkflowMutator. +func (b *MprBackend) openWorkflowForMutation(unitID model.ID) (backend.WorkflowMutator, error) { + rawBytes, err := b.reader.GetRawUnitBytes(unitID) + if err != nil { + return nil, fmt.Errorf("load raw unit bytes: %w", err) + } + var rawData bson.D + if err := bson.Unmarshal(rawBytes, &rawData); err != nil { + return nil, fmt.Errorf("unmarshal workflow BSON: %w", err) + } + return &mprWorkflowMutator{ + backend: b, + unitID: unitID, + rawData: rawData, + }, nil +} + +// --------------------------------------------------------------------------- +// WorkflowMutator interface — top-level properties +// --------------------------------------------------------------------------- + +func (m *mprWorkflowMutator) SetProperty(prop string, value string) error { + switch prop { + case "DISPLAY": + wfName := dGetDoc(m.rawData, "WorkflowName") + if wfName == nil { + newName := bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Texts$Text"}, + {Key: "Text", Value: value}, + } + m.rawData = append(m.rawData, bson.E{Key: "WorkflowName", Value: newName}) + } else { + dSet(wfName, "Text", value) + } + dSet(m.rawData, "Title", value) + return nil + + case "DESCRIPTION": + wfDesc := dGetDoc(m.rawData, "WorkflowDescription") + if wfDesc == nil { + newDesc := bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Texts$Text"}, + {Key: "Text", Value: value}, + } + m.rawData = append(m.rawData, bson.E{Key: "WorkflowDescription", Value: newDesc}) + } else { + dSet(wfDesc, "Text", value) + } + return nil + + case "EXPORT_LEVEL": + dSet(m.rawData, "ExportLevel", value) + return nil + + case "DUE_DATE": + dSet(m.rawData, "DueDate", value) + return nil + + default: + return fmt.Errorf("unsupported workflow property: %s", prop) + } +} + +func (m *mprWorkflowMutator) SetPropertyWithEntity(prop string, value string, entity string) error { + switch prop { + case "OVERVIEW_PAGE": + if value == "" { + dSet(m.rawData, "AdminPage", nil) + } else { + pageRef := bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Workflows$PageReference"}, + {Key: "Page", Value: value}, + } + dSet(m.rawData, "AdminPage", pageRef) + } + return nil + + case "PARAMETER": + if value == "" { + for i, elem := range m.rawData { + if elem.Key == "Parameter" { + m.rawData[i].Value = nil + return nil + } + } + return nil + } + param := dGetDoc(m.rawData, "Parameter") + if param != nil { + dSet(param, "Entity", entity) + } else { + newParam := bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Workflows$Parameter"}, + {Key: "Entity", Value: entity}, + {Key: "Name", Value: "WorkflowContext"}, + } + for i, elem := range m.rawData { + if elem.Key == "Parameter" { + m.rawData[i].Value = newParam + return nil + } + } + m.rawData = append(m.rawData, bson.E{Key: "Parameter", Value: newParam}) + } + return nil + + default: + return fmt.Errorf("unsupported workflow property with entity: %s", prop) + } +} + +// --------------------------------------------------------------------------- +// WorkflowMutator interface — activity operations +// --------------------------------------------------------------------------- + +func (m *mprWorkflowMutator) SetActivityProperty(activityRef string, atPos int, prop string, value string) error { + actDoc, err := m.findActivityByCaption(activityRef, atPos) + if err != nil { + return err + } + + switch prop { + case "PAGE": + taskPage := dGetDoc(actDoc, "TaskPage") + if taskPage != nil { + dSet(taskPage, "Page", value) + } else { + pageRef := bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Workflows$PageReference"}, + {Key: "Page", Value: value}, + } + dSet(actDoc, "TaskPage", pageRef) + } + return nil + + case "DESCRIPTION": + taskDesc := dGetDoc(actDoc, "TaskDescription") + if taskDesc != nil { + dSet(taskDesc, "Text", value) + } + return nil + + case "TARGETING_MICROFLOW": + userTargeting := bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Workflows$MicroflowUserTargeting"}, + {Key: "Microflow", Value: value}, + } + dSet(actDoc, "UserTargeting", userTargeting) + return nil + + case "TARGETING_XPATH": + userTargeting := bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Workflows$XPathUserTargeting"}, + {Key: "XPathConstraint", Value: value}, + } + dSet(actDoc, "UserTargeting", userTargeting) + return nil + + case "DUE_DATE": + dSet(actDoc, "DueDate", value) + return nil + + default: + return fmt.Errorf("unsupported activity property: %s", prop) + } +} + +func (m *mprWorkflowMutator) InsertAfterActivity(activityRef string, atPos int, activities []workflows.WorkflowActivity) error { + idx, acts, containingFlow, err := m.findActivityIndex(activityRef, atPos) + if err != nil { + return err + } + + newBsonActs := m.serializeAndDedup(activities) + + insertIdx := idx + 1 + newArr := make([]any, 0, len(acts)+len(newBsonActs)) + newArr = append(newArr, acts[:insertIdx]...) + newArr = append(newArr, newBsonActs...) + newArr = append(newArr, acts[insertIdx:]...) + + dSetArray(containingFlow, "Activities", newArr) + return nil +} + +func (m *mprWorkflowMutator) DropActivity(activityRef string, atPos int) error { + idx, acts, containingFlow, err := m.findActivityIndex(activityRef, atPos) + if err != nil { + return err + } + + newArr := make([]any, 0, len(acts)-1) + newArr = append(newArr, acts[:idx]...) + newArr = append(newArr, acts[idx+1:]...) + + dSetArray(containingFlow, "Activities", newArr) + return nil +} + +func (m *mprWorkflowMutator) ReplaceActivity(activityRef string, atPos int, activities []workflows.WorkflowActivity) error { + idx, acts, containingFlow, err := m.findActivityIndex(activityRef, atPos) + if err != nil { + return err + } + + newBsonActs := m.serializeAndDedup(activities) + + newArr := make([]any, 0, len(acts)-1+len(newBsonActs)) + newArr = append(newArr, acts[:idx]...) + newArr = append(newArr, newBsonActs...) + newArr = append(newArr, acts[idx+1:]...) + + dSetArray(containingFlow, "Activities", newArr) + return nil +} + +// --------------------------------------------------------------------------- +// WorkflowMutator interface — outcome operations +// --------------------------------------------------------------------------- + +func (m *mprWorkflowMutator) InsertOutcome(activityRef string, atPos int, outcomeName string, activities []workflows.WorkflowActivity) error { + actDoc, err := m.findActivityByCaption(activityRef, atPos) + if err != nil { + return err + } + + outcomeDoc := bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Workflows$UserTaskOutcome"}, + } + + if len(activities) > 0 { + outcomeDoc = append(outcomeDoc, bson.E{Key: "Flow", Value: m.buildSubFlowBson(activities)}) + } + + outcomeDoc = append(outcomeDoc, + bson.E{Key: "PersistentId", Value: bsonutil.NewIDBsonBinary()}, + bson.E{Key: "Value", Value: outcomeName}, + ) + + outcomes := dGetArrayElements(dGet(actDoc, "Outcomes")) + outcomes = append(outcomes, outcomeDoc) + dSetArray(actDoc, "Outcomes", outcomes) + return nil +} + +func (m *mprWorkflowMutator) DropOutcome(activityRef string, atPos int, outcomeName string) error { + actDoc, err := m.findActivityByCaption(activityRef, atPos) + if err != nil { + return err + } + + outcomes := dGetArrayElements(dGet(actDoc, "Outcomes")) + found := false + var kept []any + for _, elem := range outcomes { + oDoc, ok := elem.(bson.D) + if !ok { + kept = append(kept, elem) + continue + } + value := dGetString(oDoc, "Value") + typeName := dGetString(oDoc, "$Type") + matched := value == outcomeName + if !matched && strings.EqualFold(outcomeName, "Default") && typeName == "Workflows$VoidConditionOutcome" { + matched = true + } + if matched && !found { + found = true + continue + } + kept = append(kept, elem) + } + if !found { + return fmt.Errorf("outcome %q not found on activity %q", outcomeName, activityRef) + } + dSetArray(actDoc, "Outcomes", kept) + return nil +} + +// --------------------------------------------------------------------------- +// WorkflowMutator interface — path operations (parallel split) +// --------------------------------------------------------------------------- + +func (m *mprWorkflowMutator) InsertPath(activityRef string, atPos int, pathCaption string, activities []workflows.WorkflowActivity) error { + actDoc, err := m.findActivityByCaption(activityRef, atPos) + if err != nil { + return err + } + + pathDoc := bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Workflows$ParallelSplitOutcome"}, + } + + if len(activities) > 0 { + pathDoc = append(pathDoc, bson.E{Key: "Flow", Value: m.buildSubFlowBson(activities)}) + } + + pathDoc = append(pathDoc, bson.E{Key: "PersistentId", Value: bsonutil.NewIDBsonBinary()}) + + outcomes := dGetArrayElements(dGet(actDoc, "Outcomes")) + outcomes = append(outcomes, pathDoc) + dSetArray(actDoc, "Outcomes", outcomes) + return nil +} + +func (m *mprWorkflowMutator) DropPath(activityRef string, atPos int, pathCaption string) error { + actDoc, err := m.findActivityByCaption(activityRef, atPos) + if err != nil { + return err + } + + outcomes := dGetArrayElements(dGet(actDoc, "Outcomes")) + if pathCaption == "" && len(outcomes) > 0 { + outcomes = outcomes[:len(outcomes)-1] + dSetArray(actDoc, "Outcomes", outcomes) + return nil + } + + pathIdx := -1 + for i := range outcomes { + if fmt.Sprintf("Path %d", i+1) == pathCaption { + pathIdx = i + break + } + } + if pathIdx < 0 { + return fmt.Errorf("path %q not found on parallel split %q", pathCaption, activityRef) + } + + newOutcomes := make([]any, 0, len(outcomes)-1) + newOutcomes = append(newOutcomes, outcomes[:pathIdx]...) + newOutcomes = append(newOutcomes, outcomes[pathIdx+1:]...) + dSetArray(actDoc, "Outcomes", newOutcomes) + return nil +} + +// --------------------------------------------------------------------------- +// WorkflowMutator interface — branch operations (exclusive split) +// --------------------------------------------------------------------------- + +func (m *mprWorkflowMutator) InsertBranch(activityRef string, atPos int, condition string, activities []workflows.WorkflowActivity) error { + actDoc, err := m.findActivityByCaption(activityRef, atPos) + if err != nil { + return err + } + + var outcomeDoc bson.D + switch strings.ToLower(condition) { + case "true": + outcomeDoc = bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Workflows$BooleanConditionOutcome"}, + {Key: "Value", Value: true}, + } + case "false": + outcomeDoc = bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Workflows$BooleanConditionOutcome"}, + {Key: "Value", Value: false}, + } + case "default": + outcomeDoc = bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Workflows$VoidConditionOutcome"}, + } + default: + outcomeDoc = bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Workflows$EnumerationValueConditionOutcome"}, + {Key: "Value", Value: condition}, + } + } + + if len(activities) > 0 { + outcomeDoc = append(outcomeDoc, bson.E{Key: "Flow", Value: m.buildSubFlowBson(activities)}) + } + + outcomes := dGetArrayElements(dGet(actDoc, "Outcomes")) + outcomes = append(outcomes, outcomeDoc) + dSetArray(actDoc, "Outcomes", outcomes) + return nil +} + +func (m *mprWorkflowMutator) DropBranch(activityRef string, atPos int, branchName string) error { + actDoc, err := m.findActivityByCaption(activityRef, atPos) + if err != nil { + return err + } + + outcomes := dGetArrayElements(dGet(actDoc, "Outcomes")) + found := false + var kept []any + for _, elem := range outcomes { + oDoc, ok := elem.(bson.D) + if !ok { + kept = append(kept, elem) + continue + } + if !found { + typeName := dGetString(oDoc, "$Type") + switch strings.ToLower(branchName) { + case "true": + if typeName == "Workflows$BooleanConditionOutcome" { + if v, ok := dGet(oDoc, "Value").(bool); ok && v { + found = true + continue + } + } + case "false": + if typeName == "Workflows$BooleanConditionOutcome" { + if v, ok := dGet(oDoc, "Value").(bool); ok && !v { + found = true + continue + } + } + case "default": + if typeName == "Workflows$VoidConditionOutcome" { + found = true + continue + } + default: + value := dGetString(oDoc, "Value") + if value == branchName { + found = true + continue + } + } + } + kept = append(kept, elem) + } + if !found { + return fmt.Errorf("branch %q not found on activity %q", branchName, activityRef) + } + dSetArray(actDoc, "Outcomes", kept) + return nil +} + +// --------------------------------------------------------------------------- +// WorkflowMutator interface — boundary event operations +// --------------------------------------------------------------------------- + +func (m *mprWorkflowMutator) InsertBoundaryEvent(activityRef string, atPos int, eventType string, delay string, activities []workflows.WorkflowActivity) error { + actDoc, err := m.findActivityByCaption(activityRef, atPos) + if err != nil { + return err + } + + typeName := "Workflows$InterruptingTimerBoundaryEvent" + switch eventType { + case "NonInterruptingTimer": + typeName = "Workflows$NonInterruptingTimerBoundaryEvent" + case "Timer": + typeName = "Workflows$TimerBoundaryEvent" + } + + eventDoc := bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: typeName}, + {Key: "Caption", Value: ""}, + } + + if delay != "" { + eventDoc = append(eventDoc, bson.E{Key: "FirstExecutionTime", Value: delay}) + } + + if len(activities) > 0 { + eventDoc = append(eventDoc, bson.E{Key: "Flow", Value: m.buildSubFlowBson(activities)}) + } + + eventDoc = append(eventDoc, bson.E{Key: "PersistentId", Value: bsonutil.NewIDBsonBinary()}) + + if typeName == "Workflows$NonInterruptingTimerBoundaryEvent" { + eventDoc = append(eventDoc, bson.E{Key: "Recurrence", Value: nil}) + } + + events := dGetArrayElements(dGet(actDoc, "BoundaryEvents")) + events = append(events, eventDoc) + dSetArray(actDoc, "BoundaryEvents", events) + return nil +} + +func (m *mprWorkflowMutator) DropBoundaryEvent(activityRef string, atPos int) error { + actDoc, err := m.findActivityByCaption(activityRef, atPos) + if err != nil { + return err + } + + events := dGetArrayElements(dGet(actDoc, "BoundaryEvents")) + if len(events) == 0 { + return fmt.Errorf("activity %q has no boundary events", activityRef) + } + + // Drop the first boundary event silently. + dSetArray(actDoc, "BoundaryEvents", events[1:]) + return nil +} + +// --------------------------------------------------------------------------- +// Save +// --------------------------------------------------------------------------- + +func (m *mprWorkflowMutator) Save() error { + outBytes, err := bson.Marshal(m.rawData) + if err != nil { + return fmt.Errorf("marshal modified workflow: %w", err) + } + return m.backend.writer.UpdateRawUnit(string(m.unitID), outBytes) +} + +// --------------------------------------------------------------------------- +// Internal helpers — activity search +// --------------------------------------------------------------------------- + +// findActivityByCaption searches the workflow for an activity matching caption. +func (m *mprWorkflowMutator) findActivityByCaption(caption string, atPosition int) (bson.D, error) { + flow := dGetDoc(m.rawData, "Flow") + if flow == nil { + return nil, fmt.Errorf("workflow has no Flow") + } + + var matches []bson.D + findActivitiesRecursive(flow, caption, &matches) + + if len(matches) == 0 { + return nil, fmt.Errorf("activity %q not found", 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)) + } + 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 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 matches[atPosition-1], nil +} + +// findActivitiesRecursive collects all activities matching caption in a flow and nested sub-flows. +func findActivitiesRecursive(flow bson.D, caption string, matches *[]bson.D) { + activities := dGetArrayElements(dGet(flow, "Activities")) + for _, elem := range activities { + actDoc, ok := elem.(bson.D) + if !ok { + continue + } + actCaption := dGetString(actDoc, "Caption") + actName := dGetString(actDoc, "Name") + if actCaption == caption || actName == caption { + *matches = append(*matches, actDoc) + } + for _, nestedFlow := range getNestedFlows(actDoc) { + findActivitiesRecursive(nestedFlow, caption, matches) + } + } +} + +// getNestedFlows returns all sub-flows within an activity. +func getNestedFlows(actDoc bson.D) []bson.D { + var flows []bson.D + outcomes := dGetArrayElements(dGet(actDoc, "Outcomes")) + for _, o := range outcomes { + oDoc, ok := o.(bson.D) + if !ok { + continue + } + if f := dGetDoc(oDoc, "Flow"); f != nil { + flows = append(flows, f) + } + } + events := dGetArrayElements(dGet(actDoc, "BoundaryEvents")) + for _, e := range events { + eDoc, ok := e.(bson.D) + if !ok { + continue + } + if f := dGetDoc(eDoc, "Flow"); f != nil { + flows = append(flows, f) + } + } + return flows +} + +// activityIndexMatch holds search result for findActivityIndex. +type activityIndexMatch struct { + idx int + activities []any + flow bson.D +} + +// findActivityIndex returns the index, activities array, and containing flow of an activity. +func (m *mprWorkflowMutator) findActivityIndex(caption string, atPosition int) (int, []any, bson.D, error) { + flow := dGetDoc(m.rawData, "Flow") + if flow == nil { + return -1, nil, nil, fmt.Errorf("workflow has no Flow") + } + + var matches []activityIndexMatch + findActivityIndexRecursive(flow, caption, &matches) + + if len(matches) == 0 { + return -1, nil, nil, fmt.Errorf("activity %q not found", 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)) + } + if pos >= len(matches) { + return -1, nil, nil, fmt.Errorf("activity %q at position %d not found (found %d matches)", caption, atPosition, len(matches)) + } + am := matches[pos] + return am.idx, am.activities, am.flow, nil +} + +func findActivityIndexRecursive(flow bson.D, caption string, matches *[]activityIndexMatch) { + activities := dGetArrayElements(dGet(flow, "Activities")) + for i, elem := range activities { + actDoc, ok := elem.(bson.D) + if !ok { + continue + } + actCaption := dGetString(actDoc, "Caption") + actName := dGetString(actDoc, "Name") + if actCaption == caption || actName == caption { + *matches = append(*matches, activityIndexMatch{idx: i, activities: activities, flow: flow}) + } + for _, nestedFlow := range getNestedFlows(actDoc) { + findActivityIndexRecursive(nestedFlow, caption, matches) + } + } +} + +// --------------------------------------------------------------------------- +// Internal helpers — name collection & deduplication +// --------------------------------------------------------------------------- + +// collectAllActivityNames collects all activity names from the entire workflow BSON. +func (m *mprWorkflowMutator) collectAllActivityNames() map[string]bool { + names := make(map[string]bool) + flow := dGetDoc(m.rawData, "Flow") + if flow != nil { + collectNamesRecursive(flow, names) + } + return names +} + +func collectNamesRecursive(flow bson.D, names map[string]bool) { + activities := dGetArrayElements(dGet(flow, "Activities")) + for _, elem := range activities { + actDoc, ok := elem.(bson.D) + if !ok { + continue + } + if name := dGetString(actDoc, "Name"); name != "" { + names[name] = true + } + for _, nested := range getNestedFlows(actDoc) { + collectNamesRecursive(nested, names) + } + } +} + +// deduplicateNewActivityName ensures a new activity name doesn't conflict. +func deduplicateNewActivityName(act workflows.WorkflowActivity, existingNames map[string]bool) { + name := act.GetName() + if name == "" || !existingNames[name] { + return + } + for i := 2; i < 1000; i++ { + candidate := fmt.Sprintf("%s_%d", name, i) + if !existingNames[candidate] { + act.SetName(candidate) + existingNames[candidate] = true + return + } + } +} + +// --------------------------------------------------------------------------- +// Internal helpers — serialization +// --------------------------------------------------------------------------- + +// serializeAndDedup serializes workflow activities to BSON, deduplicating names. +func (m *mprWorkflowMutator) serializeAndDedup(activities []workflows.WorkflowActivity) []any { + existingNames := m.collectAllActivityNames() + for _, act := range activities { + deduplicateNewActivityName(act, existingNames) + } + + result := make([]any, 0, len(activities)) + for _, act := range activities { + bsonDoc := mpr.SerializeWorkflowActivity(act) + if bsonDoc != nil { + result = append(result, bsonDoc) + } + } + return result +} + +// buildSubFlowBson builds a Workflows$Flow BSON document from activities. +func (m *mprWorkflowMutator) buildSubFlowBson(activities []workflows.WorkflowActivity) bson.D { + existingNames := m.collectAllActivityNames() + for _, act := range activities { + deduplicateNewActivityName(act, existingNames) + } + + var subActsBson bson.A + subActsBson = append(subActsBson, bsonArrayMarker) + for _, act := range activities { + bsonDoc := mpr.SerializeWorkflowActivity(act) + if bsonDoc != nil { + subActsBson = append(subActsBson, bsonDoc) + } + } + return bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Workflows$Flow"}, + {Key: "Activities", Value: subActsBson}, + } +} diff --git a/mdl/executor/cmd_alter_workflow_test.go b/mdl/backend/mpr/workflow_mutator_test.go similarity index 50% rename from mdl/executor/cmd_alter_workflow_test.go rename to mdl/backend/mpr/workflow_mutator_test.go index fe9de805..f8c1f43b 100644 --- a/mdl/executor/cmd_alter_workflow_test.go +++ b/mdl/backend/mpr/workflow_mutator_test.go @@ -1,22 +1,18 @@ // SPDX-License-Identifier: Apache-2.0 -package executor +package mprbackend import ( - "io" "strings" "testing" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" - - "github.com/mendixlabs/mxcli/mdl/ast" ) // makeWorkflowDoc builds a minimal workflow BSON document for testing. -// Activities are placed inside a Flow sub-document, matching real workflow structure. func makeWorkflowDoc(activities ...bson.D) bson.D { - actArr := bson.A{int32(3)} // Mendix array marker + actArr := bson.A{int32(3)} for _, a := range activities { actArr = append(actArr, a) } @@ -42,8 +38,7 @@ func makeWorkflowDoc(activities ...bson.D) bson.D { } } -// makeWorkflowActivity builds a minimal workflow activity BSON with a caption and name. -func makeWorkflowActivity(typeName, caption, name string) bson.D { +func makeWfActivity(typeName, caption, name string) bson.D { return bson.D{ {Key: "$ID", Value: primitive.Binary{Subtype: 0x04, Data: make([]byte, 16)}}, {Key: "$Type", Value: typeName}, @@ -52,8 +47,7 @@ func makeWorkflowActivity(typeName, caption, name string) bson.D { } } -// makeWorkflowActivityWithBoundaryEvents builds an activity with boundary events. -func makeWorkflowActivityWithBoundaryEvents(caption string, events ...bson.D) bson.D { +func makeWfActivityWithBoundaryEvents(caption string, events ...bson.D) bson.D { evtArr := bson.A{int32(3)} for _, e := range events { evtArr = append(evtArr, e) @@ -67,7 +61,7 @@ func makeWorkflowActivityWithBoundaryEvents(caption string, events ...bson.D) bs } } -func makeBoundaryEvent(typeName string) bson.D { +func makeWfBoundaryEvent(typeName string) bson.D { return bson.D{ {Key: "$ID", Value: primitive.Binary{Subtype: 0x04, Data: make([]byte, 16)}}, {Key: "$Type", Value: typeName}, @@ -75,22 +69,25 @@ func makeBoundaryEvent(typeName string) bson.D { } } -// --- SET DISPLAY tests --- +// newMutator creates a mprWorkflowMutator for testing (no real backend). +func newMutator(doc bson.D) *mprWorkflowMutator { + return &mprWorkflowMutator{rawData: doc} +} + +// --- SetProperty tests --- -func TestSetWorkflowProperty_Display(t *testing.T) { +func TestWorkflowMutator_SetProperty_Display(t *testing.T) { doc := makeWorkflowDoc() + m := newMutator(doc) - op := &ast.SetWorkflowPropertyOp{Property: "DISPLAY", Value: "New Title"} - if err := applySetWorkflowProperty(&doc, op); err != nil { - t.Fatalf("SET DISPLAY failed: %v", err) + if err := m.SetProperty("DISPLAY", "New Title"); err != nil { + t.Fatalf("SetProperty DISPLAY failed: %v", err) } - // Title should be updated - if got := dGetString(doc, "Title"); got != "New Title" { + if got := dGetString(m.rawData, "Title"); got != "New Title" { t.Errorf("Title = %q, want %q", got, "New Title") } - // WorkflowName.Text should be updated - wfName := dGetDoc(doc, "WorkflowName") + wfName := dGetDoc(m.rawData, "WorkflowName") if wfName == nil { t.Fatal("WorkflowName is nil") } @@ -99,8 +96,7 @@ func TestSetWorkflowProperty_Display(t *testing.T) { } } -func TestSetWorkflowProperty_Display_NilSubDoc(t *testing.T) { - // Build doc without WorkflowName to test auto-creation +func TestWorkflowMutator_SetProperty_Display_NilSubDoc(t *testing.T) { doc := bson.D{ {Key: "$ID", Value: primitive.Binary{Subtype: 0x04, Data: make([]byte, 16)}}, {Key: "$Type", Value: "Workflows$Workflow"}, @@ -109,39 +105,33 @@ func TestSetWorkflowProperty_Display_NilSubDoc(t *testing.T) { {Key: "Activities", Value: bson.A{int32(3)}}, }}, } + m := newMutator(doc) - op := &ast.SetWorkflowPropertyOp{Property: "DISPLAY", Value: "Created Title"} - if err := applySetWorkflowProperty(&doc, op); err != nil { - t.Fatalf("SET DISPLAY with nil sub-doc failed: %v", err) + if err := m.SetProperty("DISPLAY", "Created Title"); err != nil { + t.Fatalf("SetProperty DISPLAY with nil sub-doc failed: %v", err) } - if got := dGetString(doc, "Title"); got != "Created Title" { + if got := dGetString(m.rawData, "Title"); got != "Created Title" { t.Errorf("Title = %q, want %q", got, "Created Title") } - - wfName := dGetDoc(doc, "WorkflowName") + wfName := dGetDoc(m.rawData, "WorkflowName") if wfName == nil { t.Fatal("WorkflowName should have been auto-created") } if got := dGetString(wfName, "Text"); got != "Created Title" { t.Errorf("WorkflowName.Text = %q, want %q", got, "Created Title") } - if got := dGetString(wfName, "$Type"); got != "Texts$Text" { - t.Errorf("WorkflowName.$Type = %q, want %q", got, "Texts$Text") - } } -// --- SET DESCRIPTION tests --- - -func TestSetWorkflowProperty_Description(t *testing.T) { +func TestWorkflowMutator_SetProperty_Description(t *testing.T) { doc := makeWorkflowDoc() + m := newMutator(doc) - op := &ast.SetWorkflowPropertyOp{Property: "DESCRIPTION", Value: "Updated desc"} - if err := applySetWorkflowProperty(&doc, op); err != nil { - t.Fatalf("SET DESCRIPTION failed: %v", err) + if err := m.SetProperty("DESCRIPTION", "Updated desc"); err != nil { + t.Fatalf("SetProperty DESCRIPTION failed: %v", err) } - wfDesc := dGetDoc(doc, "WorkflowDescription") + wfDesc := dGetDoc(m.rawData, "WorkflowDescription") if wfDesc == nil { t.Fatal("WorkflowDescription is nil") } @@ -150,7 +140,7 @@ func TestSetWorkflowProperty_Description(t *testing.T) { } } -func TestSetWorkflowProperty_Description_NilSubDoc(t *testing.T) { +func TestWorkflowMutator_SetProperty_Description_NilSubDoc(t *testing.T) { doc := bson.D{ {Key: "$ID", Value: primitive.Binary{Subtype: 0x04, Data: make([]byte, 16)}}, {Key: "$Type", Value: "Workflows$Workflow"}, @@ -159,13 +149,13 @@ func TestSetWorkflowProperty_Description_NilSubDoc(t *testing.T) { {Key: "Activities", Value: bson.A{int32(3)}}, }}, } + m := newMutator(doc) - op := &ast.SetWorkflowPropertyOp{Property: "DESCRIPTION", Value: "New desc"} - if err := applySetWorkflowProperty(&doc, op); err != nil { - t.Fatalf("SET DESCRIPTION with nil sub-doc failed: %v", err) + if err := m.SetProperty("DESCRIPTION", "New desc"); err != nil { + t.Fatalf("SetProperty DESCRIPTION with nil sub-doc failed: %v", err) } - wfDesc := dGetDoc(doc, "WorkflowDescription") + wfDesc := dGetDoc(m.rawData, "WorkflowDescription") if wfDesc == nil { t.Fatal("WorkflowDescription should have been auto-created") } @@ -174,13 +164,11 @@ func TestSetWorkflowProperty_Description_NilSubDoc(t *testing.T) { } } -// --- SET unsupported property --- - -func TestSetWorkflowProperty_UnsupportedProperty(t *testing.T) { +func TestWorkflowMutator_SetProperty_Unsupported(t *testing.T) { doc := makeWorkflowDoc() + m := newMutator(doc) - op := &ast.SetWorkflowPropertyOp{Property: "UNKNOWN_PROP", Value: "x"} - err := applySetWorkflowProperty(&doc, op) + err := m.SetProperty("UNKNOWN_PROP", "x") if err == nil { t.Fatal("Expected error for unsupported property") } @@ -189,14 +177,27 @@ func TestSetWorkflowProperty_UnsupportedProperty(t *testing.T) { } } +func TestWorkflowMutator_SetProperty_ExportLevel(t *testing.T) { + doc := makeWorkflowDoc() + doc = append(doc, bson.E{Key: "ExportLevel", Value: "Usable"}) + m := newMutator(doc) + + if err := m.SetProperty("EXPORT_LEVEL", "Hidden"); err != nil { + t.Fatalf("SetProperty EXPORT_LEVEL failed: %v", err) + } + if got := dGetString(m.rawData, "ExportLevel"); got != "Hidden" { + t.Errorf("ExportLevel = %q, want %q", got, "Hidden") + } +} + // --- findActivityByCaption tests --- -func TestFindActivityByCaption_Found(t *testing.T) { - act1 := makeWorkflowActivity("Workflows$UserTask", "Review", "task1") - act2 := makeWorkflowActivity("Workflows$UserTask", "Approve", "task2") - doc := makeWorkflowDoc(act1, act2) +func TestWorkflowMutator_FindActivity_Found(t *testing.T) { + act1 := makeWfActivity("Workflows$UserTask", "Review", "task1") + act2 := makeWfActivity("Workflows$UserTask", "Approve", "task2") + m := newMutator(makeWorkflowDoc(act1, act2)) - result, err := findActivityByCaption(doc, "Approve", 0) + result, err := m.findActivityByCaption("Approve", 0) if err != nil { t.Fatalf("findActivityByCaption failed: %v", err) } @@ -205,11 +206,11 @@ func TestFindActivityByCaption_Found(t *testing.T) { } } -func TestFindActivityByCaption_ByName(t *testing.T) { - act1 := makeWorkflowActivity("Workflows$UserTask", "Review", "ReviewTask") - doc := makeWorkflowDoc(act1) +func TestWorkflowMutator_FindActivity_ByName(t *testing.T) { + act1 := makeWfActivity("Workflows$UserTask", "Review", "ReviewTask") + m := newMutator(makeWorkflowDoc(act1)) - result, err := findActivityByCaption(doc, "ReviewTask", 0) + result, err := m.findActivityByCaption("ReviewTask", 0) if err != nil { t.Fatalf("findActivityByCaption by name failed: %v", err) } @@ -218,11 +219,11 @@ func TestFindActivityByCaption_ByName(t *testing.T) { } } -func TestFindActivityByCaption_NotFound(t *testing.T) { - act1 := makeWorkflowActivity("Workflows$UserTask", "Review", "task1") - doc := makeWorkflowDoc(act1) +func TestWorkflowMutator_FindActivity_NotFound(t *testing.T) { + act1 := makeWfActivity("Workflows$UserTask", "Review", "task1") + m := newMutator(makeWorkflowDoc(act1)) - _, err := findActivityByCaption(doc, "NonExistent", 0) + _, err := m.findActivityByCaption("NonExistent", 0) if err == nil { t.Fatal("Expected error for missing activity") } @@ -231,12 +232,12 @@ func TestFindActivityByCaption_NotFound(t *testing.T) { } } -func TestFindActivityByCaption_Ambiguous(t *testing.T) { - act1 := makeWorkflowActivity("Workflows$UserTask", "Review", "task1") - act2 := makeWorkflowActivity("Workflows$UserTask", "Review", "task2") - doc := makeWorkflowDoc(act1, act2) +func TestWorkflowMutator_FindActivity_Ambiguous(t *testing.T) { + act1 := makeWfActivity("Workflows$UserTask", "Review", "task1") + act2 := makeWfActivity("Workflows$UserTask", "Review", "task2") + m := newMutator(makeWorkflowDoc(act1, act2)) - _, err := findActivityByCaption(doc, "Review", 0) + _, err := m.findActivityByCaption("Review", 0) if err == nil { t.Fatal("Expected error for ambiguous activity") } @@ -245,12 +246,12 @@ func TestFindActivityByCaption_Ambiguous(t *testing.T) { } } -func TestFindActivityByCaption_AtPosition(t *testing.T) { - act1 := makeWorkflowActivity("Workflows$UserTask", "Review", "task1") - act2 := makeWorkflowActivity("Workflows$UserTask", "Review", "task2") - doc := makeWorkflowDoc(act1, act2) +func TestWorkflowMutator_FindActivity_AtPosition(t *testing.T) { + act1 := makeWfActivity("Workflows$UserTask", "Review", "task1") + act2 := makeWfActivity("Workflows$UserTask", "Review", "task2") + m := newMutator(makeWorkflowDoc(act1, act2)) - result, err := findActivityByCaption(doc, "Review", 2) + result, err := m.findActivityByCaption("Review", 2) if err != nil { t.Fatalf("findActivityByCaption @2 failed: %v", err) } @@ -259,11 +260,11 @@ func TestFindActivityByCaption_AtPosition(t *testing.T) { } } -func TestFindActivityByCaption_AtPosition_OutOfRange(t *testing.T) { - act1 := makeWorkflowActivity("Workflows$UserTask", "Review", "task1") - doc := makeWorkflowDoc(act1) +func TestWorkflowMutator_FindActivity_AtPosition_OutOfRange(t *testing.T) { + act1 := makeWfActivity("Workflows$UserTask", "Review", "task1") + m := newMutator(makeWorkflowDoc(act1)) - _, err := findActivityByCaption(doc, "Review", 5) + _, err := m.findActivityByCaption("Review", 5) if err == nil { t.Fatal("Expected error for out-of-range position") } @@ -272,25 +273,23 @@ func TestFindActivityByCaption_AtPosition_OutOfRange(t *testing.T) { } } -// --- DROP activity tests --- +// --- DropActivity tests --- -func TestDropActivity_ByCaption(t *testing.T) { - act1 := makeWorkflowActivity("Workflows$UserTask", "Review", "task1") - act2 := makeWorkflowActivity("Workflows$UserTask", "Approve", "task2") - act3 := makeWorkflowActivity("Workflows$UserTask", "Finalize", "task3") - doc := makeWorkflowDoc(act1, act2, act3) +func TestWorkflowMutator_DropActivity(t *testing.T) { + act1 := makeWfActivity("Workflows$UserTask", "Review", "task1") + act2 := makeWfActivity("Workflows$UserTask", "Approve", "task2") + act3 := makeWfActivity("Workflows$UserTask", "Finalize", "task3") + m := newMutator(makeWorkflowDoc(act1, act2, act3)) - op := &ast.DropActivityOp{ActivityRef: "Approve"} - if err := applyDropActivity(doc, op); err != nil { - t.Fatalf("DROP ACTIVITY failed: %v", err) + if err := m.DropActivity("Approve", 0); err != nil { + t.Fatalf("DropActivity failed: %v", err) } - flow := dGetDoc(doc, "Flow") + flow := dGetDoc(m.rawData, "Flow") activities := dGetArrayElements(dGet(flow, "Activities")) if len(activities) != 2 { t.Fatalf("Expected 2 activities after drop, got %d", len(activities)) } - name0 := dGetString(activities[0].(bson.D), "Caption") name1 := dGetString(activities[1].(bson.D), "Caption") if name0 != "Review" { @@ -301,65 +300,60 @@ func TestDropActivity_ByCaption(t *testing.T) { } } -func TestDropActivity_NotFound(t *testing.T) { - act1 := makeWorkflowActivity("Workflows$UserTask", "Review", "task1") - doc := makeWorkflowDoc(act1) +func TestWorkflowMutator_DropActivity_NotFound(t *testing.T) { + act1 := makeWfActivity("Workflows$UserTask", "Review", "task1") + m := newMutator(makeWorkflowDoc(act1)) - op := &ast.DropActivityOp{ActivityRef: "NonExistent"} - err := applyDropActivity(doc, op) + err := m.DropActivity("NonExistent", 0) if err == nil { t.Fatal("Expected error for dropping nonexistent activity") } } -// --- DROP BOUNDARY EVENT tests --- +// --- DropBoundaryEvent tests --- -func TestDropBoundaryEvent_Single(t *testing.T) { - evt := makeBoundaryEvent("Workflows$InterruptingTimerBoundaryEvent") - act := makeWorkflowActivityWithBoundaryEvents("Review", evt) - doc := makeWorkflowDoc(act) +func TestWorkflowMutator_DropBoundaryEvent_Single(t *testing.T) { + evt := makeWfBoundaryEvent("Workflows$InterruptingTimerBoundaryEvent") + act := makeWfActivityWithBoundaryEvents("Review", evt) + m := newMutator(makeWorkflowDoc(act)) - op := &ast.DropBoundaryEventOp{ActivityRef: "Review"} - if err := applyDropBoundaryEvent(io.Discard, doc, op); err != nil { - t.Fatalf("DROP BOUNDARY EVENT failed: %v", err) + if err := m.DropBoundaryEvent("Review", 0); err != nil { + t.Fatalf("DropBoundaryEvent failed: %v", err) } - actDoc, _ := findActivityByCaption(doc, "Review", 0) + actDoc, _ := m.findActivityByCaption("Review", 0) events := dGetArrayElements(dGet(actDoc, "BoundaryEvents")) if len(events) != 0 { t.Errorf("Expected 0 boundary events after drop, got %d", len(events)) } } -func TestDropBoundaryEvent_Multiple_DropsFirst(t *testing.T) { - evt1 := makeBoundaryEvent("Workflows$InterruptingTimerBoundaryEvent") - evt2 := makeBoundaryEvent("Workflows$NonInterruptingTimerBoundaryEvent") - act := makeWorkflowActivityWithBoundaryEvents("Review", evt1, evt2) - doc := makeWorkflowDoc(act) +func TestWorkflowMutator_DropBoundaryEvent_Multiple(t *testing.T) { + evt1 := makeWfBoundaryEvent("Workflows$InterruptingTimerBoundaryEvent") + evt2 := makeWfBoundaryEvent("Workflows$NonInterruptingTimerBoundaryEvent") + act := makeWfActivityWithBoundaryEvents("Review", evt1, evt2) + m := newMutator(makeWorkflowDoc(act)) - op := &ast.DropBoundaryEventOp{ActivityRef: "Review"} - if err := applyDropBoundaryEvent(io.Discard, doc, op); err != nil { - t.Fatalf("DROP BOUNDARY EVENT failed: %v", err) + if err := m.DropBoundaryEvent("Review", 0); err != nil { + t.Fatalf("DropBoundaryEvent failed: %v", err) } - actDoc, _ := findActivityByCaption(doc, "Review", 0) + actDoc, _ := m.findActivityByCaption("Review", 0) events := dGetArrayElements(dGet(actDoc, "BoundaryEvents")) if len(events) != 1 { t.Fatalf("Expected 1 boundary event after drop, got %d", len(events)) } - remaining := events[0].(bson.D) if got := dGetString(remaining, "$Type"); got != "Workflows$NonInterruptingTimerBoundaryEvent" { t.Errorf("Remaining event type = %q, want NonInterruptingTimerBoundaryEvent", got) } } -func TestDropBoundaryEvent_NoEvents(t *testing.T) { - act := makeWorkflowActivityWithBoundaryEvents("Review") // no events - doc := makeWorkflowDoc(act) +func TestWorkflowMutator_DropBoundaryEvent_NoEvents(t *testing.T) { + act := makeWfActivityWithBoundaryEvents("Review") + m := newMutator(makeWorkflowDoc(act)) - op := &ast.DropBoundaryEventOp{ActivityRef: "Review"} - err := applyDropBoundaryEvent(io.Discard, doc, op) + err := m.DropBoundaryEvent("Review", 0) if err == nil { t.Fatal("Expected error when dropping from activity with no boundary events") } @@ -370,12 +364,12 @@ func TestDropBoundaryEvent_NoEvents(t *testing.T) { // --- findActivityIndex tests --- -func TestFindActivityIndex_Basic(t *testing.T) { - act1 := makeWorkflowActivity("Workflows$UserTask", "Review", "task1") - act2 := makeWorkflowActivity("Workflows$UserTask", "Approve", "task2") - doc := makeWorkflowDoc(act1, act2) +func TestWorkflowMutator_FindActivityIndex(t *testing.T) { + act1 := makeWfActivity("Workflows$UserTask", "Review", "task1") + act2 := makeWfActivity("Workflows$UserTask", "Approve", "task2") + m := newMutator(makeWorkflowDoc(act1, act2)) - idx, activities, flow, err := findActivityIndex(doc, "Approve", 0) + idx, activities, flow, err := m.findActivityIndex("Approve", 0) if err != nil { t.Fatalf("findActivityIndex failed: %v", err) } @@ -390,12 +384,13 @@ func TestFindActivityIndex_Basic(t *testing.T) { } } -func TestFindActivityIndex_NoFlow(t *testing.T) { +func TestWorkflowMutator_FindActivityIndex_NoFlow(t *testing.T) { doc := bson.D{ {Key: "$Type", Value: "Workflows$Workflow"}, } + m := newMutator(doc) - _, _, _, err := findActivityIndex(doc, "Review", 0) + _, _, _, err := m.findActivityIndex("Review", 0) if err == nil { t.Fatal("Expected error for doc without Flow") } @@ -406,12 +401,12 @@ func TestFindActivityIndex_NoFlow(t *testing.T) { // --- collectAllActivityNames tests --- -func TestCollectAllActivityNames(t *testing.T) { - act1 := makeWorkflowActivity("Workflows$UserTask", "Review", "ReviewTask") - act2 := makeWorkflowActivity("Workflows$UserTask", "Approve", "ApproveTask") - doc := makeWorkflowDoc(act1, act2) +func TestWorkflowMutator_CollectAllActivityNames(t *testing.T) { + act1 := makeWfActivity("Workflows$UserTask", "Review", "ReviewTask") + act2 := makeWfActivity("Workflows$UserTask", "Approve", "ApproveTask") + m := newMutator(makeWorkflowDoc(act1, act2)) - names := collectAllActivityNames(doc) + names := m.collectAllActivityNames() if !names["ReviewTask"] { t.Error("Expected ReviewTask in names") } @@ -423,64 +418,38 @@ func TestCollectAllActivityNames(t *testing.T) { } } -func TestCollectAllActivityNames_NoFlow(t *testing.T) { +func TestWorkflowMutator_CollectAllActivityNames_NoFlow(t *testing.T) { doc := bson.D{{Key: "$Type", Value: "Workflows$Workflow"}} + m := newMutator(doc) - names := collectAllActivityNames(doc) + names := m.collectAllActivityNames() if len(names) != 0 { t.Errorf("Expected empty names map, got %d entries", len(names)) } } -// --- SET EXPORT_LEVEL / DUE_DATE --- - -func TestSetWorkflowProperty_ExportLevel(t *testing.T) { - doc := makeWorkflowDoc() - // ExportLevel must exist in the doc for dSet to update it - doc = append(doc, bson.E{Key: "ExportLevel", Value: "Usable"}) - - op := &ast.SetWorkflowPropertyOp{Property: "EXPORT_LEVEL", Value: "Hidden"} - if err := applySetWorkflowProperty(&doc, op); err != nil { - t.Fatalf("SET EXPORT_LEVEL failed: %v", err) - } - - if got := dGetString(doc, "ExportLevel"); got != "Hidden" { - t.Errorf("ExportLevel = %q, want %q", got, "Hidden") - } -} - -// --- applySetActivityProperty tests --- +// --- SetActivityProperty tests --- -func TestSetActivityProperty_DueDate(t *testing.T) { - act := makeWorkflowActivity("Workflows$UserTask", "Review", "task1") +func TestWorkflowMutator_SetActivityProperty_DueDate(t *testing.T) { + act := makeWfActivity("Workflows$UserTask", "Review", "task1") act = append(act, bson.E{Key: "DueDate", Value: ""}) - doc := makeWorkflowDoc(act) + m := newMutator(makeWorkflowDoc(act)) - op := &ast.SetActivityPropertyOp{ - ActivityRef: "Review", - Property: "DUE_DATE", - Value: "${PT48H}", - } - if err := applySetActivityProperty(doc, op); err != nil { - t.Fatalf("SET DUE_DATE failed: %v", err) + if err := m.SetActivityProperty("Review", 0, "DUE_DATE", "${PT48H}"); err != nil { + t.Fatalf("SetActivityProperty DUE_DATE failed: %v", err) } - actDoc, _ := findActivityByCaption(doc, "Review", 0) + actDoc, _ := m.findActivityByCaption("Review", 0) if got := dGetString(actDoc, "DueDate"); got != "${PT48H}" { t.Errorf("DueDate = %q, want %q", got, "${PT48H}") } } -func TestSetActivityProperty_UnsupportedProperty(t *testing.T) { - act := makeWorkflowActivity("Workflows$UserTask", "Review", "task1") - doc := makeWorkflowDoc(act) +func TestWorkflowMutator_SetActivityProperty_Unsupported(t *testing.T) { + act := makeWfActivity("Workflows$UserTask", "Review", "task1") + m := newMutator(makeWorkflowDoc(act)) - op := &ast.SetActivityPropertyOp{ - ActivityRef: "Review", - Property: "INVALID", - Value: "x", - } - err := applySetActivityProperty(doc, op) + err := m.SetActivityProperty("Review", 0, "INVALID", "x") if err == nil { t.Fatal("Expected error for unsupported activity property") } @@ -489,16 +458,14 @@ func TestSetActivityProperty_UnsupportedProperty(t *testing.T) { } } -// --- applyDropOutcome tests --- +// --- DropOutcome tests --- -func TestDropOutcome_NotFound(t *testing.T) { - act := makeWorkflowActivity("Workflows$UserTask", "Review", "task1") - // Add empty Outcomes array +func TestWorkflowMutator_DropOutcome_NotFound(t *testing.T) { + act := makeWfActivity("Workflows$UserTask", "Review", "task1") act = append(act, bson.E{Key: "Outcomes", Value: bson.A{int32(3)}}) - doc := makeWorkflowDoc(act) + m := newMutator(makeWorkflowDoc(act)) - op := &ast.DropOutcomeOp{ActivityRef: "Review", OutcomeName: "NonExistent"} - err := applyDropOutcome(doc, op) + err := m.DropOutcome("Review", 0, "NonExistent") if err == nil { t.Fatal("Expected error for dropping nonexistent outcome") } @@ -509,7 +476,7 @@ func TestDropOutcome_NotFound(t *testing.T) { // --- bsonArrayMarker constant test --- -func TestBsonArrayMarkerConstant(t *testing.T) { +func TestWorkflowMutator_BsonArrayMarkerConstant(t *testing.T) { if bsonArrayMarker != int32(3) { t.Errorf("bsonArrayMarker = %v, want int32(3)", bsonArrayMarker) } diff --git a/mdl/backend/mutation.go b/mdl/backend/mutation.go index cb0846df..5d02b0b1 100644 --- a/mdl/backend/mutation.go +++ b/mdl/backend/mutation.go @@ -8,6 +8,23 @@ import ( "github.com/mendixlabs/mxcli/sdk/workflows" ) +// WidgetRef identifies a widget or a column within a widget. +type WidgetRef struct { + Widget string + Column string // empty for non-column targeting +} + +// IsColumn returns true if this targets a column within a widget. +func (r WidgetRef) IsColumn() bool { return r.Column != "" } + +// Name returns the full reference string for error messages. +func (r WidgetRef) Name() string { + if r.Column != "" { + return r.Widget + "." + r.Column + } + return r.Widget +} + // PageMutator provides fine-grained mutation operations on a single // page, layout, or snippet unit. Obtain one via PageMutationBackend.OpenPageForMutation. // All methods operate on the in-memory representation; call Save to persist. @@ -31,14 +48,19 @@ type PageMutator interface { // --- Widget tree operations --- // InsertWidget inserts serialized widgets at the given position - // relative to the target widget. Position is "before" or "after". - InsertWidget(targetWidget string, position string, widgets []pages.Widget) error + // relative to the target widget or column. Position is "before" or "after". + // columnRef is "" for widget targeting; non-empty for column targeting. + InsertWidget(widgetRef string, columnRef string, position string, widgets []pages.Widget) error - // DropWidget removes widgets by name from the tree. - DropWidget(widgetRefs []string) error + // DropWidget removes widgets by ref from the tree. + DropWidget(refs []WidgetRef) error - // ReplaceWidget replaces the target widget with the given widgets. - ReplaceWidget(targetWidget string, widgets []pages.Widget) error + // ReplaceWidget replaces the target widget or column with the given widgets. + // columnRef is "" for widget targeting. + ReplaceWidget(widgetRef string, columnRef string, widgets []pages.Widget) error + + // FindWidget checks if a widget with the given name exists in the tree. + FindWidget(name string) bool // --- Variable operations --- @@ -71,6 +93,10 @@ type PageMutator interface { // WidgetScope returns a map of widget name → unit ID for all widgets in the tree. WidgetScope() map[string]model.ID + // ParamScope returns page/snippet parameter maps: + // paramIDs maps param name → entity ID, paramEntityNames maps param name → qualified entity name. + ParamScope() (paramIDs map[string]model.ID, paramEntityNames map[string]string) + // Save persists the mutations to the backend. Save() error } @@ -179,3 +205,67 @@ type WidgetSerializationBackend interface { // SerializeWorkflowActivity converts a domain WorkflowActivity to storage format. SerializeWorkflowActivity(a workflows.WorkflowActivity) (any, error) } + +// WidgetObjectBuilder provides BSON-free operations on a loaded pluggable widget template. +// The executor calls these methods with domain-typed values; the backend handles +// all storage-specific manipulation internally. +// +// Workflow: LoadTemplate → apply operations → EnsureRequiredObjectLists → Finalize +type WidgetObjectBuilder interface { + // --- Property operations --- + // Each operation finds the property by key (via TypePointer matching) and updates its value. + + SetAttribute(propertyKey string, attributePath string) + SetAssociation(propertyKey string, assocPath string, entityName string) + SetPrimitive(propertyKey string, value string) + SetSelection(propertyKey string, value string) + SetExpression(propertyKey string, value string) + SetDataSource(propertyKey string, ds pages.DataSource) + SetChildWidgets(propertyKey string, children []pages.Widget) + SetTextTemplate(propertyKey string, text string) + SetTextTemplateWithParams(propertyKey string, text string, entityContext string) + SetAction(propertyKey string, action pages.ClientAction) + SetAttributeObjects(propertyKey string, attributePaths []string) + + // --- Template metadata --- + + // PropertyTypeIDs returns the property type metadata for the loaded template. + PropertyTypeIDs() map[string]pages.PropertyTypeIDEntry + + // --- Object list defaults --- + + // EnsureRequiredObjectLists auto-populates required empty object lists. + EnsureRequiredObjectLists() + + // --- Gallery-specific --- + + // CloneGallerySelectionProperty clones the itemSelection property with a new Selection value. + CloneGallerySelectionProperty(propertyKey string, selectionMode string) + + // --- Finalize --- + + // Finalize builds the CustomWidget from the mutated template. + // Returns the widget with RawType/RawObject set from the internal BSON state. + Finalize(id model.ID, name string, label string, editable string) *pages.CustomWidget +} + +// WidgetBuilderBackend provides pluggable widget construction capabilities. +type WidgetBuilderBackend interface { + // LoadWidgetTemplate loads a widget template by ID and returns a builder + // for applying property operations. projectPath is used for runtime template + // augmentation from .mpk files. + LoadWidgetTemplate(widgetID string, projectPath string) (WidgetObjectBuilder, error) + + // SerializeWidgetToOpaque converts a domain Widget to an opaque form + // suitable for passing to WidgetObjectBuilder.SetChildWidgets. + // This replaces the direct mpr.SerializeWidget call. + SerializeWidgetToOpaque(w pages.Widget) any + + // SerializeDataSourceToOpaque converts a domain DataSource to an opaque + // form suitable for embedding in widget property BSON. + SerializeDataSourceToOpaque(ds pages.DataSource) any + + // BuildCreateAttributeObject creates an attribute object for filter widgets. + // Returns an opaque value to be collected into attribute object lists. + BuildCreateAttributeObject(attributePath string, objectTypeID, propertyTypeID, valueTypeID string) (any, error) +} diff --git a/mdl/executor/bson_helpers.go b/mdl/executor/bson_helpers.go new file mode 100644 index 00000000..eafed38a --- /dev/null +++ b/mdl/executor/bson_helpers.go @@ -0,0 +1,481 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "fmt" + "strings" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + + "github.com/mendixlabs/mxcli/mdl/bsonutil" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/types" +) + +// ============================================================================ +// bson.D helper functions for ordered document access +// ============================================================================ + +// dGet returns the value for a key in a bson.D, or nil if not found. +func dGet(doc bson.D, key string) any { + for _, elem := range doc { + if elem.Key == key { + return elem.Value + } + } + return nil +} + +// dGetDoc returns a nested bson.D field value, or nil. +func dGetDoc(doc bson.D, key string) bson.D { + v := dGet(doc, key) + if d, ok := v.(bson.D); ok { + return d + } + return nil +} + +// dGetString returns a string field value, or "". +func dGetString(doc bson.D, key string) string { + v := dGet(doc, key) + if s, ok := v.(string); ok { + return s + } + return "" +} + +// dSet sets a field value in a bson.D in place. If the key exists, it's updated +// and returns true. If the key is not found, returns false. +func dSet(doc bson.D, key string, value any) bool { + for i := range doc { + if doc[i].Key == key { + doc[i].Value = value + return true + } + } + return false +} + +// dGetArrayElements extracts Mendix array elements from a bson.D field value. +// Handles the int32 type marker at index 0. +func dGetArrayElements(val any) []any { + arr := toBsonA(val) + if len(arr) == 0 { + return nil + } + if _, ok := arr[0].(int32); ok { + return arr[1:] + } + if _, ok := arr[0].(int); ok { + return arr[1:] + } + return arr +} + +// toBsonA converts various BSON array types to []any. +func toBsonA(v any) []any { + switch arr := v.(type) { + case bson.A: + return []any(arr) + case []any: + return arr + default: + return nil + } +} + +// dSetArray sets a Mendix-style BSON array field, preserving the int32 marker. +func dSetArray(doc bson.D, key string, elements []any) { + existing := toBsonA(dGet(doc, key)) + var marker any + if len(existing) > 0 { + if _, ok := existing[0].(int32); ok { + marker = existing[0] + } else if _, ok := existing[0].(int); ok { + marker = existing[0] + } + } + var result bson.A + if marker != nil { + result = make(bson.A, 0, len(elements)+1) + result = append(result, marker) + result = append(result, elements...) + } else { + result = make(bson.A, len(elements)) + copy(result, elements) + } + dSet(doc, key, result) +} + +// extractBinaryIDFromDoc extracts a binary ID string from a bson.D field. +func extractBinaryIDFromDoc(val any) string { + if bin, ok := val.(primitive.Binary); ok { + return types.BlobToUUID(bin.Data) + } + return "" +} + +// ============================================================================ +// BSON widget tree walking (used by cmd_widgets.go) +// ============================================================================ + +// bsonWidgetResult holds a found widget and its parent context. +type bsonWidgetResult struct { + widget bson.D + parentArr []any + parentKey string + parentDoc bson.D + index int + colPropKeys map[string]string +} + +// widgetFinder is a function type for locating widgets in a raw BSON tree. +type widgetFinder func(rawData bson.D, widgetName string) *bsonWidgetResult + +// findBsonWidget searches the raw BSON page tree for a widget by name. +func findBsonWidget(rawData bson.D, widgetName string) *bsonWidgetResult { + formCall := dGetDoc(rawData, "FormCall") + if formCall == nil { + return nil + } + args := dGetArrayElements(dGet(formCall, "Arguments")) + for _, arg := range args { + argDoc, ok := arg.(bson.D) + if !ok { + continue + } + if result := findInWidgetArray(argDoc, "Widgets", widgetName); result != nil { + return result + } + } + return nil +} + +// findBsonWidgetInSnippet searches the raw BSON snippet tree for a widget by name. +func findBsonWidgetInSnippet(rawData bson.D, widgetName string) *bsonWidgetResult { + if result := findInWidgetArray(rawData, "Widgets", widgetName); result != nil { + return result + } + if widgetContainer := dGetDoc(rawData, "Widget"); widgetContainer != nil { + if result := findInWidgetArray(widgetContainer, "Widgets", widgetName); result != nil { + return result + } + } + return nil +} + +// findInWidgetArray searches a widget array (by key in parentDoc) for a named widget. +func findInWidgetArray(parentDoc bson.D, key string, widgetName string) *bsonWidgetResult { + elements := dGetArrayElements(dGet(parentDoc, key)) + for i, elem := range elements { + wDoc, ok := elem.(bson.D) + if !ok { + continue + } + if dGetString(wDoc, "Name") == widgetName { + return &bsonWidgetResult{ + widget: wDoc, + parentArr: elements, + parentKey: key, + parentDoc: parentDoc, + index: i, + } + } + if result := findInWidgetChildren(wDoc, widgetName); result != nil { + return result + } + } + return nil +} + +// findInWidgetChildren recursively searches widget children for a named widget. +func findInWidgetChildren(wDoc bson.D, widgetName string) *bsonWidgetResult { + typeName := dGetString(wDoc, "$Type") + + if result := findInWidgetArray(wDoc, "Widgets", widgetName); result != nil { + return result + } + if result := findInWidgetArray(wDoc, "FooterWidgets", widgetName); result != nil { + return result + } + + // LayoutGrid: Rows[].Columns[].Widgets[] + if strings.Contains(typeName, "LayoutGrid") { + rows := dGetArrayElements(dGet(wDoc, "Rows")) + for _, row := range rows { + rowDoc, ok := row.(bson.D) + if !ok { + continue + } + cols := dGetArrayElements(dGet(rowDoc, "Columns")) + for _, col := range cols { + colDoc, ok := col.(bson.D) + if !ok { + continue + } + if result := findInWidgetArray(colDoc, "Widgets", widgetName); result != nil { + return result + } + } + } + } + + // TabContainer: TabPages[].Widgets[] + tabPages := dGetArrayElements(dGet(wDoc, "TabPages")) + for _, tp := range tabPages { + tpDoc, ok := tp.(bson.D) + if !ok { + continue + } + if result := findInWidgetArray(tpDoc, "Widgets", widgetName); result != nil { + return result + } + } + + // ControlBar + if controlBar := dGetDoc(wDoc, "ControlBar"); controlBar != nil { + if result := findInWidgetArray(controlBar, "Items", widgetName); result != nil { + return result + } + } + + // CustomWidget (pluggable): Object.Properties[].Value.Widgets[] + if strings.Contains(typeName, "CustomWidget") { + if obj := dGetDoc(wDoc, "Object"); obj != nil { + props := dGetArrayElements(dGet(obj, "Properties")) + for _, prop := range props { + propDoc, ok := prop.(bson.D) + if !ok { + continue + } + if valDoc := dGetDoc(propDoc, "Value"); valDoc != nil { + if result := findInWidgetArray(valDoc, "Widgets", widgetName); result != nil { + return result + } + } + } + } + } + + return nil +} + +// setTranslatableText sets a translatable text value in BSON. +func setTranslatableText(parent bson.D, key string, value interface{}) { + strVal, ok := value.(string) + if !ok { + return + } + + target := parent + if key != "" { + if nested := dGetDoc(parent, key); nested != nil { + target = nested + } else { + dSet(parent, key, strVal) + return + } + } + + translations := dGetArrayElements(dGet(target, "Translations")) + if len(translations) > 0 { + if tDoc, ok := translations[0].(bson.D); ok { + dSet(tDoc, "Text", strVal) + return + } + } + dSet(target, "Text", strVal) +} + +// ============================================================================ +// Widget property setting (used by cmd_widgets.go) +// ============================================================================ + +// setRawWidgetProperty sets a property on a raw BSON widget document. +func setRawWidgetProperty(widget bson.D, propName string, value interface{}) error { + switch propName { + case "Caption": + return setWidgetCaption(widget, value) + case "Content": + return setWidgetContent(widget, value) + case "Label": + return setWidgetLabel(widget, value) + case "ButtonStyle": + if s, ok := value.(string); ok { + dSet(widget, "ButtonStyle", s) + } + return nil + case "Class": + if appearance := dGetDoc(widget, "Appearance"); appearance != nil { + if s, ok := value.(string); ok { + dSet(appearance, "Class", s) + } + } + return nil + case "Style": + if appearance := dGetDoc(widget, "Appearance"); appearance != nil { + if s, ok := value.(string); ok { + dSet(appearance, "Style", s) + } + } + return nil + case "Editable": + if s, ok := value.(string); ok { + dSet(widget, "Editable", s) + } + return nil + case "Visible": + if s, ok := value.(string); ok { + dSet(widget, "Visible", s) + } else if b, ok := value.(bool); ok { + if b { + dSet(widget, "Visible", "True") + } else { + dSet(widget, "Visible", "False") + } + } + return nil + case "Name": + if s, ok := value.(string); ok { + dSet(widget, "Name", s) + } + return nil + case "Attribute": + return setWidgetAttributeRef(widget, value) + default: + return setPluggableWidgetProperty(widget, propName, value) + } +} + +func setWidgetCaption(widget bson.D, value interface{}) error { + caption := dGetDoc(widget, "Caption") + if caption == nil { + setTranslatableText(widget, "Caption", value) + return nil + } + setTranslatableText(caption, "", value) + return nil +} + +func setWidgetContent(widget bson.D, value interface{}) error { + strVal, ok := value.(string) + if !ok { + return mdlerrors.NewValidation("Content value must be a string") + } + content := dGetDoc(widget, "Content") + if content == nil { + return mdlerrors.NewValidation("widget has no Content property") + } + template := dGetDoc(content, "Template") + if template == nil { + return mdlerrors.NewValidation("Content has no Template") + } + items := dGetArrayElements(dGet(template, "Items")) + if len(items) > 0 { + if itemDoc, ok := items[0].(bson.D); ok { + dSet(itemDoc, "Text", strVal) + return nil + } + } + return mdlerrors.NewValidation("Content.Template has no Items with Text") +} + +func setWidgetLabel(widget bson.D, value interface{}) error { + label := dGetDoc(widget, "Label") + if label == nil { + return nil + } + setTranslatableText(label, "Caption", value) + return nil +} + +func setWidgetAttributeRef(widget bson.D, value interface{}) error { + attrPath, ok := value.(string) + if !ok { + return mdlerrors.NewValidation("Attribute value must be a string") + } + + var attrRefValue interface{} + if strings.Count(attrPath, ".") >= 2 { + attrRefValue = bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "DomainModels$AttributeRef"}, + {Key: "Attribute", Value: attrPath}, + {Key: "EntityRef", Value: nil}, + } + } else { + attrRefValue = nil + } + + for i, elem := range widget { + if elem.Key == "AttributeRef" { + widget[i].Value = attrRefValue + return nil + } + } + return mdlerrors.NewValidation("widget does not have an AttributeRef property; Attribute can only be SET on input widgets (TextBox, TextArea, DatePicker, etc.)") +} + +func setPluggableWidgetProperty(widget bson.D, propName string, value interface{}) error { + obj := dGetDoc(widget, "Object") + if obj == nil { + return mdlerrors.NewNotFoundMsg("property", propName, fmt.Sprintf("property %q not found (widget has no pluggable Object)", propName)) + } + + propTypeKeyMap := make(map[string]string) + if widgetType := dGetDoc(widget, "Type"); widgetType != nil { + if objType := dGetDoc(widgetType, "ObjectType"); objType != nil { + propTypes := dGetArrayElements(dGet(objType, "PropertyTypes")) + for _, pt := range propTypes { + ptDoc, ok := pt.(bson.D) + if !ok { + continue + } + key := dGetString(ptDoc, "PropertyKey") + if key == "" { + continue + } + id := extractBinaryIDFromDoc(dGet(ptDoc, "$ID")) + if id != "" { + propTypeKeyMap[id] = key + } + } + } + } + + props := dGetArrayElements(dGet(obj, "Properties")) + for _, prop := range props { + propDoc, ok := prop.(bson.D) + if !ok { + continue + } + typePointerID := extractBinaryIDFromDoc(dGet(propDoc, "TypePointer")) + propKey := propTypeKeyMap[typePointerID] + if propKey != propName { + continue + } + if valDoc := dGetDoc(propDoc, "Value"); valDoc != nil { + switch v := value.(type) { + case string: + dSet(valDoc, "PrimitiveValue", v) + case bool: + if v { + dSet(valDoc, "PrimitiveValue", "yes") + } else { + dSet(valDoc, "PrimitiveValue", "no") + } + case int: + dSet(valDoc, "PrimitiveValue", fmt.Sprintf("%d", v)) + case float64: + dSet(valDoc, "PrimitiveValue", fmt.Sprintf("%g", v)) + default: + dSet(valDoc, "PrimitiveValue", fmt.Sprintf("%v", v)) + } + return nil + } + return mdlerrors.NewValidation(fmt.Sprintf("property %q has no Value map", propName)) + } + return mdlerrors.NewNotFound("pluggable property", propName) +} diff --git a/mdl/executor/cmd_alter_page.go b/mdl/executor/cmd_alter_page.go index cfb5b04b..b05faa28 100644 --- a/mdl/executor/cmd_alter_page.go +++ b/mdl/executor/cmd_alter_page.go @@ -6,15 +6,11 @@ import ( "fmt" "strings" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" - "github.com/mendixlabs/mxcli/mdl/ast" - "github.com/mendixlabs/mxcli/mdl/bsonutil" + "github.com/mendixlabs/mxcli/mdl/backend" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" - "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/sdk/pages" ) // execAlterPage handles ALTER PAGE/SNIPPET Module.Name { operations }. @@ -54,57 +50,47 @@ func execAlterPage(ctx *ExecContext, s *ast.AlterPageStmt) error { containerID = h.FindModuleID(page.ContainerID) } - // Load raw BSON as ordered document (bson.D preserves field ordering, - // which is required by Mendix Studio Pro). - rawBytes, err := ctx.Backend.GetRawUnitBytes(unitID) + // Open the page for mutation via the backend + mutator, err := ctx.Backend.OpenPageForMutation(unitID) if err != nil { - return mdlerrors.NewBackend("load raw "+strings.ToLower(containerType)+" data", err) - } - var rawData bson.D - if err := bson.Unmarshal(rawBytes, &rawData); err != nil { - return mdlerrors.NewBackend("unmarshal "+strings.ToLower(containerType)+" BSON", err) + return mdlerrors.NewBackend("open "+strings.ToLower(containerType)+" for mutation", err) } // Resolve module name for building new widgets modName := h.GetModuleName(containerID) - // Apply operations sequentially using the appropriate BSON finder - findWidget := findBsonWidget // page default - if containerType == "SNIPPET" { - findWidget = findBsonWidgetInSnippet - } - for _, op := range s.Operations { switch o := op.(type) { case *ast.SetPropertyOp: - if err := applySetPropertyWith(rawData, o, findWidget); err != nil { + if err := applySetPropertyMutator(mutator, o); err != nil { return mdlerrors.NewBackend("SET", err) } case *ast.InsertWidgetOp: - if err := applyInsertWidgetWith(ctx, rawData, o, modName, containerID, findWidget); err != nil { + if err := applyInsertWidgetMutator(ctx, mutator, o, modName, containerID); err != nil { return mdlerrors.NewBackend("INSERT", err) } case *ast.DropWidgetOp: - if err := applyDropWidgetWith(rawData, o, findWidget); err != nil { + if err := applyDropWidgetMutator(mutator, o); err != nil { return mdlerrors.NewBackend("DROP", err) } case *ast.ReplaceWidgetOp: - if err := applyReplaceWidgetWith(ctx, rawData, o, modName, containerID, findWidget); err != nil { + if err := applyReplaceWidgetMutator(ctx, mutator, o, modName, containerID); err != nil { return mdlerrors.NewBackend("REPLACE", err) } case *ast.AddVariableOp: - if err := applyAddVariable(&rawData, o); err != nil { + if err := mutator.AddVariable(o.Variable.Name, o.Variable.DataType, o.Variable.DefaultValue); err != nil { return mdlerrors.NewBackend("ADD VARIABLE", err) } case *ast.DropVariableOp: - if err := applyDropVariable(rawData, o); err != nil { + if err := mutator.DropVariable(o.VariableName); err != nil { return mdlerrors.NewBackend("DROP VARIABLE", err) } case *ast.SetLayoutOp: if containerType == "SNIPPET" { return mdlerrors.NewUnsupported("SET Layout is not supported for snippets") } - if err := applySetLayout(rawData, o); err != nil { + newLayoutQN := o.NewLayout.Module + "." + o.NewLayout.Name + if err := mutator.SetLayout(newLayoutQN, o.Mappings); err != nil { return mdlerrors.NewBackend("SET Layout", err) } default: @@ -112,14 +98,8 @@ func execAlterPage(ctx *ExecContext, s *ast.AlterPageStmt) error { } } - // Marshal back to BSON bytes (bson.D preserves field ordering) - outBytes, err := bson.Marshal(rawData) - if err != nil { - return mdlerrors.NewBackend("marshal modified "+strings.ToLower(containerType), err) - } - - // Save - if err := ctx.Backend.UpdateRawUnit(string(unitID), outBytes); err != nil { + // Persist + if err := mutator.Save(); err != nil { return mdlerrors.NewBackend("save modified "+strings.ToLower(containerType), err) } @@ -127,1417 +107,125 @@ func execAlterPage(ctx *ExecContext, s *ast.AlterPageStmt) error { return nil } -// applySetLayout rewrites the FormCall to reference a new layout. -// It updates the Form field and remaps Parameter strings in each FormCallArgument. -func applySetLayout(rawData bson.D, op *ast.SetLayoutOp) error { - newLayoutQN := op.NewLayout.Module + "." + op.NewLayout.Name - - // Find FormCall in the page BSON - var formCall bson.D - for _, elem := range rawData { - if elem.Key == "FormCall" { - if doc, ok := elem.Value.(bson.D); ok { - formCall = doc - } - break - } - } - if formCall == nil { - return mdlerrors.NewValidation("page has no FormCall (layout reference)") - } - - // Detect the old layout name from existing Parameter values - oldLayoutQN := "" - for _, elem := range formCall { - if elem.Key == "Form" { - if s, ok := elem.Value.(string); ok && s != "" { - oldLayoutQN = s - } - } - if elem.Key == "Arguments" { - if arr, ok := elem.Value.(bson.A); ok { - for _, item := range arr { - if doc, ok := item.(bson.D); ok { - for _, field := range doc { - if field.Key == "Parameter" { - if s, ok := field.Value.(string); ok && oldLayoutQN == "" { - // Extract layout QN from "Atlas_Core.Atlas_TopBar.Main" - if lastDot := strings.LastIndex(s, "."); lastDot > 0 { - oldLayoutQN = s[:lastDot] - } - } - } - } - } - } - } - } - } - - if oldLayoutQN == "" { - return mdlerrors.NewValidation("cannot determine current layout from FormCall") - } - - if oldLayoutQN == newLayoutQN { - return nil // Already using the target layout - } - - // Update Form field - for i, elem := range formCall { - if elem.Key == "Form" { - formCall[i].Value = newLayoutQN - } - } - - // If Form field doesn't exist, add it - hasForm := false - for _, elem := range formCall { - if elem.Key == "Form" { - hasForm = true - break - } - } - if !hasForm { - // Insert before Arguments - for i, elem := range formCall { - if elem.Key == "Arguments" { - formCall = append(formCall[:i+1], formCall[i:]...) - formCall[i] = bson.E{Key: "Form", Value: newLayoutQN} - break - } - } - } - - // Remap Parameter strings in each FormCallArgument - for _, elem := range formCall { - if elem.Key != "Arguments" { - continue - } - arr, ok := elem.Value.(bson.A) - if !ok { - continue - } - for _, item := range arr { - doc, ok := item.(bson.D) - if !ok { - continue - } - for j, field := range doc { - if field.Key != "Parameter" { - continue - } - paramStr, ok := field.Value.(string) - if !ok { - continue - } - // Extract placeholder name: "Atlas_Core.Atlas_Default.Main" -> "Main" - placeholder := paramStr - if strings.HasPrefix(paramStr, oldLayoutQN+".") { - placeholder = paramStr[len(oldLayoutQN)+1:] - } - - // Apply explicit mapping if provided - if op.Mappings != nil { - if mapped, ok := op.Mappings[placeholder]; ok { - placeholder = mapped - } - } - - // Write new parameter value - doc[j].Value = newLayoutQN + "." + placeholder - } - } - } - - // Write FormCall back into rawData - for i, elem := range rawData { - if elem.Key == "FormCall" { - rawData[i].Value = formCall - break - } - } - - return nil -} - -// ============================================================================ -// bson.D helper functions for ordered document access -// ============================================================================ - -// dGet returns the value for a key in a bson.D, or nil if not found. -func dGet(doc bson.D, key string) any { - for _, elem := range doc { - if elem.Key == key { - return elem.Value - } - } - return nil -} - -// dGetDoc returns a nested bson.D field value, or nil. -func dGetDoc(doc bson.D, key string) bson.D { - v := dGet(doc, key) - if d, ok := v.(bson.D); ok { - return d - } - return nil -} - -// dGetString returns a string field value, or "". -func dGetString(doc bson.D, key string) string { - v := dGet(doc, key) - if s, ok := v.(string); ok { - return s - } - return "" -} - -// dSet sets a field value in a bson.D in place. If the key exists, it's updated -// and returns true. If the key is not found, returns false. -func dSet(doc bson.D, key string, value any) bool { - for i := range doc { - if doc[i].Key == key { - doc[i].Value = value - return true - } - } - return false -} - -// dGetArrayElements extracts Mendix array elements from a bson.D field value. -// Handles the int32 type marker at index 0. Works with bson.A and []any. -func dGetArrayElements(val any) []any { - arr := toBsonA(val) - if len(arr) == 0 { - return nil - } - // Skip type marker (int32) at index 0 - if _, ok := arr[0].(int32); ok { - return arr[1:] - } - if _, ok := arr[0].(int); ok { - return arr[1:] - } - return arr -} - -// toBsonA converts various BSON array types to []any. -func toBsonA(v any) []any { - switch arr := v.(type) { - case bson.A: - return []any(arr) - case []any: - return arr - default: - return nil - } -} - -// dSetArray sets a Mendix-style BSON array field, preserving the int32 marker. -func dSetArray(doc bson.D, key string, elements []any) { - existing := toBsonA(dGet(doc, key)) - var marker any - if len(existing) > 0 { - if _, ok := existing[0].(int32); ok { - marker = existing[0] - } else if _, ok := existing[0].(int); ok { - marker = existing[0] - } - } - var result bson.A - if marker != nil { - result = make(bson.A, 0, len(elements)+1) - result = append(result, marker) - result = append(result, elements...) - } else { - result = make(bson.A, len(elements)) - copy(result, elements) - } - dSet(doc, key, result) -} - -// extractBinaryIDFromDoc extracts a binary ID string from a bson.D field. -func extractBinaryIDFromDoc(val any) string { - if bin, ok := val.(primitive.Binary); ok { - return types.BlobToUUID(bin.Data) - } - return "" -} - -// ============================================================================ -// BSON widget tree walking -// ============================================================================ - -// bsonWidgetResult holds a found widget and its parent context. -type bsonWidgetResult struct { - widget bson.D // the widget document itself - parentArr []any // the parent array elements (without marker) - parentKey string // key in the parent doc that holds this array - parentDoc bson.D // the doc containing parentKey - index int // index in parentArr - colPropKeys map[string]string // column property TypePointer → key map (only set for column results) -} - -// widgetFinder is a function type for locating widgets in a raw BSON tree. -type widgetFinder func(rawData bson.D, widgetName string) *bsonWidgetResult - -// findBsonWidget searches the raw BSON page tree for a widget by name. -// Page format: FormCall.Arguments[].Widgets[] -func findBsonWidget(rawData bson.D, widgetName string) *bsonWidgetResult { - formCall := dGetDoc(rawData, "FormCall") - if formCall == nil { - return nil - } - - args := dGetArrayElements(dGet(formCall, "Arguments")) - for _, arg := range args { - argDoc, ok := arg.(bson.D) - if !ok { - continue - } - if result := findInWidgetArray(argDoc, "Widgets", widgetName); result != nil { - return result - } - } - return nil -} - -// findBsonWidgetInSnippet searches the raw BSON snippet tree for a widget by name. -// Snippet format: Widgets[] (Studio Pro) or Widget.Widgets[] (mxcli). -func findBsonWidgetInSnippet(rawData bson.D, widgetName string) *bsonWidgetResult { - // Studio Pro format: top-level "Widgets" array - if result := findInWidgetArray(rawData, "Widgets", widgetName); result != nil { - return result - } - // mxcli format: "Widget" (singular) container with "Widgets" inside - if widgetContainer := dGetDoc(rawData, "Widget"); widgetContainer != nil { - if result := findInWidgetArray(widgetContainer, "Widgets", widgetName); result != nil { - return result - } - } - return nil -} - -// findInWidgetArray searches a widget array (by key in parentDoc) for a named widget. -func findInWidgetArray(parentDoc bson.D, key string, widgetName string) *bsonWidgetResult { - elements := dGetArrayElements(dGet(parentDoc, key)) - for i, elem := range elements { - wDoc, ok := elem.(bson.D) - if !ok { - continue - } - if dGetString(wDoc, "Name") == widgetName { - return &bsonWidgetResult{ - widget: wDoc, - parentArr: elements, - parentKey: key, - parentDoc: parentDoc, - index: i, - } - } - // Recurse into children - if result := findInWidgetChildren(wDoc, widgetName); result != nil { - return result - } - } - return nil -} - -// findInWidgetChildren recursively searches widget children for a named widget. -func findInWidgetChildren(wDoc bson.D, widgetName string) *bsonWidgetResult { - typeName := dGetString(wDoc, "$Type") - - // Direct Widgets[] children (Container, DataView body, TabPage, GroupBox, etc.) - if result := findInWidgetArray(wDoc, "Widgets", widgetName); result != nil { - return result - } - - // FooterWidgets[] (DataView footer) - if result := findInWidgetArray(wDoc, "FooterWidgets", widgetName); result != nil { - return result - } - - // LayoutGrid: Rows[].Columns[].Widgets[] - if strings.Contains(typeName, "LayoutGrid") { - rows := dGetArrayElements(dGet(wDoc, "Rows")) - for _, row := range rows { - rowDoc, ok := row.(bson.D) - if !ok { - continue - } - cols := dGetArrayElements(dGet(rowDoc, "Columns")) - for _, col := range cols { - colDoc, ok := col.(bson.D) - if !ok { - continue - } - if result := findInWidgetArray(colDoc, "Widgets", widgetName); result != nil { - return result - } - } - } - } - - // TabContainer: TabPages[].Widgets[] - if result := findInTabPages(wDoc, widgetName); result != nil { - return result - } - - // ControlBar widgets - if result := findInControlBar(wDoc, widgetName); result != nil { - return result - } - - // CustomWidget (pluggable): Object.Properties[].Value.Widgets[] - if strings.Contains(typeName, "CustomWidget") { - if obj := dGetDoc(wDoc, "Object"); obj != nil { - props := dGetArrayElements(dGet(obj, "Properties")) - for _, prop := range props { - propDoc, ok := prop.(bson.D) - if !ok { - continue - } - if valDoc := dGetDoc(propDoc, "Value"); valDoc != nil { - if result := findInWidgetArray(valDoc, "Widgets", widgetName); result != nil { - return result - } - } - } - } - } - - return nil -} - -// findInTabPages searches TabPages[].Widgets[] for a named widget. -func findInTabPages(wDoc bson.D, widgetName string) *bsonWidgetResult { - tabPages := dGetArrayElements(dGet(wDoc, "TabPages")) - for _, tp := range tabPages { - tpDoc, ok := tp.(bson.D) - if !ok { - continue - } - if result := findInWidgetArray(tpDoc, "Widgets", widgetName); result != nil { - return result - } - } - return nil -} - -// findInControlBar searches ControlBarItems within a ControlBar for a named widget. -func findInControlBar(wDoc bson.D, widgetName string) *bsonWidgetResult { - controlBar := dGetDoc(wDoc, "ControlBar") - if controlBar == nil { - return nil - } - return findInWidgetArray(controlBar, "Items", widgetName) -} - // ============================================================================ -// DataGrid2 column finder +// SET property via mutator // ============================================================================ -// findBsonColumn finds a column inside a DataGrid2 widget by derived name. -// It locates the grid widget first, then searches its columns Objects[] array. -// Returns a bsonWidgetResult where parentArr/parentDoc/parentKey point to the -// columns array, so INSERT/DROP/REPLACE work via standard array manipulation. -func findBsonColumn(rawData bson.D, gridName, columnName string, find widgetFinder) *bsonWidgetResult { - // Find the DataGrid2 widget - gridResult := find(rawData, gridName) - if gridResult == nil { - return nil - } - - // Build grid-level PropertyTypeID -> key map - gridPropKeyMap := buildPropKeyMap(gridResult.widget) - - // Navigate to the "columns" property's Value.Objects[] - obj := dGetDoc(gridResult.widget, "Object") - if obj == nil { - return nil - } - - props := dGetArrayElements(dGet(obj, "Properties")) - for _, prop := range props { - propDoc, ok := prop.(bson.D) - if !ok { - continue - } - typePointerID := extractBinaryIDFromDoc(dGet(propDoc, "TypePointer")) - propKey := gridPropKeyMap[typePointerID] - if propKey != "columns" { - continue - } - - valDoc := dGetDoc(propDoc, "Value") - if valDoc == nil { - return nil - } - - // Build column-level PropertyTypeID -> key map for name derivation - colPropKeyMap := buildColumnPropKeyMap(gridResult.widget, typePointerID) - - // Search columns by derived name - columns := dGetArrayElements(dGet(valDoc, "Objects")) - for i, colItem := range columns { - colDoc, ok := colItem.(bson.D) - if !ok { - continue - } - derived := deriveColumnNameBson(colDoc, colPropKeyMap, i) - if derived == columnName { - return &bsonWidgetResult{ - widget: colDoc, - parentArr: columns, - parentKey: "Objects", - parentDoc: valDoc, - index: i, - colPropKeys: colPropKeyMap, - } - } - } - return nil // found columns property but no matching column - } - return nil -} - -// buildPropKeyMap builds a TypePointer ID -> PropertyKey map from a widget's -// Type.ObjectType.PropertyTypes array. -func buildPropKeyMap(widgetDoc bson.D) map[string]string { - m := make(map[string]string) - widgetType := dGetDoc(widgetDoc, "Type") - if widgetType == nil { - return m - } - objType := dGetDoc(widgetType, "ObjectType") - if objType == nil { - return m - } - for _, pt := range dGetArrayElements(dGet(objType, "PropertyTypes")) { - ptDoc, ok := pt.(bson.D) - if !ok { - continue - } - key := dGetString(ptDoc, "PropertyKey") - id := extractBinaryIDFromDoc(dGet(ptDoc, "$ID")) - if key != "" && id != "" { - m[id] = key - } - } - return m -} - -// buildColumnPropKeyMap builds a TypePointer ID -> PropertyKey map for column -// properties. It navigates: Type.ObjectType.PropertyTypes["columns"].ValueType.ObjectType.PropertyTypes -func buildColumnPropKeyMap(widgetDoc bson.D, columnsTypePointerID string) map[string]string { - m := make(map[string]string) - widgetType := dGetDoc(widgetDoc, "Type") - if widgetType == nil { - return m - } - objType := dGetDoc(widgetType, "ObjectType") - if objType == nil { - return m - } - // Find the columns PropertyType entry - for _, pt := range dGetArrayElements(dGet(objType, "PropertyTypes")) { - ptDoc, ok := pt.(bson.D) - if !ok { - continue - } - id := extractBinaryIDFromDoc(dGet(ptDoc, "$ID")) - if id != columnsTypePointerID { - continue - } - // Navigate to ValueType.ObjectType.PropertyTypes - valType := dGetDoc(ptDoc, "ValueType") - if valType == nil { - return m - } - colObjType := dGetDoc(valType, "ObjectType") - if colObjType == nil { - return m - } - for _, cpt := range dGetArrayElements(dGet(colObjType, "PropertyTypes")) { - cptDoc, ok := cpt.(bson.D) - if !ok { - continue - } - key := dGetString(cptDoc, "PropertyKey") - cid := extractBinaryIDFromDoc(dGet(cptDoc, "$ID")) - if key != "" && cid != "" { - m[cid] = key - } - } - return m - } - return m -} - -// deriveColumnNameBson derives a column name from its BSON WidgetObject, -// matching the logic in deriveColumnName() in cmd_pages_describe_output.go. -func deriveColumnNameBson(colDoc bson.D, propKeyMap map[string]string, index int) string { - var attribute, caption string - - props := dGetArrayElements(dGet(colDoc, "Properties")) - for _, prop := range props { - propDoc, ok := prop.(bson.D) - if !ok { - continue - } - typePointerID := extractBinaryIDFromDoc(dGet(propDoc, "TypePointer")) - propKey := propKeyMap[typePointerID] - - valDoc := dGetDoc(propDoc, "Value") - if valDoc == nil { - continue - } - - switch propKey { - case "attribute": - // Extract attribute path from AttributeRef - if attrRef := dGetString(valDoc, "AttributeRef"); attrRef != "" { - attribute = attrRef - } else if attrDoc := dGetDoc(valDoc, "AttributeRef"); attrDoc != nil { - attribute = dGetString(attrDoc, "Attribute") - } - case "header": - // Extract caption from TextTemplate - if tmpl := dGetDoc(valDoc, "TextTemplate"); tmpl != nil { - items := dGetArrayElements(dGet(tmpl, "Items")) - for _, item := range items { - if itemDoc, ok := item.(bson.D); ok { - if text := dGetString(itemDoc, "Text"); text != "" { - caption = text - } - } - } - } - } - } - - // Apply same derivation logic as deriveColumnName - if attribute != "" { - parts := strings.Split(attribute, ".") - return parts[len(parts)-1] - } - if caption != "" { - return sanitizeColumnName(caption) - } - return fmt.Sprintf("col%d", index+1) -} - -// sanitizeColumnName converts a caption string into a valid column identifier. -func sanitizeColumnName(caption string) string { - var result []rune - for _, r := range caption { - if (r >= 'a' && r <= 'z') || (r >= 'A' && r <= 'Z') || (r >= '0' && r <= '9') || r == '_' { - result = append(result, r) - } else { - result = append(result, '_') - } - } - return string(result) -} - -// columnPropertyAliases maps user-facing property names to internal column property keys. -var columnPropertyAliases = map[string]string{ - "Caption": "header", - "Attribute": "attribute", - "Visible": "visible", - "Alignment": "alignment", - "WrapText": "wrapText", - "Sortable": "sortable", - "Resizable": "resizable", - "Draggable": "draggable", - "Hidable": "hidable", - "ColumnWidth": "width", - "Size": "size", - "ShowContentAs": "showContentAs", - "ColumnClass": "columnClass", - "Tooltip": "tooltip", -} - -// setColumnProperty sets a property on a DataGrid2 column WidgetObject. -// propKeyMap maps TypePointer IDs to property keys (from the parent grid's column type). -func setColumnProperty(colDoc bson.D, propKeyMap map[string]string, propName string, value interface{}) error { - // Map user-facing name to internal property key - internalKey := columnPropertyAliases[propName] - if internalKey == "" { - internalKey = propName - } - - // Search column Properties[] for matching property and update - props := dGetArrayElements(dGet(colDoc, "Properties")) - for _, prop := range props { - propDoc, ok := prop.(bson.D) - if !ok { - continue - } - typePointerID := extractBinaryIDFromDoc(dGet(propDoc, "TypePointer")) - propKey := propKeyMap[typePointerID] - if propKey != internalKey { - continue - } - if valDoc := dGetDoc(propDoc, "Value"); valDoc != nil { - strVal := fmt.Sprintf("%v", value) - dSet(valDoc, "PrimitiveValue", strVal) - return nil - } - return mdlerrors.NewValidation(fmt.Sprintf("column property %q has no Value", propName)) - } - return mdlerrors.NewNotFound("column property", propName) -} - -// ============================================================================ -// SET property -// ============================================================================ - -// applySetProperty modifies widget properties in the raw BSON tree (page format). -func applySetProperty(rawData bson.D, op *ast.SetPropertyOp) error { - return applySetPropertyWith(rawData, op, findBsonWidget) -} - -// applySetPropertyWith modifies widget properties using the given widget finder. -func applySetPropertyWith(rawData bson.D, op *ast.SetPropertyOp, find widgetFinder) error { - if op.Target.Widget == "" { - // Page/snippet-level SET - return applyPageLevelSet(rawData, op.Properties) - } - - // Find the widget (or column via dotted ref) - var result *bsonWidgetResult - if op.Target.IsColumn() { - result = findBsonColumn(rawData, op.Target.Widget, op.Target.Column, find) - } else { - result = find(rawData, op.Target.Widget) - } - if result == nil { - return mdlerrors.NewNotFound("widget", op.Target.Name()) - } - - // Apply each property +func applySetPropertyMutator(mutator backend.PageMutator, op *ast.SetPropertyOp) error { for propName, value := range op.Properties { if op.Target.IsColumn() { - if err := setColumnProperty(result.widget, result.colPropKeys, propName, value); err != nil { + if err := mutator.SetColumnProperty(op.Target.Widget, op.Target.Column, propName, value); err != nil { return mdlerrors.NewBackend("set "+propName+" on "+op.Target.Name(), err) } - } else { - if err := setRawWidgetProperty(result.widget, propName, value); err != nil { - return mdlerrors.NewBackend("set "+propName+" on "+op.Target.Name(), err) - } - } - } - return nil -} - -// applyPageLevelSet handles page-level SET (e.g., SET Title = 'New Title'). -func applyPageLevelSet(rawData bson.D, properties map[string]interface{}) error { - for propName, value := range properties { - switch propName { - case "Title": - // Title is stored as FormCall.Title or at the top level - if formCall := dGetDoc(rawData, "FormCall"); formCall != nil { - setTranslatableText(formCall, "Title", value) - } else { - setTranslatableText(rawData, "Title", value) - } - case "Url": - // URL is stored as a plain string at the top level - strVal, _ := value.(string) - dSet(rawData, "Url", strVal) - default: - return mdlerrors.NewUnsupported("unsupported page-level property: " + propName) - } - } - return nil -} - -// setRawWidgetProperty sets a property on a raw BSON widget document. -func setRawWidgetProperty(widget bson.D, propName string, value interface{}) error { - // Handle known standard BSON properties - switch propName { - case "Caption": - return setWidgetCaption(widget, value) - case "Content": - return setWidgetContent(widget, value) - case "Label": - return setWidgetLabel(widget, value) - case "ButtonStyle": - if s, ok := value.(string); ok { - dSet(widget, "ButtonStyle", s) - } - return nil - case "Class": - if appearance := dGetDoc(widget, "Appearance"); appearance != nil { - if s, ok := value.(string); ok { - dSet(appearance, "Class", s) + } else if propName == "DataSource" { + // DataSource requires special handling via SetWidgetDataSource + ds, err := convertASTDataSource(value) + if err != nil { + return err } - } - return nil - case "Style": - if appearance := dGetDoc(widget, "Appearance"); appearance != nil { - if s, ok := value.(string); ok { - dSet(appearance, "Style", s) + if err := mutator.SetWidgetDataSource(op.Target.Widget, ds); err != nil { + return mdlerrors.NewBackend("set DataSource on "+op.Target.Name(), err) } - } - return nil - case "Editable": - if s, ok := value.(string); ok { - dSet(widget, "Editable", s) - } - return nil - case "Visible": - if s, ok := value.(string); ok { - dSet(widget, "Visible", s) - } else if b, ok := value.(bool); ok { - if b { - dSet(widget, "Visible", "True") - } else { - dSet(widget, "Visible", "False") + } else { + if err := mutator.SetWidgetProperty(op.Target.Widget, propName, value); err != nil { + return mdlerrors.NewBackend("set "+propName+" on "+op.Target.Name(), err) } } - return nil - case "Name": - if s, ok := value.(string); ok { - dSet(widget, "Name", s) - } - return nil - case "Attribute": - return setWidgetAttributeRef(widget, value) - case "DataSource": - return setWidgetDataSource(widget, value) - default: - // Try as pluggable widget property (quoted string property name) - return setPluggableWidgetProperty(widget, propName, value) } -} - -// setWidgetCaption sets the Caption property on a button or text widget. -func setWidgetCaption(widget bson.D, value interface{}) error { - caption := dGetDoc(widget, "Caption") - if caption == nil { - // Try direct caption text - setTranslatableText(widget, "Caption", value) - return nil - } - setTranslatableText(caption, "", value) return nil } -// setWidgetAttributeRef sets or updates the AttributeRef on an input widget. -// The value must be a fully qualified path (Module.Entity.Attribute, 2+ dots). -// If not fully qualified, AttributeRef is set to nil to avoid Studio Pro crash. -func setWidgetAttributeRef(widget bson.D, value interface{}) error { - attrPath, ok := value.(string) - if !ok { - return mdlerrors.NewValidation("Attribute value must be a string") - } - - // Build the new AttributeRef value - var attrRefValue interface{} - if strings.Count(attrPath, ".") >= 2 { - attrRefValue = bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "DomainModels$AttributeRef"}, - {Key: "Attribute", Value: attrPath}, - {Key: "EntityRef", Value: nil}, - } - } else { - // Not fully qualified — clear the ref to avoid Mendix crash - attrRefValue = nil - } - - // Try to update existing AttributeRef field - for i, elem := range widget { - if elem.Key == "AttributeRef" { - widget[i].Value = attrRefValue - return nil - } - } - - // No existing AttributeRef field — this widget may not support it - 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 { +// convertASTDataSource converts an AST DataSource value to a pages.DataSource. +func convertASTDataSource(value interface{}) (pages.DataSource, error) { ds, ok := value.(*ast.DataSourceV3) if !ok { - return mdlerrors.NewValidation("DataSource value must be a datasource expression") + return nil, mdlerrors.NewValidation("DataSource value must be a datasource expression") } - var serialized interface{} - switch ds.Type { case "selection": - // SELECTION widgetName → Forms$ListenTargetSource - serialized = bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Forms$ListenTargetSource"}, - {Key: "ListenTarget", Value: ds.Reference}, - } + return &pages.ListenToWidgetSource{WidgetName: ds.Reference}, nil case "database": - // DATABASE Entity → Forms$DataViewSource with entity ref - var entityRef interface{} - if ds.Reference != "" { - entityRef = bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "DomainModels$DirectEntityRef"}, - {Key: "Entity", Value: ds.Reference}, - } - } - serialized = bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Forms$DataViewSource"}, - {Key: "EntityRef", Value: entityRef}, - {Key: "ForceFullObjects", Value: false}, - {Key: "SourceVariable", Value: nil}, - } + return &pages.DatabaseSource{EntityName: ds.Reference}, nil case "microflow": - serialized = bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Forms$MicroflowSource"}, - {Key: "MicroflowSettings", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Forms$MicroflowSettings"}, - {Key: "Asynchronous", Value: false}, - {Key: "ConfirmationInfo", Value: nil}, - {Key: "FormValidations", Value: "All"}, - {Key: "Microflow", Value: ds.Reference}, - {Key: "ParameterMappings", Value: bson.A{int32(3)}}, - {Key: "ProgressBar", Value: "None"}, - {Key: "ProgressMessage", Value: nil}, - }}, - } + return &pages.MicroflowSource{Microflow: ds.Reference}, nil case "nanoflow": - serialized = bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Forms$NanoflowSource"}, - {Key: "NanoflowSettings", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Forms$NanoflowSettings"}, - {Key: "Nanoflow", Value: ds.Reference}, - {Key: "ParameterMappings", Value: bson.A{int32(3)}}, - }}, - } + return &pages.NanoflowSource{Nanoflow: ds.Reference}, nil default: - return mdlerrors.NewUnsupported("unsupported DataSource type for ALTER PAGE SET: " + ds.Type) - } - - dSet(widget, "DataSource", serialized) - return nil -} - -// setWidgetLabel sets the Label.Caption text on input widgets. -func setWidgetLabel(widget bson.D, value interface{}) error { - label := dGetDoc(widget, "Label") - if label == nil { - return nil - } - setTranslatableText(label, "Caption", value) - return nil -} - -// setWidgetContent sets the Content property on a DYNAMICTEXT widget. -// Content is stored as Forms$ClientTemplate → Template (Forms$Text) → Items[] → Translation{Text}. -// This mirrors extractTextContent which reads Content.Template.Items[].Text. -func setWidgetContent(widget bson.D, value interface{}) error { - strVal, ok := value.(string) - if !ok { - return mdlerrors.NewValidation("Content value must be a string") - } - content := dGetDoc(widget, "Content") - if content == nil { - return mdlerrors.NewValidation("widget has no Content property") - } - template := dGetDoc(content, "Template") - if template == nil { - return mdlerrors.NewValidation("Content has no Template") - } - items := dGetArrayElements(dGet(template, "Items")) - if len(items) > 0 { - if itemDoc, ok := items[0].(bson.D); ok { - dSet(itemDoc, "Text", strVal) - return nil - } + return nil, mdlerrors.NewUnsupported("unsupported DataSource type for ALTER PAGE SET: " + ds.Type) } - return mdlerrors.NewValidation("Content.Template has no Items with Text") -} - -// setTranslatableText sets a translatable text value in BSON. -// If key is empty, modifies the doc directly; otherwise navigates to doc[key]. -func setTranslatableText(parent bson.D, key string, value interface{}) { - strVal, ok := value.(string) - if !ok { - return - } - - target := parent - if key != "" { - if nested := dGetDoc(parent, key); nested != nil { - target = nested - } else { - // Try to set directly - dSet(parent, key, strVal) - return - } - } - - // Navigate to Translations[].Text - translations := dGetArrayElements(dGet(target, "Translations")) - if len(translations) > 0 { - if tDoc, ok := translations[0].(bson.D); ok { - dSet(tDoc, "Text", strVal) - return - } - } - - // Direct text value - dSet(target, "Text", strVal) -} - -// setPluggableWidgetProperty sets a property on a pluggable widget's Object.Properties[]. -// Properties are identified by TypePointer referencing a PropertyType entry in the widget's -// Type.ObjectType.PropertyTypes array, NOT by a "Key" field on the property itself. -func setPluggableWidgetProperty(widget bson.D, propName string, value interface{}) error { - obj := dGetDoc(widget, "Object") - if obj == nil { - 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 - propTypeKeyMap := make(map[string]string) - if widgetType := dGetDoc(widget, "Type"); widgetType != nil { - if objType := dGetDoc(widgetType, "ObjectType"); objType != nil { - propTypes := dGetArrayElements(dGet(objType, "PropertyTypes")) - for _, pt := range propTypes { - ptDoc, ok := pt.(bson.D) - if !ok { - continue - } - key := dGetString(ptDoc, "PropertyKey") - if key == "" { - continue - } - id := extractBinaryIDFromDoc(dGet(ptDoc, "$ID")) - if id != "" { - propTypeKeyMap[id] = key - } - } - } - } - - props := dGetArrayElements(dGet(obj, "Properties")) - for _, prop := range props { - propDoc, ok := prop.(bson.D) - if !ok { - continue - } - // Resolve property key via TypePointer - typePointerID := extractBinaryIDFromDoc(dGet(propDoc, "TypePointer")) - propKey := propTypeKeyMap[typePointerID] - if propKey != propName { - continue - } - // Set the value - if valDoc := dGetDoc(propDoc, "Value"); valDoc != nil { - switch v := value.(type) { - case string: - dSet(valDoc, "PrimitiveValue", v) - case bool: - if v { - dSet(valDoc, "PrimitiveValue", "yes") - } else { - dSet(valDoc, "PrimitiveValue", "no") - } - case int: - dSet(valDoc, "PrimitiveValue", fmt.Sprintf("%d", v)) - case float64: - dSet(valDoc, "PrimitiveValue", fmt.Sprintf("%g", v)) - default: - dSet(valDoc, "PrimitiveValue", fmt.Sprintf("%v", v)) - } - return nil - } - return mdlerrors.NewValidation(fmt.Sprintf("property %q has no Value map", propName)) - } - return mdlerrors.NewNotFound("pluggable property", propName) } // ============================================================================ -// INSERT widget +// INSERT widget via mutator // ============================================================================ -// applyInsertWidget inserts new widgets before or after a target widget (page format). -func applyInsertWidget(ctx *ExecContext, rawData bson.D, op *ast.InsertWidgetOp, moduleName string, moduleID model.ID) error { - return applyInsertWidgetWith(ctx, rawData, op, moduleName, moduleID, findBsonWidget) -} - -// applyInsertWidgetWith inserts new widgets using the given widget finder. -func applyInsertWidgetWith(ctx *ExecContext, rawData bson.D, op *ast.InsertWidgetOp, moduleName string, moduleID model.ID, find widgetFinder) error { - var result *bsonWidgetResult - if op.Target.IsColumn() { - result = findBsonColumn(rawData, op.Target.Widget, op.Target.Column, find) - } else { - result = find(rawData, op.Target.Widget) - } - if result == nil { - return mdlerrors.NewNotFound("widget", op.Target.Name()) - } - +func applyInsertWidgetMutator(ctx *ExecContext, mutator backend.PageMutator, op *ast.InsertWidgetOp, moduleName string, moduleID model.ID) error { // Check for duplicate widget names before building for _, w := range op.Widgets { - if w.Name != "" && find(rawData, w.Name) != nil { + if w.Name != "" && mutator.FindWidget(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)) } } // Find entity context from enclosing DataView/DataGrid/ListView - entityCtx := findEnclosingEntityContext(rawData, op.Target.Widget) + entityCtx := mutator.EnclosingEntity(op.Target.Widget) - // Build new widget BSON from AST (pass rawData for page param + widget scope resolution) - newBsonWidgets, err := buildWidgetsBson(ctx, op.Widgets, moduleName, moduleID, entityCtx, rawData) + // Build new widgets from AST + widgets, err := buildWidgetsFromAST(ctx, op.Widgets, moduleName, moduleID, entityCtx, mutator) if err != nil { return mdlerrors.NewBackend("build widgets", err) } - // Calculate insertion index - insertIdx := result.index - if op.Position == "AFTER" { - insertIdx = result.index + 1 - } - - // Insert into the parent array - newArr := make([]any, 0, len(result.parentArr)+len(newBsonWidgets)) - newArr = append(newArr, result.parentArr[:insertIdx]...) - newArr = append(newArr, newBsonWidgets...) - newArr = append(newArr, result.parentArr[insertIdx:]...) - - // Update parent - dSetArray(result.parentDoc, result.parentKey, newArr) - - return nil + return mutator.InsertWidget(op.Target.Widget, op.Target.Column, op.Position, widgets) } // ============================================================================ -// DROP widget +// DROP widget via mutator // ============================================================================ -// applyDropWidget removes widgets from the raw BSON tree (page format). -func applyDropWidget(rawData bson.D, op *ast.DropWidgetOp) error { - return applyDropWidgetWith(rawData, op, findBsonWidget) -} - -// applyDropWidgetWith removes widgets using the given widget finder. -func applyDropWidgetWith(rawData bson.D, op *ast.DropWidgetOp, find widgetFinder) error { - for _, target := range op.Targets { - var result *bsonWidgetResult - if target.IsColumn() { - result = findBsonColumn(rawData, target.Widget, target.Column, find) - } else { - result = find(rawData, target.Widget) - } - if result == nil { - return mdlerrors.NewNotFound("widget", target.Name()) - } - - // Remove from parent array - newArr := make([]any, 0, len(result.parentArr)-1) - newArr = append(newArr, result.parentArr[:result.index]...) - newArr = append(newArr, result.parentArr[result.index+1:]...) - - // Update parent - dSetArray(result.parentDoc, result.parentKey, newArr) +func applyDropWidgetMutator(mutator backend.PageMutator, op *ast.DropWidgetOp) error { + refs := make([]backend.WidgetRef, len(op.Targets)) + for i, t := range op.Targets { + refs[i] = backend.WidgetRef{Widget: t.Widget, Column: t.Column} } - return nil + return mutator.DropWidget(refs) } // ============================================================================ -// REPLACE widget +// REPLACE widget via mutator // ============================================================================ -// applyReplaceWidget replaces a widget with new widgets (page format). -func applyReplaceWidget(ctx *ExecContext, rawData bson.D, op *ast.ReplaceWidgetOp, moduleName string, moduleID model.ID) error { - return applyReplaceWidgetWith(ctx, rawData, op, moduleName, moduleID, findBsonWidget) -} - -// applyReplaceWidgetWith replaces a widget using the given widget finder. -func applyReplaceWidgetWith(ctx *ExecContext, rawData bson.D, op *ast.ReplaceWidgetOp, moduleName string, moduleID model.ID, find widgetFinder) error { - var result *bsonWidgetResult - if op.Target.IsColumn() { - result = findBsonColumn(rawData, op.Target.Widget, op.Target.Column, find) - } else { - result = find(rawData, op.Target.Widget) - } - if result == nil { - return mdlerrors.NewNotFound("widget", op.Target.Name()) - } - +func applyReplaceWidgetMutator(ctx *ExecContext, mutator backend.PageMutator, op *ast.ReplaceWidgetOp, moduleName string, moduleID model.ID) error { // 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 { + if w.Name != "" && w.Name != op.Target.Widget && mutator.FindWidget(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)) } } // Find entity context from enclosing DataView/DataGrid/ListView - entityCtx := findEnclosingEntityContext(rawData, op.Target.Widget) + entityCtx := mutator.EnclosingEntity(op.Target.Widget) - // Build new widget BSON from AST (pass rawData for page param + widget scope resolution) - newBsonWidgets, err := buildWidgetsBson(ctx, op.NewWidgets, moduleName, moduleID, entityCtx, rawData) + // Build new widgets from AST + widgets, err := buildWidgetsFromAST(ctx, op.NewWidgets, moduleName, moduleID, entityCtx, mutator) if err != nil { return mdlerrors.NewBackend("build replacement widgets", err) } - // Replace: remove old widget, insert new ones at same position - newArr := make([]any, 0, len(result.parentArr)-1+len(newBsonWidgets)) - newArr = append(newArr, result.parentArr[:result.index]...) - newArr = append(newArr, newBsonWidgets...) - newArr = append(newArr, result.parentArr[result.index+1:]...) - - // Update parent - dSetArray(result.parentDoc, result.parentKey, newArr) - - return nil -} - -// ============================================================================ -// Entity context extraction from BSON tree -// ============================================================================ - -// findEnclosingEntityContext walks the raw BSON tree to find the DataView, DataGrid, -// ListView, or Gallery ancestor of a target widget and extracts the entity name. -// This is needed for INSERT/REPLACE operations so that input widget Binds can be -// resolved to fully qualified attribute paths. -func findEnclosingEntityContext(rawData bson.D, widgetName string) string { - // Start from FormCall.Arguments[].Widgets[] (page format) - if formCall := dGetDoc(rawData, "FormCall"); formCall != nil { - args := dGetArrayElements(dGet(formCall, "Arguments")) - for _, arg := range args { - argDoc, ok := arg.(bson.D) - if !ok { - continue - } - if ctx := findEntityContextInWidgets(argDoc, "Widgets", widgetName, ""); ctx != "" { - return ctx - } - } - } - // Snippet format: Widgets[] or Widget.Widgets[] - if ctx := findEntityContextInWidgets(rawData, "Widgets", widgetName, ""); ctx != "" { - return ctx - } - if widgetContainer := dGetDoc(rawData, "Widget"); widgetContainer != nil { - if ctx := findEntityContextInWidgets(widgetContainer, "Widgets", widgetName, ""); ctx != "" { - return ctx - } - } - return "" -} - -// findEntityContextInWidgets searches a widget array for the target widget, -// tracking entity context from DataView/DataGrid/ListView/Gallery ancestors. -func findEntityContextInWidgets(parentDoc bson.D, key string, widgetName string, currentEntity string) string { - elements := dGetArrayElements(dGet(parentDoc, key)) - for _, elem := range elements { - wDoc, ok := elem.(bson.D) - if !ok { - continue - } - if dGetString(wDoc, "Name") == widgetName { - return currentEntity - } - // Update entity context if this is a data container - entityCtx := currentEntity - if ent := extractEntityFromDataSource(wDoc); ent != "" { - entityCtx = ent - } - // Recurse into children - if ctx := findEntityContextInChildren(wDoc, widgetName, entityCtx); ctx != "" { - return ctx - } - } - return "" -} - -// findEntityContextInChildren recursively searches widget children for the target, -// tracking entity context. Mirrors the traversal logic of findInWidgetChildren. -func findEntityContextInChildren(wDoc bson.D, widgetName string, currentEntity string) string { - typeName := dGetString(wDoc, "$Type") - - // Direct Widgets[] children - if ctx := findEntityContextInWidgets(wDoc, "Widgets", widgetName, currentEntity); ctx != "" { - return ctx - } - // FooterWidgets[] - if ctx := findEntityContextInWidgets(wDoc, "FooterWidgets", widgetName, currentEntity); ctx != "" { - return ctx - } - // LayoutGrid: Rows[].Columns[].Widgets[] - if strings.Contains(typeName, "LayoutGrid") { - rows := dGetArrayElements(dGet(wDoc, "Rows")) - for _, row := range rows { - rowDoc, ok := row.(bson.D) - if !ok { - continue - } - cols := dGetArrayElements(dGet(rowDoc, "Columns")) - for _, col := range cols { - colDoc, ok := col.(bson.D) - if !ok { - continue - } - if ctx := findEntityContextInWidgets(colDoc, "Widgets", widgetName, currentEntity); ctx != "" { - return ctx - } - } - } - } - // TabContainer: TabPages[].Widgets[] - tabPages := dGetArrayElements(dGet(wDoc, "TabPages")) - for _, tp := range tabPages { - tpDoc, ok := tp.(bson.D) - if !ok { - continue - } - if ctx := findEntityContextInWidgets(tpDoc, "Widgets", widgetName, currentEntity); ctx != "" { - return ctx - } - } - // ControlBar - if controlBar := dGetDoc(wDoc, "ControlBar"); controlBar != nil { - if ctx := findEntityContextInWidgets(controlBar, "Items", widgetName, currentEntity); ctx != "" { - return ctx - } - } - // CustomWidget (pluggable): Object.Properties[].Value.Widgets[] - if strings.Contains(typeName, "CustomWidget") { - if obj := dGetDoc(wDoc, "Object"); obj != nil { - props := dGetArrayElements(dGet(obj, "Properties")) - for _, prop := range props { - propDoc, ok := prop.(bson.D) - if !ok { - continue - } - if valDoc := dGetDoc(propDoc, "Value"); valDoc != nil { - if ctx := findEntityContextInWidgets(valDoc, "Widgets", widgetName, currentEntity); ctx != "" { - return ctx - } - } - } - } - } - return "" -} - -// extractEntityFromDataSource extracts the entity qualified name from a widget's -// DataSource BSON. Handles DataView, DataGrid, ListView, and Gallery data sources. -func extractEntityFromDataSource(wDoc bson.D) string { - ds := dGetDoc(wDoc, "DataSource") - if ds == nil { - return "" - } - // EntityRef.Entity contains the qualified name (e.g., "Module.Entity") - if entityRef := dGetDoc(ds, "EntityRef"); entityRef != nil { - if entity := dGetString(entityRef, "Entity"); entity != "" { - return entity - } - } - return "" + return mutator.ReplaceWidget(op.Target.Widget, op.Target.Column, widgets) } // ============================================================================ -// ADD / DROP variable +// Widget building from AST (domain logic stays in executor) // ============================================================================ -// applyAddVariable adds a new LocalVariable to the raw BSON page/snippet. -func applyAddVariable(rawData *bson.D, op *ast.AddVariableOp) error { - // Check for duplicate variable name - existingVars := dGetArrayElements(dGet(*rawData, "Variables")) - for _, ev := range existingVars { - if evDoc, ok := ev.(bson.D); ok { - if dGetString(evDoc, "Name") == op.Variable.Name { - return mdlerrors.NewAlreadyExists("variable", "$"+op.Variable.Name) - } - } - } - - // Build VariableType BSON - varTypeID := types.GenerateID() - bsonTypeName := mdlTypeToBsonType(op.Variable.DataType) - varType := bson.D{ - {Key: "$ID", Value: bsonutil.IDToBsonBinary(varTypeID)}, - {Key: "$Type", Value: bsonTypeName}, - } - if bsonTypeName == "DataTypes$ObjectType" { - varType = append(varType, bson.E{Key: "Entity", Value: op.Variable.DataType}) - } - - // Build LocalVariable BSON document - varID := types.GenerateID() - varDoc := bson.D{ - {Key: "$ID", Value: bsonutil.IDToBsonBinary(varID)}, - {Key: "$Type", Value: "Forms$LocalVariable"}, - {Key: "DefaultValue", Value: op.Variable.DefaultValue}, - {Key: "Name", Value: op.Variable.Name}, - {Key: "VariableType", Value: varType}, - } - - // Append to existing Variables array, or create new field - existing := toBsonA(dGet(*rawData, "Variables")) - if existing != nil { - elements := dGetArrayElements(dGet(*rawData, "Variables")) - elements = append(elements, varDoc) - dSetArray(*rawData, "Variables", elements) - } else { - // Field doesn't exist — append to the document - *rawData = append(*rawData, bson.E{Key: "Variables", Value: bson.A{int32(3), varDoc}}) - } - - return nil -} - -// applyDropVariable removes a LocalVariable from the raw BSON page/snippet. -func applyDropVariable(rawData bson.D, op *ast.DropVariableOp) error { - elements := dGetArrayElements(dGet(rawData, "Variables")) - if elements == nil { - return mdlerrors.NewNotFound("variable", "$"+op.VariableName) - } - - // Find and remove the variable - found := false - var kept []any - for _, elem := range elements { - if doc, ok := elem.(bson.D); ok { - if dGetString(doc, "Name") == op.VariableName { - found = true - continue - } - } - kept = append(kept, elem) - } - - if !found { - return mdlerrors.NewNotFound("variable", "$"+op.VariableName) - } - - dSetArray(rawData, "Variables", kept) - return nil -} - -// ============================================================================ -// Widget BSON building -// ============================================================================ - -// buildWidgetsBson converts AST widgets to ordered BSON documents. -// Returns bson.D elements (not map[string]any) to preserve field ordering. -// rawPageData is the full page/snippet BSON, used to extract page parameters -// and existing widget IDs for PARAMETER/SELECTION DataSource resolution. -func buildWidgetsBson(ctx *ExecContext, widgets []*ast.WidgetV3, moduleName string, moduleID model.ID, entityContext string, rawPageData bson.D) ([]any, error) { +// buildWidgetsFromAST converts AST widgets to pages.Widget domain objects. +// It uses the mutator for scope resolution (WidgetScope, ParamScope). +func buildWidgetsFromAST(ctx *ExecContext, widgets []*ast.WidgetV3, moduleName string, moduleID model.ID, entityContext string, mutator backend.PageMutator) ([]pages.Widget, error) { e := ctx.executor - paramScope, paramEntityNames := extractPageParamsFromBSON(rawPageData) - widgetScope := extractWidgetScopeFromBSON(rawPageData) + paramScope, paramEntityNames := mutator.ParamScope() + widgetScope := mutator.WidgetScope() pb := &pageBuilder{ writer: e.writer, @@ -1551,171 +239,19 @@ func buildWidgetsBson(ctx *ExecContext, widgets []*ast.WidgetV3, moduleName stri execCache: ctx.Cache, fragments: ctx.Fragments, themeRegistry: ctx.GetThemeRegistry(), + widgetBackend: ctx.Backend, } - var result []any + var result []pages.Widget for _, w := range widgets { - bsonD, err := pb.buildWidgetV3ToBSON(w) + widget, err := pb.buildWidgetV3(w) if err != nil { return nil, mdlerrors.NewBackend("build widget "+w.Name, err) } - if bsonD == nil { + if widget == nil { continue } - - // Keep as bson.D (ordered document) - no conversion to map[string]any needed. - // This preserves field ordering when marshaled back to BSON bytes. - result = append(result, bsonD) + result = append(result, widget) } return result, nil } - -// extractPageParamsFromBSON extracts page/snippet parameter names and entity -// IDs from the raw BSON document. This enables ALTER PAGE REPLACE/INSERT -// operations to resolve PARAMETER DataSource references (e.g., DataSource: $Customer). -func extractPageParamsFromBSON(rawData bson.D) (map[string]model.ID, map[string]string) { - paramScope := make(map[string]model.ID) - paramEntityNames := make(map[string]string) - if rawData == nil { - return paramScope, paramEntityNames - } - - params := dGetArrayElements(dGet(rawData, "Parameters")) - for _, p := range params { - pDoc, ok := p.(bson.D) - if !ok { - continue - } - name := dGetString(pDoc, "Name") - if name == "" { - continue - } - paramType := dGetDoc(pDoc, "ParameterType") - if paramType == nil { - continue - } - typeName := dGetString(paramType, "$Type") - if typeName != "DataTypes$ObjectType" { - continue - } - entityName := dGetString(paramType, "Entity") - if entityName == "" { - continue - } - idVal := dGet(pDoc, "$ID") - paramID := model.ID(extractBinaryIDFromDoc(idVal)) - paramScope[name] = paramID - paramEntityNames[name] = entityName - } - return paramScope, paramEntityNames -} - -// extractWidgetScopeFromBSON walks the entire raw BSON widget tree and -// collects a map of widget name → widget ID. This enables ALTER PAGE INSERT -// operations to resolve SELECTION DataSource references to existing sibling widgets. -func extractWidgetScopeFromBSON(rawData bson.D) map[string]model.ID { - scope := make(map[string]model.ID) - if rawData == nil { - return scope - } - // Page format: FormCall.Arguments[].Widgets[] - if formCall := dGetDoc(rawData, "FormCall"); formCall != nil { - args := dGetArrayElements(dGet(formCall, "Arguments")) - for _, arg := range args { - argDoc, ok := arg.(bson.D) - if !ok { - continue - } - collectWidgetScope(argDoc, "Widgets", scope) - } - } - // Snippet format: Widgets[] or Widget.Widgets[] - collectWidgetScope(rawData, "Widgets", scope) - if widgetContainer := dGetDoc(rawData, "Widget"); widgetContainer != nil { - collectWidgetScope(widgetContainer, "Widgets", scope) - } - return scope -} - -// collectWidgetScope recursively walks a widget array and collects name→ID mappings. -func collectWidgetScope(parentDoc bson.D, key string, scope map[string]model.ID) { - elements := dGetArrayElements(dGet(parentDoc, key)) - for _, elem := range elements { - wDoc, ok := elem.(bson.D) - if !ok { - continue - } - name := dGetString(wDoc, "Name") - if name != "" { - idVal := dGet(wDoc, "$ID") - if wID := extractBinaryIDFromDoc(idVal); wID != "" { - scope[name] = model.ID(wID) - } - } - // Also register entity context for widgets with DataSource - // so SELECTION can resolve the entity type - collectWidgetScopeInChildren(wDoc, scope) - } -} - -// collectWidgetScopeInChildren recursively walks widget children to collect scope. -func collectWidgetScopeInChildren(wDoc bson.D, scope map[string]model.ID) { - typeName := dGetString(wDoc, "$Type") - - // Direct Widgets[] - collectWidgetScope(wDoc, "Widgets", scope) - // FooterWidgets[] - collectWidgetScope(wDoc, "FooterWidgets", scope) - // LayoutGrid: Rows[].Columns[].Widgets[] - if strings.Contains(typeName, "LayoutGrid") { - rows := dGetArrayElements(dGet(wDoc, "Rows")) - for _, row := range rows { - rowDoc, ok := row.(bson.D) - if !ok { - continue - } - cols := dGetArrayElements(dGet(rowDoc, "Columns")) - for _, col := range cols { - colDoc, ok := col.(bson.D) - if !ok { - continue - } - collectWidgetScope(colDoc, "Widgets", scope) - } - } - } - // TabContainer: TabPages[].Widgets[] - tabPages := dGetArrayElements(dGet(wDoc, "TabPages")) - for _, tp := range tabPages { - tpDoc, ok := tp.(bson.D) - if !ok { - continue - } - collectWidgetScope(tpDoc, "Widgets", scope) - } - // ControlBar - if controlBar := dGetDoc(wDoc, "ControlBar"); controlBar != nil { - collectWidgetScope(controlBar, "Items", scope) - } - // CustomWidget (pluggable): Object.Properties[].Value.Widgets[] - if strings.Contains(typeName, "CustomWidget") { - if obj := dGetDoc(wDoc, "Object"); obj != nil { - props := dGetArrayElements(dGet(obj, "Properties")) - for _, prop := range props { - propDoc, ok := prop.(bson.D) - if !ok { - continue - } - if valDoc := dGetDoc(propDoc, "Value"); valDoc != nil { - collectWidgetScope(valDoc, "Widgets", scope) - } - } - } - } -} - -// ============================================================================ -// Helper: SerializeWidget is already available via mpr package -// ============================================================================ - -var _ = mpr.SerializeWidget // ensure import is used diff --git a/mdl/executor/cmd_alter_workflow.go b/mdl/executor/cmd_alter_workflow.go index e7552e10..4559ad1e 100644 --- a/mdl/executor/cmd_alter_workflow.go +++ b/mdl/executor/cmd_alter_workflow.go @@ -4,23 +4,13 @@ package executor import ( "fmt" - "io" - "strings" - - "go.mongodb.org/mongo-driver/bson" "github.com/mendixlabs/mxcli/mdl/ast" - "github.com/mendixlabs/mxcli/mdl/bsonutil" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/workflows" ) -// bsonArrayMarker is the Mendix BSON array type marker (storageListType 3) -// that prefixes versioned arrays in serialized documents. -const bsonArrayMarker = int32(3) - // execAlterWorkflow handles ALTER WORKFLOW Module.Name { operations }. func execAlterWorkflow(ctx *ExecContext, s *ast.AlterWorkflowStmt) error { if !ctx.Connected() { @@ -61,84 +51,117 @@ func execAlterWorkflow(ctx *ExecContext, s *ast.AlterWorkflowStmt) error { return mdlerrors.NewNotFound("workflow", s.Name.Module+"."+s.Name.Name) } - // Load raw BSON as ordered document - rawBytes, err := ctx.Backend.GetRawUnitBytes(wfID) + // Open mutator + mutator, err := ctx.Backend.OpenWorkflowForMutation(wfID) if err != nil { - return mdlerrors.NewBackend("load raw workflow data", err) - } - var rawData bson.D - if err := bson.Unmarshal(rawBytes, &rawData); err != nil { - return mdlerrors.NewBackend("unmarshal workflow BSON", err) + return mdlerrors.NewBackend("open workflow for mutation", err) } // Apply operations sequentially for _, op := range s.Operations { switch o := op.(type) { case *ast.SetWorkflowPropertyOp: - if err := applySetWorkflowProperty(&rawData, o); err != nil { - return mdlerrors.NewBackend("SET "+o.Property, err) + switch o.Property { + case "OVERVIEW_PAGE", "PARAMETER": + qn := o.Entity.Module + "." + o.Entity.Name + if qn == "." { + qn = "" + } + if err := mutator.SetPropertyWithEntity(o.Property, o.Value, qn); err != nil { + return mdlerrors.NewBackend("SET "+o.Property, err) + } + default: + if err := mutator.SetProperty(o.Property, o.Value); err != nil { + return mdlerrors.NewBackend("SET "+o.Property, err) + } } + case *ast.SetActivityPropertyOp: - if err := applySetActivityProperty(rawData, o); err != nil { + value := o.Value + switch o.Property { + case "PAGE": + value = o.PageName.Module + "." + o.PageName.Name + case "TARGETING_MICROFLOW": + value = o.Microflow.Module + "." + o.Microflow.Name + } + if err := mutator.SetActivityProperty(o.ActivityRef, o.AtPosition, o.Property, value); err != nil { return mdlerrors.NewBackend("SET ACTIVITY", err) } + case *ast.InsertAfterOp: - if err := applyInsertAfterActivity(ctx, rawData, o); err != nil { + acts := buildAndBindActivities(ctx, []ast.WorkflowActivityNode{o.NewActivity}) + if len(acts) == 0 { + return mdlerrors.NewValidation("failed to build new activity") + } + if err := mutator.InsertAfterActivity(o.ActivityRef, o.AtPosition, acts); err != nil { return mdlerrors.NewBackend("INSERT AFTER", err) } + case *ast.DropActivityOp: - if err := applyDropActivity(rawData, o); err != nil { + if err := mutator.DropActivity(o.ActivityRef, o.AtPosition); err != nil { return mdlerrors.NewBackend("DROP ACTIVITY", err) } + case *ast.ReplaceActivityOp: - if err := applyReplaceActivity(ctx, rawData, o); err != nil { + acts := buildAndBindActivities(ctx, []ast.WorkflowActivityNode{o.NewActivity}) + if len(acts) == 0 { + return mdlerrors.NewValidation("failed to build replacement activity") + } + if err := mutator.ReplaceActivity(o.ActivityRef, o.AtPosition, acts); err != nil { return mdlerrors.NewBackend("REPLACE ACTIVITY", err) } + case *ast.InsertOutcomeOp: - if err := applyInsertOutcome(ctx, rawData, o); err != nil { + acts := buildAndBindActivities(ctx, o.Activities) + if err := mutator.InsertOutcome(o.ActivityRef, o.AtPosition, o.OutcomeName, acts); err != nil { return mdlerrors.NewBackend("INSERT OUTCOME", err) } + case *ast.DropOutcomeOp: - if err := applyDropOutcome(rawData, o); err != nil { + if err := mutator.DropOutcome(o.ActivityRef, o.AtPosition, o.OutcomeName); err != nil { return mdlerrors.NewBackend("DROP OUTCOME", err) } + case *ast.InsertPathOp: - if err := applyInsertPath(ctx, rawData, o); err != nil { + acts := buildAndBindActivities(ctx, o.Activities) + if err := mutator.InsertPath(o.ActivityRef, o.AtPosition, "", acts); err != nil { return mdlerrors.NewBackend("INSERT PATH", err) } + case *ast.DropPathOp: - if err := applyDropPath(rawData, o); err != nil { + if err := mutator.DropPath(o.ActivityRef, o.AtPosition, o.PathCaption); err != nil { return mdlerrors.NewBackend("DROP PATH", err) } + case *ast.InsertBranchOp: - if err := applyInsertBranch(ctx, rawData, o); err != nil { + acts := buildAndBindActivities(ctx, o.Activities) + if err := mutator.InsertBranch(o.ActivityRef, o.AtPosition, o.Condition, acts); err != nil { return mdlerrors.NewBackend("INSERT BRANCH", err) } + case *ast.DropBranchOp: - if err := applyDropBranch(rawData, o); err != nil { + if err := mutator.DropBranch(o.ActivityRef, o.AtPosition, o.BranchName); err != nil { return mdlerrors.NewBackend("DROP BRANCH", err) } + case *ast.InsertBoundaryEventOp: - if err := applyInsertBoundaryEvent(ctx, rawData, o); err != nil { + acts := buildAndBindActivities(ctx, o.Activities) + if err := mutator.InsertBoundaryEvent(o.ActivityRef, o.AtPosition, o.EventType, o.Delay, acts); err != nil { return mdlerrors.NewBackend("INSERT BOUNDARY EVENT", err) } + case *ast.DropBoundaryEventOp: - if err := applyDropBoundaryEvent(ctx.Output, rawData, o); err != nil { + if err := mutator.DropBoundaryEvent(o.ActivityRef, o.AtPosition); err != nil { return mdlerrors.NewBackend("DROP BOUNDARY EVENT", err) } + default: 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 mdlerrors.NewBackend("marshal modified workflow", err) - } - // Save - if err := ctx.Backend.UpdateRawUnit(string(wfID), outBytes); err != nil { + if err := mutator.Save(); err != nil { return mdlerrors.NewBackend("save modified workflow", err) } @@ -147,741 +170,9 @@ func execAlterWorkflow(ctx *ExecContext, s *ast.AlterWorkflowStmt) error { return nil } -// applySetWorkflowProperty sets a workflow-level property in raw BSON. -func applySetWorkflowProperty(doc *bson.D, op *ast.SetWorkflowPropertyOp) error { - switch op.Property { - case "DISPLAY": - // WorkflowName is a StringTemplate with Text field - wfName := dGetDoc(*doc, "WorkflowName") - if wfName == nil { - // Auto-create the WorkflowName sub-document - newName := bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Texts$Text"}, - {Key: "Text", Value: op.Value}, - } - *doc = append(*doc, bson.E{Key: "WorkflowName", Value: newName}) - } else { - dSet(wfName, "Text", op.Value) - } - // Also update Title (top-level string) - dSet(*doc, "Title", op.Value) - return nil - - case "DESCRIPTION": - // WorkflowDescription is a StringTemplate with Text field - wfDesc := dGetDoc(*doc, "WorkflowDescription") - if wfDesc == nil { - // Auto-create the WorkflowDescription sub-document - newDesc := bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Texts$Text"}, - {Key: "Text", Value: op.Value}, - } - *doc = append(*doc, bson.E{Key: "WorkflowDescription", Value: newDesc}) - } else { - dSet(wfDesc, "Text", op.Value) - } - return nil - - case "EXPORT_LEVEL": - dSet(*doc, "ExportLevel", op.Value) - return nil - - case "DUE_DATE": - dSet(*doc, "DueDate", op.Value) - return nil - - case "OVERVIEW_PAGE": - qn := op.Entity.Module + "." + op.Entity.Name - if qn == "." { - // Clear overview page - dSet(*doc, "AdminPage", nil) - } else { - pageRef := bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Workflows$PageReference"}, - {Key: "Page", Value: qn}, - } - dSet(*doc, "AdminPage", pageRef) - } - return nil - - case "PARAMETER": - qn := op.Entity.Module + "." + op.Entity.Name - if qn == "." { - // Clear parameter — remove it - for i, elem := range *doc { - if elem.Key == "Parameter" { - (*doc)[i].Value = nil - return nil - } - } - return nil - } - // Check if Parameter already exists - param := dGetDoc(*doc, "Parameter") - if param != nil { - dSet(param, "Entity", qn) - } else { - // Create new Parameter - newParam := bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Workflows$Parameter"}, - {Key: "Entity", Value: qn}, - {Key: "Name", Value: "WorkflowContext"}, - } - // Check if field exists with nil value - for i, elem := range *doc { - if elem.Key == "Parameter" { - (*doc)[i].Value = newParam - return nil - } - } - // Field doesn't exist — append it - *doc = append(*doc, bson.E{Key: "Parameter", Value: newParam}) - } - return nil - - default: - return mdlerrors.NewUnsupported("unsupported workflow property: " + op.Property) - } -} - -// applySetActivityProperty sets a property on a named workflow activity in raw BSON. -func applySetActivityProperty(doc bson.D, op *ast.SetActivityPropertyOp) error { - actDoc, err := findActivityByCaption(doc, op.ActivityRef, op.AtPosition) - if err != nil { - return err - } - - switch op.Property { - case "PAGE": - qn := op.PageName.Module + "." + op.PageName.Name - // TaskPage is a PageReference object - taskPage := dGetDoc(actDoc, "TaskPage") - if taskPage != nil { - dSet(taskPage, "Page", qn) - } else { - // Create TaskPage - pageRef := bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Workflows$PageReference"}, - {Key: "Page", Value: qn}, - } - dSet(actDoc, "TaskPage", pageRef) - } - return nil - - case "DESCRIPTION": - // TaskDescription is a StringTemplate - taskDesc := dGetDoc(actDoc, "TaskDescription") - if taskDesc != nil { - dSet(taskDesc, "Text", op.Value) - } - return nil - - case "TARGETING_MICROFLOW": - qn := op.Microflow.Module + "." + op.Microflow.Name - userTargeting := bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Workflows$MicroflowUserTargeting"}, - {Key: "Microflow", Value: qn}, - } - dSet(actDoc, "UserTargeting", userTargeting) - return nil - - case "TARGETING_XPATH": - userTargeting := bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Workflows$XPathUserTargeting"}, - {Key: "XPathConstraint", Value: op.Value}, - } - dSet(actDoc, "UserTargeting", userTargeting) - return nil - - case "DUE_DATE": - dSet(actDoc, "DueDate", op.Value) - return nil - - default: - return mdlerrors.NewUnsupported("unsupported activity property: " + op.Property) - } -} - -// findActivityByCaption searches the workflow for an activity matching the given caption. -// Searches recursively through nested flows (Decision outcomes, ParallelSplit paths, UserTask outcomes, BoundaryEvents). -// atPosition provides positional disambiguation when multiple activities share the same caption (1-based). -func findActivityByCaption(doc bson.D, caption string, atPosition int) (bson.D, error) { - flow := dGetDoc(doc, "Flow") - if flow == nil { - return nil, mdlerrors.NewValidation("workflow has no Flow") - } - - var matches []bson.D - findActivitiesRecursive(flow, caption, &matches) - - if len(matches) == 0 { - return nil, mdlerrors.NewNotFound("activity", caption) - } - if len(matches) == 1 || atPosition == 0 { - if atPosition > 0 && 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, 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, 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 -} - -// findActivitiesRecursive collects all activities matching caption in a flow and its nested sub-flows. -func findActivitiesRecursive(flow bson.D, caption string, matches *[]bson.D) { - activities := dGetArrayElements(dGet(flow, "Activities")) - for _, elem := range activities { - actDoc, ok := elem.(bson.D) - if !ok { - continue - } - actCaption := dGetString(actDoc, "Caption") - actName := dGetString(actDoc, "Name") - if actCaption == caption || actName == caption { - *matches = append(*matches, actDoc) - } - // Recurse into nested flows: Outcomes (Decision, UserTask, CallMicroflow), Paths (ParallelSplit), BoundaryEvents - for _, nestedFlow := range getNestedFlows(actDoc) { - findActivitiesRecursive(nestedFlow, caption, matches) - } - } -} - -// getNestedFlows returns all sub-flows within an activity (outcomes, paths, boundary events). -func getNestedFlows(actDoc bson.D) []bson.D { - var flows []bson.D - // Outcomes (UserTask, Decision, CallMicroflow) - outcomes := dGetArrayElements(dGet(actDoc, "Outcomes")) - for _, o := range outcomes { - oDoc, ok := o.(bson.D) - if !ok { - continue - } - if f := dGetDoc(oDoc, "Flow"); f != nil { - flows = append(flows, f) - } - } - // BoundaryEvents - events := dGetArrayElements(dGet(actDoc, "BoundaryEvents")) - for _, e := range events { - eDoc, ok := e.(bson.D) - if !ok { - continue - } - if f := dGetDoc(eDoc, "Flow"); f != nil { - flows = append(flows, f) - } - } - return flows -} - -// findActivityIndex returns the index, activities array, and containing flow of an activity. -// Searches recursively through nested flows. -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, mdlerrors.NewValidation("workflow has no Flow") - } - - var matches []activityMatch - findActivityIndexRecursive(flow, caption, &matches) - - if len(matches) == 0 { - 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, mdlerrors.NewValidation(fmt.Sprintf("ambiguous activity %q — %d matches. Use @N to disambiguate", caption, len(matches))) - } - if pos >= 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 -} - -type activityMatch struct { - idx int - activities []any - flow bson.D -} - -func findActivityIndexRecursive(flow bson.D, caption string, matches *[]activityMatch) { - activities := dGetArrayElements(dGet(flow, "Activities")) - for i, elem := range activities { - actDoc, ok := elem.(bson.D) - if !ok { - continue - } - actCaption := dGetString(actDoc, "Caption") - actName := dGetString(actDoc, "Name") - if actCaption == caption || actName == caption { - *matches = append(*matches, activityMatch{idx: i, activities: activities, flow: flow}) - } - for _, nestedFlow := range getNestedFlows(actDoc) { - findActivityIndexRecursive(nestedFlow, caption, matches) - } - } -} - -// collectAllActivityNames collects all activity names from the entire workflow BSON (recursively). -func collectAllActivityNames(doc bson.D) map[string]bool { - names := make(map[string]bool) - flow := dGetDoc(doc, "Flow") - if flow != nil { - collectNamesRecursive(flow, names) - } - return names -} - -func collectNamesRecursive(flow bson.D, names map[string]bool) { - activities := dGetArrayElements(dGet(flow, "Activities")) - for _, elem := range activities { - actDoc, ok := elem.(bson.D) - if !ok { - continue - } - if name := dGetString(actDoc, "Name"); name != "" { - names[name] = true - } - for _, nested := range getNestedFlows(actDoc) { - collectNamesRecursive(nested, names) - } - } -} - -// deduplicateNewActivityName ensures a new activity name doesn't conflict with existing names. -func deduplicateNewActivityName(act workflows.WorkflowActivity, existingNames map[string]bool) { - name := act.GetName() - if name == "" || !existingNames[name] { - return - } - for i := 2; i < 1000; i++ { - candidate := fmt.Sprintf("%s_%d", name, i) - if !existingNames[candidate] { - act.SetName(candidate) - existingNames[candidate] = true - return - } - } -} - -// buildSubFlowBson builds a Workflows$Flow BSON document from AST activity nodes, -// with auto-binding and name deduplication against existing workflow activities. -func buildSubFlowBson(ctx *ExecContext, doc bson.D, activities []ast.WorkflowActivityNode) bson.D { - subActs := buildWorkflowActivities(activities) - autoBindActivitiesInFlow(ctx, subActs) - existingNames := collectAllActivityNames(doc) - for _, act := range subActs { - deduplicateNewActivityName(act, existingNames) - } - var subActsBson bson.A - subActsBson = append(subActsBson, bsonArrayMarker) - for _, act := range subActs { - bsonDoc := mpr.SerializeWorkflowActivity(act) - if bsonDoc != nil { - subActsBson = append(subActsBson, bsonDoc) - } - } - return bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Workflows$Flow"}, - {Key: "Activities", Value: subActsBson}, - } -} - -// applyInsertAfterActivity inserts a new activity after a named activity. -func applyInsertAfterActivity(ctx *ExecContext, doc bson.D, op *ast.InsertAfterOp) error { - idx, activities, containingFlow, err := findActivityIndex(doc, op.ActivityRef, op.AtPosition) - if err != nil { - return err - } - - newActs := buildWorkflowActivities([]ast.WorkflowActivityNode{op.NewActivity}) - if len(newActs) == 0 { - return mdlerrors.NewValidation("failed to build new activity") - } - - // Auto-bind parameters and deduplicate against existing workflow names - autoBindActivitiesInFlow(ctx, newActs) - existingNames := collectAllActivityNames(doc) - for _, act := range newActs { - deduplicateNewActivityName(act, existingNames) - } - - newBsonActs := make([]any, 0, len(newActs)) - for _, act := range newActs { - bsonDoc := mpr.SerializeWorkflowActivity(act) - if bsonDoc != nil { - newBsonActs = append(newBsonActs, bsonDoc) - } - } - - insertIdx := idx + 1 - newArr := make([]any, 0, len(activities)+len(newBsonActs)) - newArr = append(newArr, activities[:insertIdx]...) - newArr = append(newArr, newBsonActs...) - newArr = append(newArr, activities[insertIdx:]...) - - dSetArray(containingFlow, "Activities", newArr) - return nil -} - -// applyDropActivity removes an activity from the flow. -func applyDropActivity(doc bson.D, op *ast.DropActivityOp) error { - idx, activities, containingFlow, err := findActivityIndex(doc, op.ActivityRef, op.AtPosition) - if err != nil { - return err - } - - newArr := make([]any, 0, len(activities)-1) - newArr = append(newArr, activities[:idx]...) - newArr = append(newArr, activities[idx+1:]...) - - dSetArray(containingFlow, "Activities", newArr) - return nil -} - -// applyReplaceActivity replaces an activity in place. -func applyReplaceActivity(ctx *ExecContext, doc bson.D, op *ast.ReplaceActivityOp) error { - idx, activities, containingFlow, err := findActivityIndex(doc, op.ActivityRef, op.AtPosition) - if err != nil { - return err - } - - newActs := buildWorkflowActivities([]ast.WorkflowActivityNode{op.NewActivity}) - if len(newActs) == 0 { - return mdlerrors.NewValidation("failed to build replacement activity") - } - - autoBindActivitiesInFlow(ctx, newActs) - existingNames := collectAllActivityNames(doc) - for _, act := range newActs { - deduplicateNewActivityName(act, existingNames) - } - - newBsonActs := make([]any, 0, len(newActs)) - for _, act := range newActs { - bsonDoc := mpr.SerializeWorkflowActivity(act) - if bsonDoc != nil { - newBsonActs = append(newBsonActs, bsonDoc) - } - } - - newArr := make([]any, 0, len(activities)-1+len(newBsonActs)) - newArr = append(newArr, activities[:idx]...) - newArr = append(newArr, newBsonActs...) - newArr = append(newArr, activities[idx+1:]...) - - dSetArray(containingFlow, "Activities", newArr) - return nil -} - -// applyInsertOutcome adds a new outcome to a user task. -func applyInsertOutcome(ctx *ExecContext, doc bson.D, op *ast.InsertOutcomeOp) error { - actDoc, err := findActivityByCaption(doc, op.ActivityRef, op.AtPosition) - if err != nil { - return err - } - - // Build outcome BSON - outcomeDoc := bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Workflows$UserTaskOutcome"}, - } - - // Build sub-flow if activities provided - if len(op.Activities) > 0 { - outcomeDoc = append(outcomeDoc, bson.E{Key: "Flow", Value: buildSubFlowBson(ctx, doc, op.Activities)}) - } - - outcomeDoc = append(outcomeDoc, - bson.E{Key: "PersistentId", Value: bsonutil.NewIDBsonBinary()}, - bson.E{Key: "Value", Value: op.OutcomeName}, - ) - - // Append to Outcomes array - outcomes := dGetArrayElements(dGet(actDoc, "Outcomes")) - outcomes = append(outcomes, outcomeDoc) - dSetArray(actDoc, "Outcomes", outcomes) - return nil -} - -// applyDropOutcome removes an outcome from a user task. -func applyDropOutcome(doc bson.D, op *ast.DropOutcomeOp) error { - actDoc, err := findActivityByCaption(doc, op.ActivityRef, op.AtPosition) - if err != nil { - return err - } - - outcomes := dGetArrayElements(dGet(actDoc, "Outcomes")) - found := false - var kept []any - for _, elem := range outcomes { - oDoc, ok := elem.(bson.D) - if !ok { - kept = append(kept, elem) - continue - } - value := dGetString(oDoc, "Value") - typeName := dGetString(oDoc, "$Type") - // Match by Value, or by $Type for VoidConditionOutcome ("Default") - matched := value == op.OutcomeName - if !matched && strings.EqualFold(op.OutcomeName, "Default") && typeName == "Workflows$VoidConditionOutcome" { - matched = true - } - if matched && !found { - found = true - continue - } - kept = append(kept, elem) - } - if !found { - 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 -} - -// applyInsertPath adds a new path to a parallel split. -func applyInsertPath(ctx *ExecContext, doc bson.D, op *ast.InsertPathOp) error { - actDoc, err := findActivityByCaption(doc, op.ActivityRef, op.AtPosition) - if err != nil { - return err - } - - pathDoc := bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Workflows$ParallelSplitOutcome"}, - } - - if len(op.Activities) > 0 { - pathDoc = append(pathDoc, bson.E{Key: "Flow", Value: buildSubFlowBson(ctx, doc, op.Activities)}) - } - - pathDoc = append(pathDoc, bson.E{Key: "PersistentId", Value: bsonutil.NewIDBsonBinary()}) - - outcomes := dGetArrayElements(dGet(actDoc, "Outcomes")) - outcomes = append(outcomes, pathDoc) - dSetArray(actDoc, "Outcomes", outcomes) - return nil -} - -// applyDropPath removes a path from a parallel split by caption. -func applyDropPath(doc bson.D, op *ast.DropPathOp) error { - actDoc, err := findActivityByCaption(doc, op.ActivityRef, op.AtPosition) - if err != nil { - return err - } - - outcomes := dGetArrayElements(dGet(actDoc, "Outcomes")) - if op.PathCaption == "" && len(outcomes) > 0 { - // Drop last path - outcomes = outcomes[:len(outcomes)-1] - dSetArray(actDoc, "Outcomes", outcomes) - return nil - } - - // Find by index (paths are numbered 1-based in MDL) - pathIdx := -1 - for i := range outcomes { - // Path captions are typically "Path 1", "Path 2" etc. - if fmt.Sprintf("Path %d", i+1) == op.PathCaption { - pathIdx = i - break - } - } - if pathIdx < 0 { - 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) - newOutcomes = append(newOutcomes, outcomes[:pathIdx]...) - newOutcomes = append(newOutcomes, outcomes[pathIdx+1:]...) - dSetArray(actDoc, "Outcomes", newOutcomes) - return nil -} - -// applyInsertBranch adds a new branch to a decision. -func applyInsertBranch(ctx *ExecContext, doc bson.D, op *ast.InsertBranchOp) error { - actDoc, err := findActivityByCaption(doc, op.ActivityRef, op.AtPosition) - if err != nil { - return err - } - - // Build the condition outcome BSON - var outcomeDoc bson.D - switch strings.ToLower(op.Condition) { - case "true": - outcomeDoc = bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Workflows$BooleanConditionOutcome"}, - {Key: "Value", Value: true}, - } - case "false": - outcomeDoc = bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Workflows$BooleanConditionOutcome"}, - {Key: "Value", Value: false}, - } - case "default": - outcomeDoc = bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Workflows$VoidConditionOutcome"}, - } - default: - outcomeDoc = bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Workflows$EnumerationValueConditionOutcome"}, - {Key: "Value", Value: op.Condition}, - } - } - - if len(op.Activities) > 0 { - outcomeDoc = append(outcomeDoc, bson.E{Key: "Flow", Value: buildSubFlowBson(ctx, doc, op.Activities)}) - } - - outcomes := dGetArrayElements(dGet(actDoc, "Outcomes")) - outcomes = append(outcomes, outcomeDoc) - dSetArray(actDoc, "Outcomes", outcomes) - return nil -} - -// applyDropBranch removes a branch from a decision. -func applyDropBranch(doc bson.D, op *ast.DropBranchOp) error { - actDoc, err := findActivityByCaption(doc, op.ActivityRef, op.AtPosition) - if err != nil { - return err - } - - outcomes := dGetArrayElements(dGet(actDoc, "Outcomes")) - found := false - var kept []any - for _, elem := range outcomes { - oDoc, ok := elem.(bson.D) - if !ok { - kept = append(kept, elem) - continue - } - if !found { - // Match by Value or $Type for void outcomes - typeName := dGetString(oDoc, "$Type") - switch strings.ToLower(op.BranchName) { - case "true": - if typeName == "Workflows$BooleanConditionOutcome" { - if v, ok := dGet(oDoc, "Value").(bool); ok && v { - found = true - continue - } - } - case "false": - if typeName == "Workflows$BooleanConditionOutcome" { - if v, ok := dGet(oDoc, "Value").(bool); ok && !v { - found = true - continue - } - } - case "default": - if typeName == "Workflows$VoidConditionOutcome" { - found = true - continue - } - default: - value := dGetString(oDoc, "Value") - if value == op.BranchName { - found = true - continue - } - } - } - kept = append(kept, elem) - } - if !found { - 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 -} - -// applyInsertBoundaryEvent adds a boundary event to an activity. -func applyInsertBoundaryEvent(ctx *ExecContext, doc bson.D, op *ast.InsertBoundaryEventOp) error { - actDoc, err := findActivityByCaption(doc, op.ActivityRef, op.AtPosition) - if err != nil { - return err - } - - typeName := "Workflows$InterruptingTimerBoundaryEvent" - switch op.EventType { - case "NonInterruptingTimer": - typeName = "Workflows$NonInterruptingTimerBoundaryEvent" - case "Timer": - typeName = "Workflows$TimerBoundaryEvent" - } - - eventDoc := bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: typeName}, - {Key: "Caption", Value: ""}, - } - - if op.Delay != "" { - eventDoc = append(eventDoc, bson.E{Key: "FirstExecutionTime", Value: op.Delay}) - } - - if len(op.Activities) > 0 { - eventDoc = append(eventDoc, bson.E{Key: "Flow", Value: buildSubFlowBson(ctx, doc, op.Activities)}) - } - - eventDoc = append(eventDoc, bson.E{Key: "PersistentId", Value: bsonutil.NewIDBsonBinary()}) - - if typeName == "Workflows$NonInterruptingTimerBoundaryEvent" { - eventDoc = append(eventDoc, bson.E{Key: "Recurrence", Value: nil}) - } - - // Append to BoundaryEvents array - events := dGetArrayElements(dGet(actDoc, "BoundaryEvents")) - events = append(events, eventDoc) - dSetArray(actDoc, "BoundaryEvents", events) - return nil -} - -// applyDropBoundaryEvent removes the first boundary event from an activity. -// -// Limitation: this always removes events[0]. There is currently no syntax to -// target a specific boundary event by name or type when multiple exist. -func applyDropBoundaryEvent(w io.Writer, doc bson.D, op *ast.DropBoundaryEventOp) error { - actDoc, err := findActivityByCaption(doc, op.ActivityRef, op.AtPosition) - if err != nil { - return err - } - - events := dGetArrayElements(dGet(actDoc, "BoundaryEvents")) - if len(events) == 0 { - return mdlerrors.NewValidation(fmt.Sprintf("activity %q has no boundary events", op.ActivityRef)) - } - - if len(events) > 1 { - fmt.Fprintf(w, "warning: activity %q has %d boundary events; dropping the first one\n", op.ActivityRef, len(events)) - } - - // Drop the first boundary event - dSetArray(actDoc, "BoundaryEvents", events[1:]) - return nil +// buildAndBindActivities builds workflow activities from AST nodes and auto-binds parameters. +func buildAndBindActivities(ctx *ExecContext, nodes []ast.WorkflowActivityNode) []workflows.WorkflowActivity { + acts := buildWorkflowActivities(nodes) + autoBindActivitiesInFlow(ctx, acts) + return acts } diff --git a/mdl/executor/cmd_diff_local.go b/mdl/executor/cmd_diff_local.go index 12ff4285..0900f438 100644 --- a/mdl/executor/cmd_diff_local.go +++ b/mdl/executor/cmd_diff_local.go @@ -14,7 +14,6 @@ import ( "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" "go.mongodb.org/mongo-driver/bson/primitive" ) @@ -497,7 +496,7 @@ func attributeBsonToMDL(_ *ExecContext, raw map[string]any) string { // Falls back to a header-only stub if parsing fails. func microflowBsonToMDL(ctx *ExecContext, raw map[string]any, qualifiedName string) string { qn := splitQualifiedName(qualifiedName) - mf := mpr.ParseMicroflowFromRaw(raw, model.ID(qn.Name), "") + mf := ctx.Backend.ParseMicroflowFromRaw(raw, model.ID(qn.Name), "") entityNames, microflowNames := buildNameLookups(ctx) return renderMicroflowMDL(ctx, mf, qn, entityNames, microflowNames, nil) diff --git a/mdl/executor/cmd_pages_builder.go b/mdl/executor/cmd_pages_builder.go index ed74a533..30426f40 100644 --- a/mdl/executor/cmd_pages_builder.go +++ b/mdl/executor/cmd_pages_builder.go @@ -9,6 +9,7 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" @@ -35,6 +36,7 @@ type pageBuilder struct { isSnippet bool // True if building a snippet (affects parameter datasource) fragments map[string]*ast.DefineFragmentStmt // Fragment registry from executor themeRegistry *ThemeRegistry // Theme design property definitions (may be nil) + widgetBackend backend.WidgetBuilderBackend // Backend for pluggable widget construction // Pluggable widget engine (lazily initialized) widgetRegistry *WidgetRegistry @@ -68,11 +70,19 @@ func (pb *pageBuilder) initPluggableEngine() { } } pb.widgetRegistry = registry - pb.pluggableEngine = NewPluggableWidgetEngine(NewOperationRegistry(), pb) + pb.pluggableEngine = NewPluggableWidgetEngine(pb.widgetBackend, pb) } // registerWidgetName registers a widget name and returns an error if it's already used. // Widget names must be unique within a page/snippet. + +// getProjectPath returns the project directory path from the underlying reader. +func (pb *pageBuilder) getProjectPath() string { + if pb.reader != nil { + return pb.reader.Path() + } + return "" +} func (pb *pageBuilder) registerWidgetName(name string, id model.ID) error { if name == "" { return nil // Anonymous widgets are allowed diff --git a/mdl/executor/cmd_pages_builder_input.go b/mdl/executor/cmd_pages_builder_input.go index 4a8cfed1..c98c9152 100644 --- a/mdl/executor/cmd_pages_builder_input.go +++ b/mdl/executor/cmd_pages_builder_input.go @@ -11,7 +11,6 @@ import ( "github.com/mendixlabs/mxcli/mdl/bsonutil" "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/pages" "github.com/mendixlabs/mxcli/sdk/widgets" "go.mongodb.org/mongo-driver/bson" @@ -176,19 +175,6 @@ func setPrimitiveValue(val bson.D, value string) bson.D { return result } -// setDataSource sets the DataSource field in a WidgetValue. -func setDataSource(val bson.D, ds pages.DataSource) bson.D { - result := make(bson.D, 0, len(val)) - for _, elem := range val { - if elem.Key == "DataSource" { - result = append(result, bson.E{Key: "DataSource", Value: mpr.SerializeCustomWidgetDataSource(ds)}) - } else { - result = append(result, elem) - } - } - return result -} - // setAssociationRef sets the EntityRef field in a WidgetValue for an association binding // on a pluggable widget. Uses DomainModels$IndirectEntityRef with a Steps array containing // a DomainModels$EntityRefStep that specifies the association and destination entity. diff --git a/mdl/executor/cmd_pages_builder_input_datagrid.go b/mdl/executor/cmd_pages_builder_input_datagrid.go index b86a98dc..e49121ae 100644 --- a/mdl/executor/cmd_pages_builder_input_datagrid.go +++ b/mdl/executor/cmd_pages_builder_input_datagrid.go @@ -8,7 +8,6 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/bsonutil" - "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/pages" "go.mongodb.org/mongo-driver/bson" ) @@ -85,7 +84,7 @@ func (pb *pageBuilder) buildDataGrid2Property(entry pages.PropertyTypeIDEntry, d // Build the datasource BSON if provided var datasourceBSON any if datasource != nil { - datasourceBSON = mpr.SerializeCustomWidgetDataSource(datasource) + datasourceBSON = pb.widgetBackend.SerializeDataSourceToOpaque(datasource) } // Build attribute ref if provided diff --git a/mdl/executor/cmd_pages_builder_v3_pluggable.go b/mdl/executor/cmd_pages_builder_v3_pluggable.go index b79db2b9..93655b2d 100644 --- a/mdl/executor/cmd_pages_builder_v3_pluggable.go +++ b/mdl/executor/cmd_pages_builder_v3_pluggable.go @@ -12,7 +12,6 @@ import ( "github.com/mendixlabs/mxcli/mdl/bsonutil" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/mdl/types" - "github.com/mendixlabs/mxcli/sdk/mpr" ) // ============================================================================= @@ -82,7 +81,7 @@ func (pb *pageBuilder) cloneActionWithNewID(actionMap bson.D) bson.D { return result } -// buildWidgetV3ToBSON builds a V3 widget and serializes it directly to BSON. +// buildWidgetV3ToBSON builds a V3 widget and serializes it to an opaque storage form. func (pb *pageBuilder) buildWidgetV3ToBSON(w *ast.WidgetV3) (bson.D, error) { widget, err := pb.buildWidgetV3(w) if err != nil { @@ -91,7 +90,15 @@ func (pb *pageBuilder) buildWidgetV3ToBSON(w *ast.WidgetV3) (bson.D, error) { if widget == nil { return nil, nil } - return mpr.SerializeWidget(widget), nil + raw := pb.widgetBackend.SerializeWidgetToOpaque(widget) + if raw == nil { + return nil, nil + } + bsonD, ok := raw.(bson.D) + if !ok { + return nil, mdlerrors.NewValidationf("SerializeWidgetToOpaque returned unexpected type %T", raw) + } + return bsonD, nil } // createAttributeObject creates a single attribute object entry for filter widget Attributes. diff --git a/mdl/executor/cmd_pages_create_v3.go b/mdl/executor/cmd_pages_create_v3.go index 398ecefb..67b52c21 100644 --- a/mdl/executor/cmd_pages_create_v3.go +++ b/mdl/executor/cmd_pages_create_v3.go @@ -63,6 +63,7 @@ func execCreatePageV3(ctx *ExecContext, s *ast.CreatePageStmtV3) error { execCache: ctx.Cache, fragments: ctx.Fragments, themeRegistry: ctx.GetThemeRegistry(), + widgetBackend: ctx.Backend, } page, err := pb.buildPageV3(s) @@ -139,6 +140,7 @@ func execCreateSnippetV3(ctx *ExecContext, s *ast.CreateSnippetStmtV3) error { execCache: ctx.Cache, fragments: ctx.Fragments, themeRegistry: ctx.GetThemeRegistry(), + widgetBackend: ctx.Backend, } snippet, err := pb.buildSnippetV3(s) diff --git a/mdl/executor/widget_defaults.go b/mdl/executor/widget_defaults.go deleted file mode 100644 index 256fbfd7..00000000 --- a/mdl/executor/widget_defaults.go +++ /dev/null @@ -1,151 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package executor - -import ( - "github.com/mendixlabs/mxcli/sdk/pages" - "go.mongodb.org/mongo-driver/bson" -) - -// ============================================================================= -// Default Object List Population -// ============================================================================= - -// ensureRequiredObjectLists populates empty Object list properties with one default -// entry. This prevents CE0642 "Property 'X' is required" errors for widget properties -// like Accordion groups, AreaChart series, etc. -func ensureRequiredObjectLists(obj bson.D, propertyTypeIDs map[string]pages.PropertyTypeIDEntry) bson.D { - for propKey, entry := range propertyTypeIDs { - if entry.ObjectTypeID == "" || len(entry.NestedPropertyIDs) == 0 { - continue - } - // Skip non-required object lists that have nested DataSource properties — - // auto-populating these creates entries that trigger widget-level validation errors. - // Required object lists (like AreaChart series) are populated even with nested DataSource - // because the DataSource is conditional (e.g., depends on dataSet enum). - if !entry.Required { - hasNestedDS := false - for _, nested := range entry.NestedPropertyIDs { - if nested.ValueType == "DataSource" { - hasNestedDS = true - break - } - } - if hasNestedDS { - continue - } - } - // Skip if any Required nested property is Attribute (needs entity context) - hasRequiredAttr := false - for _, nested := range entry.NestedPropertyIDs { - if nested.Required && nested.ValueType == "Attribute" { - hasRequiredAttr = true - break - } - } - if hasRequiredAttr { - continue - } - obj = updateWidgetPropertyValue(obj, propertyTypeIDs, propKey, func(val bson.D) bson.D { - for _, elem := range val { - if elem.Key == "Objects" { - if arr, ok := elem.Value.(bson.A); ok && len(arr) <= 1 { - // Empty Objects array — create one default entry - defaultObj := createDefaultWidgetObject(entry.ObjectTypeID, entry.NestedPropertyIDs) - newArr := bson.A{int32(2), defaultObj} - result := make(bson.D, 0, len(val)) - for _, e := range val { - if e.Key == "Objects" { - result = append(result, bson.E{Key: "Objects", Value: newArr}) - } else { - result = append(result, e) - } - } - return result - } - } - } - return val - }) - } - return obj -} - -// createDefaultWidgetObject creates a minimal WidgetObject BSON entry for an object list. -func createDefaultWidgetObject(objectTypeID string, nestedProps map[string]pages.PropertyTypeIDEntry) bson.D { - propsArr := bson.A{int32(2)} // version marker - for _, entry := range nestedProps { - prop := createDefaultWidgetProperty(entry) - propsArr = append(propsArr, prop) - } - return bson.D{ - {Key: "$ID", Value: generateBinaryID()}, - {Key: "$Type", Value: "CustomWidgets$WidgetObject"}, - {Key: "TypePointer", Value: hexIDToBlob(objectTypeID)}, - {Key: "Properties", Value: propsArr}, - } -} - -// createDefaultWidgetProperty creates a WidgetProperty with default WidgetValue. -func createDefaultWidgetProperty(entry pages.PropertyTypeIDEntry) bson.D { - return bson.D{ - {Key: "$ID", Value: generateBinaryID()}, - {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: hexIDToBlob(entry.PropertyTypeID)}, - {Key: "Value", Value: createDefaultWidgetValue(entry)}, - } -} - -// createDefaultWidgetValue creates a WidgetValue with standard default fields. -// Sets type-specific defaults: Expression→Expression field, TextTemplate→template, etc. -func createDefaultWidgetValue(entry pages.PropertyTypeIDEntry) bson.D { - primitiveVal := entry.DefaultValue - expressionVal := "" - var textTemplate interface{} // nil by default - - // Route default value to the correct field based on ValueType - switch entry.ValueType { - case "Expression": - expressionVal = primitiveVal - primitiveVal = "" - case "TextTemplate": - // Create a ClientTemplate with a placeholder translation to satisfy CE4899 - text := primitiveVal - if text == "" { - text = " " // non-empty to satisfy "required" translation check - } - textTemplate = createDefaultClientTemplateBSON(text) - case "String": - if primitiveVal == "" { - primitiveVal = " " // non-empty to satisfy required String properties - } - } - - return bson.D{ - {Key: "$ID", Value: generateBinaryID()}, - {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, - {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: generateBinaryID()}, - {Key: "$Type", Value: "Forms$NoAction"}, - {Key: "DisabledDuringExecution", Value: true}, - }}, - {Key: "AttributeRef", Value: nil}, - {Key: "DataSource", Value: nil}, - {Key: "EntityRef", Value: nil}, - {Key: "Expression", Value: expressionVal}, - {Key: "Form", Value: ""}, - {Key: "Icon", Value: nil}, - {Key: "Image", Value: ""}, - {Key: "Microflow", Value: ""}, - {Key: "Nanoflow", Value: ""}, - {Key: "Objects", Value: bson.A{int32(2)}}, - {Key: "PrimitiveValue", Value: primitiveVal}, - {Key: "Selection", Value: "None"}, - {Key: "SourceVariable", Value: nil}, - {Key: "TextTemplate", Value: textTemplate}, - {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: hexIDToBlob(entry.ValueTypeID)}, - {Key: "Widgets", Value: bson.A{int32(2)}}, - {Key: "XPathConstraint", Value: ""}, - } -} diff --git a/mdl/executor/widget_engine.go b/mdl/executor/widget_engine.go index 548fa01b..5ffd656b 100644 --- a/mdl/executor/widget_engine.go +++ b/mdl/executor/widget_engine.go @@ -7,13 +7,11 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/pages" - "github.com/mendixlabs/mxcli/sdk/widgets" - "go.mongodb.org/mongo-driver/bson" ) // defaultSlotContainer is the MDLContainer name that receives default (non-containerized) child widgets. @@ -36,6 +34,16 @@ type WidgetDefinition struct { Modes []WidgetMode `json:"modes,omitempty"` } +// PropertyMapping maps an MDL source (attribute, association, literal, etc.) +// to a pluggable widget property key via a named operation. +type PropertyMapping struct { + PropertyKey string `json:"propertyKey"` + Source string `json:"source,omitempty"` + Value string `json:"value,omitempty"` + Operation string `json:"operation"` + Default string `json:"default,omitempty"` +} + // WidgetMode defines a conditional configuration variant for a widget. // For example, ComboBox has "enumeration" and "association" modes. // Modes are evaluated in order; the first matching condition wins. @@ -64,8 +72,7 @@ type BuildContext struct { EntityName string PrimitiveVal string DataSource pages.DataSource - ChildWidgets []bson.D - ActionBSON bson.D // Serialized client action BSON for opAction + Action pages.ClientAction // Domain-typed client action pageBuilder *pageBuilder } @@ -75,14 +82,14 @@ type BuildContext struct { // PluggableWidgetEngine builds CustomWidget instances from WidgetDefinition + AST. type PluggableWidgetEngine struct { - operations *OperationRegistry + backend backend.WidgetBuilderBackend pageBuilder *pageBuilder } -// NewPluggableWidgetEngine creates a new engine with the given registry and page builder. -func NewPluggableWidgetEngine(ops *OperationRegistry, pb *pageBuilder) *PluggableWidgetEngine { +// NewPluggableWidgetEngine creates a new engine with the given backend and page builder. +func NewPluggableWidgetEngine(b backend.WidgetBuilderBackend, pb *pageBuilder) *PluggableWidgetEngine { return &PluggableWidgetEngine{ - operations: ops, + backend: b, pageBuilder: pb, } } @@ -93,18 +100,16 @@ func (e *PluggableWidgetEngine) Build(def *WidgetDefinition, w *ast.WidgetV3) (* oldEntityContext := e.pageBuilder.entityContext defer func() { e.pageBuilder.entityContext = oldEntityContext }() - // 1. Load template - embeddedType, embeddedObject, embeddedIDs, embeddedObjectTypeID, err := - widgets.GetTemplateFullBSON(def.WidgetID, types.GenerateID, e.pageBuilder.reader.Path()) + // 1. Load template via backend + builder, err := e.backend.LoadWidgetTemplate(def.WidgetID, e.pageBuilder.getProjectPath()) if err != nil { return nil, mdlerrors.NewBackend("load "+def.MDLName+" template", err) } - if embeddedType == nil || embeddedObject == nil { + if builder == nil { return nil, mdlerrors.NewNotFound("template", def.MDLName) } - propertyTypeIDs := convertPropertyTypeIDs(embeddedIDs) - updatedObject := embeddedObject + propertyTypeIDs := builder.PropertyTypeIDs() // 2. Select mode and get mappings/slots mappings, slots, err := e.selectMappings(def, w) @@ -119,21 +124,17 @@ func (e *PluggableWidgetEngine) Build(def *WidgetDefinition, w *ast.WidgetV3) (* return nil, mdlerrors.NewBackend("resolve mapping for "+mapping.PropertyKey, err) } - op := e.operations.Lookup(mapping.Operation) - if op == nil { - return nil, mdlerrors.NewValidationf("unknown operation %q for property %s", mapping.Operation, mapping.PropertyKey) + if err := e.applyOperation(builder, mapping.Operation, mapping.PropertyKey, ctx); err != nil { + return nil, err } - - updatedObject = op(updatedObject, propertyTypeIDs, mapping.PropertyKey, ctx) } // 4. Apply child slots (.def.json) - if err := e.applyChildSlots(slots, w, propertyTypeIDs, &updatedObject); err != nil { + if err := e.applyChildSlots(builder, slots, w, propertyTypeIDs); err != nil { return nil, err } // 4.1 Auto datasource: map AST DataSource to first DataSource-type property. - // Must run BEFORE child slots and explicit properties so entityContext is set. dsHandledByMapping := false for _, m := range mappings { if m.Source == "DataSource" { @@ -149,8 +150,7 @@ func (e *PluggableWidgetEngine) Build(def *WidgetDefinition, w *ast.WidgetV3) (* if err != nil { return nil, mdlerrors.NewBackend("auto datasource for "+propKey, err) } - ctx := &BuildContext{DataSource: dataSource, EntityName: entityName} - updatedObject = opDatasource(updatedObject, propertyTypeIDs, propKey, ctx) + builder.SetDataSource(propKey, dataSource) if entityName != "" { e.pageBuilder.entityContext = entityName } @@ -161,15 +161,10 @@ func (e *PluggableWidgetEngine) Build(def *WidgetDefinition, w *ast.WidgetV3) (* } // 4.3 Auto child slots: match AST children to Widgets-type template properties. - // Two matching strategies: - // 1. Named match: CONTAINER trigger { ... } → property "trigger" (by child name) - // 2. Default slot: direct children not matching any named slot → first Widgets property - // This allows pluggable widget child containers without requiring .def.json ChildSlot entries. handledSlotKeys := make(map[string]bool) for _, s := range slots { handledSlotKeys[s.PropertyKey] = true } - // Collect Widgets-type property keys var widgetsPropKeys []string for propKey, entry := range propertyTypeIDs { if entry.ValueType == "Widgets" && !handledSlotKeys[propKey] { @@ -177,7 +172,7 @@ func (e *PluggableWidgetEngine) Build(def *WidgetDefinition, w *ast.WidgetV3) (* } } // Phase 1: Named matching — match children by name against property keys - matchedChildren := make(map[int]bool) // indices of matched children + matchedChildren := make(map[int]bool) for _, propKey := range widgetsPropKeys { upperKey := strings.ToUpper(propKey) for i, child := range w.Children { @@ -185,18 +180,18 @@ func (e *PluggableWidgetEngine) Build(def *WidgetDefinition, w *ast.WidgetV3) (* continue } if strings.ToUpper(child.Name) == upperKey { - var childBSONs []bson.D + var childWidgets []pages.Widget for _, slotChild := range child.Children { - widgetBSON, err := e.pageBuilder.buildWidgetV3ToBSON(slotChild) + widget, err := e.pageBuilder.buildWidgetV3(slotChild) if err != nil { return nil, err } - if widgetBSON != nil { - childBSONs = append(childBSONs, widgetBSON) + if widget != nil { + childWidgets = append(childWidgets, widget) } } - if len(childBSONs) > 0 { - updatedObject = opWidgets(updatedObject, propertyTypeIDs, propKey, &BuildContext{ChildWidgets: childBSONs}) + if len(childWidgets) > 0 { + builder.SetChildWidgets(propKey, childWidgets) handledSlotKeys[propKey] = true } matchedChildren[i] = true @@ -205,12 +200,11 @@ func (e *PluggableWidgetEngine) Build(def *WidgetDefinition, w *ast.WidgetV3) (* } } // Phase 2: Default slot — unmatched direct children go to first unmatched Widgets property. - // Skip if .def.json has childSlots defined — applyChildSlots already handles direct children. defSlotContainers := make(map[string]bool) for _, s := range slots { defSlotContainers[strings.ToUpper(s.MDLContainer)] = true } - var defaultWidgetBSONs []bson.D + var defaultWidgets []pages.Widget for i, child := range w.Children { if matchedChildren[i] { continue @@ -221,18 +215,18 @@ func (e *PluggableWidgetEngine) Build(def *WidgetDefinition, w *ast.WidgetV3) (* if defSlotContainers[strings.ToUpper(child.Type)] { continue } - widgetBSON, err := e.pageBuilder.buildWidgetV3ToBSON(child) + widget, err := e.pageBuilder.buildWidgetV3(child) if err != nil { return nil, err } - if widgetBSON != nil { - defaultWidgetBSONs = append(defaultWidgetBSONs, widgetBSON) + if widget != nil { + defaultWidgets = append(defaultWidgets, widget) } } - if len(defaultWidgetBSONs) > 0 { + if len(defaultWidgets) > 0 { for _, propKey := range widgetsPropKeys { if !handledSlotKeys[propKey] { - updatedObject = opWidgets(updatedObject, propertyTypeIDs, propKey, &BuildContext{ChildWidgets: defaultWidgetBSONs}) + builder.SetChildWidgets(propKey, defaultWidgets) break } } @@ -270,81 +264,47 @@ func (e *PluggableWidgetEngine) Build(def *WidgetDefinition, w *ast.WidgetV3) (* default: continue } - ctx := &BuildContext{} // Route by ValueType when available switch entry.ValueType { case "Expression": - // Expression properties: set Expression field (not PrimitiveValue) - ctx.PrimitiveVal = strVal - updatedObject = opExpression(updatedObject, propertyTypeIDs, propName, ctx) + builder.SetExpression(propName, strVal) case "TextTemplate": - // TextTemplate properties: create ClientTemplate with attribute parameter binding. - // Syntax: '{AttributeName} - {OtherAttr}' → text '{1} - {2}' with TemplateParameters. entityCtx := e.pageBuilder.entityContext - tmplBSON := createClientTemplateBSONWithParams(strVal, entityCtx) - updatedObject = updateWidgetPropertyValue(updatedObject, propertyTypeIDs, propName, func(val bson.D) bson.D { - result := make(bson.D, 0, len(val)) - for _, elem := range val { - if elem.Key == "TextTemplate" { - result = append(result, bson.E{Key: "TextTemplate", Value: tmplBSON}) - } else { - result = append(result, elem) - } - } - return result - }) + builder.SetTextTemplateWithParams(propName, strVal, entityCtx) case "Attribute": - // Attribute properties: resolve path + attrPath := "" if strings.Count(strVal, ".") >= 2 { - ctx.AttributePath = strVal + attrPath = strVal } else if e.pageBuilder.entityContext != "" { - ctx.AttributePath = e.pageBuilder.resolveAttributePath(strVal) + attrPath = e.pageBuilder.resolveAttributePath(strVal) } - if ctx.AttributePath != "" { - updatedObject = opAttribute(updatedObject, propertyTypeIDs, propName, ctx) + if attrPath != "" { + builder.SetAttribute(propName, attrPath) } default: // Known non-attribute types: always use primitive if entry.ValueType != "" && entry.ValueType != "Attribute" { - ctx.PrimitiveVal = strVal - updatedObject = opPrimitive(updatedObject, propertyTypeIDs, propName, ctx) + builder.SetPrimitive(propName, strVal) continue } // Legacy routing for properties without ValueType info if strings.Count(strVal, ".") >= 2 { - ctx.AttributePath = strVal - updatedObject = opAttribute(updatedObject, propertyTypeIDs, propName, ctx) + builder.SetAttribute(propName, strVal) } else if e.pageBuilder.entityContext != "" && !strings.ContainsAny(strVal, " '\"") { - ctx.AttributePath = e.pageBuilder.resolveAttributePath(strVal) - updatedObject = opAttribute(updatedObject, propertyTypeIDs, propName, ctx) + builder.SetAttribute(propName, e.pageBuilder.resolveAttributePath(strVal)) } else { - ctx.PrimitiveVal = strVal - updatedObject = opPrimitive(updatedObject, propertyTypeIDs, propName, ctx) + builder.SetPrimitive(propName, strVal) } } } - // 4.9 Auto-populate required empty object lists (e.g., Accordion groups, AreaChart series) - updatedObject = ensureRequiredObjectLists(updatedObject, propertyTypeIDs) + // 4.9 Auto-populate required empty object lists + builder.EnsureRequiredObjectLists() // 5. Build CustomWidget widgetID := model.ID(types.GenerateID()) - cw := &pages.CustomWidget{ - BaseWidget: pages.BaseWidget{ - BaseElement: model.BaseElement{ - ID: widgetID, - TypeName: "CustomWidgets$CustomWidget", - }, - Name: w.Name, - }, - Label: w.GetLabel(), - Editable: def.DefaultEditable, - RawType: embeddedType, - RawObject: updatedObject, - PropertyTypeIDMap: propertyTypeIDs, - ObjectTypeID: embeddedObjectTypeID, - } + cw := builder.Finalize(widgetID, w.Name, w.GetLabel(), def.DefaultEditable) if err := e.pageBuilder.registerWidgetName(w.Name, cw.ID); err != nil { return nil, err @@ -353,16 +313,41 @@ func (e *PluggableWidgetEngine) Build(def *WidgetDefinition, w *ast.WidgetV3) (* return cw, nil } +// applyOperation dispatches a named operation to the corresponding builder method. +func (e *PluggableWidgetEngine) applyOperation(builder backend.WidgetObjectBuilder, opName string, propKey string, ctx *BuildContext) error { + switch opName { + case "attribute": + builder.SetAttribute(propKey, ctx.AttributePath) + case "association": + builder.SetAssociation(propKey, ctx.AssocPath, ctx.EntityName) + case "primitive": + builder.SetPrimitive(propKey, ctx.PrimitiveVal) + case "selection": + builder.SetSelection(propKey, ctx.PrimitiveVal) + case "expression": + builder.SetExpression(propKey, ctx.PrimitiveVal) + case "datasource": + builder.SetDataSource(propKey, ctx.DataSource) + case "widgets": + // ctx doesn't carry child widgets for this path — handled by applyChildSlots + case "texttemplate": + builder.SetTextTemplate(propKey, ctx.PrimitiveVal) + case "action": + builder.SetAction(propKey, ctx.Action) + case "attributeObjects": + builder.SetAttributeObjects(propKey, ctx.AttributePaths) + default: + return mdlerrors.NewValidationf("unknown operation %q for property %s", opName, propKey) + } + return nil +} + // selectMappings selects the active PropertyMappings and ChildSlotMappings based on mode. -// Modes are evaluated in definition order; the first matching condition wins. -// A mode with no condition acts as the default fallback. func (e *PluggableWidgetEngine) selectMappings(def *WidgetDefinition, w *ast.WidgetV3) ([]PropertyMapping, []ChildSlotMapping, error) { - // No modes defined — use top-level mappings directly if len(def.Modes) == 0 { return def.PropertyMappings, def.ChildSlots, nil } - // Evaluate modes in order; first match wins var fallback *WidgetMode var fallbackCount int for i := range def.Modes { @@ -379,7 +364,6 @@ func (e *PluggableWidgetEngine) selectMappings(def *WidgetDefinition, w *ast.Wid } } - // Use fallback mode if fallback != nil { if fallbackCount > 1 { return nil, nil, mdlerrors.NewValidationf("widget %s has %d modes without conditions; only one default mode is allowed", def.MDLName, fallbackCount) @@ -409,7 +393,6 @@ func (e *PluggableWidgetEngine) evaluateCondition(condition string, w *ast.Widge func (e *PluggableWidgetEngine) resolveMapping(mapping PropertyMapping, w *ast.WidgetV3) (*BuildContext, error) { ctx := &BuildContext{pageBuilder: e.pageBuilder} - // Static value takes priority if mapping.Value != "" { ctx.PrimitiveVal = mapping.Value return ctx, nil @@ -459,7 +442,6 @@ func (e *PluggableWidgetEngine) resolveMapping(mapping PropertyMapping, w *ast.W case "CaptionAttribute": if captionAttr := w.GetStringProp("CaptionAttribute"); captionAttr != "" { - // Resolve relative to entity context if !strings.Contains(captionAttr, ".") && e.pageBuilder.entityContext != "" { captionAttr = e.pageBuilder.entityContext + "." + captionAttr } @@ -467,28 +449,24 @@ func (e *PluggableWidgetEngine) resolveMapping(mapping PropertyMapping, w *ast.W } case "Association": - // For association operation: resolve both assoc path AND entity name from DataSource if attr := w.GetAttribute(); attr != "" { ctx.AssocPath = e.pageBuilder.resolveAssociationPath(attr) } - // 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, mdlerrors.NewValidationf("association %q requires an entity context (add a DataSource mapping before Association)", ctx.AssocPath) } case "OnClick": - // Resolve AST action (stored as Properties["Action"]) into serialized BSON if action := w.GetAction(); action != nil { act, err := e.pageBuilder.buildClientActionV3(action) if err != nil { return nil, mdlerrors.NewBackend("build action", err) } - ctx.ActionBSON = mpr.SerializeClientAction(act) + ctx.Action = act } default: - // Generic fallback: treat source as a property name on the AST widget val := w.GetStringProp(source) if val == "" && mapping.Default != "" { val = mapping.Default @@ -500,65 +478,58 @@ func (e *PluggableWidgetEngine) resolveMapping(mapping PropertyMapping, w *ast.W } // applyChildSlots processes child slot mappings, building child widgets and embedding them. -func (e *PluggableWidgetEngine) applyChildSlots(slots []ChildSlotMapping, w *ast.WidgetV3, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, updatedObject *bson.D) error { +func (e *PluggableWidgetEngine) applyChildSlots(builder backend.WidgetObjectBuilder, slots []ChildSlotMapping, w *ast.WidgetV3, propertyTypeIDs map[string]pages.PropertyTypeIDEntry) error { if len(slots) == 0 { return nil } - // Build a set of slot container names for matching slotContainers := make(map[string]*ChildSlotMapping, len(slots)) for i := range slots { slotContainers[slots[i].MDLContainer] = &slots[i] } - // Group children by slot - slotWidgets := make(map[string][]bson.D) - var defaultWidgets []bson.D + slotWidgets := make(map[string][]pages.Widget) + var defaultWidgets []pages.Widget for _, child := range w.Children { upperType := strings.ToUpper(child.Type) if slot, ok := slotContainers[upperType]; ok { - // Container matches a slot — build its children for _, slotChild := range child.Children { - widgetBSON, err := e.pageBuilder.buildWidgetV3ToBSON(slotChild) + widget, err := e.pageBuilder.buildWidgetV3(slotChild) if err != nil { return err } - if widgetBSON != nil { - slotWidgets[slot.PropertyKey] = append(slotWidgets[slot.PropertyKey], widgetBSON) + if widget != nil { + slotWidgets[slot.PropertyKey] = append(slotWidgets[slot.PropertyKey], widget) } } } else { - // Direct child — default content - widgetBSON, err := e.pageBuilder.buildWidgetV3ToBSON(child) + widget, err := e.pageBuilder.buildWidgetV3(child) if err != nil { return err } - if widgetBSON != nil { - defaultWidgets = append(defaultWidgets, widgetBSON) + if widget != nil { + defaultWidgets = append(defaultWidgets, widget) } } } - // Apply each slot's widgets via its operation for _, slot := range slots { - childBSONs := slotWidgets[slot.PropertyKey] - // If no explicit container children, use default widgets for the first slot - if len(childBSONs) == 0 && len(defaultWidgets) > 0 && slot.MDLContainer == defaultSlotContainer { - childBSONs = defaultWidgets - defaultWidgets = nil // consume once + children := slotWidgets[slot.PropertyKey] + if len(children) == 0 && len(defaultWidgets) > 0 && slot.MDLContainer == defaultSlotContainer { + children = defaultWidgets + defaultWidgets = nil } - if len(childBSONs) == 0 { + if len(children) == 0 { continue } - op := e.operations.Lookup(slot.Operation) - if op == nil { - return mdlerrors.NewValidationf("unknown operation %q for child slot %s", slot.Operation, slot.PropertyKey) + ctx := &BuildContext{} + if err := e.applyOperation(builder, slot.Operation, slot.PropertyKey, ctx); err != nil { + return err } - - ctx := &BuildContext{ChildWidgets: childBSONs} - *updatedObject = op(*updatedObject, propertyTypeIDs, slot.PropertyKey, ctx) + // SetChildWidgets directly — applyOperation skips "widgets" since ctx doesn't carry children + builder.SetChildWidgets(slot.PropertyKey, children) } return nil diff --git a/mdl/executor/widget_engine_test.go b/mdl/executor/widget_engine_test.go index bb4502de..f7be9510 100644 --- a/mdl/executor/widget_engine_test.go +++ b/mdl/executor/widget_engine_test.go @@ -8,9 +8,6 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/mdl/types" - "github.com/mendixlabs/mxcli/sdk/pages" - "go.mongodb.org/mongo-driver/bson" ) func TestWidgetDefinitionJSONRoundTrip(t *testing.T) { @@ -133,44 +130,16 @@ func TestWidgetDefinitionJSONOmitsEmptyOptionalFields(t *testing.T) { } } -func TestOperationRegistryLookupFound(t *testing.T) { - reg := NewOperationRegistry() - - builtinOps := []string{"attribute", "association", "primitive", "selection", "datasource", "widgets"} +func TestKnownOperationsSet(t *testing.T) { + builtinOps := []string{"attribute", "association", "primitive", "selection", "datasource", "widgets", "expression", "texttemplate", "action", "attributeObjects"} for _, name := range builtinOps { - fn := reg.Lookup(name) - if fn == nil { - t.Errorf("Lookup(%q) returned nil, want non-nil", name) + if !knownOperations[name] { + t.Errorf("knownOperations[%q] = false, want true", name) } } -} -func TestOperationRegistryLookupNotFound(t *testing.T) { - reg := NewOperationRegistry() - - fn := reg.Lookup("nonexistent") - if fn != nil { - t.Error("Lookup(\"nonexistent\") should return nil") - } -} - -func TestOperationRegistryCustomRegistration(t *testing.T) { - reg := NewOperationRegistry() - - called := false - reg.Register("custom", func(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D { - called = true - return obj - }) - - fn := reg.Lookup("custom") - if fn == nil { - t.Fatal("Lookup(\"custom\") returned nil after Register") - } - - fn(bson.D{}, nil, "test", &BuildContext{}) - if !called { - t.Error("custom operation was not called") + if knownOperations["nonexistent"] { + t.Error("knownOperations[\"nonexistent\"] should be false") } } @@ -179,9 +148,7 @@ func TestOperationRegistryCustomRegistration(t *testing.T) { // ============================================================================= func TestEvaluateCondition(t *testing.T) { - engine := &PluggableWidgetEngine{ - operations: NewOperationRegistry(), - } + engine := &PluggableWidgetEngine{} tests := []struct { name string @@ -248,9 +215,7 @@ func TestEvaluateCondition(t *testing.T) { } func TestEvaluateConditionUnknownReturnsFalse(t *testing.T) { - engine := &PluggableWidgetEngine{ - operations: NewOperationRegistry(), - } + engine := &PluggableWidgetEngine{} w := &ast.WidgetV3{Properties: map[string]any{}} result := engine.evaluateCondition("typoCondition", w) @@ -261,7 +226,7 @@ func TestEvaluateConditionUnknownReturnsFalse(t *testing.T) { } func TestSelectMappings_NoModes(t *testing.T) { - engine := &PluggableWidgetEngine{operations: NewOperationRegistry()} + engine := &PluggableWidgetEngine{} def := &WidgetDefinition{ PropertyMappings: []PropertyMapping{ @@ -286,7 +251,7 @@ func TestSelectMappings_NoModes(t *testing.T) { } func TestSelectMappings_WithModes(t *testing.T) { - engine := &PluggableWidgetEngine{operations: NewOperationRegistry()} + engine := &PluggableWidgetEngine{} def := &WidgetDefinition{ Modes: []WidgetMode{ @@ -330,7 +295,7 @@ func TestSelectMappings_WithModes(t *testing.T) { } func TestResolveMapping_StaticValue(t *testing.T) { - engine := &PluggableWidgetEngine{operations: NewOperationRegistry()} + engine := &PluggableWidgetEngine{} mapping := PropertyMapping{ PropertyKey: "optionsSourceType", @@ -355,7 +320,6 @@ func TestResolveMapping_AttributeSource(t *testing.T) { widgetScope: map[string]model.ID{}, } engine := &PluggableWidgetEngine{ - operations: NewOperationRegistry(), pageBuilder: pb, } @@ -376,7 +340,7 @@ func TestResolveMapping_AttributeSource(t *testing.T) { } func TestResolveMapping_SelectionWithDefault(t *testing.T) { - engine := &PluggableWidgetEngine{operations: NewOperationRegistry()} + engine := &PluggableWidgetEngine{} mapping := PropertyMapping{ PropertyKey: "itemSelection", @@ -409,7 +373,7 @@ func TestResolveMapping_SelectionWithDefault(t *testing.T) { } func TestResolveMapping_GenericProp(t *testing.T) { - engine := &PluggableWidgetEngine{operations: NewOperationRegistry()} + engine := &PluggableWidgetEngine{} mapping := PropertyMapping{ PropertyKey: "customProp", @@ -428,7 +392,7 @@ func TestResolveMapping_GenericProp(t *testing.T) { } func TestResolveMapping_EmptySource(t *testing.T) { - engine := &PluggableWidgetEngine{operations: NewOperationRegistry()} + engine := &PluggableWidgetEngine{} mapping := PropertyMapping{ PropertyKey: "someProp", @@ -452,7 +416,6 @@ func TestResolveMapping_CaptionAttribute(t *testing.T) { widgetScope: map[string]model.ID{}, } engine := &PluggableWidgetEngine{ - operations: NewOperationRegistry(), pageBuilder: pb, } @@ -479,7 +442,6 @@ func TestResolveMapping_Association(t *testing.T) { widgetScope: map[string]model.ID{}, } engine := &PluggableWidgetEngine{ - operations: NewOperationRegistry(), pageBuilder: pb, } @@ -501,110 +463,3 @@ func TestResolveMapping_Association(t *testing.T) { t.Errorf("expected EntityName='Module.Order', got %q", ctx.EntityName) } } - -func TestSetChildWidgets(t *testing.T) { - val := bson.D{ - {Key: "PrimitiveValue", Value: ""}, - {Key: "Widgets", Value: bson.A{int32(2)}}, - {Key: "XPathConstraint", Value: ""}, - } - - childWidgets := []bson.D{ - {{Key: "$Type", Value: "Forms$TextBox"}, {Key: "Name", Value: "textBox1"}}, - {{Key: "$Type", Value: "Forms$TextBox"}, {Key: "Name", Value: "textBox2"}}, - } - - updated := setChildWidgets(val, childWidgets) - - // Find Widgets field - for _, elem := range updated { - if elem.Key == "Widgets" { - arr, ok := elem.Value.(bson.A) - if !ok { - t.Fatal("Widgets value is not bson.A") - } - // Should have version marker + 2 widgets - if len(arr) != 3 { - t.Errorf("Widgets array length: got %d, want 3", len(arr)) - } - // First element should be version marker - if marker, ok := arr[0].(int32); !ok || marker != 2 { - t.Errorf("Widgets[0]: got %v, want int32(2)", arr[0]) - } - return - } - } - t.Error("Widgets field not found in result") -} - -func TestOpSelection(t *testing.T) { - // Call the real opSelection function with a properly structured widget BSON. - typePointerBytes := []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16} - typePointerUUID := types.BlobToUUID(typePointerBytes) - - widgetObj := bson.D{ - {Key: "Properties", Value: bson.A{ - int32(2), // version marker - bson.D{ - {Key: "TypePointer", Value: typePointerBytes}, - {Key: "Value", Value: bson.D{ - {Key: "PrimitiveValue", Value: ""}, - {Key: "Selection", Value: "None"}, - }}, - }, - }}, - } - - propTypeIDs := map[string]pages.PropertyTypeIDEntry{ - "selectionType": {PropertyTypeID: typePointerUUID}, - } - - ctx := &BuildContext{PrimitiveVal: "Multi"} - result := opSelection(widgetObj, propTypeIDs, "selectionType", ctx) - - // Extract the updated Value from Properties - var props bson.A - for _, elem := range result { - if elem.Key == "Properties" { - props = elem.Value.(bson.A) - } - } - prop := props[1].(bson.D) // skip version marker at index 0 - var val bson.D - for _, elem := range prop { - if elem.Key == "Value" { - val = elem.Value.(bson.D) - } - } - - selectionFound := false - for _, elem := range val { - if elem.Key == "Selection" { - selectionFound = true - if elem.Value != "Multi" { - t.Errorf("Selection: got %q, want %q", elem.Value, "Multi") - } - } - if elem.Key == "PrimitiveValue" { - if elem.Value != "" { - t.Errorf("PrimitiveValue should remain empty, got %q", elem.Value) - } - } - } - if !selectionFound { - t.Error("Selection field not found in result") - } -} - -func TestOpSelectionEmptyValue(t *testing.T) { - widgetObj := bson.D{ - {Key: "Properties", Value: bson.A{int32(2)}}, - } - ctx := &BuildContext{PrimitiveVal: ""} - result := opSelection(widgetObj, nil, "any", ctx) - - // With empty PrimitiveVal, opSelection returns obj unchanged - if len(result) != len(widgetObj) { - t.Errorf("expected unchanged obj, got different length: %d vs %d", len(result), len(widgetObj)) - } -} diff --git a/mdl/executor/widget_operations.go b/mdl/executor/widget_operations.go deleted file mode 100644 index 183d57c9..00000000 --- a/mdl/executor/widget_operations.go +++ /dev/null @@ -1,301 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package executor - -import ( - "log" - - "github.com/mendixlabs/mxcli/mdl/bsonutil" - "github.com/mendixlabs/mxcli/mdl/types" - "github.com/mendixlabs/mxcli/sdk/pages" - "go.mongodb.org/mongo-driver/bson" -) - -// PropertyMapping maps an MDL source (attribute, association, literal, etc.) -// to a pluggable widget property key via a named operation. -type PropertyMapping struct { - PropertyKey string `json:"propertyKey"` - Source string `json:"source,omitempty"` - Value string `json:"value,omitempty"` - Operation string `json:"operation"` - Default string `json:"default,omitempty"` -} - -// OperationFunc updates a template object's property identified by propertyKey. -// It receives the current object BSON, the property type ID map, the target key, -// and the build context containing resolved values. -type OperationFunc func(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D - -// OperationRegistry maps operation names to their implementations. -type OperationRegistry struct { - operations map[string]OperationFunc -} - -// NewOperationRegistry creates a registry pre-loaded with the built-in operations. -func NewOperationRegistry() *OperationRegistry { - reg := &OperationRegistry{ - operations: make(map[string]OperationFunc), - } - reg.Register("attribute", opAttribute) - reg.Register("association", opAssociation) - reg.Register("primitive", opPrimitive) - reg.Register("selection", opSelection) - reg.Register("datasource", opDatasource) - reg.Register("widgets", opWidgets) - reg.Register("texttemplate", opTextTemplate) - reg.Register("action", opAction) - reg.Register("attributeObjects", opAttributeObjects) - return reg -} - -// Register adds or replaces an operation by name. -func (r *OperationRegistry) Register(name string, fn OperationFunc) { - r.operations[name] = fn -} - -// Lookup returns the operation function for the given name, or nil if not found. -func (r *OperationRegistry) Lookup(name string) OperationFunc { - return r.operations[name] -} - -// Has returns true if the named operation is registered. -func (r *OperationRegistry) Has(name string) bool { - _, ok := r.operations[name] - return ok -} - -// ============================================================================= -// Built-in Operations -// ============================================================================= - -// opAttribute sets an attribute reference on a widget property. -func opAttribute(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D { - if ctx.AttributePath == "" { - return obj - } - return updateWidgetPropertyValue(obj, propTypeIDs, propertyKey, func(val bson.D) bson.D { - return setAttributeRef(val, ctx.AttributePath) - }) -} - -// opAssociation sets an association reference (AttributeRef + EntityRef) on a widget property. -func opAssociation(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D { - if ctx.AssocPath == "" { - return obj - } - return updateWidgetPropertyValue(obj, propTypeIDs, propertyKey, func(val bson.D) bson.D { - return setAssociationRef(val, ctx.AssocPath, ctx.EntityName) - }) -} - -// opPrimitive sets a primitive string value on a widget property. -func opPrimitive(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D { - if ctx.PrimitiveVal == "" { - return obj - } - return updateWidgetPropertyValue(obj, propTypeIDs, propertyKey, func(val bson.D) bson.D { - return setPrimitiveValue(val, ctx.PrimitiveVal) - }) -} - -// opSelection sets a selection mode on a widget property, updating the Selection field -// inside the WidgetValue (which requires a deeper update than opPrimitive's PrimitiveValue). -func opSelection(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D { - if ctx.PrimitiveVal == "" { - return obj - } - return updateWidgetPropertyValue(obj, propTypeIDs, propertyKey, func(val bson.D) bson.D { - result := make(bson.D, 0, len(val)) - for _, elem := range val { - if elem.Key == "Selection" { - result = append(result, bson.E{Key: "Selection", Value: ctx.PrimitiveVal}) - } else { - result = append(result, elem) - } - } - return result - }) -} - -// opExpression sets an expression string on a widget property. -func opExpression(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D { - if ctx.PrimitiveVal == "" { - return obj - } - return updateWidgetPropertyValue(obj, propTypeIDs, propertyKey, func(val bson.D) bson.D { - result := make(bson.D, 0, len(val)) - for _, elem := range val { - if elem.Key == "Expression" { - result = append(result, bson.E{Key: "Expression", Value: ctx.PrimitiveVal}) - } else { - result = append(result, elem) - } - } - return result - }) -} - -// opDatasource sets a data source on a widget property. -func opDatasource(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D { - if ctx.DataSource == nil { - return obj - } - return updateWidgetPropertyValue(obj, propTypeIDs, propertyKey, func(val bson.D) bson.D { - return setDataSource(val, ctx.DataSource) - }) -} - -// opWidgets replaces the Widgets array in a widget property value with child widgets. -func opWidgets(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D { - if len(ctx.ChildWidgets) == 0 { - return obj - } - result := updateWidgetPropertyValue(obj, propTypeIDs, propertyKey, func(val bson.D) bson.D { - return setChildWidgets(val, ctx.ChildWidgets) - }) - return result -} - -// setChildWidgets replaces the Widgets field in a WidgetValue with the given child widgets. -func setChildWidgets(val bson.D, childWidgets []bson.D) bson.D { - widgetsArr := bson.A{int32(2)} // version marker - for _, w := range childWidgets { - widgetsArr = append(widgetsArr, w) - } - - result := make(bson.D, 0, len(val)) - for _, elem := range val { - if elem.Key == "Widgets" { - result = append(result, bson.E{Key: "Widgets", Value: widgetsArr}) - } else { - result = append(result, elem) - } - } - return result -} - -// opTextTemplate sets a text template value on a widget property. -// It replaces the Template.Items in the TextTemplate with a single text item. -func opTextTemplate(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D { - if ctx.PrimitiveVal == "" { - return obj - } - return updateWidgetPropertyValue(obj, propTypeIDs, propertyKey, func(val bson.D) bson.D { - return setTextTemplateValue(val, ctx.PrimitiveVal) - }) -} - -// setTextTemplateValue sets the text content in a TextTemplate WidgetValue field. -func setTextTemplateValue(val bson.D, text string) bson.D { - result := make(bson.D, 0, len(val)) - for _, elem := range val { - if elem.Key == "TextTemplate" { - if tmpl, ok := elem.Value.(bson.D); ok && tmpl != nil { - result = append(result, bson.E{Key: "TextTemplate", Value: updateTemplateText(tmpl, text)}) - } else { - // TextTemplate was null in the template — skip. - // Creating a TextTemplate from null triggers CE0463 because Studio Pro - // detects the structural change. The template must be extracted from a - // widget that already has this property configured in Studio Pro. - result = append(result, elem) - } - } else { - result = append(result, elem) - } - } - return result -} - -// updateTemplateText updates the Template.Items in a Forms$ClientTemplate with a text value. -func updateTemplateText(tmpl bson.D, text string) bson.D { - result := make(bson.D, 0, len(tmpl)) - for _, elem := range tmpl { - if elem.Key == "Template" { - if template, ok := elem.Value.(bson.D); ok { - updated := make(bson.D, 0, len(template)) - for _, tElem := range template { - if tElem.Key == "Items" { - updated = append(updated, bson.E{Key: "Items", Value: bson.A{ - int32(3), - bson.D{ - {Key: "$ID", Value: bsonutil.IDToBsonBinary(types.GenerateID())}, - {Key: "$Type", Value: "Texts$Translation"}, - {Key: "LanguageCode", Value: "en_US"}, - {Key: "Text", Value: text}, - }, - }}) - } else { - updated = append(updated, tElem) - } - } - result = append(result, bson.E{Key: "Template", Value: updated}) - } else { - result = append(result, elem) - } - } else { - result = append(result, elem) - } - } - return result -} - -// opAction sets a client action on a widget property. -func opAction(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D { - if ctx.ActionBSON == nil { - return obj - } - return updateWidgetPropertyValue(obj, propTypeIDs, propertyKey, func(val bson.D) bson.D { - result := make(bson.D, 0, len(val)) - for _, elem := range val { - if elem.Key == "Action" { - result = append(result, bson.E{Key: "Action", Value: ctx.ActionBSON}) - } else { - result = append(result, elem) - } - } - return result - }) -} - -// opAttributeObjects populates the Objects array in an "attributes" property -// with attribute reference objects. Used by filter widgets (TEXTFILTER, etc.). -func opAttributeObjects(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, ctx *BuildContext) bson.D { - if len(ctx.AttributePaths) == 0 { - return obj - } - - entry, ok := propTypeIDs[propertyKey] - if !ok || entry.ObjectTypeID == "" { - return obj - } - - // Get nested "attribute" property IDs from the PropertyTypeIDEntry - nestedEntry, ok := entry.NestedPropertyIDs["attribute"] - if !ok { - return obj - } - - return updateWidgetPropertyValue(obj, propTypeIDs, propertyKey, func(val bson.D) bson.D { - objects := make([]any, 0, len(ctx.AttributePaths)+1) - objects = append(objects, int32(2)) // BSON array version marker - - for _, attrPath := range ctx.AttributePaths { - attrObj, err := ctx.pageBuilder.createAttributeObject(attrPath, entry.ObjectTypeID, nestedEntry.PropertyTypeID, nestedEntry.ValueTypeID) - if err != nil { - log.Printf("warning: skipping attribute %s: %v", attrPath, err) - continue - } - objects = append(objects, attrObj) - } - - result := make(bson.D, 0, len(val)) - for _, elem := range val { - if elem.Key == "Objects" { - result = append(result, bson.E{Key: "Objects", Value: bson.A(objects)}) - } else { - result = append(result, elem) - } - } - return result - }) -} diff --git a/mdl/executor/widget_registry.go b/mdl/executor/widget_registry.go index a0614d84..dcbabfb8 100644 --- a/mdl/executor/widget_registry.go +++ b/mdl/executor/widget_registry.go @@ -18,23 +18,27 @@ import ( type WidgetRegistry struct { byMDLName map[string]*WidgetDefinition // keyed by uppercase MDLName byWidgetID map[string]*WidgetDefinition // keyed by widgetId - opReg *OperationRegistry // used for validating definition operations } -// NewWidgetRegistry creates a registry pre-loaded with embedded definitions. -// Uses a default OperationRegistry for validation. Use NewWidgetRegistryWithOps -// to provide a custom registry with additional operations. -func NewWidgetRegistry() (*WidgetRegistry, error) { - return NewWidgetRegistryWithOps(NewOperationRegistry()) +// knownOperations is the set of operation names supported by the widget engine. +var knownOperations = map[string]bool{ + "attribute": true, + "association": true, + "primitive": true, + "selection": true, + "expression": true, + "datasource": true, + "widgets": true, + "texttemplate": true, + "action": true, + "attributeObjects": true, } -// NewWidgetRegistryWithOps creates a registry pre-loaded with embedded definitions, -// validating operations against the provided OperationRegistry. -func NewWidgetRegistryWithOps(opReg *OperationRegistry) (*WidgetRegistry, error) { +// NewWidgetRegistry creates a registry pre-loaded with embedded definitions. +func NewWidgetRegistry() (*WidgetRegistry, error) { reg := &WidgetRegistry{ byMDLName: make(map[string]*WidgetDefinition), byWidgetID: make(map[string]*WidgetDefinition), - opReg: opReg, } entries, err := definitions.EmbeddedFS.ReadDir(".") @@ -57,7 +61,7 @@ func NewWidgetRegistryWithOps(opReg *OperationRegistry) (*WidgetRegistry, error) return nil, mdlerrors.NewBackend(fmt.Sprintf("parse definition %s", entry.Name()), err) } - if err := validateDefinitionOperations(&def, entry.Name(), opReg); err != nil { + if err := validateDefinitionOperations(&def, entry.Name()); err != nil { return nil, err } @@ -152,7 +156,7 @@ func (r *WidgetRegistry) loadDefinitionsFromDir(dir string) error { return mdlerrors.NewValidationf("invalid definition %s: widgetId and mdlName are required", entry.Name()) } - if err := validateDefinitionOperations(&def, entry.Name(), r.opReg); err != nil { + if err := validateDefinitionOperations(&def, entry.Name()); err != nil { return err } @@ -174,24 +178,24 @@ func (r *WidgetRegistry) loadDefinitionsFromDir(dir string) error { } // validateDefinitionOperations checks that all operation names in a definition -// are recognized by the given OperationRegistry, and validates source/operation +// are recognized by the known operations set, and validates source/operation // compatibility and mapping order dependencies. -func validateDefinitionOperations(def *WidgetDefinition, source string, opReg *OperationRegistry) error { - if err := validateMappings(def.PropertyMappings, source, "", opReg); err != nil { +func validateDefinitionOperations(def *WidgetDefinition, source string) error { + if err := validateMappings(def.PropertyMappings, source, ""); err != nil { return err } for _, s := range def.ChildSlots { - if !opReg.Has(s.Operation) { + if !knownOperations[s.Operation] { return mdlerrors.NewValidationf("%s: unknown operation %q in childSlots for key %q", source, s.Operation, s.PropertyKey) } } for _, mode := range def.Modes { ctx := fmt.Sprintf("mode %q ", mode.Name) - if err := validateMappings(mode.PropertyMappings, source, ctx, opReg); err != nil { + if err := validateMappings(mode.PropertyMappings, source, ctx); err != nil { return err } for _, s := range mode.ChildSlots { - if !opReg.Has(s.Operation) { + if !knownOperations[s.Operation] { return mdlerrors.NewValidationf("%s: unknown operation %q in %schildSlots for key %q", source, s.Operation, ctx, s.PropertyKey) } } @@ -209,10 +213,10 @@ var incompatibleSourceOps = map[string]map[string]bool{ // validateMappings validates a slice of property mappings for operation existence, // source/operation compatibility, and mapping order (Association requires prior DataSource). -func validateMappings(mappings []PropertyMapping, source, modeCtx string, opReg *OperationRegistry) error { +func validateMappings(mappings []PropertyMapping, source, modeCtx string) error { hasDataSource := false for _, m := range mappings { - if !opReg.Has(m.Operation) { + if !knownOperations[m.Operation] { return mdlerrors.NewValidationf("%s: unknown operation %q in %spropertyMappings for key %q", source, m.Operation, modeCtx, m.PropertyKey) } // Check source/operation compatibility diff --git a/mdl/executor/widget_registry_test.go b/mdl/executor/widget_registry_test.go index 6314d65c..b3e8608f 100644 --- a/mdl/executor/widget_registry_test.go +++ b/mdl/executor/widget_registry_test.go @@ -190,8 +190,6 @@ func TestRegistryLoadUserDefinitions(t *testing.T) { } func TestValidateDefinitionOperations_MappingOrderDependency(t *testing.T) { - opReg := NewOperationRegistry() - // Association before DataSource should fail validation badDef := &WidgetDefinition{ WidgetID: "com.example.Bad", @@ -201,7 +199,7 @@ func TestValidateDefinitionOperations_MappingOrderDependency(t *testing.T) { {PropertyKey: "dsProp", Source: "DataSource", Operation: "datasource"}, }, } - if err := validateDefinitionOperations(badDef, "bad.def.json", opReg); err == nil { + if err := validateDefinitionOperations(badDef, "bad.def.json"); err == nil { t.Error("expected error for Association before DataSource, got nil") } @@ -214,7 +212,7 @@ func TestValidateDefinitionOperations_MappingOrderDependency(t *testing.T) { {PropertyKey: "assocProp", Source: "Association", Operation: "association"}, }, } - if err := validateDefinitionOperations(goodDef, "good.def.json", opReg); err != nil { + if err := validateDefinitionOperations(goodDef, "good.def.json"); err != nil { t.Errorf("unexpected error for DataSource before Association: %v", err) } @@ -232,14 +230,12 @@ func TestValidateDefinitionOperations_MappingOrderDependency(t *testing.T) { }, }, } - if err := validateDefinitionOperations(modeDef, "mode.def.json", opReg); err == nil { + if err := validateDefinitionOperations(modeDef, "mode.def.json"); err == nil { t.Error("expected error for Association before DataSource in mode, got nil") } } func TestValidateDefinitionOperations_SourceOperationCompatibility(t *testing.T) { - opReg := NewOperationRegistry() - // Source "Attribute" with Operation "association" should fail badDef := &WidgetDefinition{ WidgetID: "com.example.Bad", @@ -248,7 +244,7 @@ func TestValidateDefinitionOperations_SourceOperationCompatibility(t *testing.T) {PropertyKey: "prop", Source: "Attribute", Operation: "association"}, }, } - if err := validateDefinitionOperations(badDef, "bad.def.json", opReg); err == nil { + if err := validateDefinitionOperations(badDef, "bad.def.json"); err == nil { t.Error("expected error for Source='Attribute' with Operation='association', got nil") } @@ -260,7 +256,7 @@ func TestValidateDefinitionOperations_SourceOperationCompatibility(t *testing.T) {PropertyKey: "prop", Source: "Association", Operation: "attribute"}, }, } - if err := validateDefinitionOperations(badDef2, "bad2.def.json", opReg); err == nil { + if err := validateDefinitionOperations(badDef2, "bad2.def.json"); err == nil { t.Error("expected error for Source='Association' with Operation='attribute', got nil") } } diff --git a/mdl/executor/widget_templates.go b/mdl/executor/widget_templates.go deleted file mode 100644 index d5d5e9c9..00000000 --- a/mdl/executor/widget_templates.go +++ /dev/null @@ -1,162 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package executor - -import ( - "encoding/hex" - "fmt" - "regexp" - "strings" - - "github.com/mendixlabs/mxcli/mdl/types" - "go.mongodb.org/mongo-driver/bson" -) - -// createClientTemplateBSONWithParams creates a Forms$ClientTemplate that supports -// attribute parameter binding. Syntax: '{AttrName} - {OtherAttr}' extracts attribute -// names from curly braces, replaces them with {1}, {2}, etc., and generates -// TemplateParameter entries with AttributeRef bindings. -// If no {AttrName} patterns are found, creates a static text template. -func createClientTemplateBSONWithParams(text string, entityContext string) bson.D { - // Extract {AttributeName} patterns and build parameter list - re := regexp.MustCompile(`\{([A-Za-z][A-Za-z0-9_]*)\}`) - matches := re.FindAllStringSubmatchIndex(text, -1) - - if len(matches) == 0 { - // No attribute references — static text - return createDefaultClientTemplateBSON(text) - } - - // Replace {AttrName} with {1}, {2}, etc. and collect attribute names - var attrNames []string - paramText := text - // Process in reverse to preserve indices - for i := len(matches) - 1; i >= 0; i-- { - match := matches[i] - attrName := text[match[2]:match[3]] - // Check if it's a pure number (like {1}) — keep as-is - if _, err := fmt.Sscanf(attrName, "%d", new(int)); err == nil { - continue - } - attrNames = append([]string{attrName}, attrNames...) // prepend - paramText = paramText[:match[0]] + fmt.Sprintf("{%d}", len(attrNames)) + paramText[match[1]:] - } - - // Rebuild paramText with sequential numbering - paramText = text - attrNames = nil - for i := 0; i < len(matches); i++ { - match := matches[i] - attrName := text[match[2]:match[3]] - if _, err := fmt.Sscanf(attrName, "%d", new(int)); err == nil { - continue - } - attrNames = append(attrNames, attrName) - } - paramText = re.ReplaceAllStringFunc(text, func(s string) string { - name := s[1 : len(s)-1] - if _, err := fmt.Sscanf(name, "%d", new(int)); err == nil { - return s // keep numeric {1} as-is - } - for i, an := range attrNames { - if an == name { - return fmt.Sprintf("{%d}", i+1) - } - } - return s - }) - - // Build parameters BSON - params := bson.A{int32(2)} // version marker for non-empty array - for _, attrName := range attrNames { - attrPath := attrName - if entityContext != "" && !strings.Contains(attrName, ".") { - attrPath = entityContext + "." + attrName - } - params = append(params, bson.D{ - {Key: "$ID", Value: generateBinaryID()}, - {Key: "$Type", Value: "Forms$ClientTemplateParameter"}, - {Key: "AttributeRef", Value: bson.D{ - {Key: "$ID", Value: generateBinaryID()}, - {Key: "$Type", Value: "DomainModels$AttributeRef"}, - {Key: "Attribute", Value: attrPath}, - {Key: "EntityRef", Value: nil}, - }}, - {Key: "Expression", Value: ""}, - {Key: "FormattingInfo", Value: bson.D{ - {Key: "$ID", Value: generateBinaryID()}, - {Key: "$Type", Value: "Forms$FormattingInfo"}, - {Key: "CustomDateFormat", Value: ""}, - {Key: "DateFormat", Value: "Date"}, - {Key: "DecimalPrecision", Value: int64(2)}, - {Key: "EnumFormat", Value: "Text"}, - {Key: "GroupDigits", Value: false}, - {Key: "TimeFormat", Value: "HoursMinutes"}, - }}, - {Key: "SourceVariable", Value: nil}, - }) - } - - makeText := func(t string) bson.D { - return bson.D{ - {Key: "$ID", Value: generateBinaryID()}, - {Key: "$Type", Value: "Texts$Text"}, - {Key: "Items", Value: bson.A{int32(3), bson.D{ - {Key: "$ID", Value: generateBinaryID()}, - {Key: "$Type", Value: "Texts$Translation"}, - {Key: "LanguageCode", Value: "en_US"}, - {Key: "Text", Value: t}, - }}}, - } - } - - return bson.D{ - {Key: "$ID", Value: generateBinaryID()}, - {Key: "$Type", Value: "Forms$ClientTemplate"}, - {Key: "Fallback", Value: makeText(paramText)}, - {Key: "Parameters", Value: params}, - {Key: "Template", Value: makeText(paramText)}, - } -} - -// createDefaultClientTemplateBSON creates a Forms$ClientTemplate with an en_US translation. -func createDefaultClientTemplateBSON(text string) bson.D { - makeText := func(t string) bson.D { - return bson.D{ - {Key: "$ID", Value: generateBinaryID()}, - {Key: "$Type", Value: "Texts$Text"}, - {Key: "Items", Value: bson.A{int32(3), bson.D{ - {Key: "$ID", Value: generateBinaryID()}, - {Key: "$Type", Value: "Texts$Translation"}, - {Key: "LanguageCode", Value: "en_US"}, - {Key: "Text", Value: t}, - }}}, - } - } - return bson.D{ - {Key: "$ID", Value: generateBinaryID()}, - {Key: "$Type", Value: "Forms$ClientTemplate"}, - {Key: "Fallback", Value: makeText(text)}, - {Key: "Parameters", Value: bson.A{int32(2)}}, - {Key: "Template", Value: makeText(text)}, - } -} - -// generateBinaryID creates a new random 16-byte UUID in Microsoft GUID binary format. -func generateBinaryID() []byte { - return hexIDToBlob(types.GenerateID()) -} - -// hexIDToBlob converts a hex UUID string to a 16-byte binary blob in Microsoft GUID format. -func hexIDToBlob(hexStr string) []byte { - hexStr = strings.ReplaceAll(hexStr, "-", "") - data, err := hex.DecodeString(hexStr) - if err != nil || len(data) != 16 { - return data - } - // Swap bytes to match Microsoft GUID format (little-endian for first 3 segments) - data[0], data[1], data[2], data[3] = data[3], data[2], data[1], data[0] - data[4], data[5] = data[5], data[4] - data[6], data[7] = data[7], data[6] - return data -} From 017f20636c0fc023d2fc3ba5e497aeb16072f42e Mon Sep 17 00:00:00 2001 From: Andrew Vasilyev Date: Sun, 19 Apr 2026 17:43:17 +0200 Subject: [PATCH 4/5] refactor: decouple executor from storage layer, extract remaining BSON MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace *mpr.Reader/*mpr.Writer with backend.FullBackend throughout executor. Inject BackendFactory to remove mprbackend import from executor_connect.go. Move all remaining write-path BSON construction (DataGrid2, filters, cloning, widget property updates) behind backend interface. - Remove writer/reader fields from Executor struct - Add BackendFactory injection pattern for connect/disconnect - Create mdl/backend/mpr/datagrid_builder.go (1260 lines) - Add BuildDataGrid2Widget, BuildFilterWidget to WidgetBuilderBackend - Delete bson_helpers.go, cmd_pages_builder_input_cloning.go, cmd_pages_builder_input_datagrid.go, cmd_pages_builder_v3_pluggable.go - Remaining BSON: 3 read-only files (describe, diff) — WASM-safe --- cmd/mxcli/main.go | 3 + examples/create_datagrid2_page/main.go | 3 + mdl/backend/mock/backend.go | 2 + mdl/backend/mock/mock_mutation.go | 14 + mdl/backend/mpr/backend.go | 10 + mdl/backend/mpr/datagrid_builder.go | 1260 +++++++++++++++++ .../mpr/datagrid_builder_test.go} | 10 +- mdl/backend/mutation.go | 36 + mdl/executor/bson_helpers.go | 481 ------- mdl/executor/cmd_alter_page.go | 4 +- mdl/executor/cmd_catalog.go | 8 +- mdl/executor/cmd_export_mappings.go | 19 +- mdl/executor/cmd_features.go | 2 +- mdl/executor/cmd_import_mappings.go | 19 +- mdl/executor/cmd_lint.go | 2 +- mdl/executor/cmd_microflows_builder.go | 4 +- mdl/executor/cmd_microflows_create.go | 2 +- mdl/executor/cmd_pages_builder.go | 30 +- mdl/executor/cmd_pages_builder_input.go | 183 +-- .../cmd_pages_builder_input_cloning.go | 272 ---- .../cmd_pages_builder_input_datagrid.go | 1133 --------------- .../cmd_pages_builder_input_filters.go | 91 -- mdl/executor/cmd_pages_builder_v3.go | 10 +- .../cmd_pages_builder_v3_pluggable.go | 316 ----- mdl/executor/cmd_pages_builder_v3_widgets.go | 199 +-- mdl/executor/cmd_pages_create_v3.go | 6 +- mdl/executor/cmd_widgets.go | 96 +- mdl/executor/executor.go | 67 +- mdl/executor/executor_connect.go | 45 +- mdl/executor/hierarchy.go | 57 +- mdl/executor/widget_property.go | 254 +--- mdl/repl/repl.go | 6 +- 32 files changed, 1523 insertions(+), 3121 deletions(-) create mode 100644 mdl/backend/mpr/datagrid_builder.go rename mdl/{executor/cmd_pages_builder_input_cloning_test.go => backend/mpr/datagrid_builder_test.go} (91%) delete mode 100644 mdl/executor/bson_helpers.go delete mode 100644 mdl/executor/cmd_pages_builder_input_cloning.go delete mode 100644 mdl/executor/cmd_pages_builder_input_datagrid.go delete mode 100644 mdl/executor/cmd_pages_builder_v3_pluggable.go diff --git a/cmd/mxcli/main.go b/cmd/mxcli/main.go index 6b1949e4..2ec38505 100644 --- a/cmd/mxcli/main.go +++ b/cmd/mxcli/main.go @@ -9,6 +9,8 @@ import ( "os" "strings" + "github.com/mendixlabs/mxcli/mdl/backend" + mprbackend "github.com/mendixlabs/mxcli/mdl/backend/mpr" "github.com/mendixlabs/mxcli/mdl/diaglog" "github.com/mendixlabs/mxcli/mdl/executor" "github.com/mendixlabs/mxcli/mdl/repl" @@ -194,6 +196,7 @@ func resolveFormat(cmd *cobra.Command, defaultFormat string) string { func newLoggedExecutor(mode string) (*executor.Executor, *diaglog.Logger) { logger := diaglog.Init(version, mode) exec := executor.New(os.Stdout) + exec.SetBackendFactory(func() backend.FullBackend { return mprbackend.New() }) exec.SetLogger(logger) if globalJSONFlag { exec.SetFormat(executor.FormatJSON) diff --git a/examples/create_datagrid2_page/main.go b/examples/create_datagrid2_page/main.go index aa7a0b2c..705c7c3d 100644 --- a/examples/create_datagrid2_page/main.go +++ b/examples/create_datagrid2_page/main.go @@ -22,6 +22,8 @@ import ( "os" "strings" + "github.com/mendixlabs/mxcli/mdl/backend" + mprbackend "github.com/mendixlabs/mxcli/mdl/backend/mpr" "github.com/mendixlabs/mxcli/mdl/executor" "github.com/mendixlabs/mxcli/mdl/visitor" ) @@ -51,6 +53,7 @@ func main() { // Create the MDL executor with stdout for output exec := executor.New(os.Stdout) + exec.SetBackendFactory(func() backend.FullBackend { return mprbackend.New() }) // Define the MDL script to create a page with DataGrid2 // Note: Adjust module name, entity name, and attributes to match your project diff --git a/mdl/backend/mock/backend.go b/mdl/backend/mock/backend.go index 01a4f1b9..0b32c931 100644 --- a/mdl/backend/mock/backend.go +++ b/mdl/backend/mock/backend.go @@ -276,6 +276,8 @@ type MockBackend struct { SerializeWidgetToOpaqueFunc func(w pages.Widget) any SerializeDataSourceToOpaqueFunc func(ds pages.DataSource) any BuildCreateAttributeObjectFunc func(attributePath string, objectTypeID, propertyTypeID, valueTypeID string) (any, error) + BuildDataGrid2WidgetFunc func(id model.ID, name string, spec backend.DataGridSpec, projectPath string) (*pages.CustomWidget, error) + BuildFilterWidgetFunc func(spec backend.FilterWidgetSpec, projectPath string) (pages.Widget, error) // AgentEditorBackend ListAgentEditorModelsFunc func() ([]*agenteditor.Model, error) diff --git a/mdl/backend/mock/mock_mutation.go b/mdl/backend/mock/mock_mutation.go index 82fabe96..9e6d7337 100644 --- a/mdl/backend/mock/mock_mutation.go +++ b/mdl/backend/mock/mock_mutation.go @@ -94,3 +94,17 @@ func (m *MockBackend) BuildCreateAttributeObject(attributePath string, objectTyp } return nil, nil } + +func (m *MockBackend) BuildDataGrid2Widget(id model.ID, name string, spec backend.DataGridSpec, projectPath string) (*pages.CustomWidget, error) { + if m.BuildDataGrid2WidgetFunc != nil { + return m.BuildDataGrid2WidgetFunc(id, name, spec, projectPath) + } + return nil, nil +} + +func (m *MockBackend) BuildFilterWidget(spec backend.FilterWidgetSpec, projectPath string) (pages.Widget, error) { + if m.BuildFilterWidgetFunc != nil { + return m.BuildFilterWidgetFunc(spec, projectPath) + } + return nil, nil +} diff --git a/mdl/backend/mpr/backend.go b/mdl/backend/mpr/backend.go index 4c5613c7..cb1a9f32 100644 --- a/mdl/backend/mpr/backend.go +++ b/mdl/backend/mpr/backend.go @@ -34,6 +34,11 @@ type MprBackend struct { path string } +// New creates a new unconnected MprBackend. Call Connect(path) to open a project. +func New() *MprBackend { + return &MprBackend{} +} + // Wrap creates an MprBackend that wraps an existing Writer (and its Reader). // This is used during migration when the Executor already owns the Writer // and we want to expose it through the Backend interface without opening @@ -75,6 +80,11 @@ func (b *MprBackend) Disconnect() error { func (b *MprBackend) IsConnected() bool { return b.writer != nil } func (b *MprBackend) Path() string { return b.path } +// MprReader returns the underlying *mpr.Reader for callers that still +// require direct SDK access (e.g. linter rules). Prefer Backend methods +// for new code. +func (b *MprBackend) MprReader() *mpr.Reader { return b.reader } + func (b *MprBackend) Version() types.MPRVersion { return convertMPRVersion(b.reader.Version()) } func (b *MprBackend) ProjectVersion() *types.ProjectVersion { return convertProjectVersion(b.reader.ProjectVersion()) } func (b *MprBackend) GetMendixVersion() (string, error) { return b.reader.GetMendixVersion() } diff --git a/mdl/backend/mpr/datagrid_builder.go b/mdl/backend/mpr/datagrid_builder.go new file mode 100644 index 00000000..aade56fd --- /dev/null +++ b/mdl/backend/mpr/datagrid_builder.go @@ -0,0 +1,1260 @@ +// SPDX-License-Identifier: Apache-2.0 + +package mprbackend + +import ( + "fmt" + "strings" + + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/bson/primitive" + + "github.com/mendixlabs/mxcli/mdl/backend" + "github.com/mendixlabs/mxcli/mdl/bsonutil" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/mpr" + "github.com/mendixlabs/mxcli/sdk/pages" + "github.com/mendixlabs/mxcli/sdk/widgets" +) + +// BuildDataGrid2Widget builds a complete DataGrid2 CustomWidget from domain-typed inputs. +func (b *MprBackend) BuildDataGrid2Widget(id model.ID, name string, spec backend.DataGridSpec, projectPath string) (*pages.CustomWidget, error) { + // Load embedded template + embeddedType, embeddedObject, embeddedIDs, embeddedObjectTypeID, err := + widgets.GetTemplateFullBSON(pages.WidgetIDDataGrid2, types.GenerateID, projectPath) + if err != nil { + return nil, mdlerrors.NewBackend("load DataGrid2 template", err) + } + if embeddedType == nil || embeddedObject == nil { + return nil, mdlerrors.NewNotFound("widget template", "DataGrid2") + } + + propertyTypeIDs := convertPropertyTypeIDs(embeddedIDs) + + // Build the object + var updatedObject bson.D + if len(spec.Columns) > 0 || len(spec.HeaderWidgets) > 0 { + updatedObject = b.updateDataGrid2Object(embeddedObject, propertyTypeIDs, spec) + } else { + updatedObject = b.cloneDataGrid2ObjectWithDatasourceOnly(embeddedObject, propertyTypeIDs, spec.DataSource) + } + + // Apply paging overrides + if len(spec.PagingOverrides) > 0 { + updatedObject = b.applyDataGridPagingProps(updatedObject, propertyTypeIDs, spec.PagingOverrides) + } + + // Apply selection mode + if spec.SelectionMode != "" { + updatedObject = b.applyDataGridSelectionProp(updatedObject, propertyTypeIDs, spec.SelectionMode) + } + + grid := &pages.CustomWidget{ + BaseWidget: pages.BaseWidget{ + BaseElement: model.BaseElement{ + ID: id, + TypeName: "CustomWidgets$CustomWidget", + }, + Name: name, + }, + Editable: "Always", + RawType: embeddedType, + RawObject: updatedObject, + PropertyTypeIDMap: propertyTypeIDs, + ObjectTypeID: embeddedObjectTypeID, + } + + return grid, nil +} + +// BuildFilterWidget builds a filter widget for DataGrid2. +func (b *MprBackend) BuildFilterWidget(spec backend.FilterWidgetSpec, projectPath string) (pages.Widget, error) { + bsonD := b.buildFilterWidgetBSON(spec.WidgetID, spec.FilterName, projectPath) + + // Wrap the BSON in a CustomWidget + w := &pages.CustomWidget{ + BaseWidget: pages.BaseWidget{ + BaseElement: model.BaseElement{ + ID: model.ID(types.GenerateID()), + TypeName: "CustomWidgets$CustomWidget", + }, + Name: spec.FilterName, + }, + Editable: "Inherited", + RawObject: getBsonField(bsonD, "Object"), + RawType: getBsonField(bsonD, "Type"), + } + return w, nil +} + +// =========================================================================== +// DataGrid2 BSON construction (moved from executor) +// =========================================================================== + +func (b *MprBackend) updateDataGrid2Object(templateObject bson.D, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, spec backend.DataGridSpec) bson.D { + result := make(bson.D, 0, len(templateObject)) + + for _, elem := range templateObject { + if elem.Key == "$ID" { + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) + } else if elem.Key == "Properties" { + if propsArr, ok := elem.Value.(bson.A); ok { + updatedProps := b.updateDataGrid2Properties(propsArr, propertyTypeIDs, spec) + result = append(result, bson.E{Key: "Properties", Value: updatedProps}) + } else { + result = append(result, elem) + } + } else { + result = append(result, elem) + } + } + + return result +} + +func (b *MprBackend) updateDataGrid2Properties(props bson.A, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, spec backend.DataGridSpec) bson.A { + result := bson.A{int32(2)} + + datasourceEntry := propertyTypeIDs["datasource"] + columnsEntry := propertyTypeIDs["columns"] + filtersPlaceholderEntry := propertyTypeIDs["filtersPlaceholder"] + + // Serialize header widgets to BSON + var headerWidgetsBSON []bson.D + for _, w := range spec.HeaderWidgets { + headerWidgetsBSON = append(headerWidgetsBSON, mpr.SerializeWidget(w)) + } + + for _, propVal := range props { + if _, ok := propVal.(int32); ok { + continue + } + + propMap, ok := propVal.(bson.D) + if !ok { + continue + } + + typePointer := getTypePointerFromProperty(propMap) + + if typePointer == datasourceEntry.PropertyTypeID { + result = append(result, buildDataGrid2Property(datasourceEntry, spec.DataSource, "", "", b)) + } else if typePointer == columnsEntry.PropertyTypeID { + result = append(result, b.cloneAndUpdateColumnsProperty(propMap, columnsEntry, propertyTypeIDs, spec.Columns)) + } else if typePointer == filtersPlaceholderEntry.PropertyTypeID && len(headerWidgetsBSON) > 0 { + result = append(result, buildFiltersPlaceholderProperty(filtersPlaceholderEntry, headerWidgetsBSON)) + } else { + result = append(result, clonePropertyWithNewIDs(propMap)) + } + } + + return result +} + +func (b *MprBackend) cloneAndUpdateColumnsProperty(templateProp bson.D, columnsEntry pages.PropertyTypeIDEntry, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, columns []backend.DataGridColumnSpec) bson.D { + // Extract template column object + var templateColumnObj bson.D + for _, elem := range templateProp { + if elem.Key == "Value" { + if valMap, ok := elem.Value.(bson.D); ok { + for _, ve := range valMap { + if ve.Key == "Objects" { + if objArr, ok := ve.Value.(bson.A); ok { + for _, obj := range objArr { + if colObj, ok := obj.(bson.D); ok { + templateColumnObj = colObj + break + } + } + } + } + } + } + } + } + + columnObjects := bson.A{int32(2)} + for i := range columns { + col := &columns[i] + if templateColumnObj != nil { + columnObjects = append(columnObjects, b.cloneAndUpdateColumnObject(templateColumnObj, col, columnsEntry.NestedPropertyIDs)) + } else { + columnObjects = append(columnObjects, b.buildDataGrid2ColumnObject(col, columnsEntry.ObjectTypeID, columnsEntry.NestedPropertyIDs)) + } + } + + result := make(bson.D, 0, len(templateProp)) + for _, elem := range templateProp { + if elem.Key == "$ID" { + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) + } else if elem.Key == "Value" { + if valMap, ok := elem.Value.(bson.D); ok { + newVal := make(bson.D, 0, len(valMap)) + for _, ve := range valMap { + if ve.Key == "$ID" { + newVal = append(newVal, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) + } else if ve.Key == "Objects" { + newVal = append(newVal, bson.E{Key: "Objects", Value: columnObjects}) + } else if ve.Key == "Action" { + if actionMap, ok := ve.Value.(bson.D); ok { + newVal = append(newVal, bson.E{Key: "Action", Value: deepCloneWithNewIDs(actionMap)}) + } else { + newVal = append(newVal, ve) + } + } else { + newVal = append(newVal, ve) + } + } + result = append(result, bson.E{Key: "Value", Value: newVal}) + } else { + result = append(result, elem) + } + } else { + result = append(result, elem) + } + } + + return result +} + +func (b *MprBackend) cloneAndUpdateColumnObject(templateCol bson.D, col *backend.DataGridColumnSpec, columnPropertyIDs map[string]pages.PropertyTypeIDEntry) bson.D { + attrPath := col.Attribute + caption := col.Caption + if caption == "" { + caption = col.Attribute + } + + // Serialize child widgets to BSON + var contentWidgets []bson.D + for _, child := range col.ChildWidgets { + contentWidgets = append(contentWidgets, mpr.SerializeWidget(child)) + } + + result := make(bson.D, 0, len(templateCol)) + for _, elem := range templateCol { + if elem.Key == "$ID" { + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) + } else if elem.Key == "Properties" { + if propsArr, ok := elem.Value.(bson.A); ok { + result = append(result, bson.E{Key: "Properties", Value: b.cloneAndUpdateColumnProperties(propsArr, columnPropertyIDs, col, attrPath, caption, contentWidgets)}) + } else { + result = append(result, elem) + } + } else { + result = append(result, elem) + } + } + return result +} + +func (b *MprBackend) cloneAndUpdateColumnProperties(templateProps bson.A, columnPropertyIDs map[string]pages.PropertyTypeIDEntry, col *backend.DataGridColumnSpec, attrPath, caption string, contentWidgets []bson.D) bson.A { + result := bson.A{int32(2)} + + addedProps := make(map[string]bool) + hasCustomContent := len(contentWidgets) > 0 + + for _, propVal := range templateProps { + if _, ok := propVal.(int32); ok { + continue + } + propMap, ok := propVal.(bson.D) + if !ok { + continue + } + + typePointer := getTypePointerFromProperty(propMap) + + propKey := "" + for key, entry := range columnPropertyIDs { + if entry.PropertyTypeID == typePointer { + addedProps[key] = true + propKey = key + break + } + } + + switch propKey { + case "showContentAs": + if hasCustomContent { + result = append(result, clonePropertyWithPrimitiveValue(propMap, "customContent")) + } else { + result = append(result, clonePropertyWithNewIDs(propMap)) + } + case "attribute": + if attrPath != "" { + entry := columnPropertyIDs["attribute"] + result = append(result, buildColumnAttributeProperty(entry, attrPath)) + } else { + result = append(result, clonePropertyWithNewIDs(propMap)) + } + case "header": + entry := columnPropertyIDs["header"] + result = append(result, buildColumnHeaderProperty(entry, caption)) + case "content": + if hasCustomContent { + entry := columnPropertyIDs["content"] + result = append(result, buildColumnContentProperty(entry, contentWidgets)) + } else { + result = append(result, clonePropertyWithNewIDs(propMap)) + } + case "visible": + visExpr := "true" + if col.Properties != nil { + if v, ok := col.Properties["Visible"]; ok { + if sv, isStr := v.(string); isStr && sv != "" { + visExpr = sv + } + } + } + result = append(result, clonePropertyWithExpression(propMap, visExpr)) + + case "columnClass": + classExpr := "" + if col.Properties != nil { + if v, ok := col.Properties["DynamicCellClass"]; ok { + if sv, isStr := v.(string); isStr { + classExpr = sv + } + } + } + result = append(result, clonePropertyWithExpression(propMap, classExpr)) + + case "tooltip": + if hasCustomContent { + result = append(result, clonePropertyClearingTextTemplate(propMap)) + } else { + tooltipText := "" + if col.Properties != nil { + if v, ok := col.Properties["Tooltip"]; ok { + if sv, isStr := v.(string); isStr { + tooltipText = sv + } + } + } + if tooltipText != "" { + entry := columnPropertyIDs["tooltip"] + result = append(result, buildColumnHeaderProperty(entry, tooltipText)) + } else { + result = append(result, clonePropertyWithNewIDs(propMap)) + } + } + case "exportValue": + if hasCustomContent { + entry := columnPropertyIDs["exportValue"] + result = append(result, buildColumnHeaderProperty(entry, "")) + } else { + result = append(result, clonePropertyWithNewIDs(propMap)) + } + case "allowEventPropagation": + result = append(result, clonePropertyWithNewIDs(propMap)) + + case "sortable": + defaultSortable := "false" + if attrPath != "" { + defaultSortable = "true" + } + sortVal := colPropBool(col.Properties, "Sortable", defaultSortable) + result = append(result, clonePropertyWithPrimitiveValue(propMap, sortVal)) + + case "resizable": + resVal := colPropBool(col.Properties, "Resizable", "true") + result = append(result, clonePropertyWithPrimitiveValue(propMap, resVal)) + + case "draggable": + dragVal := colPropBool(col.Properties, "Draggable", "true") + result = append(result, clonePropertyWithPrimitiveValue(propMap, dragVal)) + + case "hidable": + hidVal := colPropString(col.Properties, "Hidable", "yes") + result = append(result, clonePropertyWithPrimitiveValue(propMap, hidVal)) + + case "width": + widthVal := colPropString(col.Properties, "ColumnWidth", "autoFill") + result = append(result, clonePropertyWithPrimitiveValue(propMap, widthVal)) + + case "size": + sizeVal := colPropInt(col.Properties, "Size", "1") + result = append(result, clonePropertyWithPrimitiveValue(propMap, sizeVal)) + + case "wrapText": + wrapVal := "false" + if col.Properties != nil { + if v, ok := col.Properties["WrapText"]; ok { + if bv, isBool := v.(bool); isBool && bv { + wrapVal = "true" + } else if sv, isStr := v.(string); isStr { + wrapVal = strings.ToLower(sv) + } + } + } + result = append(result, clonePropertyWithPrimitiveValue(propMap, wrapVal)) + + case "alignment": + alignVal := "left" + if col.Properties != nil { + if v, ok := col.Properties["Alignment"]; ok { + if sv, isStr := v.(string); isStr && sv != "" { + alignVal = strings.ToLower(sv) + } + } + } + result = append(result, clonePropertyWithPrimitiveValue(propMap, alignVal)) + + default: + result = append(result, clonePropertyWithNewIDs(propMap)) + } + } + + // Add required properties that were missing from template + if !addedProps["visible"] { + if visibleEntry, ok := columnPropertyIDs["visible"]; ok { + visExpr := "true" + if col.Properties != nil { + if v, ok := col.Properties["Visible"]; ok { + if sv, isStr := v.(string); isStr && sv != "" { + visExpr = sv + } + } + } + result = append(result, buildColumnExpressionProperty(visibleEntry, visExpr)) + } + } + + return result +} + +func (b *MprBackend) buildDataGrid2ColumnObject(col *backend.DataGridColumnSpec, columnObjectTypeID string, columnPropertyIDs map[string]pages.PropertyTypeIDEntry) bson.D { + attrPath := col.Attribute + + // Serialize child widgets to BSON + var contentWidgets []bson.D + for _, child := range col.ChildWidgets { + contentWidgets = append(contentWidgets, mpr.SerializeWidget(child)) + } + hasCustomContent := len(contentWidgets) > 0 + + properties := bson.A{int32(2)} + + for key, entry := range columnPropertyIDs { + switch key { + case "showContentAs": + if hasCustomContent { + properties = append(properties, buildColumnPrimitiveProperty(entry, "customContent")) + } else { + properties = append(properties, buildColumnPrimitiveProperty(entry, "attribute")) + } + + case "attribute": + if attrPath != "" { + properties = append(properties, buildColumnAttributeProperty(entry, attrPath)) + } else { + properties = append(properties, buildColumnDefaultProperty(entry)) + } + + case "header": + if col.Caption != "" { + properties = append(properties, buildColumnHeaderProperty(entry, col.Caption)) + } else { + properties = append(properties, buildColumnHeaderProperty(entry, col.Attribute)) + } + + case "content": + if hasCustomContent { + properties = append(properties, buildColumnContentProperty(entry, contentWidgets)) + } else { + properties = append(properties, buildColumnContentProperty(entry, nil)) + } + + case "filter": + properties = append(properties, buildColumnContentProperty(entry, nil)) + + case "visible": + visExpr := "true" + if col.Properties != nil { + if v, ok := col.Properties["Visible"]; ok { + if sv, isStr := v.(string); isStr && sv != "" { + visExpr = sv + } + } + } + properties = append(properties, buildColumnExpressionProperty(entry, visExpr)) + + case "columnClass": + classExpr := "" + if col.Properties != nil { + if v, ok := col.Properties["DynamicCellClass"]; ok { + if sv, isStr := v.(string); isStr { + classExpr = sv + } + } + } + properties = append(properties, buildColumnExpressionProperty(entry, classExpr)) + + case "sortable": + defaultSortable := "false" + if attrPath != "" { + defaultSortable = "true" + } + sortVal := colPropBool(col.Properties, "Sortable", defaultSortable) + properties = append(properties, buildColumnPrimitiveProperty(entry, sortVal)) + + case "resizable": + resVal := colPropBool(col.Properties, "Resizable", "true") + properties = append(properties, buildColumnPrimitiveProperty(entry, resVal)) + + case "draggable": + dragVal := colPropBool(col.Properties, "Draggable", "true") + properties = append(properties, buildColumnPrimitiveProperty(entry, dragVal)) + + case "wrapText": + wrapVal := colPropBool(col.Properties, "WrapText", "false") + properties = append(properties, buildColumnPrimitiveProperty(entry, wrapVal)) + + case "alignment": + alignVal := colPropString(col.Properties, "Alignment", "left") + properties = append(properties, buildColumnPrimitiveProperty(entry, alignVal)) + + case "width": + widthVal := colPropString(col.Properties, "ColumnWidth", "autoFill") + properties = append(properties, buildColumnPrimitiveProperty(entry, widthVal)) + + case "minWidth": + properties = append(properties, buildColumnPrimitiveProperty(entry, "auto")) + + case "size": + sizeVal := colPropInt(col.Properties, "Size", "1") + properties = append(properties, buildColumnPrimitiveProperty(entry, sizeVal)) + + case "hidable": + hidVal := colPropString(col.Properties, "Hidable", "yes") + properties = append(properties, buildColumnPrimitiveProperty(entry, hidVal)) + + case "tooltip": + if hasCustomContent { + properties = append(properties, buildColumnDefaultProperty(entry)) + } else { + tooltipText := "" + if col.Properties != nil { + if v, ok := col.Properties["Tooltip"]; ok { + if sv, isStr := v.(string); isStr { + tooltipText = sv + } + } + } + if tooltipText != "" { + properties = append(properties, buildColumnHeaderProperty(entry, tooltipText)) + } else { + properties = append(properties, buildColumnDefaultProperty(entry)) + } + } + + default: + switch entry.ValueType { + case "Expression": + properties = append(properties, buildColumnExpressionProperty(entry, "")) + default: + properties = append(properties, buildColumnDefaultProperty(entry)) + } + } + } + + var typePointer any + if columnObjectTypeID != "" { + typePointer = bsonutil.IDToBsonBinary(columnObjectTypeID) + } + + return bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "CustomWidgets$WidgetObject"}, + {Key: "Properties", Value: properties}, + {Key: "TypePointer", Value: typePointer}, + } +} + +func (b *MprBackend) cloneDataGrid2ObjectWithDatasourceOnly(templateObject bson.D, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, datasource pages.DataSource) bson.D { + result := make(bson.D, 0, len(templateObject)) + + for _, elem := range templateObject { + if elem.Key == "$ID" { + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) + } else if elem.Key == "Properties" { + if propsArr, ok := elem.Value.(bson.A); ok { + updatedProps := b.updateOnlyDatasource(propsArr, propertyTypeIDs, datasource) + result = append(result, bson.E{Key: "Properties", Value: updatedProps}) + } else { + result = append(result, elem) + } + } else { + result = append(result, elem) + } + } + + return result +} + +func (b *MprBackend) updateOnlyDatasource(props bson.A, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, datasource pages.DataSource) bson.A { + result := bson.A{int32(2)} + datasourceEntry := propertyTypeIDs["datasource"] + + for _, propVal := range props { + if _, ok := propVal.(int32); ok { + continue + } + propMap, ok := propVal.(bson.D) + if !ok { + continue + } + + typePointer := getTypePointerFromProperty(propMap) + if typePointer == datasourceEntry.PropertyTypeID { + result = append(result, buildDataGrid2Property(datasourceEntry, datasource, "", "", b)) + } else { + result = append(result, clonePropertyWithNewIDs(propMap)) + } + } + + return result +} + +// applyDataGridPagingProps applies paging property overrides to a DataGrid2 BSON object. +func (b *MprBackend) applyDataGridPagingProps(obj bson.D, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, overrides map[string]string) bson.D { + if len(overrides) == 0 { + return obj + } + + typePointerToKey := make(map[string]string) + for widgetKey, entry := range propertyTypeIDs { + typePointerToKey[entry.PropertyTypeID] = widgetKey + } + + result := make(bson.D, 0, len(obj)) + for _, elem := range obj { + if elem.Key == "Properties" { + if propsArr, ok := elem.Value.(bson.A); ok { + updatedProps := bson.A{propsArr[0]} + for _, propVal := range propsArr[1:] { + propMap, ok := propVal.(bson.D) + if !ok { + updatedProps = append(updatedProps, propVal) + continue + } + tp := getTypePointerFromProperty(propMap) + widgetKey := typePointerToKey[tp] + if newVal, hasOverride := overrides[widgetKey]; hasOverride { + updatedProps = append(updatedProps, clonePropertyWithPrimitiveValue(propMap, newVal)) + } else { + updatedProps = append(updatedProps, propMap) + } + } + result = append(result, bson.E{Key: "Properties", Value: updatedProps}) + } else { + result = append(result, elem) + } + } else { + result = append(result, elem) + } + } + return result +} + +// applyDataGridSelectionProp applies the Selection mode to a DataGrid2 object. +func (b *MprBackend) applyDataGridSelectionProp(obj bson.D, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, selectionMode string) bson.D { + itemSelectionEntry, ok := propertyTypeIDs["itemSelection"] + if !ok { + return obj + } + + result := make(bson.D, 0, len(obj)) + for _, elem := range obj { + if elem.Key == "Properties" { + if propsArr, ok := elem.Value.(bson.A); ok { + updatedProps := bson.A{propsArr[0]} + for _, propVal := range propsArr[1:] { + propMap, ok := propVal.(bson.D) + if !ok { + updatedProps = append(updatedProps, propVal) + continue + } + tp := getTypePointerFromProperty(propMap) + if tp == itemSelectionEntry.PropertyTypeID { + updatedProps = append(updatedProps, buildGallerySelectionProperty(propMap, selectionMode)) + } else { + updatedProps = append(updatedProps, propMap) + } + } + result = append(result, bson.E{Key: "Properties", Value: updatedProps}) + } else { + result = append(result, elem) + } + } else { + result = append(result, elem) + } + } + return result +} + +// =========================================================================== +// BSON property builders (package-level, no receiver needed) +// =========================================================================== + +func buildDataGrid2Property(entry pages.PropertyTypeIDEntry, datasource pages.DataSource, attrRef string, primitiveValue string, b *MprBackend) bson.D { + var datasourceBSON any + if datasource != nil { + datasourceBSON = mpr.SerializeCustomWidgetDataSource(datasource) + } + + var attrRefBSON any + if attrRef != "" { + attrRefBSON = bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "DomainModels$AttributeRef"}, + {Key: "Attribute", Value: attrRef}, + {Key: "EntityRef", Value: nil}, + } + } + + return bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, + {Key: "Value", Value: buildDefaultWidgetValueBSON(entry, datasourceBSON, attrRefBSON, primitiveValue, nil, nil)}, + } +} + +func buildFiltersPlaceholderProperty(entry pages.PropertyTypeIDEntry, widgetsBSON []bson.D) bson.D { + widgetsArray := bson.A{int32(2)} + for _, w := range widgetsBSON { + widgetsArray = append(widgetsArray, w) + } + + return bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, + {Key: "Value", Value: buildDefaultWidgetValueBSON(entry, nil, nil, "", nil, widgetsArray)}, + } +} + +func buildColumnPrimitiveProperty(entry pages.PropertyTypeIDEntry, value string) bson.D { + return bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, + {Key: "Value", Value: buildDefaultWidgetValueBSON(entry, nil, nil, value, nil, nil)}, + } +} + +func buildColumnExpressionProperty(entry pages.PropertyTypeIDEntry, expression string) bson.D { + return bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, + {Key: "Value", Value: bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, + {Key: "Action", Value: bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Forms$NoAction"}, + {Key: "DisabledDuringExecution", Value: true}, + }}, + {Key: "AttributeRef", Value: nil}, + {Key: "DataSource", Value: nil}, + {Key: "EntityRef", Value: nil}, + {Key: "Expression", Value: expression}, + {Key: "Form", Value: ""}, + {Key: "Icon", Value: nil}, + {Key: "Image", Value: ""}, + {Key: "Microflow", Value: ""}, + {Key: "Nanoflow", Value: ""}, + {Key: "Objects", Value: bson.A{int32(2)}}, + {Key: "PrimitiveValue", Value: ""}, + {Key: "Selection", Value: "None"}, + {Key: "SourceVariable", Value: nil}, + {Key: "TextTemplate", Value: nil}, + {Key: "TranslatableValue", Value: nil}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, + {Key: "Widgets", Value: bson.A{int32(2)}}, + {Key: "XPathConstraint", Value: ""}, + }}, + } +} + +func buildColumnAttributeProperty(entry pages.PropertyTypeIDEntry, attrPath string) bson.D { + var attributeRef any + if strings.Count(attrPath, ".") >= 2 { + attributeRef = bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "DomainModels$AttributeRef"}, + {Key: "Attribute", Value: attrPath}, + {Key: "EntityRef", Value: nil}, + } + } + return bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, + {Key: "Value", Value: buildDefaultWidgetValueBSON(entry, nil, attributeRef, "", nil, nil)}, + } +} + +func buildColumnHeaderProperty(entry pages.PropertyTypeIDEntry, caption string) bson.D { + textTemplate := buildClientTemplateWithText(caption) + + return bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, + {Key: "Value", Value: buildDefaultWidgetValueBSON(entry, nil, nil, "", textTemplate, nil)}, + } +} + +func buildColumnContentProperty(entry pages.PropertyTypeIDEntry, widgetsList any) bson.D { + widgetsArray := bson.A{int32(2)} + switch w := widgetsList.(type) { + case bson.D: + if w != nil { + widgetsArray = append(widgetsArray, w) + } + case []bson.D: + for _, widget := range w { + widgetsArray = append(widgetsArray, widget) + } + } + + return bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, + {Key: "Value", Value: buildDefaultWidgetValueBSON(entry, nil, nil, "", nil, widgetsArray)}, + } +} + +func buildColumnDefaultProperty(entry pages.PropertyTypeIDEntry) bson.D { + var textTemplate any + if entry.ValueType == "TextTemplate" { + textTemplate = buildEmptyClientTemplate() + } + + return bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, + {Key: "Value", Value: buildDefaultWidgetValueBSON(entry, nil, nil, entry.DefaultValue, textTemplate, nil)}, + } +} + +// buildDefaultWidgetValueBSON builds a WidgetValue BSON with the given overrides. +// nil values use defaults. +func buildDefaultWidgetValueBSON(entry pages.PropertyTypeIDEntry, datasourceBSON any, attrRefBSON any, primitiveValue string, textTemplate any, widgetsArray bson.A) bson.D { + if widgetsArray == nil { + widgetsArray = bson.A{int32(2)} + } + + return bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, + {Key: "Action", Value: bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Forms$NoAction"}, + {Key: "DisabledDuringExecution", Value: true}, + }}, + {Key: "AttributeRef", Value: attrRefBSON}, + {Key: "DataSource", Value: datasourceBSON}, + {Key: "EntityRef", Value: nil}, + {Key: "Expression", Value: ""}, + {Key: "Form", Value: ""}, + {Key: "Icon", Value: nil}, + {Key: "Image", Value: ""}, + {Key: "Microflow", Value: ""}, + {Key: "Nanoflow", Value: ""}, + {Key: "Objects", Value: bson.A{int32(2)}}, + {Key: "PrimitiveValue", Value: primitiveValue}, + {Key: "Selection", Value: "None"}, + {Key: "SourceVariable", Value: nil}, + {Key: "TextTemplate", Value: textTemplate}, + {Key: "TranslatableValue", Value: nil}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, + {Key: "Widgets", Value: widgetsArray}, + {Key: "XPathConstraint", Value: ""}, + } +} + +func buildClientTemplateWithText(text string) bson.D { + return bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Forms$ClientTemplate"}, + {Key: "Fallback", Value: bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Texts$Text"}, + {Key: "Items", Value: bson.A{int32(3)}}, + }}, + {Key: "Parameters", Value: bson.A{int32(2)}}, + {Key: "Template", Value: bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Texts$Text"}, + {Key: "Items", Value: bson.A{ + int32(3), + bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Texts$Translation"}, + {Key: "LanguageCode", Value: "en_US"}, + {Key: "Text", Value: text}, + }, + }}, + }}, + } +} + +func buildEmptyClientTemplate() bson.D { + return bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Forms$ClientTemplate"}, + {Key: "Fallback", Value: bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Texts$Text"}, + {Key: "Items", Value: bson.A{int32(3)}}, + }}, + {Key: "Parameters", Value: bson.A{int32(2)}}, + {Key: "Template", Value: bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "Texts$Text"}, + {Key: "Items", Value: bson.A{int32(3)}}, + }}, + } +} + +// =========================================================================== +// Cloning helpers (package-level) +// =========================================================================== + +func getTypePointerFromProperty(prop bson.D) string { + for _, elem := range prop { + if elem.Key == "TypePointer" { + switch v := elem.Value.(type) { + case primitive.Binary: + return bsonutil.BsonBinaryToID(v) + case []byte: + return types.BlobToUUID(v) + } + } + } + return "" +} + +func clonePropertyWithNewIDs(prop bson.D) bson.D { + result := make(bson.D, 0, len(prop)) + for _, elem := range prop { + if elem.Key == "$ID" { + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) + } else if elem.Key == "Value" { + if valMap, ok := elem.Value.(bson.D); ok { + result = append(result, bson.E{Key: "Value", Value: deepCloneWithNewIDs(valMap)}) + } else { + result = append(result, elem) + } + } else { + result = append(result, elem) + } + } + return result +} + +func clonePropertyWithPrimitiveValue(prop bson.D, newValue string) bson.D { + result := make(bson.D, 0, len(prop)) + for _, elem := range prop { + if elem.Key == "$ID" { + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) + } else if elem.Key == "Value" { + if valMap, ok := elem.Value.(bson.D); ok { + result = append(result, bson.E{Key: "Value", Value: cloneValueWithUpdatedPrimitive(valMap, newValue)}) + } else { + result = append(result, elem) + } + } else { + result = append(result, elem) + } + } + return result +} + +func cloneValueWithUpdatedPrimitive(val bson.D, newValue string) bson.D { + result := make(bson.D, 0, len(val)) + for _, elem := range val { + if elem.Key == "$ID" { + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) + } else if elem.Key == "PrimitiveValue" { + result = append(result, bson.E{Key: "PrimitiveValue", Value: newValue}) + } else { + result = append(result, bson.E{Key: elem.Key, Value: deepCloneValue(elem.Value)}) + } + } + return result +} + +func clonePropertyWithExpression(prop bson.D, newExpr string) bson.D { + result := make(bson.D, 0, len(prop)) + for _, elem := range prop { + if elem.Key == "$ID" { + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) + } else if elem.Key == "Value" { + if valMap, ok := elem.Value.(bson.D); ok { + result = append(result, bson.E{Key: "Value", Value: cloneValueWithUpdatedExpression(valMap, newExpr)}) + } else { + result = append(result, elem) + } + } else { + result = append(result, elem) + } + } + return result +} + +func cloneValueWithUpdatedExpression(val bson.D, newExpr string) bson.D { + result := make(bson.D, 0, len(val)) + for _, elem := range val { + if elem.Key == "$ID" { + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) + } else if elem.Key == "Expression" { + result = append(result, bson.E{Key: "Expression", Value: newExpr}) + } else { + result = append(result, bson.E{Key: elem.Key, Value: deepCloneValue(elem.Value)}) + } + } + return result +} + +func clonePropertyClearingTextTemplate(prop bson.D) bson.D { + result := make(bson.D, 0, len(prop)) + for _, elem := range prop { + if elem.Key == "$ID" { + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) + } else if elem.Key == "Value" { + if valMap, ok := elem.Value.(bson.D); ok { + result = append(result, bson.E{Key: "Value", Value: cloneValueClearingTextTemplate(valMap)}) + } else { + result = append(result, elem) + } + } else { + result = append(result, elem) + } + } + return result +} + +func cloneValueClearingTextTemplate(val bson.D) bson.D { + result := make(bson.D, 0, len(val)) + for _, elem := range val { + if elem.Key == "$ID" { + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) + } else if elem.Key == "TextTemplate" { + result = append(result, bson.E{Key: "TextTemplate", Value: nil}) + } else { + result = append(result, bson.E{Key: elem.Key, Value: deepCloneValue(elem.Value)}) + } + } + return result +} + +// =========================================================================== +// Deep cloning +// =========================================================================== + +func deepCloneWithNewIDs(doc bson.D) bson.D { + result := make(bson.D, 0, len(doc)) + for _, elem := range doc { + if elem.Key == "$ID" { + result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) + } else { + result = append(result, bson.E{Key: elem.Key, Value: deepCloneValue(elem.Value)}) + } + } + return result +} + +func deepCloneValue(v any) any { + switch val := v.(type) { + case bson.D: + return deepCloneWithNewIDs(val) + case bson.A: + return deepCloneArray(val) + case []any: + return deepCloneSlice(val) + default: + return v + } +} + +func deepCloneArray(arr bson.A) bson.A { + result := make(bson.A, len(arr)) + for i, elem := range arr { + result[i] = deepCloneValue(elem) + } + return result +} + +func deepCloneSlice(arr []any) []any { + result := make([]any, len(arr)) + for i, elem := range arr { + result[i] = deepCloneValue(elem) + } + return result +} + +// =========================================================================== +// Column property helpers (domain logic — moved from executor) +// =========================================================================== + +func colPropBool(props map[string]any, key string, defaultVal string) string { + if props == nil { + return defaultVal + } + v, ok := props[key] + if !ok { + return defaultVal + } + switch bv := v.(type) { + case bool: + if bv { + return "true" + } + return "false" + case string: + lower := strings.ToLower(bv) + if lower == "true" || lower == "false" { + return lower + } + return defaultVal + default: + return defaultVal + } +} + +func colPropString(props map[string]any, key string, defaultVal string) string { + if props == nil { + return defaultVal + } + v, ok := props[key] + if !ok { + return defaultVal + } + if sv, isStr := v.(string); isStr && sv != "" { + return strings.ToLower(sv) + } + return defaultVal +} + +func colPropInt(props map[string]any, key string, defaultVal string) string { + if props == nil { + return defaultVal + } + v, ok := props[key] + if !ok { + return defaultVal + } + switch n := v.(type) { + case int: + return fmt.Sprintf("%d", n) + case int64: + return fmt.Sprintf("%d", n) + case float64: + return fmt.Sprintf("%d", int(n)) + case string: + if n != "" { + return n + } + return defaultVal + default: + return defaultVal + } +} + +// =========================================================================== +// Filter widget BSON construction +// =========================================================================== + +func (b *MprBackend) buildFilterWidgetBSON(widgetID, filterName string, projectPath string) bson.D { + rawType, rawObject, _, _, err := widgets.GetTemplateFullBSON(widgetID, types.GenerateID, projectPath) + if err != nil || rawType == nil { + return b.buildMinimalFilterWidgetBSON(widgetID, filterName) + } + + return bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "CustomWidgets$CustomWidget"}, + {Key: "Editable", Value: "Inherited"}, + {Key: "Name", Value: filterName}, + {Key: "Object", Value: rawObject}, + {Key: "TabIndex", Value: int32(0)}, + {Key: "Type", Value: rawType}, + } +} + +func (b *MprBackend) buildMinimalFilterWidgetBSON(widgetID, filterName string) bson.D { + typeID := types.GenerateID() + objectTypeID := types.GenerateID() + objectID := types.GenerateID() + + var widgetTypeName string + switch widgetID { + case pages.WidgetIDDataGridTextFilter: + widgetTypeName = "Text filter" + case pages.WidgetIDDataGridNumberFilter: + widgetTypeName = "Number filter" + case pages.WidgetIDDataGridDateFilter: + widgetTypeName = "Date filter" + case pages.WidgetIDDataGridDropdownFilter: + widgetTypeName = "Drop-down filter" + default: + widgetTypeName = "Text filter" + } + + return bson.D{ + {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, + {Key: "$Type", Value: "CustomWidgets$CustomWidget"}, + {Key: "Editable", Value: "Inherited"}, + {Key: "Name", Value: filterName}, + {Key: "Object", Value: bson.D{ + {Key: "$ID", Value: bsonutil.IDToBsonBinary(objectID)}, + {Key: "$Type", Value: "CustomWidgets$WidgetObject"}, + {Key: "Properties", Value: bson.A{int32(2)}}, + {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(objectTypeID)}, + }}, + {Key: "TabIndex", Value: int32(0)}, + {Key: "Type", Value: bson.D{ + {Key: "$ID", Value: bsonutil.IDToBsonBinary(typeID)}, + {Key: "$Type", Value: "CustomWidgets$CustomWidgetType"}, + {Key: "HelpUrl", Value: ""}, + {Key: "ObjectType", Value: bson.D{ + {Key: "$ID", Value: bsonutil.IDToBsonBinary(objectTypeID)}, + {Key: "$Type", Value: "CustomWidgets$WidgetObjectType"}, + {Key: "PropertyTypes", Value: bson.A{int32(2)}}, + }}, + {Key: "OfflineCapable", Value: true}, + {Key: "StudioCategory", Value: "Data Controls"}, + {Key: "StudioProCategory", Value: "Data controls"}, + {Key: "SupportedPlatform", Value: "Web"}, + {Key: "WidgetDescription", Value: ""}, + {Key: "WidgetId", Value: widgetID}, + {Key: "WidgetName", Value: widgetTypeName}, + {Key: "WidgetNeedsEntityContext", Value: false}, + {Key: "WidgetPluginWidget", Value: true}, + }}, + } +} + +// =========================================================================== +// BSON field helpers +// =========================================================================== + +func getBsonField(d bson.D, key string) bson.D { + for _, elem := range d { + if elem.Key == key { + if nested, ok := elem.Value.(bson.D); ok { + return nested + } + } + } + return nil +} diff --git a/mdl/executor/cmd_pages_builder_input_cloning_test.go b/mdl/backend/mpr/datagrid_builder_test.go similarity index 91% rename from mdl/executor/cmd_pages_builder_input_cloning_test.go rename to mdl/backend/mpr/datagrid_builder_test.go index 0e11bf8b..b6ac2c43 100644 --- a/mdl/executor/cmd_pages_builder_input_cloning_test.go +++ b/mdl/backend/mpr/datagrid_builder_test.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -package executor +package mprbackend import ( "testing" @@ -33,7 +33,6 @@ func TestDeepCloneWithNewIDs_RegeneratesAllIDs(t *testing.T) { cloned := deepCloneWithNewIDs(doc) - // Verify structure is preserved if dGetString(cloned, "$Type") != "Forms$TextBox" { t.Error("$Type not preserved") } @@ -41,13 +40,11 @@ func TestDeepCloneWithNewIDs_RegeneratesAllIDs(t *testing.T) { t.Error("Name not preserved") } - // Verify top-level $ID was regenerated clonedID1 := cloned[0].Value if binaryEqual(clonedID1, origID1) { t.Error("top-level $ID was not regenerated") } - // Verify nested AttributeRef $ID was regenerated attrRef := dGetDoc(cloned, "AttributeRef") if attrRef == nil { t.Fatal("AttributeRef missing") @@ -60,7 +57,6 @@ func TestDeepCloneWithNewIDs_RegeneratesAllIDs(t *testing.T) { t.Error("Attribute value not preserved") } - // Verify deeply nested EntityRef $ID was regenerated entityRef := dGetDoc(attrRef, "EntityRef") if entityRef == nil { t.Fatal("EntityRef missing") @@ -92,7 +88,6 @@ func TestDeepCloneWithNewIDs_HandlesArrays(t *testing.T) { cloned := deepCloneWithNewIDs(doc) - // Check array item's $ID was regenerated items := cloned[1].Value.(bson.A) if len(items) != 2 { t.Fatalf("expected 2 items, got %d", len(items)) @@ -124,7 +119,8 @@ func TestDeepCloneWithNewIDs_PreservesNil(t *testing.T) { } } -// binaryEqual compares two BSON binary values. +// Test helpers + func binaryEqual(a, b any) bool { ab, aOk := a.(primitive.Binary) bb, bOk := b.(primitive.Binary) diff --git a/mdl/backend/mutation.go b/mdl/backend/mutation.go index 5d02b0b1..7eb83517 100644 --- a/mdl/backend/mutation.go +++ b/mdl/backend/mutation.go @@ -249,6 +249,32 @@ type WidgetObjectBuilder interface { Finalize(id model.ID, name string, label string, editable string) *pages.CustomWidget } +// DataGridColumnSpec carries pre-resolved column data for DataGrid2 construction. +// All attribute paths are fully qualified. Child widgets are already built as +// domain objects; the backend serializes them to storage format internally. +type DataGridColumnSpec struct { + Attribute string // Fully qualified attribute path (empty for action/custom-content columns) + Caption string // Column header caption + ChildWidgets []pages.Widget // Pre-built child widgets (for custom-content columns) + Properties map[string]any // Column properties (Sortable, Resizable, Visible, etc.) +} + +// DataGridSpec carries all inputs needed to build a DataGrid2 widget object. +type DataGridSpec struct { + DataSource pages.DataSource + Columns []DataGridColumnSpec + HeaderWidgets []pages.Widget // Pre-built CONTROLBAR widgets for filtersPlaceholder + // Paging overrides (empty string = use template default) + PagingOverrides map[string]string // camelCase widget key → string value + SelectionMode string // empty = no override +} + +// FilterWidgetSpec carries inputs for building a filter widget. +type FilterWidgetSpec struct { + WidgetID string // e.g. pages.WidgetIDDataGridTextFilter + FilterName string // widget name +} + // WidgetBuilderBackend provides pluggable widget construction capabilities. type WidgetBuilderBackend interface { // LoadWidgetTemplate loads a widget template by ID and returns a builder @@ -268,4 +294,14 @@ type WidgetBuilderBackend interface { // BuildCreateAttributeObject creates an attribute object for filter widgets. // Returns an opaque value to be collected into attribute object lists. BuildCreateAttributeObject(attributePath string, objectTypeID, propertyTypeID, valueTypeID string) (any, error) + + // BuildDataGrid2Widget builds a complete DataGrid2 CustomWidget from domain-typed inputs. + // The backend loads the template, constructs the BSON object with columns, + // datasource, header widgets, paging, and selection, and returns a fully + // assembled CustomWidget. Returns the widget with an opaque RawType/RawObject. + BuildDataGrid2Widget(id model.ID, name string, spec DataGridSpec, projectPath string) (*pages.CustomWidget, error) + + // BuildFilterWidget builds a filter widget (text, number, date, or dropdown filter) + // for use inside DataGrid2 filtersPlaceholder or CONTROLBAR sections. + BuildFilterWidget(spec FilterWidgetSpec, projectPath string) (pages.Widget, error) } diff --git a/mdl/executor/bson_helpers.go b/mdl/executor/bson_helpers.go deleted file mode 100644 index eafed38a..00000000 --- a/mdl/executor/bson_helpers.go +++ /dev/null @@ -1,481 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package executor - -import ( - "fmt" - "strings" - - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" - - "github.com/mendixlabs/mxcli/mdl/bsonutil" - mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" - "github.com/mendixlabs/mxcli/mdl/types" -) - -// ============================================================================ -// bson.D helper functions for ordered document access -// ============================================================================ - -// dGet returns the value for a key in a bson.D, or nil if not found. -func dGet(doc bson.D, key string) any { - for _, elem := range doc { - if elem.Key == key { - return elem.Value - } - } - return nil -} - -// dGetDoc returns a nested bson.D field value, or nil. -func dGetDoc(doc bson.D, key string) bson.D { - v := dGet(doc, key) - if d, ok := v.(bson.D); ok { - return d - } - return nil -} - -// dGetString returns a string field value, or "". -func dGetString(doc bson.D, key string) string { - v := dGet(doc, key) - if s, ok := v.(string); ok { - return s - } - return "" -} - -// dSet sets a field value in a bson.D in place. If the key exists, it's updated -// and returns true. If the key is not found, returns false. -func dSet(doc bson.D, key string, value any) bool { - for i := range doc { - if doc[i].Key == key { - doc[i].Value = value - return true - } - } - return false -} - -// dGetArrayElements extracts Mendix array elements from a bson.D field value. -// Handles the int32 type marker at index 0. -func dGetArrayElements(val any) []any { - arr := toBsonA(val) - if len(arr) == 0 { - return nil - } - if _, ok := arr[0].(int32); ok { - return arr[1:] - } - if _, ok := arr[0].(int); ok { - return arr[1:] - } - return arr -} - -// toBsonA converts various BSON array types to []any. -func toBsonA(v any) []any { - switch arr := v.(type) { - case bson.A: - return []any(arr) - case []any: - return arr - default: - return nil - } -} - -// dSetArray sets a Mendix-style BSON array field, preserving the int32 marker. -func dSetArray(doc bson.D, key string, elements []any) { - existing := toBsonA(dGet(doc, key)) - var marker any - if len(existing) > 0 { - if _, ok := existing[0].(int32); ok { - marker = existing[0] - } else if _, ok := existing[0].(int); ok { - marker = existing[0] - } - } - var result bson.A - if marker != nil { - result = make(bson.A, 0, len(elements)+1) - result = append(result, marker) - result = append(result, elements...) - } else { - result = make(bson.A, len(elements)) - copy(result, elements) - } - dSet(doc, key, result) -} - -// extractBinaryIDFromDoc extracts a binary ID string from a bson.D field. -func extractBinaryIDFromDoc(val any) string { - if bin, ok := val.(primitive.Binary); ok { - return types.BlobToUUID(bin.Data) - } - return "" -} - -// ============================================================================ -// BSON widget tree walking (used by cmd_widgets.go) -// ============================================================================ - -// bsonWidgetResult holds a found widget and its parent context. -type bsonWidgetResult struct { - widget bson.D - parentArr []any - parentKey string - parentDoc bson.D - index int - colPropKeys map[string]string -} - -// widgetFinder is a function type for locating widgets in a raw BSON tree. -type widgetFinder func(rawData bson.D, widgetName string) *bsonWidgetResult - -// findBsonWidget searches the raw BSON page tree for a widget by name. -func findBsonWidget(rawData bson.D, widgetName string) *bsonWidgetResult { - formCall := dGetDoc(rawData, "FormCall") - if formCall == nil { - return nil - } - args := dGetArrayElements(dGet(formCall, "Arguments")) - for _, arg := range args { - argDoc, ok := arg.(bson.D) - if !ok { - continue - } - if result := findInWidgetArray(argDoc, "Widgets", widgetName); result != nil { - return result - } - } - return nil -} - -// findBsonWidgetInSnippet searches the raw BSON snippet tree for a widget by name. -func findBsonWidgetInSnippet(rawData bson.D, widgetName string) *bsonWidgetResult { - if result := findInWidgetArray(rawData, "Widgets", widgetName); result != nil { - return result - } - if widgetContainer := dGetDoc(rawData, "Widget"); widgetContainer != nil { - if result := findInWidgetArray(widgetContainer, "Widgets", widgetName); result != nil { - return result - } - } - return nil -} - -// findInWidgetArray searches a widget array (by key in parentDoc) for a named widget. -func findInWidgetArray(parentDoc bson.D, key string, widgetName string) *bsonWidgetResult { - elements := dGetArrayElements(dGet(parentDoc, key)) - for i, elem := range elements { - wDoc, ok := elem.(bson.D) - if !ok { - continue - } - if dGetString(wDoc, "Name") == widgetName { - return &bsonWidgetResult{ - widget: wDoc, - parentArr: elements, - parentKey: key, - parentDoc: parentDoc, - index: i, - } - } - if result := findInWidgetChildren(wDoc, widgetName); result != nil { - return result - } - } - return nil -} - -// findInWidgetChildren recursively searches widget children for a named widget. -func findInWidgetChildren(wDoc bson.D, widgetName string) *bsonWidgetResult { - typeName := dGetString(wDoc, "$Type") - - if result := findInWidgetArray(wDoc, "Widgets", widgetName); result != nil { - return result - } - if result := findInWidgetArray(wDoc, "FooterWidgets", widgetName); result != nil { - return result - } - - // LayoutGrid: Rows[].Columns[].Widgets[] - if strings.Contains(typeName, "LayoutGrid") { - rows := dGetArrayElements(dGet(wDoc, "Rows")) - for _, row := range rows { - rowDoc, ok := row.(bson.D) - if !ok { - continue - } - cols := dGetArrayElements(dGet(rowDoc, "Columns")) - for _, col := range cols { - colDoc, ok := col.(bson.D) - if !ok { - continue - } - if result := findInWidgetArray(colDoc, "Widgets", widgetName); result != nil { - return result - } - } - } - } - - // TabContainer: TabPages[].Widgets[] - tabPages := dGetArrayElements(dGet(wDoc, "TabPages")) - for _, tp := range tabPages { - tpDoc, ok := tp.(bson.D) - if !ok { - continue - } - if result := findInWidgetArray(tpDoc, "Widgets", widgetName); result != nil { - return result - } - } - - // ControlBar - if controlBar := dGetDoc(wDoc, "ControlBar"); controlBar != nil { - if result := findInWidgetArray(controlBar, "Items", widgetName); result != nil { - return result - } - } - - // CustomWidget (pluggable): Object.Properties[].Value.Widgets[] - if strings.Contains(typeName, "CustomWidget") { - if obj := dGetDoc(wDoc, "Object"); obj != nil { - props := dGetArrayElements(dGet(obj, "Properties")) - for _, prop := range props { - propDoc, ok := prop.(bson.D) - if !ok { - continue - } - if valDoc := dGetDoc(propDoc, "Value"); valDoc != nil { - if result := findInWidgetArray(valDoc, "Widgets", widgetName); result != nil { - return result - } - } - } - } - } - - return nil -} - -// setTranslatableText sets a translatable text value in BSON. -func setTranslatableText(parent bson.D, key string, value interface{}) { - strVal, ok := value.(string) - if !ok { - return - } - - target := parent - if key != "" { - if nested := dGetDoc(parent, key); nested != nil { - target = nested - } else { - dSet(parent, key, strVal) - return - } - } - - translations := dGetArrayElements(dGet(target, "Translations")) - if len(translations) > 0 { - if tDoc, ok := translations[0].(bson.D); ok { - dSet(tDoc, "Text", strVal) - return - } - } - dSet(target, "Text", strVal) -} - -// ============================================================================ -// Widget property setting (used by cmd_widgets.go) -// ============================================================================ - -// setRawWidgetProperty sets a property on a raw BSON widget document. -func setRawWidgetProperty(widget bson.D, propName string, value interface{}) error { - switch propName { - case "Caption": - return setWidgetCaption(widget, value) - case "Content": - return setWidgetContent(widget, value) - case "Label": - return setWidgetLabel(widget, value) - case "ButtonStyle": - if s, ok := value.(string); ok { - dSet(widget, "ButtonStyle", s) - } - return nil - case "Class": - if appearance := dGetDoc(widget, "Appearance"); appearance != nil { - if s, ok := value.(string); ok { - dSet(appearance, "Class", s) - } - } - return nil - case "Style": - if appearance := dGetDoc(widget, "Appearance"); appearance != nil { - if s, ok := value.(string); ok { - dSet(appearance, "Style", s) - } - } - return nil - case "Editable": - if s, ok := value.(string); ok { - dSet(widget, "Editable", s) - } - return nil - case "Visible": - if s, ok := value.(string); ok { - dSet(widget, "Visible", s) - } else if b, ok := value.(bool); ok { - if b { - dSet(widget, "Visible", "True") - } else { - dSet(widget, "Visible", "False") - } - } - return nil - case "Name": - if s, ok := value.(string); ok { - dSet(widget, "Name", s) - } - return nil - case "Attribute": - return setWidgetAttributeRef(widget, value) - default: - return setPluggableWidgetProperty(widget, propName, value) - } -} - -func setWidgetCaption(widget bson.D, value interface{}) error { - caption := dGetDoc(widget, "Caption") - if caption == nil { - setTranslatableText(widget, "Caption", value) - return nil - } - setTranslatableText(caption, "", value) - return nil -} - -func setWidgetContent(widget bson.D, value interface{}) error { - strVal, ok := value.(string) - if !ok { - return mdlerrors.NewValidation("Content value must be a string") - } - content := dGetDoc(widget, "Content") - if content == nil { - return mdlerrors.NewValidation("widget has no Content property") - } - template := dGetDoc(content, "Template") - if template == nil { - return mdlerrors.NewValidation("Content has no Template") - } - items := dGetArrayElements(dGet(template, "Items")) - if len(items) > 0 { - if itemDoc, ok := items[0].(bson.D); ok { - dSet(itemDoc, "Text", strVal) - return nil - } - } - return mdlerrors.NewValidation("Content.Template has no Items with Text") -} - -func setWidgetLabel(widget bson.D, value interface{}) error { - label := dGetDoc(widget, "Label") - if label == nil { - return nil - } - setTranslatableText(label, "Caption", value) - return nil -} - -func setWidgetAttributeRef(widget bson.D, value interface{}) error { - attrPath, ok := value.(string) - if !ok { - return mdlerrors.NewValidation("Attribute value must be a string") - } - - var attrRefValue interface{} - if strings.Count(attrPath, ".") >= 2 { - attrRefValue = bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "DomainModels$AttributeRef"}, - {Key: "Attribute", Value: attrPath}, - {Key: "EntityRef", Value: nil}, - } - } else { - attrRefValue = nil - } - - for i, elem := range widget { - if elem.Key == "AttributeRef" { - widget[i].Value = attrRefValue - return nil - } - } - return mdlerrors.NewValidation("widget does not have an AttributeRef property; Attribute can only be SET on input widgets (TextBox, TextArea, DatePicker, etc.)") -} - -func setPluggableWidgetProperty(widget bson.D, propName string, value interface{}) error { - obj := dGetDoc(widget, "Object") - if obj == nil { - return mdlerrors.NewNotFoundMsg("property", propName, fmt.Sprintf("property %q not found (widget has no pluggable Object)", propName)) - } - - propTypeKeyMap := make(map[string]string) - if widgetType := dGetDoc(widget, "Type"); widgetType != nil { - if objType := dGetDoc(widgetType, "ObjectType"); objType != nil { - propTypes := dGetArrayElements(dGet(objType, "PropertyTypes")) - for _, pt := range propTypes { - ptDoc, ok := pt.(bson.D) - if !ok { - continue - } - key := dGetString(ptDoc, "PropertyKey") - if key == "" { - continue - } - id := extractBinaryIDFromDoc(dGet(ptDoc, "$ID")) - if id != "" { - propTypeKeyMap[id] = key - } - } - } - } - - props := dGetArrayElements(dGet(obj, "Properties")) - for _, prop := range props { - propDoc, ok := prop.(bson.D) - if !ok { - continue - } - typePointerID := extractBinaryIDFromDoc(dGet(propDoc, "TypePointer")) - propKey := propTypeKeyMap[typePointerID] - if propKey != propName { - continue - } - if valDoc := dGetDoc(propDoc, "Value"); valDoc != nil { - switch v := value.(type) { - case string: - dSet(valDoc, "PrimitiveValue", v) - case bool: - if v { - dSet(valDoc, "PrimitiveValue", "yes") - } else { - dSet(valDoc, "PrimitiveValue", "no") - } - case int: - dSet(valDoc, "PrimitiveValue", fmt.Sprintf("%d", v)) - case float64: - dSet(valDoc, "PrimitiveValue", fmt.Sprintf("%g", v)) - default: - dSet(valDoc, "PrimitiveValue", fmt.Sprintf("%v", v)) - } - return nil - } - return mdlerrors.NewValidation(fmt.Sprintf("property %q has no Value map", propName)) - } - return mdlerrors.NewNotFound("pluggable property", propName) -} diff --git a/mdl/executor/cmd_alter_page.go b/mdl/executor/cmd_alter_page.go index b05faa28..e7d052c7 100644 --- a/mdl/executor/cmd_alter_page.go +++ b/mdl/executor/cmd_alter_page.go @@ -223,13 +223,11 @@ func applyReplaceWidgetMutator(ctx *ExecContext, mutator backend.PageMutator, op // buildWidgetsFromAST converts AST widgets to pages.Widget domain objects. // It uses the mutator for scope resolution (WidgetScope, ParamScope). func buildWidgetsFromAST(ctx *ExecContext, widgets []*ast.WidgetV3, moduleName string, moduleID model.ID, entityContext string, mutator backend.PageMutator) ([]pages.Widget, error) { - e := ctx.executor paramScope, paramEntityNames := mutator.ParamScope() widgetScope := mutator.WidgetScope() pb := &pageBuilder{ - writer: e.writer, - reader: e.reader, + backend: ctx.Backend, moduleID: moduleID, moduleName: moduleName, entityContext: entityContext, diff --git a/mdl/executor/cmd_catalog.go b/mdl/executor/cmd_catalog.go index 86722751..dedf3b92 100644 --- a/mdl/executor/cmd_catalog.go +++ b/mdl/executor/cmd_catalog.go @@ -702,12 +702,12 @@ func captureDescribeParallel(ctx *ExecContext, objectType string, qualifiedName MprPath: ctx.MprPath, } // If a backing Executor exists, create a local one for handlers that still - // need e.reader/e.output (e.g., describeMicroflow via writeDescribeJSON). + // need e.backend/e.output (e.g., describeMicroflow via writeDescribeJSON). if ctx.executor != nil { local := &Executor{ - reader: ctx.executor.reader, - output: &buf, - cache: ctx.Cache, + backend: ctx.executor.backend, + output: &buf, + cache: ctx.Cache, } localCtx.executor = local } diff --git a/mdl/executor/cmd_export_mappings.go b/mdl/executor/cmd_export_mappings.go index 2633b97c..035ab0b3 100644 --- a/mdl/executor/cmd_export_mappings.go +++ b/mdl/executor/cmd_export_mappings.go @@ -9,10 +9,10 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/mdl/types" - "github.com/mendixlabs/mxcli/sdk/mpr" ) // showExportMappings prints a table of all export mapping documents. @@ -178,7 +178,6 @@ func printExportMappingElement(w io.Writer, elem *model.ExportMappingElement, de // execCreateExportMapping creates a new export mapping. func execCreateExportMapping(ctx *ExecContext, s *ast.CreateExportMappingStmt) error { - e := ctx.executor if !ctx.ConnectedForWrite() { return mdlerrors.NewNotConnectedWrite() } @@ -217,7 +216,7 @@ func execCreateExportMapping(ctx *ExecContext, s *ast.CreateExportMappingStmt) e // Build element tree from the AST definition, cloning JSON structure properties if s.RootElement != nil { - root := buildExportMappingElementModel(s.Name.Module, s.RootElement, "", "(Object)", jsElems, e.reader, true) + root := buildExportMappingElementModel(s.Name.Module, s.RootElement, "", "(Object)", jsElems, ctx.Backend, true) em.Elements = append(em.Elements, root) } @@ -233,10 +232,10 @@ func execCreateExportMapping(ctx *ExecContext, s *ast.CreateExportMappingStmt) e // buildExportMappingElementModel converts an AST element definition to a model element. // It clones properties from the matching JSON structure element and adds mapping bindings. -func buildExportMappingElementModel(moduleName string, def *ast.ExportMappingElementDef, parentEntity, parentPath string, jsElems map[string]*types.JsonElement, reader *mpr.Reader, isRoot bool) *model.ExportMappingElement { +func buildExportMappingElementModel(moduleName string, def *ast.ExportMappingElementDef, parentEntity, parentPath string, jsElems map[string]*types.JsonElement, b backend.FullBackend, isRoot bool) *model.ExportMappingElement { elem := &model.ExportMappingElement{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), }, } @@ -308,7 +307,7 @@ func buildExportMappingElementModel(moduleName string, def *ast.ExportMappingEle itemElem := &model.ExportMappingElement{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "ExportMappings$ObjectMappingElement", }, Kind: "Object", @@ -327,13 +326,13 @@ func buildExportMappingElementModel(moduleName string, def *ast.ExportMappingEle } // Item's children are the value elements for _, valChild := range itemDef.Children { - itemElem.Children = append(itemElem.Children, buildExportMappingElementModel(moduleName, valChild, itemEntity, itemPath, jsElems, reader, false)) + itemElem.Children = append(itemElem.Children, buildExportMappingElementModel(moduleName, valChild, itemEntity, itemPath, jsElems, b, false)) } elem.Children = append(elem.Children, itemElem) } else { // Fallback: treat children as direct item children (no intermediate entity) for _, child := range def.Children { - elem.Children = append(elem.Children, buildExportMappingElementModel(moduleName, child, entity, itemPath, jsElems, reader, false)) + elem.Children = append(elem.Children, buildExportMappingElementModel(moduleName, child, entity, itemPath, jsElems, b, false)) } } } else { @@ -342,14 +341,14 @@ func buildExportMappingElementModel(moduleName string, def *ast.ExportMappingEle elem.Association = assoc elem.ObjectHandling = handling for _, child := range def.Children { - elem.Children = append(elem.Children, buildExportMappingElementModel(moduleName, child, entity, lookupPath, jsElems, reader, false)) + elem.Children = append(elem.Children, buildExportMappingElementModel(moduleName, child, entity, lookupPath, jsElems, b, false)) } } } else { // Value mapping — bind to attribute elem.Kind = "Value" elem.TypeName = "ExportMappings$ValueMappingElement" - elem.DataType = resolveAttributeType(parentEntity, def.Attribute, reader) + elem.DataType = resolveAttributeType(parentEntity, def.Attribute, b) attr := def.Attribute if parentEntity != "" && !strings.Contains(attr, ".") { attr = parentEntity + "." + attr diff --git a/mdl/executor/cmd_features.go b/mdl/executor/cmd_features.go index ccf58809..370d93a0 100644 --- a/mdl/executor/cmd_features.go +++ b/mdl/executor/cmd_features.go @@ -13,7 +13,7 @@ import ( // checkFeature verifies that a feature is available in the connected project's // version. Returns nil if available, or an actionable error with the version -// requirement and a hint. Safe to call when e.reader is nil (returns nil). +// requirement and a hint. Safe to call when not connected (returns nil). func checkFeature(ctx *ExecContext, area, name, statement, hint string) error { if !ctx.Connected() { diff --git a/mdl/executor/cmd_import_mappings.go b/mdl/executor/cmd_import_mappings.go index 4837a8c3..c0a7a8c1 100644 --- a/mdl/executor/cmd_import_mappings.go +++ b/mdl/executor/cmd_import_mappings.go @@ -9,10 +9,10 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/mdl/types" - "github.com/mendixlabs/mxcli/sdk/mpr" ) // showImportMappings prints a table of all import mapping documents. @@ -191,7 +191,6 @@ func printImportMappingElement(w io.Writer, elem *model.ImportMappingElement, de // execCreateImportMapping creates a new import mapping. func execCreateImportMapping(ctx *ExecContext, s *ast.CreateImportMappingStmt) error { - e := ctx.executor if !ctx.ConnectedForWrite() { return mdlerrors.NewNotConnectedWrite() } @@ -226,7 +225,7 @@ func execCreateImportMapping(ctx *ExecContext, s *ast.CreateImportMappingStmt) e // Build element tree from the AST definition, cloning JSON structure properties if s.RootElement != nil { - root := buildImportMappingElementModel(s.Name.Module, s.RootElement, "", "(Object)", e.reader, jsElementsByPath, true) + root := buildImportMappingElementModel(s.Name.Module, s.RootElement, "", "(Object)", ctx.Backend, jsElementsByPath, true) im.Elements = append(im.Elements, root) } @@ -244,10 +243,10 @@ func execCreateImportMapping(ctx *ExecContext, s *ast.CreateImportMappingStmt) e // It clones properties from the matching JSON structure element (ExposedName, JsonPath, // MaxOccurs, ElementType, etc.) and adds mapping-specific bindings (Entity, Attribute, // Association, ObjectHandling). -func buildImportMappingElementModel(moduleName string, def *ast.ImportMappingElementDef, parentEntity, parentPath string, reader *mpr.Reader, jsElems map[string]*types.JsonElement, isRoot bool) *model.ImportMappingElement { +func buildImportMappingElementModel(moduleName string, def *ast.ImportMappingElementDef, parentEntity, parentPath string, b backend.FullBackend, jsElems map[string]*types.JsonElement, isRoot bool) *model.ImportMappingElement { elem := &model.ImportMappingElement{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), }, } @@ -318,13 +317,13 @@ func buildImportMappingElementModel(moduleName string, def *ast.ImportMappingEle } for _, child := range def.Children { - elem.Children = append(elem.Children, buildImportMappingElementModel(moduleName, child, entity, childPath, reader, jsElems, false)) + elem.Children = append(elem.Children, buildImportMappingElementModel(moduleName, child, entity, childPath, b, jsElems, false)) } } else { // Value mapping — bind to attribute elem.Kind = "Value" elem.TypeName = "ImportMappings$ValueMappingElement" - elem.DataType = resolveAttributeType(parentEntity, def.Attribute, reader) + elem.DataType = resolveAttributeType(parentEntity, def.Attribute, b) elem.IsKey = def.IsKey attr := def.Attribute if parentEntity != "" && !strings.Contains(attr, ".") { @@ -349,15 +348,15 @@ func buildJsonElementPathMap(elems []*types.JsonElement, m map[string]*types.Jso // resolveAttributeType looks up the data type of an entity attribute from the project. // Returns "String" as default if the attribute cannot be found. -func resolveAttributeType(entityQN, attrName string, reader *mpr.Reader) string { - if reader == nil || entityQN == "" { +func resolveAttributeType(entityQN, attrName string, b backend.DomainModelBackend) string { + if b == nil || entityQN == "" { return "String" } parts := strings.SplitN(entityQN, ".", 2) if len(parts) != 2 { return "String" } - dms, err := reader.ListDomainModels() + dms, err := b.ListDomainModels() if err != nil { return "String" } diff --git a/mdl/executor/cmd_lint.go b/mdl/executor/cmd_lint.go index 7b7ca383..246d7a3b 100644 --- a/mdl/executor/cmd_lint.go +++ b/mdl/executor/cmd_lint.go @@ -35,7 +35,7 @@ func execLint(ctx *ExecContext, s *ast.LintStmt) error { // Create lint context lintCtx := linter.NewLintContext(ctx.Catalog) - lintCtx.SetReader(e.reader) + lintCtx.SetReader(e.Reader()) // Load configuration projectDir := filepath.Dir(ctx.MprPath) diff --git a/mdl/executor/cmd_microflows_builder.go b/mdl/executor/cmd_microflows_builder.go index e4f49597..7f0d95ff 100644 --- a/mdl/executor/cmd_microflows_builder.go +++ b/mdl/executor/cmd_microflows_builder.go @@ -8,9 +8,9 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/sdk/mpr" ) // flowBuilder helps construct the flow graph from AST statements. @@ -30,7 +30,7 @@ type flowBuilder struct { 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 + reader backend.FullBackend // 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 dedaa545..dd85ae1a 100644 --- a/mdl/executor/cmd_microflows_create.go +++ b/mdl/executor/cmd_microflows_create.go @@ -213,7 +213,7 @@ func execCreateMicroflow(ctx *ExecContext, s *ast.CreateMicroflowStmt) error { varTypes: varTypes, declaredVars: declaredVars, measurer: &layoutMeasurer{varTypes: varTypes}, - reader: e.reader, + reader: ctx.Backend, hierarchy: hierarchy, restServices: restServices, } diff --git a/mdl/executor/cmd_pages_builder.go b/mdl/executor/cmd_pages_builder.go index 30426f40..2da161f3 100644 --- a/mdl/executor/cmd_pages_builder.go +++ b/mdl/executor/cmd_pages_builder.go @@ -15,7 +15,6 @@ import ( "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/sdk/mpr" "github.com/mendixlabs/mxcli/sdk/pages" ) @@ -25,8 +24,7 @@ import ( // pageBuilder constructs pages from AST. type pageBuilder struct { - writer *mpr.Writer - reader *mpr.Reader + backend backend.FullBackend moduleID model.ID moduleName string widgetScope map[string]model.ID // widget name -> widget ID @@ -64,8 +62,8 @@ func (pb *pageBuilder) initPluggableEngine() { log.Printf("warning: %v", pb.pluggableEngineErr) return } - if pb.reader != nil { - if loadErr := registry.LoadUserDefinitions(pb.reader.Path()); loadErr != nil { + if pb.backend != nil { + if loadErr := registry.LoadUserDefinitions(pb.backend.Path()); loadErr != nil { log.Printf("warning: loading user widget definitions: %v", loadErr) } } @@ -78,8 +76,8 @@ func (pb *pageBuilder) initPluggableEngine() { // getProjectPath returns the project directory path from the underlying reader. func (pb *pageBuilder) getProjectPath() string { - if pb.reader != nil { - return pb.reader.Path() + if pb.backend != nil { + return pb.backend.Path() } return "" } @@ -99,7 +97,7 @@ func (pb *pageBuilder) getModules() []*model.Module { if pb.execCache != nil && pb.execCache.modules != nil { return pb.execCache.modules } - modules, _ := pb.reader.ListModules() + modules, _ := pb.backend.ListModules() if pb.execCache != nil { pb.execCache.modules = modules } @@ -111,7 +109,7 @@ func (pb *pageBuilder) getHierarchy() (*ContainerHierarchy, error) { if pb.execCache != nil && pb.execCache.hierarchy != nil { return pb.execCache.hierarchy, nil } - h, err := NewContainerHierarchy(pb.reader) + h, err := NewContainerHierarchyFromBackend(pb.backend) if err != nil { return nil, err } @@ -125,7 +123,7 @@ func (pb *pageBuilder) getHierarchy() (*ContainerHierarchy, error) { func (pb *pageBuilder) getLayouts() ([]*pages.Layout, error) { if pb.layoutsCache == nil { var err error - pb.layoutsCache, err = pb.reader.ListLayouts() + pb.layoutsCache, err = pb.backend.ListLayouts() if err != nil { return nil, err } @@ -138,7 +136,7 @@ func (pb *pageBuilder) getDomainModels() ([]*domainmodel.DomainModel, error) { if pb.execCache != nil && pb.execCache.domainModels != nil { return pb.execCache.domainModels, nil } - domainModels, err := pb.reader.ListDomainModels() + domainModels, err := pb.backend.ListDomainModels() if err != nil { return nil, err } @@ -152,7 +150,7 @@ func (pb *pageBuilder) getDomainModels() ([]*domainmodel.DomainModel, error) { func (pb *pageBuilder) getPages() ([]*pages.Page, error) { if pb.pagesCache == nil { var err error - pb.pagesCache, err = pb.reader.ListPages() + pb.pagesCache, err = pb.backend.ListPages() if err != nil { return nil, err } @@ -164,7 +162,7 @@ func (pb *pageBuilder) getPages() ([]*pages.Page, error) { func (pb *pageBuilder) getMicroflows() ([]*microflows.Microflow, error) { if pb.microflowsCache == nil { var err error - pb.microflowsCache, err = pb.reader.ListMicroflows() + pb.microflowsCache, err = pb.backend.ListMicroflows() if err != nil { return nil, err } @@ -267,7 +265,7 @@ func (pb *pageBuilder) getMainPlaceholderRef(layoutName string) string { func (pb *pageBuilder) getFolders() ([]*types.FolderInfo, error) { if pb.foldersCache == nil { var err error - pb.foldersCache, err = pb.reader.ListFolders() + pb.foldersCache, err = pb.backend.ListFolders() if err != nil { return nil, err } @@ -331,14 +329,14 @@ func (pb *pageBuilder) resolveFolder(folderPath string) (model.ID, error) { func (pb *pageBuilder) createFolder(name string, containerID model.ID) (model.ID, error) { folder := &model.Folder{ BaseElement: model.BaseElement{ - ID: model.ID(mpr.GenerateID()), + ID: model.ID(types.GenerateID()), TypeName: "Projects$Folder", }, ContainerID: containerID, Name: name, } - if err := pb.writer.CreateFolder(folder); err != nil { + if err := pb.backend.CreateFolder(folder); err != nil { return "", err } diff --git a/mdl/executor/cmd_pages_builder_input.go b/mdl/executor/cmd_pages_builder_input.go index c98c9152..0b07d61e 100644 --- a/mdl/executor/cmd_pages_builder_input.go +++ b/mdl/executor/cmd_pages_builder_input.go @@ -3,18 +3,10 @@ package executor import ( - "fmt" - "log" "strings" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" - "github.com/mendixlabs/mxcli/mdl/bsonutil" - "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/pages" - "github.com/mendixlabs/mxcli/sdk/widgets" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" ) // unquoteIdentifier strips surrounding double-quotes or backticks from a quoted identifier. @@ -75,179 +67,6 @@ func (pb *pageBuilder) resolveAssociationPath(assocName string) string { return assocName } -// updateWidgetPropertyValue finds and updates a specific property value in a WidgetObject. -// The updateFn is called with the WidgetValue and should return the modified value. -func updateWidgetPropertyValue(obj bson.D, propTypeIDs map[string]pages.PropertyTypeIDEntry, propertyKey string, updateFn func(bson.D) bson.D) bson.D { - // Find the PropertyTypeID for this key - propEntry, ok := propTypeIDs[propertyKey] - if !ok { - return obj - } - - result := make(bson.D, 0, len(obj)) - for _, elem := range obj { - if elem.Key == "Properties" { - if arr, ok := elem.Value.(bson.A); ok { - result = append(result, bson.E{Key: "Properties", Value: updatePropertyInArray(arr, propEntry.PropertyTypeID, updateFn)}) - continue - } - } - result = append(result, elem) - } - return result -} - -// updatePropertyInArray finds a property by TypePointer and updates its value. -func updatePropertyInArray(arr bson.A, propertyTypeID string, updateFn func(bson.D) bson.D) bson.A { - result := make(bson.A, len(arr)) - matched := false - for i, item := range arr { - if prop, ok := item.(bson.D); ok { - if matchesTypePointer(prop, propertyTypeID) { - result[i] = updatePropertyValue(prop, updateFn) - matched = true - } else { - result[i] = item - } - } else { - result[i] = item - } - } - if !matched { - log.Printf("WARNING: updatePropertyInArray: no match for TypePointer %s in %d properties", propertyTypeID, len(arr)-1) - } - return result -} - -// matchesTypePointer checks if a WidgetProperty has the given TypePointer. -func matchesTypePointer(prop bson.D, propertyTypeID string) bool { - // Normalize: strip dashes for comparison (BlobToUUID returns dashed format, - // but propertyTypeIDs from template loader use undashed 32-char hex). - normalizedTarget := strings.ReplaceAll(propertyTypeID, "-", "") - for _, elem := range prop { - if elem.Key == "TypePointer" { - // Handle both primitive.Binary (from MPR) and []byte (from JSON templates) - switch v := elem.Value.(type) { - case primitive.Binary: - propID := strings.ReplaceAll(types.BlobToUUID(v.Data), "-", "") - return propID == normalizedTarget - case []byte: - propID := strings.ReplaceAll(types.BlobToUUID(v), "-", "") - if propID == normalizedTarget { - return true - } - // Also try raw hex encoding (no GUID swap) for templates - rawHex := fmt.Sprintf("%x", v) - return rawHex == normalizedTarget - } - } - } - return false -} - -// updatePropertyValue updates the Value field in a WidgetProperty. -func updatePropertyValue(prop bson.D, updateFn func(bson.D) bson.D) bson.D { - result := make(bson.D, 0, len(prop)) - for _, elem := range prop { - if elem.Key == "Value" { - if val, ok := elem.Value.(bson.D); ok { - result = append(result, bson.E{Key: "Value", Value: updateFn(val)}) - } else { - result = append(result, elem) - } - } else { - result = append(result, elem) - } - } - return result -} - -// setPrimitiveValue sets the PrimitiveValue field in a WidgetValue. -func setPrimitiveValue(val bson.D, value string) bson.D { - result := make(bson.D, 0, len(val)) - for _, elem := range val { - if elem.Key == "PrimitiveValue" { - result = append(result, bson.E{Key: "PrimitiveValue", Value: value}) - } else { - result = append(result, elem) - } - } - return result -} - -// setAssociationRef sets the EntityRef field in a WidgetValue for an association binding -// on a pluggable widget. Uses DomainModels$IndirectEntityRef with a Steps array containing -// a DomainModels$EntityRefStep that specifies the association and destination entity. -// MxBuild requires the EntityRef to resolve the association target (CE0642). -func setAssociationRef(val bson.D, assocPath string, entityName string) bson.D { - result := make(bson.D, 0, len(val)) - for _, elem := range val { - if elem.Key == "EntityRef" && entityName != "" { - result = append(result, bson.E{Key: "EntityRef", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "DomainModels$IndirectEntityRef"}, - {Key: "Steps", Value: bson.A{ - int32(2), // version marker - bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "DomainModels$EntityRefStep"}, - {Key: "Association", Value: assocPath}, - {Key: "DestinationEntity", Value: entityName}, - }, - }}, - }}) - } else { - result = append(result, elem) - } - } - return result -} - -// setAttributeRef sets the AttributeRef field in a WidgetValue. -// The attrPath must be fully qualified (Module.Entity.Attribute, 2+ dots). -// If not fully qualified, AttributeRef is set to nil to avoid Studio Pro crash. -func setAttributeRef(val bson.D, attrPath string) bson.D { - result := make(bson.D, 0, len(val)) - for _, elem := range val { - if elem.Key == "AttributeRef" { - if strings.Count(attrPath, ".") >= 2 { - result = append(result, bson.E{Key: "AttributeRef", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "DomainModels$AttributeRef"}, - {Key: "Attribute", Value: attrPath}, - {Key: "EntityRef", Value: nil}, - }}) - } else { - result = append(result, bson.E{Key: "AttributeRef", Value: nil}) - } - } else { - result = append(result, elem) - } - } - return result -} - -// convertPropertyTypeIDs converts widgets.PropertyTypeIDEntry to pages.PropertyTypeIDEntry. -func convertPropertyTypeIDs(src map[string]widgets.PropertyTypeIDEntry) map[string]pages.PropertyTypeIDEntry { - result := make(map[string]pages.PropertyTypeIDEntry) - for k, v := range src { - entry := pages.PropertyTypeIDEntry{ - PropertyTypeID: v.PropertyTypeID, - ValueTypeID: v.ValueTypeID, - DefaultValue: v.DefaultValue, - ValueType: v.ValueType, - Required: v.Required, - ObjectTypeID: v.ObjectTypeID, - } - // Convert nested property IDs if present - if len(v.NestedPropertyIDs) > 0 { - entry.NestedPropertyIDs = convertPropertyTypeIDs(v.NestedPropertyIDs) - } - result[k] = entry - } - return result -} - // resolveSnippetRef resolves a snippet qualified name to its ID. func (pb *pageBuilder) resolveSnippetRef(snippetRef string) (model.ID, error) { if snippetRef == "" { @@ -264,7 +83,7 @@ func (pb *pageBuilder) resolveSnippetRef(snippetRef string) (model.ID, error) { snippetName = snippetRef } - snippets, err := pb.reader.ListSnippets() + snippets, err := pb.backend.ListSnippets() if err != nil { return "", err } diff --git a/mdl/executor/cmd_pages_builder_input_cloning.go b/mdl/executor/cmd_pages_builder_input_cloning.go deleted file mode 100644 index af8681e2..00000000 --- a/mdl/executor/cmd_pages_builder_input_cloning.go +++ /dev/null @@ -1,272 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package executor - -import ( - "github.com/mendixlabs/mxcli/mdl/bsonutil" - "github.com/mendixlabs/mxcli/mdl/types" - "github.com/mendixlabs/mxcli/sdk/pages" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/bson/primitive" -) - -// cloneDataGrid2ObjectWithDatasourceOnly clones a template Object, only updating the datasource. -// This is for testing to isolate whether column building is the issue. -func (pb *pageBuilder) cloneDataGrid2ObjectWithDatasourceOnly(templateObject bson.D, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, datasource pages.DataSource) bson.D { - result := make(bson.D, 0, len(templateObject)) - - for _, elem := range templateObject { - if elem.Key == "$ID" { - // Generate new ID for the object - result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) - } else if elem.Key == "Properties" { - // Update only datasource property - if propsArr, ok := elem.Value.(bson.A); ok { - updatedProps := pb.updateOnlyDatasource(propsArr, propertyTypeIDs, datasource) - result = append(result, bson.E{Key: "Properties", Value: updatedProps}) - } else { - result = append(result, elem) - } - } else { - result = append(result, elem) - } - } - - return result -} - -// updateOnlyDatasource only updates the datasource property, keeping everything else as-is. -func (pb *pageBuilder) updateOnlyDatasource(props bson.A, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, datasource pages.DataSource) bson.A { - result := bson.A{int32(2)} // Version marker - datasourceEntry := propertyTypeIDs["datasource"] - - for _, propVal := range props { - if _, ok := propVal.(int32); ok { - continue // Skip version markers - } - propMap, ok := propVal.(bson.D) - if !ok { - continue - } - - typePointer := pb.getTypePointerFromProperty(propMap) - if typePointer == datasourceEntry.PropertyTypeID { - // Replace datasource - result = append(result, pb.buildDataGrid2Property(datasourceEntry, datasource, "", "")) - } else { - // Keep as-is but with new IDs - result = append(result, pb.clonePropertyWithNewIDs(propMap)) - } - } - - return result -} - -// getTypePointerFromProperty extracts the TypePointer ID from a WidgetProperty. -func (pb *pageBuilder) getTypePointerFromProperty(prop bson.D) string { - for _, elem := range prop { - if elem.Key == "TypePointer" { - switch v := elem.Value.(type) { - case primitive.Binary: - return bsonutil.BsonBinaryToID(v) - case []byte: - // When loaded from JSON template, binary is []byte instead of primitive.Binary - return types.BlobToUUID(v) - } - } - } - return "" -} - -// clonePropertyWithNewIDs clones a WidgetProperty with new IDs. -func (pb *pageBuilder) clonePropertyWithNewIDs(prop bson.D) bson.D { - result := make(bson.D, 0, len(prop)) - for _, elem := range prop { - if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) - } else if elem.Key == "Value" { - if valMap, ok := elem.Value.(bson.D); ok { - result = append(result, bson.E{Key: "Value", Value: pb.cloneValueWithNewIDs(valMap)}) - } else { - result = append(result, elem) - } - } else { - result = append(result, elem) - } - } - return result -} - -// cloneValueWithNewIDs clones a WidgetValue with new IDs. -// Recursively regenerates $ID fields in all nested bson.D documents -// (AttributeRef, EntityRef, SortItems, DesignProperties, etc.). -func (pb *pageBuilder) cloneValueWithNewIDs(val bson.D) bson.D { - return deepCloneWithNewIDs(val) -} - -// clonePropertyWithPrimitiveValue clones a WidgetProperty with new IDs and an updated PrimitiveValue. -// This preserves the template's exact structure (TextTemplate, Objects, etc.) while only changing the value. -func (pb *pageBuilder) clonePropertyWithPrimitiveValue(prop bson.D, newValue string) bson.D { - result := make(bson.D, 0, len(prop)) - for _, elem := range prop { - if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) - } else if elem.Key == "Value" { - if valMap, ok := elem.Value.(bson.D); ok { - result = append(result, bson.E{Key: "Value", Value: pb.cloneValueWithUpdatedPrimitive(valMap, newValue)}) - } else { - result = append(result, elem) - } - } else { - result = append(result, elem) - } - } - return result -} - -// cloneValueWithUpdatedPrimitive clones a WidgetValue with new IDs and an updated PrimitiveValue. -func (pb *pageBuilder) cloneValueWithUpdatedPrimitive(val bson.D, newValue string) bson.D { - result := make(bson.D, 0, len(val)) - for _, elem := range val { - if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) - } else if elem.Key == "PrimitiveValue" { - result = append(result, bson.E{Key: "PrimitiveValue", Value: newValue}) - } else { - result = append(result, bson.E{Key: elem.Key, Value: deepCloneValue(elem.Value)}) - } - } - return result -} - -// clonePropertyClearingTextTemplate clones a WidgetProperty with new IDs but sets TextTemplate to nil. -// Used for mode-dependent properties where TextTemplate should not be present. -func (pb *pageBuilder) clonePropertyClearingTextTemplate(prop bson.D) bson.D { - result := make(bson.D, 0, len(prop)) - for _, elem := range prop { - if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) - } else if elem.Key == "Value" { - if valMap, ok := elem.Value.(bson.D); ok { - result = append(result, bson.E{Key: "Value", Value: pb.cloneValueClearingTextTemplate(valMap)}) - } else { - result = append(result, elem) - } - } else { - result = append(result, elem) - } - } - return result -} - -// cloneValueClearingTextTemplate clones a WidgetValue with new IDs and TextTemplate set to nil. -func (pb *pageBuilder) cloneValueClearingTextTemplate(val bson.D) bson.D { - result := make(bson.D, 0, len(val)) - for _, elem := range val { - if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) - } else if elem.Key == "TextTemplate" { - result = append(result, bson.E{Key: "TextTemplate", Value: nil}) - } else { - result = append(result, bson.E{Key: elem.Key, Value: deepCloneValue(elem.Value)}) - } - } - return result -} - -// cloneWithNewID clones a BSON document, recursively regenerating all $ID fields. -func (pb *pageBuilder) cloneWithNewID(doc bson.D) bson.D { - return deepCloneWithNewIDs(doc) -} - -// cloneTextTemplateWithNewIDs clones a Forms$ClientTemplate with new IDs. -func (pb *pageBuilder) cloneTextTemplateWithNewIDs(tt bson.D) bson.D { - return deepCloneWithNewIDs(tt) -} - -// clonePropertyWithExpression clones a WidgetProperty with new IDs and an updated Expression. -// Same as clonePropertyWithPrimitiveValue but replaces the Expression field instead. -func (pb *pageBuilder) clonePropertyWithExpression(prop bson.D, newExpr string) bson.D { - result := make(bson.D, 0, len(prop)) - for _, elem := range prop { - if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) - } else if elem.Key == "Value" { - if valMap, ok := elem.Value.(bson.D); ok { - result = append(result, bson.E{Key: "Value", Value: pb.cloneValueWithUpdatedExpression(valMap, newExpr)}) - } else { - result = append(result, elem) - } - } else { - result = append(result, elem) - } - } - return result -} - -// cloneValueWithUpdatedExpression clones a WidgetValue with new IDs and an updated Expression. -func (pb *pageBuilder) cloneValueWithUpdatedExpression(val bson.D, newExpr string) bson.D { - result := make(bson.D, 0, len(val)) - for _, elem := range val { - if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) - } else if elem.Key == "Expression" { - result = append(result, bson.E{Key: "Expression", Value: newExpr}) - } else { - result = append(result, bson.E{Key: elem.Key, Value: deepCloneValue(elem.Value)}) - } - } - return result -} - -// ============================================================================ -// Deep recursive ID regeneration -// ============================================================================ - -// deepCloneWithNewIDs deep-clones a bson.D, regenerating every $ID field -// throughout the entire nested structure. This ensures no stale GUIDs -// from templates or old widgets persist in the output. -func deepCloneWithNewIDs(doc bson.D) bson.D { - result := make(bson.D, 0, len(doc)) - for _, elem := range doc { - if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) - } else { - result = append(result, bson.E{Key: elem.Key, Value: deepCloneValue(elem.Value)}) - } - } - return result -} - -// deepCloneValue recursively clones a BSON value, regenerating $ID fields -// in any nested bson.D documents. Non-document values are returned as-is. -func deepCloneValue(v any) any { - switch val := v.(type) { - case bson.D: - return deepCloneWithNewIDs(val) - case bson.A: - return deepCloneArray(val) - case []any: - return deepCloneSlice(val) - default: - return v - } -} - -// deepCloneArray clones a bson.A, recursing into nested documents. -func deepCloneArray(arr bson.A) bson.A { - result := make(bson.A, len(arr)) - for i, elem := range arr { - result[i] = deepCloneValue(elem) - } - return result -} - -// deepCloneSlice clones a []any, recursing into nested documents. -func deepCloneSlice(arr []any) []any { - result := make([]any, len(arr)) - for i, elem := range arr { - result[i] = deepCloneValue(elem) - } - return result -} diff --git a/mdl/executor/cmd_pages_builder_input_datagrid.go b/mdl/executor/cmd_pages_builder_input_datagrid.go deleted file mode 100644 index e49121ae..00000000 --- a/mdl/executor/cmd_pages_builder_input_datagrid.go +++ /dev/null @@ -1,1133 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package executor - -import ( - "fmt" - "strings" - - "github.com/mendixlabs/mxcli/mdl/ast" - "github.com/mendixlabs/mxcli/mdl/bsonutil" - "github.com/mendixlabs/mxcli/sdk/pages" - "go.mongodb.org/mongo-driver/bson" -) - -// colPropBool reads a bool/string property from col.Properties and returns "true"/"false". -func colPropBool(props map[string]any, key string, defaultVal string) string { - if props == nil { - return defaultVal - } - v, ok := props[key] - if !ok { - return defaultVal - } - switch bv := v.(type) { - case bool: - if bv { - return "true" - } - return "false" - case string: - lower := strings.ToLower(bv) - if lower == "true" || lower == "false" { - return lower - } - return defaultVal - default: - return defaultVal - } -} - -// colPropString reads a string property from col.Properties and lowercases it. -func colPropString(props map[string]any, key string, defaultVal string) string { - if props == nil { - return defaultVal - } - v, ok := props[key] - if !ok { - return defaultVal - } - if sv, isStr := v.(string); isStr && sv != "" { - return strings.ToLower(sv) - } - return defaultVal -} - -// colPropInt reads an int/float/string property from col.Properties and returns its string form. -func colPropInt(props map[string]any, key string, defaultVal string) string { - if props == nil { - return defaultVal - } - v, ok := props[key] - if !ok { - return defaultVal - } - switch n := v.(type) { - case int: - return fmt.Sprintf("%d", n) - case int64: - return fmt.Sprintf("%d", n) - case float64: - return fmt.Sprintf("%d", int(n)) - case string: - if n != "" { - return n - } - return defaultVal - default: - return defaultVal - } -} - -// buildDataGrid2Property creates a WidgetProperty BSON for DataGrid2. -func (pb *pageBuilder) buildDataGrid2Property(entry pages.PropertyTypeIDEntry, datasource pages.DataSource, attrRef string, primitiveValue string) bson.D { - // Build the datasource BSON if provided - var datasourceBSON any - if datasource != nil { - datasourceBSON = pb.widgetBackend.SerializeDataSourceToOpaque(datasource) - } - - // Build attribute ref if provided - var attrRefBSON any - if attrRef != "" { - attrRefBSON = bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "DomainModels$AttributeRef"}, - {Key: "Attribute", Value: attrRef}, - {Key: "EntityRef", Value: nil}, - } - } - - return bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, - {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, - {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Forms$NoAction"}, - {Key: "DisabledDuringExecution", Value: true}, - }}, - {Key: "AttributeRef", Value: attrRefBSON}, - {Key: "DataSource", Value: datasourceBSON}, - {Key: "EntityRef", Value: nil}, - {Key: "Expression", Value: ""}, - {Key: "Form", Value: ""}, - {Key: "Icon", Value: nil}, - {Key: "Image", Value: ""}, - {Key: "Microflow", Value: ""}, - {Key: "Nanoflow", Value: ""}, - {Key: "Objects", Value: bson.A{int32(2)}}, - {Key: "PrimitiveValue", Value: primitiveValue}, - {Key: "Selection", Value: "None"}, - {Key: "SourceVariable", Value: nil}, - {Key: "TextTemplate", Value: nil}, - {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, - {Key: "Widgets", Value: bson.A{int32(2)}}, - {Key: "XPathConstraint", Value: ""}, - }}, - } -} - -func (pb *pageBuilder) updateDataGrid2Object(templateObject bson.D, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, datasource pages.DataSource, columns []ast.DataGridColumnDef, headerWidgets []bson.D) bson.D { - // Clone the template object with new IDs - result := make(bson.D, 0, len(templateObject)) - - for _, elem := range templateObject { - if elem.Key == "$ID" { - // Generate new ID for the object - result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) - } else if elem.Key == "Properties" { - // Update properties - if propsArr, ok := elem.Value.(bson.A); ok { - updatedProps := pb.updateDataGrid2Properties(propsArr, propertyTypeIDs, datasource, columns, headerWidgets) - result = append(result, bson.E{Key: "Properties", Value: updatedProps}) - } else { - result = append(result, elem) - } - } else { - result = append(result, elem) - } - } - - return result -} - -func (pb *pageBuilder) updateDataGrid2Properties(props bson.A, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, datasource pages.DataSource, columns []ast.DataGridColumnDef, headerWidgets []bson.D) bson.A { - result := bson.A{int32(2)} // Version marker - - // Get the property type IDs for datasource, columns, and filtersPlaceholder - datasourceEntry := propertyTypeIDs["datasource"] - columnsEntry := propertyTypeIDs["columns"] - filtersPlaceholderEntry := propertyTypeIDs["filtersPlaceholder"] - - // Process each property from the template - for _, propVal := range props { - // Skip version markers - if _, ok := propVal.(int32); ok { - continue - } - - propMap, ok := propVal.(bson.D) - if !ok { - continue - } - - // Check if this is the datasource, columns, or filtersPlaceholder property by matching TypePointer - typePointer := pb.getTypePointerFromProperty(propMap) - - if typePointer == datasourceEntry.PropertyTypeID { - // Replace with our datasource - result = append(result, pb.buildDataGrid2Property(datasourceEntry, datasource, "", "")) - } else if typePointer == columnsEntry.PropertyTypeID { - // Clone the columns property and update with our column data - result = append(result, pb.cloneAndUpdateColumnsProperty(propMap, columnsEntry, propertyTypeIDs, columns)) - } else if typePointer == filtersPlaceholderEntry.PropertyTypeID && len(headerWidgets) > 0 { - // Replace with our header widgets (for HEADER section support) - result = append(result, pb.buildFiltersPlaceholderProperty(filtersPlaceholderEntry, headerWidgets)) - } else { - // Keep the template property as-is, but regenerate IDs - result = append(result, pb.clonePropertyWithNewIDs(propMap)) - } - } - - return result -} - -func (pb *pageBuilder) cloneAndUpdateColumnsProperty(templateProp bson.D, columnsEntry pages.PropertyTypeIDEntry, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, columns []ast.DataGridColumnDef) bson.D { - // Extract template column object from the property - var templateColumnObj bson.D - for _, elem := range templateProp { - if elem.Key == "Value" { - if valMap, ok := elem.Value.(bson.D); ok { - for _, ve := range valMap { - if ve.Key == "Objects" { - if objArr, ok := ve.Value.(bson.A); ok { - // Find first WidgetObject in the array - for _, obj := range objArr { - if colObj, ok := obj.(bson.D); ok { - templateColumnObj = colObj - break - } - } - } - } - } - } - } - } - - // Build column objects by cloning template and updating - columnObjects := bson.A{int32(2)} // Version marker - for i := range columns { - col := &columns[i] - if templateColumnObj != nil { - // Clone template column and update (preserves all template properties) - columnObjects = append(columnObjects, pb.cloneAndUpdateColumnObject(templateColumnObj, col, columnsEntry.NestedPropertyIDs)) - } else { - // Build from scratch (no template available) - columnObjects = append(columnObjects, pb.buildDataGrid2ColumnObject(col, columnsEntry.ObjectTypeID, columnsEntry.NestedPropertyIDs)) - } - } - - // Clone the property structure with new IDs and our column objects - result := make(bson.D, 0, len(templateProp)) - for _, elem := range templateProp { - if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) - } else if elem.Key == "Value" { - if valMap, ok := elem.Value.(bson.D); ok { - newVal := make(bson.D, 0, len(valMap)) - for _, ve := range valMap { - if ve.Key == "$ID" { - newVal = append(newVal, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) - } else if ve.Key == "Objects" { - newVal = append(newVal, bson.E{Key: "Objects", Value: columnObjects}) - } else if ve.Key == "Action" { - // Clone Action with new ID - if actionMap, ok := ve.Value.(bson.D); ok { - newVal = append(newVal, bson.E{Key: "Action", Value: pb.cloneWithNewID(actionMap)}) - } else { - newVal = append(newVal, ve) - } - } else { - newVal = append(newVal, ve) - } - } - result = append(result, bson.E{Key: "Value", Value: newVal}) - } else { - result = append(result, elem) - } - } else { - result = append(result, elem) - } - } - - return result -} - -func (pb *pageBuilder) cloneAndUpdateColumnObject(templateCol bson.D, col *ast.DataGridColumnDef, columnPropertyIDs map[string]pages.PropertyTypeIDEntry) bson.D { - attrPath := pb.resolveAttributePath(col.Attribute) - caption := col.Caption - if caption == "" { - caption = col.Attribute - } - - // Build content widgets if there are child widgets - var contentWidgets []bson.D - for _, child := range col.ChildrenV3 { - widgetBSON, err := pb.buildWidgetV3ToBSON(child) - if err != nil { - // Log error and continue (don't fail the entire column) - continue - } - if widgetBSON != nil { - contentWidgets = append(contentWidgets, widgetBSON) - } - } - - result := make(bson.D, 0, len(templateCol)) - for _, elem := range templateCol { - if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) - } else if elem.Key == "Properties" { - // Update properties - if propsArr, ok := elem.Value.(bson.A); ok { - result = append(result, bson.E{Key: "Properties", Value: pb.cloneAndUpdateColumnProperties(propsArr, columnPropertyIDs, col, attrPath, caption, contentWidgets)}) - } else { - result = append(result, elem) - } - } else { - result = append(result, elem) - } - } - return result -} - -func (pb *pageBuilder) cloneAndUpdateColumnProperties(templateProps bson.A, columnPropertyIDs map[string]pages.PropertyTypeIDEntry, col *ast.DataGridColumnDef, attrPath, caption string, contentWidgets []bson.D) bson.A { - result := bson.A{int32(2)} // Version marker - - // Track which properties were added - addedProps := make(map[string]bool) - - hasCustomContent := len(contentWidgets) > 0 - - for _, propVal := range templateProps { - if _, ok := propVal.(int32); ok { - continue // Skip version markers - } - propMap, ok := propVal.(bson.D) - if !ok { - continue - } - - typePointer := pb.getTypePointerFromProperty(propMap) - - // Find which property key this TypePointer corresponds to - propKey := "" - for key, entry := range columnPropertyIDs { - if entry.PropertyTypeID == typePointer { - addedProps[key] = true - propKey = key - break - } - } - - // Clone template properties, adjusting for the column mode. - // - // The editorConfig.js in the widget mpk defines mode-dependent visibility: - // attribute mode: tooltip VISIBLE, content/allowEventPropagation/exportValue HIDDEN - // customContent mode: tooltip HIDDEN, content/allowEventPropagation/exportValue VISIBLE - // - // Properties must have mode-appropriate values or CE0463 is triggered. - // See docs/03-development/PAGE_BSON_SERIALIZATION.md for details. - switch propKey { - case "showContentAs": - if hasCustomContent { - result = append(result, pb.clonePropertyWithPrimitiveValue(propMap, "customContent")) - } else { - result = append(result, pb.clonePropertyWithNewIDs(propMap)) - } - case "attribute": - if attrPath != "" { - entry := columnPropertyIDs["attribute"] - result = append(result, pb.buildColumnAttributeProperty(entry, attrPath)) - } else { - result = append(result, pb.clonePropertyWithNewIDs(propMap)) - } - case "header": - entry := columnPropertyIDs["header"] - result = append(result, pb.buildColumnHeaderProperty(entry, caption)) - case "content": - if hasCustomContent { - entry := columnPropertyIDs["content"] - result = append(result, pb.buildColumnContentProperty(entry, contentWidgets)) - } else { - result = append(result, pb.clonePropertyWithNewIDs(propMap)) - } - case "visible": - visExpr := "true" - if col.Properties != nil { - if v, ok := col.Properties["Visible"]; ok { - if sv, isStr := v.(string); isStr && sv != "" { - visExpr = sv - } - } - } - result = append(result, pb.clonePropertyWithExpression(propMap, visExpr)) - - case "columnClass": - classExpr := "" - if col.Properties != nil { - if v, ok := col.Properties["DynamicCellClass"]; ok { - if sv, isStr := v.(string); isStr { - classExpr = sv - } - } - } - result = append(result, pb.clonePropertyWithExpression(propMap, classExpr)) - - // Mode-dependent properties: adjust for customContent vs attribute mode - case "tooltip": - if hasCustomContent { - // tooltip is HIDDEN in customContent mode — clear TextTemplate - result = append(result, pb.clonePropertyClearingTextTemplate(propMap)) - } else { - tooltipText := "" - if col.Properties != nil { - if v, ok := col.Properties["Tooltip"]; ok { - if sv, isStr := v.(string); isStr { - tooltipText = sv - } - } - } - if tooltipText != "" { - entry := columnPropertyIDs["tooltip"] - result = append(result, pb.buildColumnHeaderProperty(entry, tooltipText)) - } else { - result = append(result, pb.clonePropertyWithNewIDs(propMap)) - } - } - case "exportValue": - if hasCustomContent { - // exportValue is VISIBLE in customContent mode — ensure it has a TextTemplate - entry := columnPropertyIDs["exportValue"] - result = append(result, pb.buildColumnHeaderProperty(entry, "")) - } else { - result = append(result, pb.clonePropertyWithNewIDs(propMap)) - } - case "allowEventPropagation": - // allowEventPropagation is VISIBLE in customContent mode (hidden in attribute mode). - // Clone from template preserving its default value. - result = append(result, pb.clonePropertyWithNewIDs(propMap)) - - case "sortable": - defaultSortable := "false" - if attrPath != "" { - defaultSortable = "true" - } - sortVal := colPropBool(col.Properties, "Sortable", defaultSortable) - result = append(result, pb.clonePropertyWithPrimitiveValue(propMap, sortVal)) - - case "resizable": - resVal := colPropBool(col.Properties, "Resizable", "true") - result = append(result, pb.clonePropertyWithPrimitiveValue(propMap, resVal)) - - case "draggable": - dragVal := colPropBool(col.Properties, "Draggable", "true") - result = append(result, pb.clonePropertyWithPrimitiveValue(propMap, dragVal)) - - case "hidable": - hidVal := colPropString(col.Properties, "Hidable", "yes") - result = append(result, pb.clonePropertyWithPrimitiveValue(propMap, hidVal)) - - case "width": - widthVal := colPropString(col.Properties, "ColumnWidth", "autoFill") - result = append(result, pb.clonePropertyWithPrimitiveValue(propMap, widthVal)) - - case "size": - sizeVal := colPropInt(col.Properties, "Size", "1") - result = append(result, pb.clonePropertyWithPrimitiveValue(propMap, sizeVal)) - - case "wrapText": - wrapVal := "false" - if col.Properties != nil { - if v, ok := col.Properties["WrapText"]; ok { - if bv, isBool := v.(bool); isBool && bv { - wrapVal = "true" - } else if sv, isStr := v.(string); isStr { - wrapVal = strings.ToLower(sv) - } - } - } - result = append(result, pb.clonePropertyWithPrimitiveValue(propMap, wrapVal)) - - case "alignment": - alignVal := "left" - if col.Properties != nil { - if v, ok := col.Properties["Alignment"]; ok { - if sv, isStr := v.(string); isStr && sv != "" { - alignVal = strings.ToLower(sv) - } - } - } - result = append(result, pb.clonePropertyWithPrimitiveValue(propMap, alignVal)) - - default: - // Clone all other properties from template with regenerated IDs - result = append(result, pb.clonePropertyWithNewIDs(propMap)) - } - } - - // Add required properties that were missing from template - if !addedProps["visible"] { - if visibleEntry, ok := columnPropertyIDs["visible"]; ok { - visExpr := "true" - if col.Properties != nil { - if v, ok := col.Properties["Visible"]; ok { - if sv, isStr := v.(string); isStr && sv != "" { - visExpr = sv - } - } - } - result = append(result, pb.buildColumnExpressionProperty(visibleEntry, visExpr)) - } - } - - return result -} - -func (pb *pageBuilder) buildDataGrid2Object(propertyTypeIDs map[string]pages.PropertyTypeIDEntry, objectTypeID string, datasource pages.DataSource, columns []ast.DataGridColumnDef, headerWidgets []bson.D) bson.D { - properties := bson.A{int32(2)} // Version marker for non-empty array - - // Create properties for ALL entries in propertyTypeIDs - // This ensures Studio Pro can display the widget's properties panel - for key, entry := range propertyTypeIDs { - switch key { - case "datasource": - // Use actual datasource value - properties = append(properties, pb.buildDataGrid2Property(entry, datasource, "", "")) - case "columns": - // Use actual columns - properties = append(properties, pb.buildDataGrid2ColumnsProperty(entry, propertyTypeIDs, columns)) - case "filtersPlaceholder": - // Use header widgets if provided (for HEADER section support) - if len(headerWidgets) > 0 { - properties = append(properties, pb.buildFiltersPlaceholderProperty(entry, headerWidgets)) - } else { - properties = append(properties, pb.buildDataGrid2DefaultProperty(entry)) - } - default: - // Create property with default value from template - properties = append(properties, pb.buildDataGrid2DefaultProperty(entry)) - } - } - - // Build TypePointer - references the WidgetObjectType - var typePointer any - if objectTypeID != "" { - typePointer = bsonutil.IDToBsonBinary(objectTypeID) - } - - return bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetObject"}, - {Key: "Properties", Value: properties}, - {Key: "TypePointer", Value: typePointer}, - } -} - -func (pb *pageBuilder) buildDataGrid2DefaultProperty(entry pages.PropertyTypeIDEntry) bson.D { - // Determine default values based on value type - var selectionValue string = "None" - if entry.ValueType == "Selection" { - // Selection type defaults to "None" - selectionValue = "None" - } - - // For TextTemplate properties, create a proper Forms$ClientTemplate structure - // Studio Pro expects this even for empty values, otherwise it shows "widget definition changed" - var textTemplate any - if entry.ValueType == "TextTemplate" { - textTemplate = pb.buildEmptyClientTemplate() - } - - return bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, - {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, - {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Forms$NoAction"}, - {Key: "DisabledDuringExecution", Value: true}, - }}, - {Key: "AttributeRef", Value: nil}, - {Key: "DataSource", Value: nil}, - {Key: "EntityRef", Value: nil}, - {Key: "Expression", Value: ""}, - {Key: "Form", Value: ""}, - {Key: "Icon", Value: nil}, - {Key: "Image", Value: ""}, - {Key: "Microflow", Value: ""}, - {Key: "Nanoflow", Value: ""}, - {Key: "Objects", Value: bson.A{int32(2)}}, - {Key: "PrimitiveValue", Value: entry.DefaultValue}, - {Key: "Selection", Value: selectionValue}, - {Key: "SourceVariable", Value: nil}, - {Key: "TextTemplate", Value: textTemplate}, - {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, - {Key: "Widgets", Value: bson.A{int32(2)}}, - {Key: "XPathConstraint", Value: ""}, - }}, - } -} - -func (pb *pageBuilder) buildEmptyClientTemplate() bson.D { - return bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Forms$ClientTemplate"}, - {Key: "Fallback", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Texts$Text"}, - {Key: "Items", Value: bson.A{int32(3)}}, // Empty items with version marker - }}, - {Key: "Parameters", Value: bson.A{int32(2)}}, // Empty parameters - {Key: "Template", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Texts$Text"}, - {Key: "Items", Value: bson.A{int32(3)}}, // Empty items with version marker - }}, - } -} - -func (pb *pageBuilder) buildClientTemplateWithText(text string) bson.D { - return bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Forms$ClientTemplate"}, - {Key: "Fallback", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Texts$Text"}, - {Key: "Items", Value: bson.A{int32(3)}}, - }}, - {Key: "Parameters", Value: bson.A{int32(2)}}, - {Key: "Template", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Texts$Text"}, - {Key: "Items", Value: bson.A{ - int32(3), - bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Texts$Translation"}, - {Key: "LanguageCode", Value: "en_US"}, - {Key: "Text", Value: text}, - }, - }}, - }}, - } -} - -func (pb *pageBuilder) buildFiltersPlaceholderProperty(entry pages.PropertyTypeIDEntry, widgetsBSON []bson.D) bson.D { - // Build the Widgets array with version marker - widgetsArray := bson.A{int32(2)} // Version marker for non-empty array - for _, w := range widgetsBSON { - widgetsArray = append(widgetsArray, w) - } - - return bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, - {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, - {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Forms$NoAction"}, - {Key: "DisabledDuringExecution", Value: true}, - }}, - {Key: "AttributeRef", Value: nil}, - {Key: "DataSource", Value: nil}, - {Key: "EntityRef", Value: nil}, - {Key: "Expression", Value: ""}, - {Key: "Form", Value: ""}, - {Key: "Icon", Value: nil}, - {Key: "Image", Value: ""}, - {Key: "Microflow", Value: ""}, - {Key: "Nanoflow", Value: ""}, - {Key: "Objects", Value: bson.A{int32(2)}}, - {Key: "PrimitiveValue", Value: ""}, - {Key: "Selection", Value: "None"}, - {Key: "SourceVariable", Value: nil}, - {Key: "TextTemplate", Value: nil}, - {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, - {Key: "Widgets", Value: widgetsArray}, - {Key: "XPathConstraint", Value: ""}, - }}, - } -} - -func (pb *pageBuilder) buildDataGrid2ColumnsProperty(entry pages.PropertyTypeIDEntry, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, columns []ast.DataGridColumnDef) bson.D { - // Build column objects using nested property IDs - columnObjects := bson.A{int32(2)} // Version marker - for i := range columns { - columnObjects = append(columnObjects, pb.buildDataGrid2ColumnObject(&columns[i], entry.ObjectTypeID, entry.NestedPropertyIDs)) - } - - return bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, - {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, - {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Forms$NoAction"}, - {Key: "DisabledDuringExecution", Value: true}, - }}, - {Key: "AttributeRef", Value: nil}, - {Key: "DataSource", Value: nil}, - {Key: "EntityRef", Value: nil}, - {Key: "Expression", Value: ""}, - {Key: "Form", Value: ""}, - {Key: "Icon", Value: nil}, - {Key: "Image", Value: ""}, - {Key: "Microflow", Value: ""}, - {Key: "Nanoflow", Value: ""}, - {Key: "Objects", Value: columnObjects}, - {Key: "PrimitiveValue", Value: ""}, - {Key: "Selection", Value: "None"}, - {Key: "SourceVariable", Value: nil}, - {Key: "TextTemplate", Value: nil}, - {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, - {Key: "Widgets", Value: bson.A{int32(2)}}, - {Key: "XPathConstraint", Value: ""}, - }}, - } -} - -func (pb *pageBuilder) buildDataGrid2ColumnObject(col *ast.DataGridColumnDef, columnObjectTypeID string, columnPropertyIDs map[string]pages.PropertyTypeIDEntry) bson.D { - attrPath := pb.resolveAttributePath(col.Attribute) - - // Build content widgets if there are child widgets - var contentWidgets []bson.D - for _, child := range col.ChildrenV3 { - widgetBSON, err := pb.buildWidgetV3ToBSON(child) - if err != nil { - // Log error and continue (don't fail the entire column) - continue - } - if widgetBSON != nil { - contentWidgets = append(contentWidgets, widgetBSON) - } - } - hasCustomContent := len(contentWidgets) > 0 - - // Column properties array - MUST include ALL properties from columnPropertyIDs - properties := bson.A{int32(2)} // Version marker - - // Iterate through ALL column property types and create each one - for key, entry := range columnPropertyIDs { - switch key { - case "showContentAs": - // Set to "customContent" if we have custom widgets, otherwise "attribute" - if hasCustomContent { - properties = append(properties, pb.buildColumnPrimitiveProperty(entry, "customContent")) - } else { - properties = append(properties, pb.buildColumnPrimitiveProperty(entry, "attribute")) - } - - case "attribute": - // The actual attribute path - if attrPath != "" { - properties = append(properties, pb.buildColumnAttributeProperty(entry, attrPath)) - } else { - properties = append(properties, pb.buildColumnDefaultProperty(entry)) - } - - case "header": - // Caption for the column (TextTemplate type) - if col.Caption != "" { - properties = append(properties, pb.buildColumnHeaderProperty(entry, col.Caption)) - } else { - // Use attribute name as default caption - properties = append(properties, pb.buildColumnHeaderProperty(entry, col.Attribute)) - } - - case "content": - // Content property with widgets (if any) - if hasCustomContent { - properties = append(properties, pb.buildColumnContentProperty(entry, contentWidgets)) - } else { - properties = append(properties, pb.buildColumnContentProperty(entry, nil)) - } - - case "filter": - // Filter property should have empty widget arrays (like Studio Pro) - properties = append(properties, pb.buildColumnContentProperty(entry, nil)) - - case "visible": - // Expression-type property - visExpr := "true" - if col.Properties != nil { - if v, ok := col.Properties["Visible"]; ok { - if sv, isStr := v.(string); isStr && sv != "" { - visExpr = sv - } - } - } - properties = append(properties, pb.buildColumnExpressionProperty(entry, visExpr)) - - case "columnClass": - // Expression-type property - classExpr := "" - if col.Properties != nil { - if v, ok := col.Properties["DynamicCellClass"]; ok { - if sv, isStr := v.(string); isStr { - classExpr = sv - } - } - } - properties = append(properties, pb.buildColumnExpressionProperty(entry, classExpr)) - - case "sortable": - defaultSortable := "false" - if attrPath != "" { - defaultSortable = "true" - } - sortVal := colPropBool(col.Properties, "Sortable", defaultSortable) - properties = append(properties, pb.buildColumnPrimitiveProperty(entry, sortVal)) - - case "resizable": - resVal := colPropBool(col.Properties, "Resizable", "true") - properties = append(properties, pb.buildColumnPrimitiveProperty(entry, resVal)) - - case "draggable": - dragVal := colPropBool(col.Properties, "Draggable", "true") - properties = append(properties, pb.buildColumnPrimitiveProperty(entry, dragVal)) - - case "wrapText": - wrapVal := colPropBool(col.Properties, "WrapText", "false") - properties = append(properties, pb.buildColumnPrimitiveProperty(entry, wrapVal)) - - case "alignment": - alignVal := colPropString(col.Properties, "Alignment", "left") - properties = append(properties, pb.buildColumnPrimitiveProperty(entry, alignVal)) - - case "width": - widthVal := colPropString(col.Properties, "ColumnWidth", "autoFill") - properties = append(properties, pb.buildColumnPrimitiveProperty(entry, widthVal)) - - case "minWidth": - // Enumeration-type property - "auto", "setByContent", or "manual" - properties = append(properties, pb.buildColumnPrimitiveProperty(entry, "auto")) - - case "size": - sizeVal := colPropInt(col.Properties, "Size", "1") - properties = append(properties, pb.buildColumnPrimitiveProperty(entry, sizeVal)) - - case "hidable": - hidVal := colPropString(col.Properties, "Hidable", "yes") - properties = append(properties, pb.buildColumnPrimitiveProperty(entry, hidVal)) - - case "tooltip": - if hasCustomContent { - // tooltip is HIDDEN in customContent mode — use empty TextTemplate - properties = append(properties, pb.buildColumnDefaultProperty(entry)) - } else { - tooltipText := "" - if col.Properties != nil { - if v, ok := col.Properties["Tooltip"]; ok { - if sv, isStr := v.(string); isStr { - tooltipText = sv - } - } - } - if tooltipText != "" { - properties = append(properties, pb.buildColumnHeaderProperty(entry, tooltipText)) - } else { - properties = append(properties, pb.buildColumnDefaultProperty(entry)) - } - } - - default: - // All other properties: use default value based on valueType - switch entry.ValueType { - case "Expression": - // Expression properties need an expression value - properties = append(properties, pb.buildColumnExpressionProperty(entry, "")) - case "TextTemplate": - // TextTemplate properties need proper structure - properties = append(properties, pb.buildColumnDefaultProperty(entry)) - default: - // Other types use default builder - properties = append(properties, pb.buildColumnDefaultProperty(entry)) - } - } - } - - // Column ObjectType pointer - var typePointer any - if columnObjectTypeID != "" { - typePointer = bsonutil.IDToBsonBinary(columnObjectTypeID) - } - - return bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetObject"}, - {Key: "Properties", Value: properties}, - {Key: "TypePointer", Value: typePointer}, - } -} - -func (pb *pageBuilder) buildColumnDefaultProperty(entry pages.PropertyTypeIDEntry) bson.D { - // For TextTemplate properties, create a proper Forms$ClientTemplate structure - var textTemplate any - if entry.ValueType == "TextTemplate" { - textTemplate = pb.buildEmptyClientTemplate() - } - - return bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, - {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, - {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Forms$NoAction"}, - {Key: "DisabledDuringExecution", Value: true}, - }}, - {Key: "AttributeRef", Value: nil}, - {Key: "DataSource", Value: nil}, - {Key: "EntityRef", Value: nil}, - {Key: "Expression", Value: ""}, - {Key: "Form", Value: ""}, - {Key: "Icon", Value: nil}, - {Key: "Image", Value: ""}, - {Key: "Microflow", Value: ""}, - {Key: "Nanoflow", Value: ""}, - {Key: "Objects", Value: bson.A{int32(2)}}, - {Key: "PrimitiveValue", Value: entry.DefaultValue}, - {Key: "Selection", Value: "None"}, - {Key: "SourceVariable", Value: nil}, - {Key: "TextTemplate", Value: textTemplate}, - {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, - {Key: "Widgets", Value: bson.A{int32(2)}}, - {Key: "XPathConstraint", Value: ""}, - }}, - } -} - -func (pb *pageBuilder) buildColumnPrimitiveProperty(entry pages.PropertyTypeIDEntry, value string) bson.D { - return bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, - {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, - {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Forms$NoAction"}, - {Key: "DisabledDuringExecution", Value: true}, - }}, - {Key: "AttributeRef", Value: nil}, - {Key: "DataSource", Value: nil}, - {Key: "EntityRef", Value: nil}, - {Key: "Expression", Value: ""}, - {Key: "Form", Value: ""}, - {Key: "Icon", Value: nil}, - {Key: "Image", Value: ""}, - {Key: "Microflow", Value: ""}, - {Key: "Nanoflow", Value: ""}, - {Key: "Objects", Value: bson.A{int32(2)}}, - {Key: "PrimitiveValue", Value: value}, - {Key: "Selection", Value: "None"}, - {Key: "SourceVariable", Value: nil}, - {Key: "TextTemplate", Value: nil}, - {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, - {Key: "Widgets", Value: bson.A{int32(2)}}, - {Key: "XPathConstraint", Value: ""}, - }}, - } -} - -func (pb *pageBuilder) buildColumnExpressionProperty(entry pages.PropertyTypeIDEntry, expression string) bson.D { - return bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, - {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, - {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Forms$NoAction"}, - {Key: "DisabledDuringExecution", Value: true}, - }}, - {Key: "AttributeRef", Value: nil}, - {Key: "DataSource", Value: nil}, - {Key: "EntityRef", Value: nil}, - {Key: "Expression", Value: expression}, - {Key: "Form", Value: ""}, - {Key: "Icon", Value: nil}, - {Key: "Image", Value: ""}, - {Key: "Microflow", Value: ""}, - {Key: "Nanoflow", Value: ""}, - {Key: "Objects", Value: bson.A{int32(2)}}, - {Key: "PrimitiveValue", Value: ""}, - {Key: "Selection", Value: "None"}, - {Key: "SourceVariable", Value: nil}, - {Key: "TextTemplate", Value: nil}, - {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, - {Key: "Widgets", Value: bson.A{int32(2)}}, - {Key: "XPathConstraint", Value: ""}, - }}, - } -} - -func (pb *pageBuilder) buildColumnAttributeProperty(entry pages.PropertyTypeIDEntry, attrPath string) bson.D { - // AttributeRef requires a fully qualified path (Module.Entity.Attribute, 2+ dots). - // If the path is not fully qualified, set AttributeRef to nil to avoid Studio Pro crash. - var attributeRef any - if strings.Count(attrPath, ".") >= 2 { - attributeRef = bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "DomainModels$AttributeRef"}, - {Key: "Attribute", Value: attrPath}, - {Key: "EntityRef", Value: nil}, - } - } - return bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, - {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, - {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Forms$NoAction"}, - {Key: "DisabledDuringExecution", Value: true}, - }}, - {Key: "AttributeRef", Value: attributeRef}, - {Key: "DataSource", Value: nil}, - {Key: "EntityRef", Value: nil}, - {Key: "Expression", Value: ""}, - {Key: "Form", Value: ""}, - {Key: "Icon", Value: nil}, - {Key: "Image", Value: ""}, - {Key: "Microflow", Value: ""}, - {Key: "Nanoflow", Value: ""}, - {Key: "Objects", Value: bson.A{int32(2)}}, - {Key: "PrimitiveValue", Value: ""}, - {Key: "Selection", Value: "None"}, - {Key: "SourceVariable", Value: nil}, - {Key: "TextTemplate", Value: nil}, - {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, - {Key: "Widgets", Value: bson.A{int32(2)}}, - {Key: "XPathConstraint", Value: ""}, - }}, - } -} - -func (pb *pageBuilder) buildColumnHeaderProperty(entry pages.PropertyTypeIDEntry, caption string) bson.D { - // Create the text template with the caption - textTemplate := pb.buildClientTemplateWithText(caption) - - return bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, - {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, - {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Forms$NoAction"}, - {Key: "DisabledDuringExecution", Value: true}, - }}, - {Key: "AttributeRef", Value: nil}, - {Key: "DataSource", Value: nil}, - {Key: "EntityRef", Value: nil}, - {Key: "Expression", Value: ""}, - {Key: "Form", Value: ""}, - {Key: "Icon", Value: nil}, - {Key: "Image", Value: ""}, - {Key: "Microflow", Value: ""}, - {Key: "Nanoflow", Value: ""}, - {Key: "Objects", Value: bson.A{int32(2)}}, - {Key: "PrimitiveValue", Value: ""}, - {Key: "Selection", Value: "None"}, - {Key: "SourceVariable", Value: nil}, - {Key: "TextTemplate", Value: textTemplate}, - {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, - {Key: "Widgets", Value: bson.A{int32(2)}}, - {Key: "XPathConstraint", Value: ""}, - }}, - } -} - -func (pb *pageBuilder) buildColumnContentProperty(entry pages.PropertyTypeIDEntry, widgets any) bson.D { - // Widgets array containing the widgets - widgetsArray := bson.A{int32(2)} - switch w := widgets.(type) { - case bson.D: - if w != nil { - widgetsArray = append(widgetsArray, w) - } - case []bson.D: - for _, widget := range w { - widgetsArray = append(widgetsArray, widget) - } - } - - return bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.PropertyTypeID)}, - {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, - {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "Forms$NoAction"}, - {Key: "DisabledDuringExecution", Value: true}, - }}, - {Key: "AttributeRef", Value: nil}, - {Key: "DataSource", Value: nil}, - {Key: "EntityRef", Value: nil}, - {Key: "Expression", Value: ""}, - {Key: "Form", Value: ""}, - {Key: "Icon", Value: nil}, - {Key: "Image", Value: ""}, - {Key: "Microflow", Value: ""}, - {Key: "Nanoflow", Value: ""}, - {Key: "Objects", Value: bson.A{int32(2)}}, - {Key: "PrimitiveValue", Value: ""}, - {Key: "Selection", Value: "None"}, - {Key: "SourceVariable", Value: nil}, - {Key: "TextTemplate", Value: nil}, - {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(entry.ValueTypeID)}, - {Key: "Widgets", Value: widgetsArray}, - {Key: "XPathConstraint", Value: ""}, - }}, - } -} diff --git a/mdl/executor/cmd_pages_builder_input_filters.go b/mdl/executor/cmd_pages_builder_input_filters.go index eadf5943..15831f2e 100644 --- a/mdl/executor/cmd_pages_builder_input_filters.go +++ b/mdl/executor/cmd_pages_builder_input_filters.go @@ -5,12 +5,8 @@ package executor import ( "strings" - "github.com/mendixlabs/mxcli/mdl/bsonutil" - "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/pages" - "github.com/mendixlabs/mxcli/sdk/widgets" - "go.mongodb.org/mongo-driver/bson" ) func (pb *pageBuilder) getFilterWidgetIDForAttribute(attrPath string) string { @@ -104,90 +100,3 @@ func (pb *pageBuilder) findAttributeType(attrPath string) domainmodel.AttributeT return nil } - -func (pb *pageBuilder) buildFilterWidgetBSON(widgetID, filterName string) bson.D { - // Load the filter widget template - rawType, rawObject, propertyTypeIDs, objectTypeID, err := widgets.GetTemplateFullBSON(widgetID, types.GenerateID, pb.reader.Path()) - if err != nil || rawType == nil { - // Fallback: create minimal filter widget structure - return pb.buildMinimalFilterWidgetBSON(widgetID, filterName) - } - - // The widget structure is: CustomWidgets$CustomWidget with Type and Object - widgetBSON := bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$CustomWidget"}, - {Key: "Editable", Value: "Inherited"}, - {Key: "Name", Value: filterName}, - {Key: "Object", Value: rawObject}, - {Key: "TabIndex", Value: int32(0)}, - {Key: "Type", Value: rawType}, - } - - // Set the "linkedDs" property to "auto" mode (which links to parent datasource) - if propertyTypeIDs != nil && rawObject != nil { - widgetBSON = pb.setFilterWidgetLinkedDsAuto(widgetBSON, propertyTypeIDs, objectTypeID) - } - - return widgetBSON -} - -func (pb *pageBuilder) setFilterWidgetLinkedDsAuto(widget bson.D, propertyTypeIDs map[string]widgets.PropertyTypeIDEntry, objectTypeID string) bson.D { - // The filter widgets have an "attrChoice" property that should be set to "auto" - // which makes them automatically link to the parent datasource - return widget -} - -func (pb *pageBuilder) buildMinimalFilterWidgetBSON(widgetID, filterName string) bson.D { - typeID := types.GenerateID() - objectTypeID := types.GenerateID() - objectID := types.GenerateID() - - // Get widget type name based on ID - var widgetTypeName string - switch widgetID { - case pages.WidgetIDDataGridTextFilter: - widgetTypeName = "Text filter" - case pages.WidgetIDDataGridNumberFilter: - widgetTypeName = "Number filter" - case pages.WidgetIDDataGridDateFilter: - widgetTypeName = "Date filter" - case pages.WidgetIDDataGridDropdownFilter: - widgetTypeName = "Drop-down filter" - default: - widgetTypeName = "Text filter" - } - - return bson.D{ - {Key: "$ID", Value: bsonutil.NewIDBsonBinary()}, - {Key: "$Type", Value: "CustomWidgets$CustomWidget"}, - {Key: "Editable", Value: "Inherited"}, - {Key: "Name", Value: filterName}, - {Key: "Object", Value: bson.D{ - {Key: "$ID", Value: bsonutil.IDToBsonBinary(objectID)}, - {Key: "$Type", Value: "CustomWidgets$WidgetObject"}, - {Key: "Properties", Value: bson.A{int32(2)}}, - {Key: "TypePointer", Value: bsonutil.IDToBsonBinary(objectTypeID)}, - }}, - {Key: "TabIndex", Value: int32(0)}, - {Key: "Type", Value: bson.D{ - {Key: "$ID", Value: bsonutil.IDToBsonBinary(typeID)}, - {Key: "$Type", Value: "CustomWidgets$CustomWidgetType"}, - {Key: "HelpUrl", Value: ""}, - {Key: "ObjectType", Value: bson.D{ - {Key: "$ID", Value: bsonutil.IDToBsonBinary(objectTypeID)}, - {Key: "$Type", Value: "CustomWidgets$WidgetObjectType"}, - {Key: "PropertyTypes", Value: bson.A{int32(2)}}, - }}, - {Key: "OfflineCapable", Value: true}, - {Key: "StudioCategory", Value: "Data Controls"}, - {Key: "StudioProCategory", Value: "Data controls"}, - {Key: "SupportedPlatform", Value: "Web"}, - {Key: "WidgetDescription", Value: ""}, - {Key: "WidgetId", Value: widgetID}, - {Key: "WidgetName", Value: widgetTypeName}, - {Key: "WidgetNeedsEntityContext", Value: false}, - {Key: "WidgetPluginWidget", Value: true}, - }}, - } -} diff --git a/mdl/executor/cmd_pages_builder_v3.go b/mdl/executor/cmd_pages_builder_v3.go index e4c0a8e7..1d108b9b 100644 --- a/mdl/executor/cmd_pages_builder_v3.go +++ b/mdl/executor/cmd_pages_builder_v3.go @@ -658,7 +658,7 @@ func (pb *pageBuilder) resolveAssociationDestination(assocQN, contextEntity stri } modName, assocName := parts[0], parts[1] - domainModels, err := pb.reader.ListDomainModels() + domainModels, err := pb.backend.ListDomainModels() if err != nil { return "" } @@ -701,7 +701,7 @@ func (pb *pageBuilder) entityQNByID(entityID model.ID) string { if entityID == "" { return "" } - domainModels, err := pb.reader.ListDomainModels() + domainModels, err := pb.backend.ListDomainModels() if err != nil { return "" } @@ -725,7 +725,7 @@ func (pb *pageBuilder) moduleNameByID(moduleID model.ID) string { if moduleID == "" { return "" } - modules, err := pb.reader.ListModules() + modules, err := pb.backend.ListModules() if err != nil { return "" } @@ -809,7 +809,7 @@ func (pb *pageBuilder) getNanoflowReturnEntityName(qualifiedName string) string name = qualifiedName } - nanoflows, err := pb.reader.ListNanoflows() + nanoflows, err := pb.backend.ListNanoflows() if err != nil { return "" } @@ -1134,7 +1134,7 @@ func (pb *pageBuilder) resolveNanoflowByName(nfName string) (model.ID, error) { name = nfName } - nanoflows, err := pb.reader.ListNanoflows() + nanoflows, err := pb.backend.ListNanoflows() if err != nil { return "", mdlerrors.NewBackend("list nanoflows", err) } diff --git a/mdl/executor/cmd_pages_builder_v3_pluggable.go b/mdl/executor/cmd_pages_builder_v3_pluggable.go deleted file mode 100644 index 93655b2d..00000000 --- a/mdl/executor/cmd_pages_builder_v3_pluggable.go +++ /dev/null @@ -1,316 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 - -package executor - -import ( - "fmt" - "strings" - - "go.mongodb.org/mongo-driver/bson" - - "github.com/mendixlabs/mxcli/mdl/ast" - "github.com/mendixlabs/mxcli/mdl/bsonutil" - mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" - "github.com/mendixlabs/mxcli/mdl/types" -) - -// ============================================================================= -// Custom/Pluggable Widget Builders V3 -// ============================================================================= - -// buildGallerySelectionProperty clones an itemSelection property and updates the Selection value. -func (pb *pageBuilder) buildGallerySelectionProperty(propMap bson.D, selectionMode string) bson.D { - result := make(bson.D, 0, len(propMap)) - - for _, elem := range propMap { - if elem.Key == "$ID" { - // Generate new ID - result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) - } else if elem.Key == "Value" { - // Clone Value and update Selection - if valueMap, ok := elem.Value.(bson.D); ok { - result = append(result, bson.E{Key: "Value", Value: pb.cloneGallerySelectionValue(valueMap, selectionMode)}) - } else { - result = append(result, elem) - } - } else { - result = append(result, elem) - } - } - - return result -} - -// cloneGallerySelectionValue clones a WidgetValue and updates the Selection field. -func (pb *pageBuilder) cloneGallerySelectionValue(valueMap bson.D, selectionMode string) bson.D { - result := make(bson.D, 0, len(valueMap)) - - for _, elem := range valueMap { - if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) - } else if elem.Key == "Selection" { - // Update selection mode - result = append(result, bson.E{Key: "Selection", Value: selectionMode}) - } else if elem.Key == "Action" { - // Clone action with new ID - if actionMap, ok := elem.Value.(bson.D); ok { - result = append(result, bson.E{Key: "Action", Value: pb.cloneActionWithNewID(actionMap)}) - } else { - result = append(result, elem) - } - } else { - result = append(result, elem) - } - } - - return result -} - -// cloneActionWithNewID clones an action (e.g., NoAction) with a new ID. -func (pb *pageBuilder) cloneActionWithNewID(actionMap bson.D) bson.D { - result := make(bson.D, 0, len(actionMap)) - - for _, elem := range actionMap { - if elem.Key == "$ID" { - result = append(result, bson.E{Key: "$ID", Value: bsonutil.NewIDBsonBinary()}) - } else { - result = append(result, elem) - } - } - - return result -} - -// buildWidgetV3ToBSON builds a V3 widget and serializes it to an opaque storage form. -func (pb *pageBuilder) buildWidgetV3ToBSON(w *ast.WidgetV3) (bson.D, error) { - widget, err := pb.buildWidgetV3(w) - if err != nil { - return nil, err - } - if widget == nil { - return nil, nil - } - raw := pb.widgetBackend.SerializeWidgetToOpaque(widget) - if raw == nil { - return nil, nil - } - bsonD, ok := raw.(bson.D) - if !ok { - return nil, mdlerrors.NewValidationf("SerializeWidgetToOpaque returned unexpected type %T", raw) - } - return bsonD, nil -} - -// createAttributeObject creates a single attribute object entry for filter widget Attributes. -// Used by the widget engine's opAttributeObjects operation. -// The structure follows CustomWidgets$WidgetObject with a nested WidgetProperty for "attribute". -// 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, mdlerrors.NewValidationf("invalid attribute path %q: expected Module.Entity.Attribute format", attributePath) - } - return bson.D{ - {Key: "$ID", Value: hexToBytes(types.GenerateID())}, - {Key: "$Type", Value: "CustomWidgets$WidgetObject"}, - {Key: "Properties", Value: []any{ - int32(2), - bson.D{ - {Key: "$ID", Value: hexToBytes(types.GenerateID())}, - {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: hexToBytes(propertyTypeID)}, - {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: hexToBytes(types.GenerateID())}, - {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, - {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: hexToBytes(types.GenerateID())}, - {Key: "$Type", Value: "Forms$NoAction"}, - {Key: "DisabledDuringExecution", Value: true}, - }}, - {Key: "AttributeRef", Value: bson.D{ - {Key: "$ID", Value: hexToBytes(types.GenerateID())}, - {Key: "$Type", Value: "DomainModels$AttributeRef"}, - {Key: "Attribute", Value: attributePath}, - {Key: "EntityRef", Value: nil}, - }}, - {Key: "DataSource", Value: nil}, - {Key: "EntityRef", Value: nil}, - {Key: "Expression", Value: ""}, - {Key: "Form", Value: ""}, - {Key: "Icon", Value: nil}, - {Key: "Image", Value: ""}, - {Key: "Microflow", Value: ""}, - {Key: "Nanoflow", Value: ""}, - {Key: "Objects", Value: []any{int32(2)}}, - {Key: "PrimitiveValue", Value: ""}, - {Key: "Selection", Value: "None"}, - {Key: "SourceVariable", Value: nil}, - {Key: "TextTemplate", Value: nil}, - {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: hexToBytes(valueTypeID)}, - {Key: "Widgets", Value: []any{int32(2)}}, - {Key: "XPathConstraint", Value: ""}, - }}, - }, - }}, - {Key: "TypePointer", Value: hexToBytes(objectTypeID)}, - }, nil -} - -// BSON helper functions for filter properties - -func getBsonField(d bson.D, key string) bson.D { - for _, elem := range d { - if elem.Key == key { - if nested, ok := elem.Value.(bson.D); ok { - return nested - } - } - } - return nil -} - -func getBsonArray(d bson.D, key string) []any { - for _, elem := range d { - if elem.Key == key { - switch v := elem.Value.(type) { - case []any: - return v - case bson.A: - return []any(v) - } - } - } - return nil -} - -func getBsonString(d bson.D, key string) string { - for _, elem := range d { - if elem.Key == key { - if s, ok := elem.Value.(string); ok { - return s - } - } - } - return "" -} - -func getBsonBinaryID(d bson.D, key string) string { - for _, elem := range d { - if elem.Key == key { - if b, ok := elem.Value.([]byte); ok { - return bytesToHex(b) - } - } - } - return "" -} - -func setBsonPrimitiveValue(propMap bson.D, value string) bson.D { - for i, elem := range propMap { - if elem.Key == "Value" { - if valueMap, ok := elem.Value.(bson.D); ok { - for j, vElem := range valueMap { - if vElem.Key == "PrimitiveValue" { - valueMap[j].Value = value - break - } - } - propMap[i].Value = valueMap - } - break - } - } - return propMap -} - -func setBsonArrayField(d bson.D, key string, value []any) bson.D { - for i, elem := range d { - if elem.Key == key { - d[i].Value = value - return d - } - } - return d -} - -func setBsonField(d bson.D, key string, value bson.D) bson.D { - for i, elem := range d { - if elem.Key == key { - d[i].Value = value - return d - } - } - return d -} - -// hexToBytes converts a hex string (with or without dashes) to a 16-byte blob -// in Microsoft GUID format (little-endian for first 3 segments). -// This matches the format used by Mendix and uuidToBlob in sdk/mpr/writer_core.go. -func hexToBytes(hexStr string) []byte { - // Remove dashes if present (UUID format) - clean := strings.ReplaceAll(hexStr, "-", "") - if len(clean) != 32 { - return nil - } - - // Decode hex to bytes - decoded := make([]byte, 16) - for i := range 16 { - decoded[i] = hexByte(clean[i*2])<<4 | hexByte(clean[i*2+1]) - } - - // Swap bytes to Microsoft GUID format (little-endian for first 3 segments) - blob := make([]byte, 16) - // First 4 bytes: reversed - blob[0] = decoded[3] - blob[1] = decoded[2] - blob[2] = decoded[1] - blob[3] = decoded[0] - // Next 2 bytes: reversed - blob[4] = decoded[5] - blob[5] = decoded[4] - // Next 2 bytes: reversed - blob[6] = decoded[7] - blob[7] = decoded[6] - // Last 8 bytes: unchanged - copy(blob[8:], decoded[8:]) - - return blob -} - -func hexByte(c byte) byte { - switch { - case c >= '0' && c <= '9': - return c - '0' - case c >= 'a' && c <= 'f': - return c - 'a' + 10 - case c >= 'A' && c <= 'F': - return c - 'A' + 10 - } - return 0 -} - -// bytesToHex converts a 16-byte blob from Microsoft GUID format to a hex string. -// This reverses the byte swapping done by hexToBytes, matching blobToUUID in sdk/mpr/reader.go. -func bytesToHex(b []byte) string { - if len(b) != 16 { - // Fallback for non-standard lengths - if len(b) > 1024 { - return "" // reject unreasonably large inputs - } - const hexChars = "0123456789abcdef" - result := make([]byte, len(b)*2) - for i, v := range b { - result[i*2] = hexChars[v>>4] - result[i*2+1] = hexChars[v&0x0f] - } - return string(result) - } - - // Reverse Microsoft GUID byte swapping to get canonical hex - return fmt.Sprintf("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", - b[3], b[2], b[1], b[0], // First 4 bytes: reversed - b[5], b[4], // Next 2 bytes: reversed - b[7], b[6], // Next 2 bytes: reversed - b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]) // Last 8 bytes: unchanged -} diff --git a/mdl/executor/cmd_pages_builder_v3_widgets.go b/mdl/executor/cmd_pages_builder_v3_widgets.go index 52fa365c..192e384a 100644 --- a/mdl/executor/cmd_pages_builder_v3_widgets.go +++ b/mdl/executor/cmd_pages_builder_v3_widgets.go @@ -7,14 +7,12 @@ import ( "regexp" "strings" - "go.mongodb.org/mongo-driver/bson" - "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/pages" - "github.com/mendixlabs/mxcli/sdk/widgets" ) func (pb *pageBuilder) buildDataViewV3(w *ast.WidgetV3) (*pages.DataView, error) { @@ -89,22 +87,8 @@ func (pb *pageBuilder) buildDataViewV3(w *ast.WidgetV3) (*pages.DataView, error) } func (pb *pageBuilder) buildDataGridV3(w *ast.WidgetV3) (*pages.CustomWidget, error) { - // Build DataGrid2 as a CustomWidget (pluggable widget) like V2 does. - // The built-in DataGrid (Forms$DataGrid) has serialization issues. widgetID := model.ID(types.GenerateID()) - // Load embedded template (required for pluggable widgets to work) - embeddedType, embeddedObject, embeddedIDs, embeddedObjectTypeID, err := widgets.GetTemplateFullBSON(pages.WidgetIDDataGrid2, types.GenerateID, pb.reader.Path()) - if err != nil { - return nil, mdlerrors.NewBackend("load DataGrid2 template", err) - } - if embeddedType == nil || embeddedObject == nil { - return nil, mdlerrors.NewNotFound("widget template", "DataGrid2") - } - - // Convert widget IDs to pages.PropertyTypeIDEntry format - propertyTypeIDs := convertPropertyTypeIDs(embeddedIDs) - // Build datasource from V3 DataSource property var datasource pages.DataSource if ds := w.GetDataSource(); ds != nil { @@ -121,71 +105,73 @@ func (pb *pageBuilder) buildDataGridV3(w *ast.WidgetV3) (*pages.CustomWidget, er } // Extract column definitions and CONTROLBAR widgets from children - var columns []ast.DataGridColumnDef - var headerWidgets []bson.D + var columns []backend.DataGridColumnSpec + var headerWidgets []pages.Widget for _, child := range w.Children { switch strings.ToUpper(child.Type) { case "COLUMN": attr := child.GetAttribute() - // Sugar: when no explicit Attribute: property is given, fall back to - // the column's name. This lets `COLUMN Sku (Caption: 'SKU')` work - // without repeating `Attribute: Sku`. Skip for custom-content columns - // (those with a body of child widgets), which don't bind to an attribute. if attr == "" && child.Name != "" && len(child.Children) == 0 { attr = child.Name } - col := ast.DataGridColumnDef{ - Attribute: attr, + col := backend.DataGridColumnSpec{ + Attribute: pb.resolveAttributePath(attr), Caption: child.GetCaption(), - ChildrenV3: child.Children, // Child widgets for custom content columns Properties: child.Properties, } + // Build child widgets for custom content columns + for _, grandchild := range child.Children { + childWidget, err := pb.buildWidgetV3(grandchild) + if err != nil { + return nil, mdlerrors.NewBackend("build column child widget", err) + } + if childWidget != nil { + col.ChildWidgets = append(col.ChildWidgets, childWidget) + } + } columns = append(columns, col) case "CONTROLBAR": - // Build CONTROLBAR widgets as BSON for the filtersPlaceholder property for _, controlBarChild := range child.Children { - widgetBSON, err := pb.buildWidgetV3ToBSON(controlBarChild) + childWidget, err := pb.buildWidgetV3(controlBarChild) if err != nil { return nil, mdlerrors.NewBackend("build controlbar widget", err) } - if widgetBSON != nil { - headerWidgets = append(headerWidgets, widgetBSON) + if childWidget != nil { + headerWidgets = append(headerWidgets, childWidget) } } } } - // Update the template object with datasource, columns, and header widgets - var updatedObject bson.D - if len(columns) > 0 || len(headerWidgets) > 0 { - // Use full update that replaces columns and/or header widgets - updatedObject = pb.updateDataGrid2Object(embeddedObject, propertyTypeIDs, datasource, columns, headerWidgets) - } else { - // No columns or header widgets defined, use template columns - updatedObject = pb.cloneDataGrid2ObjectWithDatasourceOnly(embeddedObject, propertyTypeIDs, datasource) + // Collect paging overrides from AST properties + pagingOverrides := make(map[string]string) + for mdlKey, widgetKey := range dataGridPagingPropMap { + if v := w.GetStringProp(mdlKey); v != "" { + pagingOverrides[widgetKey] = v + } else if iv := w.GetIntProp(mdlKey); iv > 0 { + pagingOverrides[widgetKey] = fmt.Sprintf("%d", iv) + } else if bv, ok := w.Properties[mdlKey]; ok { + if boolVal, isBool := bv.(bool); isBool { + if boolVal { + pagingOverrides[widgetKey] = "yes" + } else { + pagingOverrides[widgetKey] = "no" + } + } + } } - // Apply paging properties from AST if specified - updatedObject = pb.applyDataGridPagingProps(updatedObject, propertyTypeIDs, w) - - // Apply selection mode if specified - if selection := w.GetSelection(); selection != "" { - updatedObject = pb.applyDataGridSelectionProp(updatedObject, propertyTypeIDs, selection) + spec := backend.DataGridSpec{ + DataSource: datasource, + Columns: columns, + HeaderWidgets: headerWidgets, + PagingOverrides: pagingOverrides, + SelectionMode: w.GetSelection(), } - grid := &pages.CustomWidget{ - BaseWidget: pages.BaseWidget{ - BaseElement: model.BaseElement{ - ID: widgetID, - TypeName: "CustomWidgets$CustomWidget", - }, - Name: w.Name, - }, - Editable: "Always", - RawType: embeddedType, - RawObject: updatedObject, - PropertyTypeIDMap: propertyTypeIDs, - ObjectTypeID: embeddedObjectTypeID, + grid, err := pb.widgetBackend.BuildDataGrid2Widget(widgetID, w.Name, spec, pb.backend.Path()) + if err != nil { + return nil, err } if err := pb.registerWidgetName(w.Name, grid.ID); err != nil { @@ -905,102 +891,3 @@ var dataGridPagingPropMap = map[string]string{ // "ShowNumberOfRows" is defined in DataGrid2 type but not yet fully supported; // setting it to a non-default value causes CE0463 "widget definition changed". } - -// applyDataGridPagingProps applies paging properties from the AST to the DataGrid2 BSON object. -// It iterates through the object's Properties array, matching TypePointers to known paging -// property keys, and replaces PrimitiveValue when a corresponding AST property is set. -func (pb *pageBuilder) applyDataGridPagingProps(obj bson.D, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, w *ast.WidgetV3) bson.D { - // Collect overrides: camelCase key -> string value - overrides := make(map[string]string) - for mdlKey, widgetKey := range dataGridPagingPropMap { - if v := w.GetStringProp(mdlKey); v != "" { - overrides[widgetKey] = v - } else if iv := w.GetIntProp(mdlKey); iv > 0 { - overrides[widgetKey] = fmt.Sprintf("%d", iv) - } else if bv, ok := w.Properties[mdlKey]; ok { - if boolVal, isBool := bv.(bool); isBool { - if boolVal { - overrides[widgetKey] = "yes" - } else { - overrides[widgetKey] = "no" - } - } - } - } - if len(overrides) == 0 { - return obj - } - - // Build reverse map: TypePointer ID -> widget property key - typePointerToKey := make(map[string]string) - for widgetKey, entry := range propertyTypeIDs { - typePointerToKey[entry.PropertyTypeID] = widgetKey - } - - // Walk the object and replace properties that have overrides - result := make(bson.D, 0, len(obj)) - for _, elem := range obj { - if elem.Key == "Properties" { - if propsArr, ok := elem.Value.(bson.A); ok { - updatedProps := bson.A{propsArr[0]} // Keep version marker - for _, propVal := range propsArr[1:] { - propMap, ok := propVal.(bson.D) - if !ok { - updatedProps = append(updatedProps, propVal) - continue - } - tp := pb.getTypePointerFromProperty(propMap) - widgetKey := typePointerToKey[tp] - if newVal, hasOverride := overrides[widgetKey]; hasOverride { - updatedProps = append(updatedProps, pb.clonePropertyWithPrimitiveValue(propMap, newVal)) - } else { - updatedProps = append(updatedProps, propMap) - } - } - result = append(result, bson.E{Key: "Properties", Value: updatedProps}) - } else { - result = append(result, elem) - } - } else { - result = append(result, elem) - } - } - return result -} - -// applyDataGridSelectionProp applies the Selection mode to a DataGrid2 object. -// DataGrid2 uses the same "itemSelection" property key as Gallery. -func (pb *pageBuilder) applyDataGridSelectionProp(obj bson.D, propertyTypeIDs map[string]pages.PropertyTypeIDEntry, selectionMode string) bson.D { - itemSelectionEntry, ok := propertyTypeIDs["itemSelection"] - if !ok { - return obj - } - - result := make(bson.D, 0, len(obj)) - for _, elem := range obj { - if elem.Key == "Properties" { - if propsArr, ok := elem.Value.(bson.A); ok { - updatedProps := bson.A{propsArr[0]} // Keep version marker - for _, propVal := range propsArr[1:] { - propMap, ok := propVal.(bson.D) - if !ok { - updatedProps = append(updatedProps, propVal) - continue - } - tp := pb.getTypePointerFromProperty(propMap) - if tp == itemSelectionEntry.PropertyTypeID { - updatedProps = append(updatedProps, pb.buildGallerySelectionProperty(propMap, selectionMode)) - } else { - updatedProps = append(updatedProps, propMap) - } - } - result = append(result, bson.E{Key: "Properties", Value: updatedProps}) - } else { - result = append(result, elem) - } - } else { - result = append(result, elem) - } - } - return result -} diff --git a/mdl/executor/cmd_pages_create_v3.go b/mdl/executor/cmd_pages_create_v3.go index 67b52c21..50fb0a21 100644 --- a/mdl/executor/cmd_pages_create_v3.go +++ b/mdl/executor/cmd_pages_create_v3.go @@ -53,8 +53,7 @@ func execCreatePageV3(ctx *ExecContext, s *ast.CreatePageStmtV3) error { // Build the page BEFORE deleting the old one (atomic: if build fails, old page is preserved) pb := &pageBuilder{ - writer: e.writer, - reader: e.reader, + backend: ctx.Backend, moduleID: moduleID, moduleName: s.Name.Module, widgetScope: make(map[string]model.ID), @@ -130,8 +129,7 @@ func execCreateSnippetV3(ctx *ExecContext, s *ast.CreateSnippetStmtV3) error { // Build the snippet BEFORE deleting the old one (atomic: if build fails, old snippet is preserved) pb := &pageBuilder{ - writer: e.writer, - reader: e.reader, + backend: ctx.Backend, moduleID: moduleID, moduleName: s.Name.Module, widgetScope: make(map[string]model.ID), diff --git a/mdl/executor/cmd_widgets.go b/mdl/executor/cmd_widgets.go index ac037c35..cd5020fc 100644 --- a/mdl/executor/cmd_widgets.go +++ b/mdl/executor/cmd_widgets.go @@ -7,8 +7,6 @@ import ( "fmt" "strings" - "go.mongodb.org/mongo-driver/bson" - "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/model" @@ -197,91 +195,27 @@ func groupWidgetsByContainer(widgets []widgetRef) map[string][]widgetRef { return containers } -// updateWidgetsInContainer updates widgets within a single page or snippet. +// updateWidgetsInContainer updates widgets within a single page or snippet +// using the PageMutator backend (no direct BSON manipulation). func updateWidgetsInContainer(ctx *ExecContext, containerID string, widgetRefs []widgetRef, assignments []ast.WidgetPropertyAssignment, dryRun bool) (int, error) { if len(widgetRefs) == 0 { return 0, nil } - containerType := widgetRefs[0].ContainerType containerName := widgetRefs[0].ContainerName - // Load the page or snippet - if strings.ToLower(containerType) == "page" { - return updateWidgetsInPage(ctx, containerID, containerName, widgetRefs, assignments, dryRun) - } else if strings.ToLower(containerType) == "snippet" { - return updateWidgetsInSnippet(ctx, containerID, containerName, widgetRefs, assignments, dryRun) - } - - return 0, mdlerrors.NewUnsupported(fmt.Sprintf("unsupported container type: %s", containerType)) -} - -// updateWidgetsInPage updates widgets in a page using raw BSON. -func updateWidgetsInPage(ctx *ExecContext, containerID, containerName string, widgetRefs []widgetRef, assignments []ast.WidgetPropertyAssignment, dryRun bool) (int, error) { - - // Load raw BSON as ordered document (preserves field ordering) - rawBytes, err := ctx.Backend.GetRawUnitBytes(model.ID(containerID)) - if err != nil { - 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, mdlerrors.NewBackend(fmt.Sprintf("unmarshal page %s", containerName), err) - } - - updated := 0 - for _, ref := range widgetRefs { - result := findBsonWidget(rawData, ref.Name) - if result == nil { - fmt.Fprintf(ctx.Output, " Warning: Widget %q not found in page %s\n", ref.Name, containerName) - continue - } - for _, assignment := range assignments { - if dryRun { - fmt.Fprintf(ctx.Output, " Would set '%s' = %v on %s (%s) in %s\n", - assignment.PropertyPath, assignment.Value, ref.Name, ref.WidgetType, containerName) - } else { - if err := setRawWidgetProperty(result.widget, assignment.PropertyPath, assignment.Value); err != nil { - fmt.Fprintf(ctx.Output, " Warning: Failed to set '%s' on %s: %v\n", - assignment.PropertyPath, ref.Name, err) - } - } - } - updated++ - } - - // Save back via raw BSON (bson.D preserves field ordering) - if !dryRun && updated > 0 { - outBytes, err := bson.Marshal(rawData) - if err != nil { - return updated, mdlerrors.NewBackend(fmt.Sprintf("marshal page %s", containerName), err) - } - if err := ctx.Backend.UpdateRawUnit(containerID, outBytes); err != nil { - return updated, mdlerrors.NewBackend(fmt.Sprintf("save page %s", containerName), err) - } - } - - return updated, nil -} - -// updateWidgetsInSnippet updates widgets in a snippet using raw BSON. -func updateWidgetsInSnippet(ctx *ExecContext, containerID, containerName string, widgetRefs []widgetRef, assignments []ast.WidgetPropertyAssignment, dryRun bool) (int, error) { - - // Load raw BSON as ordered document (preserves field ordering) - rawBytes, err := ctx.Backend.GetRawUnitBytes(model.ID(containerID)) + // Open the container (page, layout, or snippet) through the backend mutator. + mutator, err := ctx.Backend.OpenPageForMutation(model.ID(containerID)) if err != nil { - 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, mdlerrors.NewBackend(fmt.Sprintf("unmarshal snippet %s", containerName), err) + return 0, mdlerrors.NewBackend(fmt.Sprintf("open %s for mutation", containerName), err) } updated := 0 for _, ref := range widgetRefs { - result := findBsonWidgetInSnippet(rawData, ref.Name) - if result == nil { - fmt.Fprintf(ctx.Output, " Warning: Widget %q not found in snippet %s\n", ref.Name, containerName) + // Verify the widget exists before attempting assignments. + if !mutator.FindWidget(ref.Name) { + fmt.Fprintf(ctx.Output, " Warning: Widget %q not found in %s %s\n", + ref.Name, mutator.ContainerType(), containerName) continue } for _, assignment := range assignments { @@ -289,7 +223,7 @@ func updateWidgetsInSnippet(ctx *ExecContext, containerID, containerName string, fmt.Fprintf(ctx.Output, " Would set '%s' = %v on %s (%s) in %s\n", assignment.PropertyPath, assignment.Value, ref.Name, ref.WidgetType, containerName) } else { - if err := setRawWidgetProperty(result.widget, assignment.PropertyPath, assignment.Value); err != nil { + if err := mutator.SetWidgetProperty(ref.Name, assignment.PropertyPath, assignment.Value); err != nil { fmt.Fprintf(ctx.Output, " Warning: Failed to set '%s' on %s: %v\n", assignment.PropertyPath, ref.Name, err) } @@ -298,14 +232,10 @@ func updateWidgetsInSnippet(ctx *ExecContext, containerID, containerName string, updated++ } - // Save back via raw BSON (bson.D preserves field ordering) + // Persist changes via the mutator. if !dryRun && updated > 0 { - outBytes, err := bson.Marshal(rawData) - if err != nil { - return updated, mdlerrors.NewBackend(fmt.Sprintf("marshal snippet %s", containerName), err) - } - if err := ctx.Backend.UpdateRawUnit(containerID, outBytes); err != nil { - return updated, mdlerrors.NewBackend(fmt.Sprintf("save snippet %s", containerName), err) + if err := mutator.Save(); err != nil { + return updated, mdlerrors.NewBackend(fmt.Sprintf("save %s", containerName), err) } } diff --git a/mdl/executor/executor.go b/mdl/executor/executor.go index e68fa89a..5b76f689 100644 --- a/mdl/executor/executor.go +++ b/mdl/executor/executor.go @@ -146,24 +146,26 @@ const ( executeTimeout = 5 * time.Minute ) +// BackendFactory creates a new backend instance for connecting to a project. +type BackendFactory func() backend.FullBackend + // Executor executes MDL statements against a Mendix project. type Executor struct { - writer *mpr.Writer - reader *mpr.Reader - backend backend.FullBackend // domain backend (populated on Connect) - output io.Writer - guard *outputGuard // line-limit wrapper around output - mprPath string - settings map[string]any - cache *executorCache - catalog *catalog.Catalog - quiet bool // suppress connection and status messages - format OutputFormat // output format (table, json) - logger *diaglog.Logger // session diagnostics logger (nil = no logging) - fragments map[string]*ast.DefineFragmentStmt // script-scoped fragment definitions - sqlMgr *sqllib.Manager // external SQL connection manager (lazy init) - themeRegistry *ThemeRegistry // cached theme design property definitions (lazy init) - registry *Registry // statement dispatch registry + backend backend.FullBackend // domain backend (populated on Connect) + backendFactory BackendFactory // factory for creating new backend instances + output io.Writer + guard *outputGuard // line-limit wrapper around output + mprPath string + settings map[string]any + cache *executorCache + catalog *catalog.Catalog + quiet bool // suppress connection and status messages + format OutputFormat // output format (table, json) + logger *diaglog.Logger // session diagnostics logger (nil = no logging) + fragments map[string]*ast.DefineFragmentStmt // script-scoped fragment definitions + sqlMgr *sqllib.Manager // external SQL connection manager (lazy init) + themeRegistry *ThemeRegistry // cached theme design property definitions (lazy init) + registry *Registry // statement dispatch registry } // New creates a new executor with the given output writer. @@ -177,6 +179,11 @@ func New(output io.Writer) *Executor { } } +// SetBackendFactory sets the factory function used to create backend instances on Connect. +func (e *Executor) SetBackendFactory(f BackendFactory) { + e.backendFactory = f +} + // SetQuiet enables or disables quiet mode (suppresses connection/status messages). func (e *Executor) SetQuiet(quiet bool) { e.quiet = quiet @@ -250,7 +257,7 @@ func (e *Executor) ExecuteProgram(prog *ast.Program) error { // trackModifiedDomainModel records a domain model that was modified during execution, // so it can be reconciled at the end of the program. func (e *Executor) trackModifiedDomainModel(moduleID model.ID, moduleName string) { - if e.writer == nil { + if e.backend == nil || !e.backend.IsConnected() { return } if e.cache == nil { @@ -265,17 +272,17 @@ func (e *Executor) trackModifiedDomainModel(moduleID model.ID, moduleName string // finalizeProgramExecution runs post-execution reconciliation on modified domain models. func (e *Executor) finalizeProgramExecution() error { - if e.writer == nil || e.cache == nil || len(e.cache.modifiedDomainModels) == 0 { + if e.backend == nil || !e.backend.IsConnected() || e.cache == nil || len(e.cache.modifiedDomainModels) == 0 { return nil } for moduleID, moduleName := range e.cache.modifiedDomainModels { - dm, err := e.reader.GetDomainModel(moduleID) + dm, err := e.backend.GetDomainModel(moduleID) if err != nil { continue // module may not have a domain model } - count, err := e.writer.ReconcileMemberAccesses(dm.ID, moduleName) + count, err := e.backend.ReconcileMemberAccesses(dm.ID, moduleName) if err != nil { return mdlerrors.NewBackend(fmt.Sprintf("reconcile security for module %s", moduleName), err) } @@ -295,21 +302,29 @@ func (e *Executor) Catalog() *catalog.Catalog { } // Reader returns the MPR reader, or nil if not connected. +// Deprecated: External callers should migrate to using Backend methods directly. func (e *Executor) Reader() *mpr.Reader { - return e.reader + if e.backend == nil { + return nil + } + type readerProvider interface { + MprReader() *mpr.Reader + } + if rp, ok := e.backend.(readerProvider); ok { + return rp.MprReader() + } + return nil } // IsConnected returns true if connected to a project. func (e *Executor) IsConnected() bool { - return e.writer != nil + return e.backend != nil && e.backend.IsConnected() } // Close closes the connection to the project and all SQL connections. func (e *Executor) Close() error { - if e.writer != nil { - e.writer.Close() - e.writer = nil - e.reader = nil + if e.backend != nil && e.backend.IsConnected() { + e.backend.Disconnect() e.backend = nil } if e.sqlMgr != nil { diff --git a/mdl/executor/executor_connect.go b/mdl/executor/executor_connect.go index 74ad9894..6ae49507 100644 --- a/mdl/executor/executor_connect.go +++ b/mdl/executor/executor_connect.go @@ -6,30 +6,27 @@ import ( "fmt" "github.com/mendixlabs/mxcli/mdl/ast" - mprbackend "github.com/mendixlabs/mxcli/mdl/backend/mpr" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" - "github.com/mendixlabs/mxcli/sdk/mpr" ) func execConnect(ctx *ExecContext, s *ast.ConnectStmt) error { e := ctx.executor - if ctx.ConnectedForWrite() { - e.writer.Close() + if e.backend != nil && e.backend.IsConnected() { + e.backend.Disconnect() } - writer, err := mpr.NewWriter(s.Path) - if err != nil { + if e.backendFactory == nil { + return mdlerrors.NewBackend("connect", fmt.Errorf("no backend factory configured")) + } + b := e.backendFactory() + if err := b.Connect(s.Path); err != nil { return mdlerrors.NewBackend("connect", err) } - e.writer = writer - e.reader = writer.Reader() + e.backend = b e.mprPath = s.Path e.cache = &executorCache{} // Initialize fresh cache - // Wrap the writer in an MprBackend for ctx.Backend propagation. - e.backend = mprbackend.Wrap(writer, s.Path) - // Propagate connection state back to ctx so subsequent code in this // dispatch cycle sees the updated values. ctx.Backend = e.backend @@ -44,7 +41,7 @@ func execConnect(ctx *ExecContext, s *ast.ConnectStmt) error { ctx.ThemeRegistry = nil // Display connection info with version - pv := e.reader.ProjectVersion() + pv := e.backend.ProjectVersion() if !ctx.Quiet { fmt.Fprintf(ctx.Output, "Connected to: %s (Mendix %s)\n", s.Path, pv.ProductVersion) } @@ -63,20 +60,18 @@ func reconnect(ctx *ExecContext) error { } // Close existing connection - if ctx.ConnectedForWrite() { - e.writer.Close() + if e.backend != nil && e.backend.IsConnected() { + e.backend.Disconnect() } // Reopen connection - writer, err := mpr.NewWriter(e.mprPath) - if err != nil { + b := e.backendFactory() + if err := b.Connect(e.mprPath); err != nil { return mdlerrors.NewBackend("reconnect", err) } - e.writer = writer - e.reader = writer.Reader() + e.backend = b e.cache = &executorCache{} // Reset cache - e.backend = mprbackend.Wrap(writer, e.mprPath) // Propagate reconnection state back to ctx. ctx.Backend = e.backend @@ -93,7 +88,7 @@ func reconnect(ctx *ExecContext) error { func execDisconnect(ctx *ExecContext) error { e := ctx.executor - if !ctx.ConnectedForWrite() { + if e.backend == nil || !e.backend.IsConnected() { fmt.Fprintln(ctx.Output, "Not connected") return nil } @@ -103,10 +98,8 @@ func execDisconnect(ctx *ExecContext) error { fmt.Fprintf(ctx.Output, "Warning: finalization error: %v\n", err) } - e.writer.Close() + e.backend.Disconnect() fmt.Fprintf(ctx.Output, "Disconnected from: %s\n", e.mprPath) - e.writer = nil - e.reader = nil e.mprPath = "" e.cache = nil e.backend = nil @@ -125,19 +118,19 @@ func execDisconnect(ctx *ExecContext) error { func execStatus(ctx *ExecContext) error { e := ctx.executor - if !ctx.ConnectedForWrite() { + if e.backend == nil || !e.backend.IsConnected() { fmt.Fprintln(ctx.Output, "Status: Not connected") return nil } - pv := e.reader.ProjectVersion() + pv := e.backend.ProjectVersion() fmt.Fprintf(ctx.Output, "Status: Connected\n") fmt.Fprintf(ctx.Output, "Project: %s\n", e.mprPath) fmt.Fprintf(ctx.Output, "Mendix Version: %s\n", pv.ProductVersion) fmt.Fprintf(ctx.Output, "MPR Format: v%d\n", pv.FormatVersion) // Show module count - modules, err := e.reader.ListModules() + modules, err := e.backend.ListModules() if err == nil { fmt.Fprintf(ctx.Output, "Modules: %d\n", len(modules)) } diff --git a/mdl/executor/hierarchy.go b/mdl/executor/hierarchy.go index 700a4c66..628c91ca 100644 --- a/mdl/executor/hierarchy.go +++ b/mdl/executor/hierarchy.go @@ -7,10 +7,18 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/backend" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" - "github.com/mendixlabs/mxcli/sdk/mpr" ) +// hierarchySource is the minimal interface needed to build a ContainerHierarchy. +// Both *mpr.Reader and backend.FullBackend satisfy this. +type hierarchySource interface { + ListModules() ([]*model.Module, error) + ListUnits() ([]*types.UnitInfo, error) + ListFolders() ([]*types.FolderInfo, error) +} + // ContainerHierarchy provides efficient module and folder resolution for documents. // It caches the container hierarchy to avoid repeated lookups. type ContainerHierarchy struct { @@ -20,43 +28,18 @@ type ContainerHierarchy struct { folderNames map[model.ID]string } -// NewContainerHierarchy creates a new hierarchy from the reader. -func NewContainerHierarchy(reader *mpr.Reader) (*ContainerHierarchy, error) { - h := &ContainerHierarchy{ - moduleIDs: make(map[model.ID]bool), - moduleNames: make(map[model.ID]string), - containerParent: make(map[model.ID]model.ID), - folderNames: make(map[model.ID]string), - } - - // Load modules - modules, err := reader.ListModules() - if err != nil { - return nil, err - } - for _, m := range modules { - h.moduleIDs[m.ID] = true - h.moduleNames[m.ID] = m.Name - } - - // Load units for container hierarchy - units, _ := reader.ListUnits() - for _, u := range units { - h.containerParent[u.ID] = u.ContainerID - } - - // Load folders - folders, _ := reader.ListFolders() - for _, f := range folders { - h.folderNames[f.ID] = f.Name - h.containerParent[f.ID] = f.ContainerID - } - - return h, nil +// NewContainerHierarchy creates a new hierarchy from any source that provides +// modules, units, and folders (e.g. *mpr.Reader or backend.FullBackend). +func NewContainerHierarchy(src hierarchySource) (*ContainerHierarchy, error) { + return newContainerHierarchyImpl(src) } // NewContainerHierarchyFromBackend creates a new hierarchy from a Backend interface. func NewContainerHierarchyFromBackend(b backend.FullBackend) (*ContainerHierarchy, error) { + return newContainerHierarchyImpl(b) +} + +func newContainerHierarchyImpl(src hierarchySource) (*ContainerHierarchy, error) { h := &ContainerHierarchy{ moduleIDs: make(map[model.ID]bool), moduleNames: make(map[model.ID]string), @@ -64,7 +47,7 @@ func NewContainerHierarchyFromBackend(b backend.FullBackend) (*ContainerHierarch folderNames: make(map[model.ID]string), } - modules, err := b.ListModules() + modules, err := src.ListModules() if err != nil { return nil, err } @@ -73,12 +56,12 @@ func NewContainerHierarchyFromBackend(b backend.FullBackend) (*ContainerHierarch h.moduleNames[m.ID] = m.Name } - units, _ := b.ListUnits() + units, _ := src.ListUnits() for _, u := range units { h.containerParent[u.ID] = u.ContainerID } - folders, _ := b.ListFolders() + folders, _ := src.ListFolders() for _, f := range folders { h.folderNames[f.ID] = f.Name h.containerParent[f.ID] = f.ContainerID diff --git a/mdl/executor/widget_property.go b/mdl/executor/widget_property.go index e44652a8..75bdafc4 100644 --- a/mdl/executor/widget_property.go +++ b/mdl/executor/widget_property.go @@ -1,254 +1,12 @@ // SPDX-License-Identifier: Apache-2.0 -// Package executor - Widget property navigation for UPDATE WIDGETS +// Package executor - Widget tree walking for styling and introspection commands. package executor import ( - "fmt" - "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" ) -// getWidgetID extracts the ID from a widget. -func getWidgetID(widget any) string { - if widget == nil { - return "" - } - - // Use reflection to get the ID field - v := reflect.ValueOf(widget) - if v.Kind() == reflect.Pointer { - v = v.Elem() - } - if v.Kind() != reflect.Struct { - return "" - } - - // Try to get BaseWidget.ID - if baseWidget := v.FieldByName("BaseWidget"); baseWidget.IsValid() { - if idField := baseWidget.FieldByName("ID"); idField.IsValid() { - return fmt.Sprintf("%v", idField.Interface()) - } - } - - // Direct ID field - if idField := v.FieldByName("ID"); idField.IsValid() { - return fmt.Sprintf("%v", idField.Interface()) - } - - return "" -} - -// setWidgetProperty sets a property value on a widget by path. -func setWidgetProperty(widget any, path string, value any) error { - if widget == nil { - return mdlerrors.NewValidation("widget is nil") - } - - // Handle CustomWidget specifically - if cw, ok := widget.(*pages.CustomWidget); ok { - return setCustomWidgetProperty(cw, path, value) - } - - // For other widget types, use reflection to set simple fields - return setWidgetFieldByReflection(widget, path, value) -} - -// setCustomWidgetProperty sets a property on a CustomWidget. -func setCustomWidgetProperty(cw *pages.CustomWidget, path string, value any) error { - // CustomWidget can have properties in RawObject (BSON) or WidgetObject (structured) - if cw.RawObject != nil { - return updateBSONWidgetProperty(cw.RawObject, path, value) - } - - if cw.WidgetObject != nil { - return updateStructuredWidgetProperty(cw.WidgetObject, cw.PropertyTypeIDMap, path, value) - } - - return mdlerrors.NewValidation("widget has no property data") -} - -// updateBSONWidgetProperty updates a property in a BSON document. -func updateBSONWidgetProperty(doc bson.D, path string, value any) error { - // Find the "Object" field which contains widget properties - for i := range doc { - if doc[i].Key == "Object" { - if objDoc, ok := doc[i].Value.(bson.D); ok { - // Find Properties array - for j := range objDoc { - if objDoc[j].Key == "Properties" { - if props, ok := objDoc[j].Value.(bson.A); ok { - return updateBSONPropertyByKey(props, path, value) - } - } - } - } - } - } - return mdlerrors.NewNotFound("property", path) -} - -// updateBSONPropertyByKey finds and updates a property by key in a BSON array. -func updateBSONPropertyByKey(props bson.A, path string, value any) error { - for i, prop := range props { - if propDoc, ok := prop.(bson.D); ok { - // Find the Key field - for _, field := range propDoc { - if field.Key == "Key" { - if key, ok := field.Value.(string); ok && key == path { - // Found the property, now update its Value - return updateBSONPropertyValueAtIndex(props, i, value) - } - } - } - } - } - 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 mdlerrors.NewValidation("property is not a BSON document") - } - - for i := range propDoc { - if propDoc[i].Key == "Value" { - if valueDoc, ok := propDoc[i].Value.(bson.D); ok { - // Find PrimitiveValue in the Value document - for j := range valueDoc { - if valueDoc[j].Key == "PrimitiveValue" { - valueDoc[j].Value = convertToBSONValue(newValue) - propDoc[i].Value = valueDoc - props[index] = propDoc - return nil - } - } - // PrimitiveValue not found, try to add it - valueDoc = append(valueDoc, bson.E{Key: "PrimitiveValue", Value: convertToBSONValue(newValue)}) - propDoc[i].Value = valueDoc - props[index] = propDoc - return nil - } - } - } - - return mdlerrors.NewValidation("Value field not found in property") -} - -// convertToBSONValue converts a Go value to appropriate BSON format. -func convertToBSONValue(value any) any { - switch v := value.(type) { - case bool: - if v { - return "true" - } - return "false" - case string: - return v - case int, int64, int32: - return fmt.Sprintf("%d", v) - case float64, float32: - return fmt.Sprintf("%v", v) - default: - return fmt.Sprintf("%v", v) - } -} - -// 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 mdlerrors.NewValidation("widget object has no properties") - } - - bsonValue := convertToBSONValue(value) - strValue, ok := bsonValue.(string) - if !ok { - strValue = fmt.Sprintf("%v", bsonValue) - } - - // Find the property by PropertyKey - for i, prop := range obj.Properties { - if prop.PropertyKey == path { - // Update the primitive value - if prop.Value != nil { - obj.Properties[i].Value.PrimitiveValue = strValue - return nil - } - // Create a value if it doesn't exist - obj.Properties[i].Value = &pages.WidgetValue{ - PrimitiveValue: strValue, - } - return nil - } - } - - // Check if we can find it via the type map (case-insensitive) - if typeMap != nil { - for key := range typeMap { - if strings.EqualFold(key, path) { - // Find the actual property - for i, prop := range obj.Properties { - if prop.PropertyKey == key { - if prop.Value != nil { - obj.Properties[i].Value.PrimitiveValue = strValue - return nil - } - obj.Properties[i].Value = &pages.WidgetValue{ - PrimitiveValue: strValue, - } - return nil - } - } - } - } - } - - return mdlerrors.NewNotFound("property", path) -} - -// setWidgetFieldByReflection sets a simple field on a widget using reflection. -func setWidgetFieldByReflection(widget any, fieldName string, value any) error { - v := reflect.ValueOf(widget) - if v.Kind() == reflect.Pointer { - v = v.Elem() - } - if v.Kind() != reflect.Struct { - return mdlerrors.NewValidation("widget is not a struct") - } - - field := v.FieldByName(fieldName) - if !field.IsValid() { - return mdlerrors.NewNotFound("field", fieldName) - } - if !field.CanSet() { - return mdlerrors.NewValidationf("field not settable: %s", fieldName) - } - - // Convert value to field type - fieldType := field.Type() - valueVal := reflect.ValueOf(value) - - if valueVal.Type().ConvertibleTo(fieldType) { - field.Set(valueVal.Convert(fieldType)) - return nil - } - - // Handle string conversion for common types - if fieldType.Kind() == reflect.String { - field.SetString(fmt.Sprintf("%v", value)) - return nil - } - - return mdlerrors.NewValidationf("cannot convert %T to %s", value, fieldType) -} - // walkPageWidgets walks all widgets in a page and calls the visitor function. func walkPageWidgets(page *pages.Page, visitor func(widget any) error) error { if page == nil || page.LayoutCall == nil { @@ -365,13 +123,3 @@ func walkWidget(widget pages.Widget, visitor func(widget any) error) error { return nil } - -// getSnippetByID finds a snippet by ID from the list. -func getSnippetByID(snippets []*pages.Snippet, id model.ID) *pages.Snippet { - for _, s := range snippets { - if s.ID == id { - return s - } - } - return nil -} diff --git a/mdl/repl/repl.go b/mdl/repl/repl.go index 5f0a9224..d82313d7 100644 --- a/mdl/repl/repl.go +++ b/mdl/repl/repl.go @@ -14,6 +14,8 @@ import ( "strings" "github.com/chzyer/readline" + "github.com/mendixlabs/mxcli/mdl/backend" + mprbackend "github.com/mendixlabs/mxcli/mdl/backend/mpr" "github.com/mendixlabs/mxcli/mdl/diaglog" "github.com/mendixlabs/mxcli/mdl/executor" "github.com/mendixlabs/mxcli/mdl/visitor" @@ -37,8 +39,10 @@ func (r *REPL) SetLogger(l *diaglog.Logger) { // New creates a new REPL with the given input and output. func New(input io.Reader, output io.Writer) *REPL { + exec := executor.New(output) + exec.SetBackendFactory(func() backend.FullBackend { return mprbackend.New() }) return &REPL{ - executor: executor.New(output), + executor: exec, input: input, output: output, prompt: "mdl> ", From bebb7e689173aeca723d7e4b555ff016fb322f3b Mon Sep 17 00:00:00 2001 From: Andrew Vasilyev Date: Sun, 19 Apr 2026 19:25:09 +0200 Subject: [PATCH 5/5] =?UTF-8?q?refactor:=20improve=20code=20quality=20?= =?UTF-8?q?=E2=80=94=20deterministic=20output,=20doc=20comments,=20naming?= =?UTF-8?q?=20consistency?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ensure deterministic map iteration order for serialization output. Add doc comments on all exported backend interfaces. Deduplicate IDToBsonBinary into single mdl/bsonutil implementation. Rename reader references to backend across executor. Guard float64-to-int64 cast with safe precision bounds. Apply go fmt formatting. --- mdl/ast/ast_agenteditor.go | 60 ++-- mdl/backend/connection.go | 12 + mdl/backend/infrastructure.go | 24 +- mdl/backend/mapping.go | 2 + mdl/backend/microflow.go | 2 +- mdl/backend/mock/backend.go | 28 +- mdl/backend/mock/mock_infrastructure.go | 2 +- mdl/backend/mock/mock_java.go | 2 +- mdl/backend/mock/mock_mapping.go | 2 +- mdl/backend/mock/mock_navigation.go | 2 +- mdl/backend/mock/mock_security.go | 2 +- mdl/backend/mock/mock_workflow.go | 2 +- mdl/backend/mpr/backend.go | 32 +- mdl/backend/mpr/convert.go | 293 +----------------- mdl/backend/mpr/datagrid_builder.go | 20 +- mdl/backend/mpr/datagrid_builder_test.go | 14 +- mdl/backend/mpr/page_mutator.go | 8 + mdl/backend/mpr/widget_builder.go | 157 +++------- mdl/backend/mpr/workflow_mutator.go | 3 +- mdl/backend/mutation.go | 52 ++-- mdl/backend/page.go | 2 +- mdl/backend/workflow.go | 20 -- mdl/bsonutil/bsonutil.go | 6 +- mdl/catalog/builder.go | 2 +- mdl/executor/bugfix_regression_test.go | 2 +- mdl/executor/bugfix_test.go | 2 +- mdl/executor/cmd_alter_page.go | 11 +- mdl/executor/cmd_businessevents.go | 2 +- mdl/executor/cmd_catalog.go | 2 +- mdl/executor/cmd_entities.go | 6 +- mdl/executor/cmd_export_mappings.go | 2 +- mdl/executor/cmd_folders.go | 2 +- mdl/executor/cmd_import_mappings.go | 2 +- mdl/executor/cmd_microflows_builder.go | 4 +- .../cmd_microflows_builder_actions.go | 24 +- .../cmd_microflows_builder_annotations.go | 2 +- mdl/executor/cmd_microflows_builder_calls.go | 18 +- .../cmd_microflows_builder_control.go | 6 +- mdl/executor/cmd_microflows_builder_flows.go | 4 +- mdl/executor/cmd_microflows_builder_graph.go | 2 +- .../cmd_microflows_builder_workflow.go | 2 +- mdl/executor/cmd_microflows_create.go | 4 +- mdl/executor/cmd_microflows_helpers.go | 6 +- mdl/executor/cmd_pages_builder.go | 4 +- mdl/executor/cmd_pages_builder_input.go | 2 +- mdl/executor/cmd_pages_builder_v3.go | 9 +- mdl/executor/cmd_pages_builder_v3_layout.go | 5 +- mdl/executor/cmd_security_write.go | 6 +- mdl/executor/cmd_structure.go | 16 +- mdl/executor/cmd_workflows_write.go | 2 +- mdl/executor/executor.go | 7 +- mdl/executor/executor_connect.go | 3 +- mdl/executor/widget_engine_test.go | 11 - mdl/executor/widget_registry_test.go | 2 +- mdl/types/asyncapi.go | 33 +- mdl/types/doc.go | 7 +- mdl/types/edmx.go | 8 +- mdl/types/id.go | 30 +- mdl/types/json_utils.go | 35 ++- sdk/mpr/reader_types.go | 2 +- sdk/mpr/utils.go | 15 +- sdk/mpr/writer_agenteditor_agent.go | 24 +- sdk/mpr/writer_core.go | 12 +- sdk/mpr/writer_jsonstructure.go | 2 - 64 files changed, 424 insertions(+), 663 deletions(-) diff --git a/mdl/ast/ast_agenteditor.go b/mdl/ast/ast_agenteditor.go index 1edd7a05..d1585840 100644 --- a/mdl/ast/ast_agenteditor.go +++ b/mdl/ast/ast_agenteditor.go @@ -16,16 +16,16 @@ package ast // [, DeepLinkURL: '...'] // ); type CreateModelStmt struct { - Name QualifiedName - Documentation string - Provider string // "MxCloudGenAI" by default - Key *QualifiedName // qualified name of the String constant holding the Portal key - DisplayName string // optional Portal-populated metadata - KeyName string // optional Portal-populated metadata - KeyID string // optional Portal-populated metadata - Environment string // optional Portal-populated metadata - ResourceName string // optional Portal-populated metadata - DeepLinkURL string // optional Portal-populated metadata + Name QualifiedName + Documentation string + Provider string // "MxCloudGenAI" by default + Key *QualifiedName // qualified name of the String constant holding the Portal key + DisplayName string // optional Portal-populated metadata + KeyName string // optional Portal-populated metadata + KeyID string // optional Portal-populated metadata + Environment string // optional Portal-populated metadata + ResourceName string // optional Portal-populated metadata + DeepLinkURL string // optional Portal-populated metadata } func (s *CreateModelStmt) isStatement() {} @@ -93,21 +93,21 @@ func (s *DropKnowledgeBaseStmt) isStatement() {} // CreateAgentStmt represents CREATE AGENT Module.Name (...) [{ body }]. type CreateAgentStmt struct { - Name QualifiedName + Name QualifiedName Documentation string - UsageType string // "Task" or "Conversational" - Description string - Model *QualifiedName // reference to a Model document - Entity *QualifiedName // reference to a domain entity - MaxTokens *int - ToolChoice string - Temperature *float64 - TopP *float64 - SystemPrompt string - UserPrompt string - Variables []AgentVarDef - Tools []AgentToolDef - KBTools []AgentKBToolDef + UsageType string // "Task" or "Conversational" + Description string + Model *QualifiedName // reference to a Model document + Entity *QualifiedName // reference to a domain entity + MaxTokens *int + ToolChoice string + Temperature *float64 + TopP *float64 + SystemPrompt string + UserPrompt string + Variables []AgentVarDef + Tools []AgentToolDef + KBTools []AgentKBToolDef } func (s *CreateAgentStmt) isStatement() {} @@ -136,10 +136,10 @@ type AgentToolDef struct { // AgentKBToolDef represents a KNOWLEDGE BASE block in CREATE AGENT body. type AgentKBToolDef struct { - Name string // per-agent identifier - Source *QualifiedName - Collection string - MaxResults int - Description string - Enabled bool + Name string // per-agent identifier + Source *QualifiedName + Collection string + MaxResults int + Description string + Enabled bool } diff --git a/mdl/backend/connection.go b/mdl/backend/connection.go index c1d3594a..02b4cb16 100644 --- a/mdl/backend/connection.go +++ b/mdl/backend/connection.go @@ -25,24 +25,36 @@ type ConnectionBackend interface { // ProjectVersion returns the Mendix project version. ProjectVersion() *types.ProjectVersion // GetMendixVersion returns the Mendix version string. + // NOTE: uses Get prefix unlike Version()/ProjectVersion() for historical SDK compatibility. GetMendixVersion() (string, error) } // ModuleBackend provides module-level operations. type ModuleBackend interface { + // ListModules returns all modules in the project. ListModules() ([]*model.Module, error) + // GetModule returns a module by ID. GetModule(id model.ID) (*model.Module, error) + // GetModuleByName returns a module by name. GetModuleByName(name string) (*model.Module, error) + // CreateModule adds a new module to the project. CreateModule(module *model.Module) error + // UpdateModule persists changes to an existing module. UpdateModule(module *model.Module) error + // DeleteModule removes a module by ID. DeleteModule(id model.ID) error + // DeleteModuleWithCleanup removes a module and cleans up associated documents. DeleteModuleWithCleanup(id model.ID, moduleName string) error } // FolderBackend provides folder operations. type FolderBackend interface { + // ListFolders returns all folders in the project. ListFolders() ([]*types.FolderInfo, error) + // CreateFolder adds a new folder. CreateFolder(folder *model.Folder) error + // DeleteFolder removes a folder by ID. DeleteFolder(id model.ID) error + // MoveFolder moves a folder to a new container. MoveFolder(id model.ID, newContainerID model.ID) error } diff --git a/mdl/backend/infrastructure.go b/mdl/backend/infrastructure.go index 779a2b19..af1db489 100644 --- a/mdl/backend/infrastructure.go +++ b/mdl/backend/infrastructure.go @@ -16,7 +16,7 @@ type RenameBackend interface { } // RawUnitBackend provides low-level unit access for operations that -// manipulate raw BSON (e.g. widget patching, alter page/workflow). +// manipulate raw unit contents (e.g. widget patching, alter page/workflow). type RawUnitBackend interface { GetRawUnit(id model.ID) (map[string]any, error) GetRawUnitBytes(id model.ID) ([]byte, error) @@ -24,6 +24,8 @@ type RawUnitBackend interface { ListRawUnits(objectType string) ([]*types.RawUnitInfo, error) GetRawUnitByName(objectType, qualifiedName string) (*types.RawUnitInfo, error) GetRawMicroflowByName(qualifiedName string) ([]byte, error) + // UpdateRawUnit replaces the contents of a unit by ID. + // Takes string (not model.ID) to match the SDK writer layer convention. UpdateRawUnit(unitID string, contents []byte) error } @@ -45,6 +47,7 @@ type WidgetBackend interface { } // AgentEditorBackend provides agent editor document operations. +// Delete methods take string IDs to match the SDK writer layer convention. type AgentEditorBackend interface { ListAgentEditorModels() ([]*agenteditor.Model, error) ListAgentEditorKnowledgeBases() ([]*agenteditor.KnowledgeBase, error) @@ -59,3 +62,22 @@ type AgentEditorBackend interface { CreateAgentEditorAgent(a *agenteditor.Agent) error DeleteAgentEditorAgent(id string) error } + +// SettingsBackend provides project settings operations. +type SettingsBackend interface { + GetProjectSettings() (*model.ProjectSettings, error) + UpdateProjectSettings(ps *model.ProjectSettings) error +} + +// ImageBackend provides image collection operations. +type ImageBackend interface { + ListImageCollections() ([]*types.ImageCollection, error) + CreateImageCollection(ic *types.ImageCollection) error + DeleteImageCollection(id string) error +} + +// ScheduledEventBackend provides scheduled event operations. +type ScheduledEventBackend interface { + ListScheduledEvents() ([]*model.ScheduledEvent, error) + GetScheduledEvent(id model.ID) (*model.ScheduledEvent, error) +} diff --git a/mdl/backend/mapping.go b/mdl/backend/mapping.go index 9bbf1efe..1018f39b 100644 --- a/mdl/backend/mapping.go +++ b/mdl/backend/mapping.go @@ -26,5 +26,7 @@ type MappingBackend interface { ListJsonStructures() ([]*types.JsonStructure, error) GetJsonStructureByQualifiedName(moduleName, name string) (*types.JsonStructure, error) CreateJsonStructure(js *types.JsonStructure) error + // DeleteJsonStructure removes a JSON structure by ID. + // Takes string (not model.ID) to match the SDK writer layer convention. DeleteJsonStructure(id string) error } diff --git a/mdl/backend/microflow.go b/mdl/backend/microflow.go index 65596ce4..1b788720 100644 --- a/mdl/backend/microflow.go +++ b/mdl/backend/microflow.go @@ -17,7 +17,7 @@ type MicroflowBackend interface { MoveMicroflow(mf *microflows.Microflow) error // ParseMicroflowFromRaw builds a Microflow from an already-unmarshalled - // BSON map. Used by diff-local and other callers that have raw map data. + // map. Used by diff-local and other callers that have raw map data. ParseMicroflowFromRaw(raw map[string]any, unitID, containerID model.ID) *microflows.Microflow ListNanoflows() ([]*microflows.Nanoflow, error) diff --git a/mdl/backend/mock/backend.go b/mdl/backend/mock/backend.go index 0b32c931..0a30eed9 100644 --- a/mdl/backend/mock/backend.go +++ b/mdl/backend/mock/backend.go @@ -7,12 +7,12 @@ package mock import ( "github.com/mendixlabs/mxcli/mdl/backend" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/agenteditor" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/javaactions" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/pages" "github.com/mendixlabs/mxcli/sdk/security" "github.com/mendixlabs/mxcli/sdk/workflows" @@ -75,19 +75,19 @@ type MockBackend struct { UpdateEnumerationRefsInAllDomainModelsFunc func(oldQualifiedName, newQualifiedName string) error // MicroflowBackend - ListMicroflowsFunc func() ([]*microflows.Microflow, error) - GetMicroflowFunc func(id model.ID) (*microflows.Microflow, error) - CreateMicroflowFunc func(mf *microflows.Microflow) error - UpdateMicroflowFunc func(mf *microflows.Microflow) error - DeleteMicroflowFunc func(id model.ID) error - MoveMicroflowFunc func(mf *microflows.Microflow) error - ParseMicroflowFromRawFunc func(raw map[string]any, unitID, containerID model.ID) *microflows.Microflow - ListNanoflowsFunc func() ([]*microflows.Nanoflow, error) - GetNanoflowFunc func(id model.ID) (*microflows.Nanoflow, error) - CreateNanoflowFunc func(nf *microflows.Nanoflow) error - UpdateNanoflowFunc func(nf *microflows.Nanoflow) error - DeleteNanoflowFunc func(id model.ID) error - MoveNanoflowFunc func(nf *microflows.Nanoflow) error + ListMicroflowsFunc func() ([]*microflows.Microflow, error) + GetMicroflowFunc func(id model.ID) (*microflows.Microflow, error) + CreateMicroflowFunc func(mf *microflows.Microflow) error + UpdateMicroflowFunc func(mf *microflows.Microflow) error + DeleteMicroflowFunc func(id model.ID) error + MoveMicroflowFunc func(mf *microflows.Microflow) error + ParseMicroflowFromRawFunc func(raw map[string]any, unitID, containerID model.ID) *microflows.Microflow + ListNanoflowsFunc func() ([]*microflows.Nanoflow, error) + GetNanoflowFunc func(id model.ID) (*microflows.Nanoflow, error) + CreateNanoflowFunc func(nf *microflows.Nanoflow) error + UpdateNanoflowFunc func(nf *microflows.Nanoflow) error + DeleteNanoflowFunc func(id model.ID) error + MoveNanoflowFunc func(nf *microflows.Nanoflow) error // PageBackend ListPagesFunc func() ([]*pages.Page, error) diff --git a/mdl/backend/mock/mock_infrastructure.go b/mdl/backend/mock/mock_infrastructure.go index 0e033f08..c643077a 100644 --- a/mdl/backend/mock/mock_infrastructure.go +++ b/mdl/backend/mock/mock_infrastructure.go @@ -3,9 +3,9 @@ package mock import ( + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/agenteditor" - "github.com/mendixlabs/mxcli/mdl/types" ) // --------------------------------------------------------------------------- diff --git a/mdl/backend/mock/mock_java.go b/mdl/backend/mock/mock_java.go index c44ce357..a37c3987 100644 --- a/mdl/backend/mock/mock_java.go +++ b/mdl/backend/mock/mock_java.go @@ -3,9 +3,9 @@ package mock import ( + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/javaactions" - "github.com/mendixlabs/mxcli/mdl/types" ) func (m *MockBackend) ListJavaActions() ([]*types.JavaAction, error) { diff --git a/mdl/backend/mock/mock_mapping.go b/mdl/backend/mock/mock_mapping.go index 27b7b0bc..8fd6704b 100644 --- a/mdl/backend/mock/mock_mapping.go +++ b/mdl/backend/mock/mock_mapping.go @@ -3,8 +3,8 @@ package mock import ( - "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" ) func (m *MockBackend) ListImportMappings() ([]*model.ImportMapping, error) { diff --git a/mdl/backend/mock/mock_navigation.go b/mdl/backend/mock/mock_navigation.go index e993bc80..b0940d9a 100644 --- a/mdl/backend/mock/mock_navigation.go +++ b/mdl/backend/mock/mock_navigation.go @@ -3,8 +3,8 @@ package mock import ( - "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" ) func (m *MockBackend) ListNavigationDocuments() ([]*types.NavigationDocument, error) { diff --git a/mdl/backend/mock/mock_security.go b/mdl/backend/mock/mock_security.go index 214f907e..cbe18112 100644 --- a/mdl/backend/mock/mock_security.go +++ b/mdl/backend/mock/mock_security.go @@ -4,8 +4,8 @@ package mock import ( "github.com/mendixlabs/mxcli/mdl/backend" - "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/security" ) diff --git a/mdl/backend/mock/mock_workflow.go b/mdl/backend/mock/mock_workflow.go index 73ea9fe0..c29a4539 100644 --- a/mdl/backend/mock/mock_workflow.go +++ b/mdl/backend/mock/mock_workflow.go @@ -3,8 +3,8 @@ package mock import ( - "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/workflows" ) diff --git a/mdl/backend/mpr/backend.go b/mdl/backend/mpr/backend.go index cb1a9f32..d0c54a2b 100644 --- a/mdl/backend/mpr/backend.go +++ b/mdl/backend/mpr/backend.go @@ -6,6 +6,8 @@ package mprbackend import ( + "fmt" + "github.com/mendixlabs/mxcli/mdl/backend" "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" @@ -85,9 +87,11 @@ func (b *MprBackend) Path() string { return b.path } // for new code. func (b *MprBackend) MprReader() *mpr.Reader { return b.reader } -func (b *MprBackend) Version() types.MPRVersion { return convertMPRVersion(b.reader.Version()) } -func (b *MprBackend) ProjectVersion() *types.ProjectVersion { return convertProjectVersion(b.reader.ProjectVersion()) } -func (b *MprBackend) GetMendixVersion() (string, error) { return b.reader.GetMendixVersion() } +func (b *MprBackend) Version() types.MPRVersion { return convertMPRVersion(b.reader.Version()) } +func (b *MprBackend) ProjectVersion() *types.ProjectVersion { + return convertProjectVersion(b.reader.ProjectVersion()) +} +func (b *MprBackend) GetMendixVersion() (string, error) { return b.reader.GetMendixVersion() } // Commit is a no-op — the MPR writer auto-commits on each write operation. func (b *MprBackend) Commit() error { return nil } @@ -112,7 +116,9 @@ func (b *MprBackend) DeleteModuleWithCleanup(id model.ID, moduleName string) err // FolderBackend // --------------------------------------------------------------------------- -func (b *MprBackend) ListFolders() ([]*types.FolderInfo, error) { return convertFolderInfoSlice(b.reader.ListFolders()) } +func (b *MprBackend) ListFolders() ([]*types.FolderInfo, error) { + return convertFolderInfoSlice(b.reader.ListFolders()) +} func (b *MprBackend) CreateFolder(folder *model.Folder) error { return b.writer.CreateFolder(folder) } func (b *MprBackend) DeleteFolder(id model.ID) error { return b.writer.DeleteFolder(id) } func (b *MprBackend) MoveFolder(id model.ID, newContainerID model.ID) error { @@ -538,7 +544,7 @@ func (b *MprBackend) GetJsonStructureByQualifiedName(moduleName, name string) (* return convertJsonStructurePtr(b.reader.GetJsonStructureByQualifiedName(moduleName, name)) } func (b *MprBackend) CreateJsonStructure(js *types.JsonStructure) error { - return b.writer.CreateJsonStructure(unconvertJsonStructure(js)) + return b.writer.CreateJsonStructure(js) } func (b *MprBackend) DeleteJsonStructure(id string) error { return b.writer.DeleteJsonStructure(id) @@ -617,7 +623,7 @@ func (b *MprBackend) ListImageCollections() ([]*types.ImageCollection, error) { return convertImageCollectionSlice(b.reader.ListImageCollections()) } func (b *MprBackend) CreateImageCollection(ic *types.ImageCollection) error { - return b.writer.CreateImageCollection(unconvertImageCollection(ic)) + return b.writer.CreateImageCollection(ic) } func (b *MprBackend) DeleteImageCollection(id string) error { return b.writer.DeleteImageCollection(id) @@ -678,8 +684,10 @@ func (b *MprBackend) UpdateRawUnit(unitID string, contents []byte) error { // MetadataBackend // --------------------------------------------------------------------------- -func (b *MprBackend) ListAllUnitIDs() ([]string, error) { return b.reader.ListAllUnitIDs() } -func (b *MprBackend) ListUnits() ([]*types.UnitInfo, error) { return convertUnitInfoSlice(b.reader.ListUnits()) } +func (b *MprBackend) ListAllUnitIDs() ([]string, error) { return b.reader.ListAllUnitIDs() } +func (b *MprBackend) ListUnits() ([]*types.UnitInfo, error) { + return convertUnitInfoSlice(b.reader.ListUnits()) +} func (b *MprBackend) GetUnitTypes() (map[string]int, error) { return b.reader.GetUnitTypes() } func (b *MprBackend) GetProjectRootID() (string, error) { return b.reader.GetProjectRootID() } func (b *MprBackend) ContentsDir() string { return b.reader.ContentsDir() } @@ -746,6 +754,7 @@ func (b *MprBackend) DeleteAgentEditorAgent(id string) error { // --------------------------------------------------------------------------- // WorkflowMutationBackend +// --------------------------------------------------------------------------- // OpenWorkflowForMutation is implemented in workflow_mutator.go. func (b *MprBackend) OpenWorkflowForMutation(unitID model.ID) (backend.WorkflowMutator, error) { @@ -754,13 +763,14 @@ func (b *MprBackend) OpenWorkflowForMutation(unitID model.ID) (backend.WorkflowM // --------------------------------------------------------------------------- // WidgetSerializationBackend +// --------------------------------------------------------------------------- func (b *MprBackend) SerializeWidget(w pages.Widget) (any, error) { - panic("MprBackend.SerializeWidget not yet implemented") + return nil, fmt.Errorf("MprBackend.SerializeWidget not yet implemented") } func (b *MprBackend) SerializeClientAction(a pages.ClientAction) (any, error) { - panic("MprBackend.SerializeClientAction not yet implemented") + return nil, fmt.Errorf("MprBackend.SerializeClientAction not yet implemented") } func (b *MprBackend) SerializeDataSource(ds pages.DataSource) (any, error) { @@ -768,5 +778,5 @@ func (b *MprBackend) SerializeDataSource(ds pages.DataSource) (any, error) { } func (b *MprBackend) SerializeWorkflowActivity(a workflows.WorkflowActivity) (any, error) { - panic("MprBackend.SerializeWorkflowActivity not yet implemented") + return nil, fmt.Errorf("MprBackend.SerializeWorkflowActivity not yet implemented") } diff --git a/mdl/backend/mpr/convert.go b/mdl/backend/mpr/convert.go index 1cfd39ca..5bea9129 100644 --- a/mdl/backend/mpr/convert.go +++ b/mdl/backend/mpr/convert.go @@ -29,29 +29,13 @@ func convertProjectVersion(v *version.ProjectVersion) *types.ProjectVersion { } } +// passthrough helpers for types that sdk/mpr already returns as types.*. func convertFolderInfoSlice(in []*types.FolderInfo, err error) ([]*types.FolderInfo, error) { - if err != nil || in == nil { - return nil, err - } - out := make([]*types.FolderInfo, len(in)) - for i, f := range in { - out[i] = &types.FolderInfo{ID: f.ID, ContainerID: f.ContainerID, Name: f.Name} - } - return out, nil + return in, err } func convertUnitInfoSlice(in []*types.UnitInfo, err error) ([]*types.UnitInfo, error) { - if err != nil || in == nil { - return nil, err - } - out := make([]*types.UnitInfo, len(in)) - for i, u := range in { - out[i] = &types.UnitInfo{ - ID: u.ID, ContainerID: u.ContainerID, - ContainmentName: u.ContainmentName, Type: u.Type, - } - } - return out, nil + return in, err } func convertRenameHitSlice(in []mpr.RenameHit, err error) ([]types.RenameHit, error) { @@ -66,16 +50,7 @@ func convertRenameHitSlice(in []mpr.RenameHit, err error) ([]types.RenameHit, er } func convertRawUnitSlice(in []*types.RawUnit, err error) ([]*types.RawUnit, error) { - if err != nil || in == nil { - return nil, err - } - out := make([]*types.RawUnit, len(in)) - for i, r := range in { - out[i] = &types.RawUnit{ - ID: r.ID, ContainerID: r.ContainerID, Type: r.Type, Contents: r.Contents, - } - } - return out, nil + return in, err } func convertRawUnitInfoSlice(in []*mpr.RawUnitInfo, err error) ([]*types.RawUnitInfo, error) { @@ -127,136 +102,35 @@ func convertRawCustomWidgetTypeSlice(in []*mpr.RawCustomWidgetType, err error) ( } func convertJavaActionSlice(in []*types.JavaAction, err error) ([]*types.JavaAction, error) { - if err != nil || in == nil { - return nil, err - } - out := make([]*types.JavaAction, len(in)) - for i, ja := range in { - out[i] = &types.JavaAction{ - BaseElement: ja.BaseElement, - ContainerID: ja.ContainerID, - Name: ja.Name, - Documentation: ja.Documentation, - } - } - return out, nil + return in, err } func convertJavaScriptActionSlice(in []*types.JavaScriptAction, err error) ([]*types.JavaScriptAction, error) { - if err != nil || in == nil { - return nil, err - } - out := make([]*types.JavaScriptAction, len(in)) - for i, jsa := range in { - out[i] = convertJavaScriptAction(jsa) - } - return out, nil + return in, err } func convertJavaScriptActionPtr(in *types.JavaScriptAction, err error) (*types.JavaScriptAction, error) { - if err != nil || in == nil { - return nil, err - } - return convertJavaScriptAction(in), nil -} - -func convertJavaScriptAction(in *types.JavaScriptAction) *types.JavaScriptAction { - return &types.JavaScriptAction{ - BaseElement: in.BaseElement, - ContainerID: in.ContainerID, - Name: in.Name, - Documentation: in.Documentation, - Platform: in.Platform, - Excluded: in.Excluded, - ExportLevel: in.ExportLevel, - ActionDefaultReturnName: in.ActionDefaultReturnName, - ReturnType: in.ReturnType, - Parameters: in.Parameters, - TypeParameters: in.TypeParameters, - MicroflowActionInfo: in.MicroflowActionInfo, - } + return in, err } func convertNavDocSlice(in []*types.NavigationDocument, err error) ([]*types.NavigationDocument, error) { - if err != nil || in == nil { - return nil, err - } - out := make([]*types.NavigationDocument, len(in)) - for i, nd := range in { - out[i] = convertNavDoc(nd) - } - return out, nil + return in, err } func convertNavDocPtr(in *types.NavigationDocument, err error) (*types.NavigationDocument, error) { - if err != nil || in == nil { - return nil, err - } - return convertNavDoc(in), nil + return in, err } -func convertNavDoc(in *types.NavigationDocument) *types.NavigationDocument { - nd := &types.NavigationDocument{ - BaseElement: in.BaseElement, - ContainerID: in.ContainerID, - Name: in.Name, - } - if in.Profiles != nil { - nd.Profiles = make([]*types.NavigationProfile, len(in.Profiles)) - for i, p := range in.Profiles { - nd.Profiles[i] = convertNavProfile(p) - } - } - return nd +func convertJsonStructureSlice(in []*types.JsonStructure, err error) ([]*types.JsonStructure, error) { + return in, err } -func convertNavProfile(in *types.NavigationProfile) *types.NavigationProfile { - p := &types.NavigationProfile{ - Name: in.Name, - Kind: in.Kind, - IsNative: in.IsNative, - LoginPage: in.LoginPage, - NotFoundPage: in.NotFoundPage, - } - if in.HomePage != nil { - p.HomePage = &types.NavHomePage{Page: in.HomePage.Page, Microflow: in.HomePage.Microflow} - } - if in.RoleBasedHomePages != nil { - p.RoleBasedHomePages = make([]*types.NavRoleBasedHome, len(in.RoleBasedHomePages)) - for i, rbh := range in.RoleBasedHomePages { - p.RoleBasedHomePages[i] = &types.NavRoleBasedHome{ - UserRole: rbh.UserRole, Page: rbh.Page, Microflow: rbh.Microflow, - } - } - } - if in.MenuItems != nil { - p.MenuItems = make([]*types.NavMenuItem, len(in.MenuItems)) - for i, mi := range in.MenuItems { - p.MenuItems[i] = convertNavMenuItem(mi) - } - } - if in.OfflineEntities != nil { - p.OfflineEntities = make([]*types.NavOfflineEntity, len(in.OfflineEntities)) - for i, oe := range in.OfflineEntities { - p.OfflineEntities[i] = &types.NavOfflineEntity{ - Entity: oe.Entity, SyncMode: oe.SyncMode, Constraint: oe.Constraint, - } - } - } - return p +func convertJsonStructurePtr(in *types.JsonStructure, err error) (*types.JsonStructure, error) { + return in, err } -func convertNavMenuItem(in *types.NavMenuItem) *types.NavMenuItem { - mi := &types.NavMenuItem{ - Caption: in.Caption, Page: in.Page, Microflow: in.Microflow, ActionType: in.ActionType, - } - if in.Items != nil { - mi.Items = make([]*types.NavMenuItem, len(in.Items)) - for i, sub := range in.Items { - mi.Items[i] = convertNavMenuItem(sub) - } - } - return mi +func convertImageCollectionSlice(in []*types.ImageCollection, err error) ([]*types.ImageCollection, error) { + return in, err } // --------------------------------------------------------------------------- @@ -318,140 +192,3 @@ func unconvertEntityAccessRevocation(in types.EntityAccessRevocation) mpr.Entity RevokeWriteAll: in.RevokeWriteAll, } } - -func convertJsonStructureSlice(in []*types.JsonStructure, err error) ([]*types.JsonStructure, error) { - if err != nil || in == nil { - return nil, err - } - out := make([]*types.JsonStructure, len(in)) - for i, js := range in { - out[i] = convertJsonStructure(js) - } - return out, nil -} - -func convertJsonStructurePtr(in *types.JsonStructure, err error) (*types.JsonStructure, error) { - if err != nil || in == nil { - return nil, err - } - return convertJsonStructure(in), nil -} - -func convertJsonStructure(in *types.JsonStructure) *types.JsonStructure { - js := &types.JsonStructure{ - BaseElement: in.BaseElement, - ContainerID: in.ContainerID, - Name: in.Name, - Documentation: in.Documentation, - JsonSnippet: in.JsonSnippet, - Excluded: in.Excluded, - ExportLevel: in.ExportLevel, - } - if in.Elements != nil { - js.Elements = make([]*types.JsonElement, len(in.Elements)) - for i, e := range in.Elements { - js.Elements[i] = convertJsonElement(e) - } - } - return js -} - -func convertJsonElement(in *types.JsonElement) *types.JsonElement { - e := &types.JsonElement{ - ExposedName: in.ExposedName, ExposedItemName: in.ExposedItemName, - Path: in.Path, ElementType: in.ElementType, PrimitiveType: in.PrimitiveType, - MinOccurs: in.MinOccurs, MaxOccurs: in.MaxOccurs, Nillable: in.Nillable, - IsDefaultType: in.IsDefaultType, MaxLength: in.MaxLength, - FractionDigits: in.FractionDigits, TotalDigits: in.TotalDigits, - OriginalValue: in.OriginalValue, - } - if in.Children != nil { - e.Children = make([]*types.JsonElement, len(in.Children)) - for i, c := range in.Children { - e.Children[i] = convertJsonElement(c) - } - } - return e -} - -func unconvertJsonStructure(in *types.JsonStructure) *types.JsonStructure { - js := &types.JsonStructure{ - BaseElement: in.BaseElement, - ContainerID: in.ContainerID, - Name: in.Name, - Documentation: in.Documentation, - JsonSnippet: in.JsonSnippet, - Excluded: in.Excluded, - ExportLevel: in.ExportLevel, - } - if in.Elements != nil { - js.Elements = make([]*types.JsonElement, len(in.Elements)) - for i, e := range in.Elements { - js.Elements[i] = unconvertJsonElement(e) - } - } - return js -} - -func unconvertJsonElement(in *types.JsonElement) *types.JsonElement { - e := &types.JsonElement{ - ExposedName: in.ExposedName, ExposedItemName: in.ExposedItemName, - Path: in.Path, ElementType: in.ElementType, PrimitiveType: in.PrimitiveType, - MinOccurs: in.MinOccurs, MaxOccurs: in.MaxOccurs, Nillable: in.Nillable, - IsDefaultType: in.IsDefaultType, MaxLength: in.MaxLength, - FractionDigits: in.FractionDigits, TotalDigits: in.TotalDigits, - OriginalValue: in.OriginalValue, - } - if in.Children != nil { - e.Children = make([]*types.JsonElement, len(in.Children)) - for i, c := range in.Children { - e.Children[i] = unconvertJsonElement(c) - } - } - return e -} - -func convertImageCollectionSlice(in []*types.ImageCollection, err error) ([]*types.ImageCollection, error) { - if err != nil || in == nil { - return nil, err - } - out := make([]*types.ImageCollection, len(in)) - for i, ic := range in { - out[i] = convertImageCollection(ic) - } - return out, nil -} - -func convertImageCollection(in *types.ImageCollection) *types.ImageCollection { - ic := &types.ImageCollection{ - BaseElement: in.BaseElement, - ContainerID: in.ContainerID, - Name: in.Name, - ExportLevel: in.ExportLevel, - Documentation: in.Documentation, - } - if in.Images != nil { - ic.Images = make([]types.Image, len(in.Images)) - for i, img := range in.Images { - ic.Images[i] = types.Image{ID: img.ID, Name: img.Name, Data: img.Data, Format: img.Format} - } - } - return ic -} - -func unconvertImageCollection(in *types.ImageCollection) *types.ImageCollection { - ic := &types.ImageCollection{ - BaseElement: in.BaseElement, - ContainerID: in.ContainerID, - Name: in.Name, - ExportLevel: in.ExportLevel, - Documentation: in.Documentation, - } - if in.Images != nil { - ic.Images = make([]types.Image, len(in.Images)) - for i, img := range in.Images { - ic.Images[i] = types.Image{ID: img.ID, Name: img.Name, Data: img.Data, Format: img.Format} - } - } - return ic -} diff --git a/mdl/backend/mpr/datagrid_builder.go b/mdl/backend/mpr/datagrid_builder.go index aade56fd..3173c284 100644 --- a/mdl/backend/mpr/datagrid_builder.go +++ b/mdl/backend/mpr/datagrid_builder.go @@ -4,6 +4,8 @@ package mprbackend import ( "fmt" + "log" + "sort" "strings" "go.mongodb.org/mongo-driver/bson" @@ -437,7 +439,15 @@ func (b *MprBackend) buildDataGrid2ColumnObject(col *backend.DataGridColumnSpec, properties := bson.A{int32(2)} - for key, entry := range columnPropertyIDs { + // Sort keys for deterministic BSON output. + colKeys := make([]string, 0, len(columnPropertyIDs)) + for k := range columnPropertyIDs { + colKeys = append(colKeys, k) + } + sort.Strings(colKeys) + + for _, key := range colKeys { + entry := columnPropertyIDs[key] switch key { case "showContentAs": if hasCustomContent { @@ -632,7 +642,7 @@ func (b *MprBackend) applyDataGridPagingProps(obj bson.D, propertyTypeIDs map[st result := make(bson.D, 0, len(obj)) for _, elem := range obj { if elem.Key == "Properties" { - if propsArr, ok := elem.Value.(bson.A); ok { + if propsArr, ok := elem.Value.(bson.A); ok && len(propsArr) > 0 { updatedProps := bson.A{propsArr[0]} for _, propVal := range propsArr[1:] { propMap, ok := propVal.(bson.D) @@ -699,6 +709,9 @@ func (b *MprBackend) applyDataGridSelectionProp(obj bson.D, propertyTypeIDs map[ // BSON property builders (package-level, no receiver needed) // =========================================================================== +// buildDataGrid2Property builds a single property BSON document for a DataGrid2 column. +// attrRef and primitiveValue are reserved for future column types that require direct +// attribute references or primitive default values; current callers pass empty strings. func buildDataGrid2Property(entry pages.PropertyTypeIDEntry, datasource pages.DataSource, attrRef string, primitiveValue string, b *MprBackend) bson.D { var datasourceBSON any if datasource != nil { @@ -1177,6 +1190,9 @@ func colPropInt(props map[string]any, key string, defaultVal string) string { func (b *MprBackend) buildFilterWidgetBSON(widgetID, filterName string, projectPath string) bson.D { rawType, rawObject, _, _, err := widgets.GetTemplateFullBSON(widgetID, types.GenerateID, projectPath) if err != nil || rawType == nil { + if err != nil { + log.Printf("WARNING: failed to load template for widget %s: %v; using minimal fallback", widgetID, err) + } return b.buildMinimalFilterWidgetBSON(widgetID, filterName) } diff --git a/mdl/backend/mpr/datagrid_builder_test.go b/mdl/backend/mpr/datagrid_builder_test.go index b6ac2c43..963331d5 100644 --- a/mdl/backend/mpr/datagrid_builder_test.go +++ b/mdl/backend/mpr/datagrid_builder_test.go @@ -3,11 +3,13 @@ package mprbackend import ( + "bytes" "testing" - "github.com/mendixlabs/mxcli/mdl/bsonutil" "go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/bson/primitive" + + "github.com/mendixlabs/mxcli/mdl/bsonutil" ) func TestDeepCloneWithNewIDs_RegeneratesAllIDs(t *testing.T) { @@ -127,13 +129,5 @@ func binaryEqual(a, b any) bool { if !aOk || !bOk { return false } - if len(ab.Data) != len(bb.Data) { - return false - } - for i := range ab.Data { - if ab.Data[i] != bb.Data[i] { - return false - } - } - return true + return bytes.Equal(ab.Data, bb.Data) } diff --git a/mdl/backend/mpr/page_mutator.go b/mdl/backend/mpr/page_mutator.go index c1d0b08f..77ebd81e 100644 --- a/mdl/backend/mpr/page_mutator.go +++ b/mdl/backend/mpr/page_mutator.go @@ -143,6 +143,7 @@ func (m *mprPageMutator) InsertWidget(widgetRef string, columnRef string, positi func (m *mprPageMutator) DropWidget(refs []backend.WidgetRef) error { for _, ref := range refs { + // Re-find widget each iteration because previous drops mutate the tree. var result *bsonWidgetResult if ref.IsColumn() { result = findBsonColumn(m.rawData, ref.Widget, ref.Column, m.widgetFinder) @@ -517,6 +518,11 @@ func dGetString(doc bson.D, key string) string { } // dSet sets a field value in a bson.D in place. Returns true if found. +// NOTE: callers generally do not check the return value because the keys +// are structurally guaranteed by the widgetFinder traversal. If a key +// is absent, the mutation is silently skipped — this is intentional for +// optional fields (e.g. Appearance, DataSource) that may not be present +// on every widget type. func dSet(doc bson.D, key string, value any) bool { for i := range doc { if doc[i].Key == key { @@ -1335,6 +1341,8 @@ func setWidgetContentMut(widget bson.D, value any) error { return fmt.Errorf("Content.Template has no Items with Text") } +// setWidgetLabelMut sets the widget's Label caption. Returns nil without error +// if the widget has no Label field — not all widget types support labels. func setWidgetLabelMut(widget bson.D, value any) error { label := dGetDoc(widget, "Label") if label == nil { diff --git a/mdl/backend/mpr/widget_builder.go b/mdl/backend/mpr/widget_builder.go index f9950c72..5bf5b002 100644 --- a/mdl/backend/mpr/widget_builder.go +++ b/mdl/backend/mpr/widget_builder.go @@ -3,10 +3,10 @@ package mprbackend import ( - "encoding/hex" "fmt" "log" "regexp" + "sort" "strings" "go.mongodb.org/mongo-driver/bson" @@ -225,6 +225,7 @@ func (ob *mprWidgetObjectBuilder) SetAttributeObjects(propertyKey string, attrib for _, attrPath := range attributePaths { attrObj, err := createAttributeObject(attrPath, entry.ObjectTypeID, nestedEntry.PropertyTypeID, nestedEntry.ValueTypeID) if err != nil { + // TODO(shared-types): propagate error instead of logging — requires interface change. log.Printf("warning: skipping attribute %s: %v", attrPath, err) continue } @@ -369,6 +370,7 @@ func updatePropertyInArray(arr bson.A, propertyTypeID string, updateFn func(bson } } if !matched { + // TODO(shared-types): propagate warning instead of logging — requires interface change. log.Printf("WARNING: updatePropertyInArray: no match for TypePointer %s in %d properties", propertyTypeID, len(arr)-1) } return result @@ -595,17 +597,17 @@ func createClientTemplateBSONWithParams(text string, entityContext string) bson. attrPath = entityContext + "." + attrName } params = append(params, bson.D{ - {Key: "$ID", Value: generateBinaryID()}, + {Key: "$ID", Value: types.UUIDToBlob(types.GenerateID())}, {Key: "$Type", Value: "Forms$ClientTemplateParameter"}, {Key: "AttributeRef", Value: bson.D{ - {Key: "$ID", Value: generateBinaryID()}, + {Key: "$ID", Value: types.UUIDToBlob(types.GenerateID())}, {Key: "$Type", Value: "DomainModels$AttributeRef"}, {Key: "Attribute", Value: attrPath}, {Key: "EntityRef", Value: nil}, }}, {Key: "Expression", Value: ""}, {Key: "FormattingInfo", Value: bson.D{ - {Key: "$ID", Value: generateBinaryID()}, + {Key: "$ID", Value: types.UUIDToBlob(types.GenerateID())}, {Key: "$Type", Value: "Forms$FormattingInfo"}, {Key: "CustomDateFormat", Value: ""}, {Key: "DateFormat", Value: "Date"}, @@ -620,10 +622,10 @@ func createClientTemplateBSONWithParams(text string, entityContext string) bson. makeText := func(t string) bson.D { return bson.D{ - {Key: "$ID", Value: generateBinaryID()}, + {Key: "$ID", Value: types.UUIDToBlob(types.GenerateID())}, {Key: "$Type", Value: "Texts$Text"}, {Key: "Items", Value: bson.A{int32(3), bson.D{ - {Key: "$ID", Value: generateBinaryID()}, + {Key: "$ID", Value: types.UUIDToBlob(types.GenerateID())}, {Key: "$Type", Value: "Texts$Translation"}, {Key: "LanguageCode", Value: "en_US"}, {Key: "Text", Value: t}, @@ -632,7 +634,7 @@ func createClientTemplateBSONWithParams(text string, entityContext string) bson. } return bson.D{ - {Key: "$ID", Value: generateBinaryID()}, + {Key: "$ID", Value: types.UUIDToBlob(types.GenerateID())}, {Key: "$Type", Value: "Forms$ClientTemplate"}, {Key: "Fallback", Value: makeText(paramText)}, {Key: "Parameters", Value: params}, @@ -643,10 +645,10 @@ func createClientTemplateBSONWithParams(text string, entityContext string) bson. func createDefaultClientTemplateBSON(text string) bson.D { makeText := func(t string) bson.D { return bson.D{ - {Key: "$ID", Value: generateBinaryID()}, + {Key: "$ID", Value: types.UUIDToBlob(types.GenerateID())}, {Key: "$Type", Value: "Texts$Text"}, {Key: "Items", Value: bson.A{int32(3), bson.D{ - {Key: "$ID", Value: generateBinaryID()}, + {Key: "$ID", Value: types.UUIDToBlob(types.GenerateID())}, {Key: "$Type", Value: "Texts$Translation"}, {Key: "LanguageCode", Value: "en_US"}, {Key: "Text", Value: t}, @@ -654,7 +656,7 @@ func createDefaultClientTemplateBSON(text string) bson.D { } } return bson.D{ - {Key: "$ID", Value: generateBinaryID()}, + {Key: "$ID", Value: types.UUIDToBlob(types.GenerateID())}, {Key: "$Type", Value: "Forms$ClientTemplate"}, {Key: "Fallback", Value: makeText(text)}, {Key: "Parameters", Value: bson.A{int32(2)}}, @@ -662,84 +664,6 @@ func createDefaultClientTemplateBSON(text string) bson.D { } } -// --------------------------------------------------------------------------- -// ID / binary helpers -// --------------------------------------------------------------------------- - -func generateBinaryID() []byte { - return hexIDToBlob(types.GenerateID()) -} - -func hexIDToBlob(hexStr string) []byte { - hexStr = strings.ReplaceAll(hexStr, "-", "") - data, err := hex.DecodeString(hexStr) - if err != nil || len(data) != 16 { - return data - } - data[0], data[1], data[2], data[3] = data[3], data[2], data[1], data[0] - data[4], data[5] = data[5], data[4] - data[6], data[7] = data[7], data[6] - return data -} - -func hexToBytes(hexStr string) []byte { - clean := strings.ReplaceAll(hexStr, "-", "") - if len(clean) != 32 { - return nil - } - - decoded := make([]byte, 16) - for i := range 16 { - decoded[i] = hexByte(clean[i*2])<<4 | hexByte(clean[i*2+1]) - } - - blob := make([]byte, 16) - blob[0] = decoded[3] - blob[1] = decoded[2] - blob[2] = decoded[1] - blob[3] = decoded[0] - blob[4] = decoded[5] - blob[5] = decoded[4] - blob[6] = decoded[7] - blob[7] = decoded[6] - copy(blob[8:], decoded[8:]) - - return blob -} - -func hexByte(c byte) byte { - switch { - case c >= '0' && c <= '9': - return c - '0' - case c >= 'a' && c <= 'f': - return c - 'a' + 10 - case c >= 'A' && c <= 'F': - return c - 'A' + 10 - } - return 0 -} - -func bytesToHex(b []byte) string { - if len(b) != 16 { - if len(b) > 1024 { - return "" - } - const hexChars = "0123456789abcdef" - result := make([]byte, len(b)*2) - for i, v := range b { - result[i*2] = hexChars[v>>4] - result[i*2+1] = hexChars[v&0x0f] - } - return string(result) - } - - return fmt.Sprintf("%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x", - b[3], b[2], b[1], b[0], - b[5], b[4], - b[7], b[6], - b[8], b[9], b[10], b[11], b[12], b[13], b[14], b[15]) -} - // --------------------------------------------------------------------------- // Property type ID conversion // --------------------------------------------------------------------------- @@ -768,7 +692,15 @@ func convertPropertyTypeIDs(src map[string]widgets.PropertyTypeIDEntry) map[stri // --------------------------------------------------------------------------- func ensureRequiredObjectLists(obj bson.D, propertyTypeIDs map[string]pages.PropertyTypeIDEntry) bson.D { - for propKey, entry := range propertyTypeIDs { + // Sort keys for deterministic BSON output. + keys := make([]string, 0, len(propertyTypeIDs)) + for k := range propertyTypeIDs { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, propKey := range keys { + entry := propertyTypeIDs[propKey] if entry.ObjectTypeID == "" || len(entry.NestedPropertyIDs) == 0 { continue } @@ -820,23 +752,30 @@ func ensureRequiredObjectLists(obj bson.D, propertyTypeIDs map[string]pages.Prop func createDefaultWidgetObject(objectTypeID string, nestedProps map[string]pages.PropertyTypeIDEntry) bson.D { propsArr := bson.A{int32(2)} - for _, entry := range nestedProps { + // Sort keys for deterministic BSON output. + nestedKeys := make([]string, 0, len(nestedProps)) + for k := range nestedProps { + nestedKeys = append(nestedKeys, k) + } + sort.Strings(nestedKeys) + for _, k := range nestedKeys { + entry := nestedProps[k] prop := createDefaultWidgetProperty(entry) propsArr = append(propsArr, prop) } return bson.D{ - {Key: "$ID", Value: generateBinaryID()}, + {Key: "$ID", Value: types.UUIDToBlob(types.GenerateID())}, {Key: "$Type", Value: "CustomWidgets$WidgetObject"}, - {Key: "TypePointer", Value: hexIDToBlob(objectTypeID)}, + {Key: "TypePointer", Value: types.UUIDToBlob(objectTypeID)}, {Key: "Properties", Value: propsArr}, } } func createDefaultWidgetProperty(entry pages.PropertyTypeIDEntry) bson.D { return bson.D{ - {Key: "$ID", Value: generateBinaryID()}, + {Key: "$ID", Value: types.UUIDToBlob(types.GenerateID())}, {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: hexIDToBlob(entry.PropertyTypeID)}, + {Key: "TypePointer", Value: types.UUIDToBlob(entry.PropertyTypeID)}, {Key: "Value", Value: createDefaultWidgetValue(entry)}, } } @@ -844,7 +783,7 @@ func createDefaultWidgetProperty(entry pages.PropertyTypeIDEntry) bson.D { func createDefaultWidgetValue(entry pages.PropertyTypeIDEntry) bson.D { primitiveVal := entry.DefaultValue expressionVal := "" - var textTemplate interface{} + var textTemplate any switch entry.ValueType { case "Expression": @@ -863,10 +802,10 @@ func createDefaultWidgetValue(entry pages.PropertyTypeIDEntry) bson.D { } return bson.D{ - {Key: "$ID", Value: generateBinaryID()}, + {Key: "$ID", Value: types.UUIDToBlob(types.GenerateID())}, {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: generateBinaryID()}, + {Key: "$ID", Value: types.UUIDToBlob(types.GenerateID())}, {Key: "$Type", Value: "Forms$NoAction"}, {Key: "DisabledDuringExecution", Value: true}, }}, @@ -885,7 +824,7 @@ func createDefaultWidgetValue(entry pages.PropertyTypeIDEntry) bson.D { {Key: "SourceVariable", Value: nil}, {Key: "TextTemplate", Value: textTemplate}, {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: hexIDToBlob(entry.ValueTypeID)}, + {Key: "TypePointer", Value: types.UUIDToBlob(entry.ValueTypeID)}, {Key: "Widgets", Value: bson.A{int32(2)}}, {Key: "XPathConstraint", Value: ""}, } @@ -960,24 +899,24 @@ func createAttributeObject(attributePath string, objectTypeID, propertyTypeID, v return nil, mdlerrors.NewValidationf("invalid attribute path %q: expected Module.Entity.Attribute format", attributePath) } return bson.D{ - {Key: "$ID", Value: hexToBytes(types.GenerateID())}, + {Key: "$ID", Value: types.UUIDToBlob(types.GenerateID())}, {Key: "$Type", Value: "CustomWidgets$WidgetObject"}, - {Key: "Properties", Value: []any{ + {Key: "Properties", Value: bson.A{ int32(2), bson.D{ - {Key: "$ID", Value: hexToBytes(types.GenerateID())}, + {Key: "$ID", Value: types.UUIDToBlob(types.GenerateID())}, {Key: "$Type", Value: "CustomWidgets$WidgetProperty"}, - {Key: "TypePointer", Value: hexToBytes(propertyTypeID)}, + {Key: "TypePointer", Value: types.UUIDToBlob(propertyTypeID)}, {Key: "Value", Value: bson.D{ - {Key: "$ID", Value: hexToBytes(types.GenerateID())}, + {Key: "$ID", Value: types.UUIDToBlob(types.GenerateID())}, {Key: "$Type", Value: "CustomWidgets$WidgetValue"}, {Key: "Action", Value: bson.D{ - {Key: "$ID", Value: hexToBytes(types.GenerateID())}, + {Key: "$ID", Value: types.UUIDToBlob(types.GenerateID())}, {Key: "$Type", Value: "Forms$NoAction"}, {Key: "DisabledDuringExecution", Value: true}, }}, {Key: "AttributeRef", Value: bson.D{ - {Key: "$ID", Value: hexToBytes(types.GenerateID())}, + {Key: "$ID", Value: types.UUIDToBlob(types.GenerateID())}, {Key: "$Type", Value: "DomainModels$AttributeRef"}, {Key: "Attribute", Value: attributePath}, {Key: "EntityRef", Value: nil}, @@ -990,18 +929,18 @@ func createAttributeObject(attributePath string, objectTypeID, propertyTypeID, v {Key: "Image", Value: ""}, {Key: "Microflow", Value: ""}, {Key: "Nanoflow", Value: ""}, - {Key: "Objects", Value: []any{int32(2)}}, + {Key: "Objects", Value: bson.A{int32(2)}}, {Key: "PrimitiveValue", Value: ""}, {Key: "Selection", Value: "None"}, {Key: "SourceVariable", Value: nil}, {Key: "TextTemplate", Value: nil}, {Key: "TranslatableValue", Value: nil}, - {Key: "TypePointer", Value: hexToBytes(valueTypeID)}, - {Key: "Widgets", Value: []any{int32(2)}}, + {Key: "TypePointer", Value: types.UUIDToBlob(valueTypeID)}, + {Key: "Widgets", Value: bson.A{int32(2)}}, {Key: "XPathConstraint", Value: ""}, }}, }, }}, - {Key: "TypePointer", Value: hexToBytes(objectTypeID)}, + {Key: "TypePointer", Value: types.UUIDToBlob(objectTypeID)}, }, nil } diff --git a/mdl/backend/mpr/workflow_mutator.go b/mdl/backend/mpr/workflow_mutator.go index 9309e414..fa399223 100644 --- a/mdl/backend/mpr/workflow_mutator.go +++ b/mdl/backend/mpr/workflow_mutator.go @@ -15,7 +15,8 @@ import ( "github.com/mendixlabs/mxcli/sdk/workflows" ) -// bsonArrayMarker is the Mendix BSON array type marker (storageListType 3). +// bsonArrayMarker is the Mendix BSON array type marker (storageListType 3 = reference/association lists). +// Contrast with int32(2) used for object lists (storageListType 2). const bsonArrayMarker = int32(3) // Compile-time check. diff --git a/mdl/backend/mutation.go b/mdl/backend/mutation.go index 7eb83517..b6ca9669 100644 --- a/mdl/backend/mutation.go +++ b/mdl/backend/mutation.go @@ -19,7 +19,7 @@ func (r WidgetRef) IsColumn() bool { return r.Column != "" } // Name returns the full reference string for error messages. func (r WidgetRef) Name() string { - if r.Column != "" { + if r.IsColumn() { return r.Widget + "." + r.Column } return r.Widget @@ -28,6 +28,11 @@ func (r WidgetRef) Name() string { // PageMutator provides fine-grained mutation operations on a single // page, layout, or snippet unit. Obtain one via PageMutationBackend.OpenPageForMutation. // All methods operate on the in-memory representation; call Save to persist. +// +// Widget addressing: most methods accept a widgetRef string (widget name). +// Column-aware operations additionally accept a columnRef string. +// DropWidget uses []WidgetRef to support mixed widget/column targets in a +// single call. type PageMutator interface { // ContainerType returns "page", "layout", or "snippet". ContainerType() string @@ -104,16 +109,16 @@ type PageMutator interface { // PluggablePropertyContext carries operation-specific values for // SetPluggableProperty. Only fields relevant to the operation are used. type PluggablePropertyContext struct { - AttributePath string // "attribute", "association" - AttributePaths []string // "attributeObjects" - AssocPath string // "association" - EntityName string // "association" - PrimitiveVal string // "primitive" - DataSource pages.DataSource // "datasource" - ChildWidgets []pages.Widget // "widgets" + AttributePath string // "attribute", "association" + AttributePaths []string // "attributeObjects" + AssocPath string // "association" + EntityName string // "association" + PrimitiveVal string // "primitive" + DataSource pages.DataSource // "datasource" + ChildWidgets []pages.Widget // "widgets" Action pages.ClientAction // "action" - TextTemplate string // "texttemplate" - Selection string // "selection" + TextTemplate string // "texttemplate" + Selection string // "selection" } // WorkflowMutator provides fine-grained mutation operations on a single @@ -155,17 +160,26 @@ type WorkflowMutator interface { // --- Path operations (parallel split) --- + // InsertPath adds a new path to a parallel split activity. InsertPath(activityRef string, atPos int, pathCaption string, activities []workflows.WorkflowActivity) error + + // DropPath removes a path by caption from a parallel split activity. DropPath(activityRef string, atPos int, pathCaption string) error // --- Branch operations (exclusive split) --- + // InsertBranch adds a new branch with a condition to an exclusive split activity. InsertBranch(activityRef string, atPos int, condition string, activities []workflows.WorkflowActivity) error + + // DropBranch removes a branch by name from an exclusive split activity. DropBranch(activityRef string, atPos int, branchName string) error // --- Boundary event operations --- + // InsertBoundaryEvent adds a boundary event to the referenced activity. InsertBoundaryEvent(activityRef string, atPos int, eventType string, delay string, activities []workflows.WorkflowActivity) error + + // DropBoundaryEvent removes the boundary event from the referenced activity. DropBoundaryEvent(activityRef string, atPos int) error // Save persists the mutations to the backend. @@ -206,7 +220,7 @@ type WidgetSerializationBackend interface { SerializeWorkflowActivity(a workflows.WorkflowActivity) (any, error) } -// WidgetObjectBuilder provides BSON-free operations on a loaded pluggable widget template. +// WidgetObjectBuilder provides storage-agnostic operations on a loaded pluggable widget template. // The executor calls these methods with domain-typed values; the backend handles // all storage-specific manipulation internally. // @@ -214,6 +228,8 @@ type WidgetSerializationBackend interface { type WidgetObjectBuilder interface { // --- Property operations --- // Each operation finds the property by key (via TypePointer matching) and updates its value. + // Set* methods do not return errors — invalid operations are logged as warnings + // and deferred to Finalize, which returns the aggregate result. SetAttribute(propertyKey string, attributePath string) SetAssociation(propertyKey string, assocPath string, entityName string) @@ -245,7 +261,7 @@ type WidgetObjectBuilder interface { // --- Finalize --- // Finalize builds the CustomWidget from the mutated template. - // Returns the widget with RawType/RawObject set from the internal BSON state. + // Returns the widget with RawType/RawObject populated from internal state. Finalize(id model.ID, name string, label string, editable string) *pages.CustomWidget } @@ -253,10 +269,10 @@ type WidgetObjectBuilder interface { // All attribute paths are fully qualified. Child widgets are already built as // domain objects; the backend serializes them to storage format internally. type DataGridColumnSpec struct { - Attribute string // Fully qualified attribute path (empty for action/custom-content columns) - Caption string // Column header caption - ChildWidgets []pages.Widget // Pre-built child widgets (for custom-content columns) - Properties map[string]any // Column properties (Sortable, Resizable, Visible, etc.) + Attribute string // Fully qualified attribute path (empty for action/custom-content columns) + Caption string // Column header caption + ChildWidgets []pages.Widget // Pre-built child widgets (for custom-content columns) + Properties map[string]any // Column properties (Sortable, Resizable, Visible, etc.) } // DataGridSpec carries all inputs needed to build a DataGrid2 widget object. @@ -288,7 +304,7 @@ type WidgetBuilderBackend interface { SerializeWidgetToOpaque(w pages.Widget) any // SerializeDataSourceToOpaque converts a domain DataSource to an opaque - // form suitable for embedding in widget property BSON. + // form suitable for embedding in widget properties. SerializeDataSourceToOpaque(ds pages.DataSource) any // BuildCreateAttributeObject creates an attribute object for filter widgets. @@ -296,7 +312,7 @@ type WidgetBuilderBackend interface { BuildCreateAttributeObject(attributePath string, objectTypeID, propertyTypeID, valueTypeID string) (any, error) // BuildDataGrid2Widget builds a complete DataGrid2 CustomWidget from domain-typed inputs. - // The backend loads the template, constructs the BSON object with columns, + // The backend loads the template, constructs the storage object with columns, // datasource, header widgets, paging, and selection, and returns a fully // assembled CustomWidget. Returns the widget with an opaque RawType/RawObject. BuildDataGrid2Widget(id model.ID, name string, spec DataGridSpec, projectPath string) (*pages.CustomWidget, error) diff --git a/mdl/backend/page.go b/mdl/backend/page.go index 9aeb4b80..21f5a890 100644 --- a/mdl/backend/page.go +++ b/mdl/backend/page.go @@ -24,7 +24,7 @@ type PageBackend interface { UpdateLayout(layout *pages.Layout) error DeleteLayout(id model.ID) error - // Snippets + // Snippets — no GetSnippet: snippets are resolved by qualified name via ListSnippets. ListSnippets() ([]*pages.Snippet, error) CreateSnippet(snippet *pages.Snippet) error UpdateSnippet(snippet *pages.Snippet) error diff --git a/mdl/backend/workflow.go b/mdl/backend/workflow.go index 87ce241d..77879976 100644 --- a/mdl/backend/workflow.go +++ b/mdl/backend/workflow.go @@ -3,7 +3,6 @@ package backend import ( - "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/workflows" ) @@ -15,22 +14,3 @@ type WorkflowBackend interface { CreateWorkflow(wf *workflows.Workflow) error DeleteWorkflow(id model.ID) error } - -// SettingsBackend provides project settings operations. -type SettingsBackend interface { - GetProjectSettings() (*model.ProjectSettings, error) - UpdateProjectSettings(ps *model.ProjectSettings) error -} - -// ImageBackend provides image collection operations. -type ImageBackend interface { - ListImageCollections() ([]*types.ImageCollection, error) - CreateImageCollection(ic *types.ImageCollection) error - DeleteImageCollection(id string) error -} - -// ScheduledEventBackend provides scheduled event operations. -type ScheduledEventBackend interface { - ListScheduledEvents() ([]*model.ScheduledEvent, error) - GetScheduledEvent(id model.ID) (*model.ScheduledEvent, error) -} diff --git a/mdl/bsonutil/bsonutil.go b/mdl/bsonutil/bsonutil.go index 558497c3..fcfb44d6 100644 --- a/mdl/bsonutil/bsonutil.go +++ b/mdl/bsonutil/bsonutil.go @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Package bsonutil provides BSON-aware ID conversion utilities for model elements. -// It depends on mdl/types (WASM-safe) and the BSON driver (also WASM-safe), +// It depends on mdl/types (CGO-free) and the BSON driver (also CGO-free), // but does NOT depend on sdk/mpr (which pulls in SQLite/CGO). package bsonutil @@ -11,6 +11,10 @@ import ( ) // IDToBsonBinary converts a hex UUID string to a BSON binary value. +// If the input is not a valid UUID, a new random ID is generated as a fallback. +// This matches the legacy sdk/mpr behavior where callers expect a valid binary +// result without error handling. Consider using ValidateID first if strict +// validation is needed. func IDToBsonBinary(id string) primitive.Binary { blob := types.UUIDToBlob(id) if blob == nil || len(blob) != 16 { diff --git a/mdl/catalog/builder.go b/mdl/catalog/builder.go index 433fba80..8b64d675 100644 --- a/mdl/catalog/builder.go +++ b/mdl/catalog/builder.go @@ -8,11 +8,11 @@ import ( "strings" "time" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/javaactions" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/pages" "github.com/mendixlabs/mxcli/sdk/security" "github.com/mendixlabs/mxcli/sdk/workflows" diff --git a/mdl/executor/bugfix_regression_test.go b/mdl/executor/bugfix_regression_test.go index fd13a268..d71df9a0 100644 --- a/mdl/executor/bugfix_regression_test.go +++ b/mdl/executor/bugfix_regression_test.go @@ -403,7 +403,7 @@ func TestResolveMemberChange_FallbackWithoutReader(t *testing.T) { // reader is nil — simulates no project context } - // Without reader: a name without dot should default to attribute + // Without backend: a name without dot should default to attribute mc := µflows.MemberChange{} fb.resolveMemberChange(mc, "Label", "Demo.Child") if mc.AttributeQualifiedName != "Demo.Child.Label" { diff --git a/mdl/executor/bugfix_test.go b/mdl/executor/bugfix_test.go index d04abf76..025686a2 100644 --- a/mdl/executor/bugfix_test.go +++ b/mdl/executor/bugfix_test.go @@ -503,7 +503,7 @@ func TestResolveAssociationPaths(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fb := &flowBuilder{ - reader: nil, // nil reader → no resolution, path unchanged + backend: nil, // nil backend → no resolution, path unchanged } got := fb.resolvePathSegments(tt.path) diff --git a/mdl/executor/cmd_alter_page.go b/mdl/executor/cmd_alter_page.go index e7d052c7..5e13a82c 100644 --- a/mdl/executor/cmd_alter_page.go +++ b/mdl/executor/cmd_alter_page.go @@ -4,6 +4,7 @@ package executor import ( "fmt" + "sort" "strings" "github.com/mendixlabs/mxcli/mdl/ast" @@ -112,7 +113,15 @@ func execAlterPage(ctx *ExecContext, s *ast.AlterPageStmt) error { // ============================================================================ func applySetPropertyMutator(mutator backend.PageMutator, op *ast.SetPropertyOp) error { - for propName, value := range op.Properties { + // Sort property names for deterministic application order. + propNames := make([]string, 0, len(op.Properties)) + for k := range op.Properties { + propNames = append(propNames, k) + } + sort.Strings(propNames) + + for _, propName := range propNames { + value := op.Properties[propName] if op.Target.IsColumn() { if err := mutator.SetColumnProperty(op.Target.Widget, op.Target.Column, propName, value); err != nil { return mdlerrors.NewBackend("set "+propName+" on "+op.Target.Name(), err) diff --git a/mdl/executor/cmd_businessevents.go b/mdl/executor/cmd_businessevents.go index 28fe7eaa..ab9e7714 100644 --- a/mdl/executor/cmd_businessevents.go +++ b/mdl/executor/cmd_businessevents.go @@ -8,8 +8,8 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" - "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" ) // showBusinessEventServices displays a table of all business event service documents. diff --git a/mdl/executor/cmd_catalog.go b/mdl/executor/cmd_catalog.go index dedf3b92..eb40e77c 100644 --- a/mdl/executor/cmd_catalog.go +++ b/mdl/executor/cmd_catalog.go @@ -680,7 +680,7 @@ func captureDescribe(ctx *ExecContext, objectType string, qualifiedName string) // captureDescribeParallel is a goroutine-safe version of captureDescribe. // It creates a lightweight ExecContext clone per call with its own output buffer, -// sharing the reader and pre-warmed cache. Call preWarmCache() before using +// sharing the backend and pre-warmed cache. Call preWarmCache() before using // this from multiple goroutines. func captureDescribeParallel(ctx *ExecContext, objectType string, qualifiedName string) (string, error) { parts := strings.SplitN(qualifiedName, ".", 2) diff --git a/mdl/executor/cmd_entities.go b/mdl/executor/cmd_entities.go index 9517c446..255da64a 100644 --- a/mdl/executor/cmd_entities.go +++ b/mdl/executor/cmd_entities.go @@ -152,7 +152,7 @@ func execCreateEntity(ctx *ExecContext, s *ast.CreateEntityStmt) error { if a.CalculatedMicroflow != nil { mfID, err := resolveMicroflowByName(ctx, a.CalculatedMicroflow.String()) if err != nil { - return fmt.Errorf("attribute '%s': %w", a.Name, err) + return mdlerrors.NewBackend(fmt.Sprintf("attribute '%s'", a.Name), err) } attrValue.MicroflowID = mfID attrValue.MicroflowName = a.CalculatedMicroflow.String() @@ -543,7 +543,7 @@ func execAlterEntity(ctx *ExecContext, s *ast.AlterEntityStmt) error { if a.CalculatedMicroflow != nil { mfID, err := resolveMicroflowByName(ctx, a.CalculatedMicroflow.String()) if err != nil { - return fmt.Errorf("attribute '%s': %w", a.Name, err) + return mdlerrors.NewBackend(fmt.Sprintf("attribute '%s'", a.Name), err) } attrValue.MicroflowID = mfID attrValue.MicroflowName = a.CalculatedMicroflow.String() @@ -635,7 +635,7 @@ func execAlterEntity(ctx *ExecContext, s *ast.AlterEntityStmt) error { if s.CalculatedMicroflow != nil { mfID, err := resolveMicroflowByName(ctx, s.CalculatedMicroflow.String()) if err != nil { - return fmt.Errorf("attribute '%s': %w", s.AttributeName, err) + return mdlerrors.NewBackend(fmt.Sprintf("attribute '%s'", s.AttributeName), err) } attrValue.MicroflowID = mfID attrValue.MicroflowName = s.CalculatedMicroflow.String() diff --git a/mdl/executor/cmd_export_mappings.go b/mdl/executor/cmd_export_mappings.go index 035ab0b3..f2579973 100644 --- a/mdl/executor/cmd_export_mappings.go +++ b/mdl/executor/cmd_export_mappings.go @@ -11,8 +11,8 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/backend" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" - "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" ) // showExportMappings prints a table of all export mapping documents. diff --git a/mdl/executor/cmd_folders.go b/mdl/executor/cmd_folders.go index 15e373eb..95dbe91a 100644 --- a/mdl/executor/cmd_folders.go +++ b/mdl/executor/cmd_folders.go @@ -9,8 +9,8 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" - "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" ) // findFolderByPath walks a folder path under a module and returns the folder ID. diff --git a/mdl/executor/cmd_import_mappings.go b/mdl/executor/cmd_import_mappings.go index c0a7a8c1..68daf7e2 100644 --- a/mdl/executor/cmd_import_mappings.go +++ b/mdl/executor/cmd_import_mappings.go @@ -11,8 +11,8 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/backend" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" - "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" ) // showImportMappings prints a table of all import mapping documents. diff --git a/mdl/executor/cmd_microflows_builder.go b/mdl/executor/cmd_microflows_builder.go index 7f0d95ff..fd3e662b 100644 --- a/mdl/executor/cmd_microflows_builder.go +++ b/mdl/executor/cmd_microflows_builder.go @@ -30,7 +30,7 @@ type flowBuilder struct { 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 backend.FullBackend // For looking up page/microflow references + backend backend.FullBackend // 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 @@ -137,7 +137,7 @@ func (fb *flowBuilder) resolveAssociationPaths(expr ast.Expression) ast.Expressi // For each segment that is a qualified association name (Module.AssocName), it looks up // the association's target entity and inserts it after the association. func (fb *flowBuilder) resolvePathSegments(path []string) []string { - if fb.reader == nil || len(path) == 0 { + if fb.backend == nil || len(path) == 0 { return path } diff --git a/mdl/executor/cmd_microflows_builder_actions.go b/mdl/executor/cmd_microflows_builder_actions.go index d60a9142..9e1104fb 100644 --- a/mdl/executor/cmd_microflows_builder_actions.go +++ b/mdl/executor/cmd_microflows_builder_actions.go @@ -18,7 +18,7 @@ import ( func (fb *flowBuilder) addCreateVariableAction(s *ast.DeclareStmt) model.ID { // Resolve TypeEnumeration → TypeEntity ambiguity using the domain model declType := s.Type - if declType.Kind == ast.TypeEnumeration && declType.EnumRef != nil && fb.reader != nil { + if declType.Kind == ast.TypeEnumeration && declType.EnumRef != nil && fb.backend != nil { if fb.isEntity(declType.EnumRef.Module, declType.EnumRef.Name) { declType = ast.DataType{Kind: ast.TypeEntity, EntityRef: declType.EnumRef} } @@ -734,14 +734,14 @@ func (fb *flowBuilder) addRemoveFromListAction(s *ast.RemoveFromListStmt) model. // isEntity checks whether a qualified name refers to an entity in the domain model. func (fb *flowBuilder) isEntity(moduleName, entityName string) bool { - if fb.reader == nil { + if fb.backend == nil { return false } - mod, err := fb.reader.GetModuleByName(moduleName) + mod, err := fb.backend.GetModuleByName(moduleName) if err != nil || mod == nil { return false } - dm, err := fb.reader.GetDomainModel(mod.ID) + dm, err := fb.backend.GetDomainModel(mod.ID) if err != nil || dm == nil { return false } @@ -755,7 +755,7 @@ func (fb *flowBuilder) isEntity(moduleName, entityName string) bool { // resolveMemberChange determines whether a member name is an association or attribute // and sets the appropriate field on the MemberChange. It queries the domain model -// to check if the name matches an association on the entity; if no reader is available, +// to check if the name matches an association on the entity; if no backend is available, // it falls back to the dot-contains heuristic. // // memberName can be either bare ("Order_Customer") or qualified ("MfTest.Order_Customer"). @@ -784,9 +784,9 @@ func (fb *flowBuilder) resolveMemberChange(mc *microflows.MemberChange, memberNa } // Query domain model to check if this member is an association - if fb.reader != nil { - if mod, err := fb.reader.GetModuleByName(moduleName); err == nil && mod != nil { - if dm, err := fb.reader.GetDomainModel(mod.ID); err == nil && dm != nil { + if fb.backend != nil { + if mod, err := fb.backend.GetModuleByName(moduleName); err == nil && mod != nil { + if dm, err := fb.backend.GetDomainModel(mod.ID); err == nil && dm != nil { for _, a := range dm.Associations { if a.Name == bareName { mc.AssociationQualifiedName = qualifiedName @@ -828,16 +828,16 @@ type assocLookupResult struct { // lookupAssociation finds an association by module and name, returning its type // and the qualified names of its parent and child entities. Returns nil if the -// association cannot be found (e.g., reader is nil or module doesn't exist). +// association cannot be found (e.g., backend is nil or module doesn't exist). func (fb *flowBuilder) lookupAssociation(moduleName, assocName string) *assocLookupResult { - if fb.reader == nil { + if fb.backend == nil { return nil } - mod, err := fb.reader.GetModuleByName(moduleName) + mod, err := fb.backend.GetModuleByName(moduleName) if err != nil || mod == nil { return nil } - dm, err := fb.reader.GetDomainModel(mod.ID) + dm, err := fb.backend.GetDomainModel(mod.ID) if err != nil || dm == nil { return nil } diff --git a/mdl/executor/cmd_microflows_builder_annotations.go b/mdl/executor/cmd_microflows_builder_annotations.go index 476863ee..ed9b0e32 100644 --- a/mdl/executor/cmd_microflows_builder_annotations.go +++ b/mdl/executor/cmd_microflows_builder_annotations.go @@ -5,9 +5,9 @@ package executor import ( "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/mdl/types" ) // getStatementAnnotations extracts the annotations field from any microflow statement. diff --git a/mdl/executor/cmd_microflows_builder_calls.go b/mdl/executor/cmd_microflows_builder_calls.go index a03b21e0..4db6bf55 100644 --- a/mdl/executor/cmd_microflows_builder_calls.go +++ b/mdl/executor/cmd_microflows_builder_calls.go @@ -8,10 +8,10 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/javaactions" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/mdl/types" ) // addLogMessageAction creates a LOG statement as a LogMessageAction. @@ -155,8 +155,8 @@ func (fb *flowBuilder) addCallJavaActionAction(s *ast.CallJavaActionStmt) model. // Try to look up the Java action definition to detect EntityTypeParameterType parameters var jaDef *javaactions.JavaAction - if fb.reader != nil { - jaDef, _ = fb.reader.ReadJavaActionByName(actionQN) + if fb.backend != nil { + jaDef, _ = fb.backend.ReadJavaActionByName(actionQN) } // Build a map of parameter name -> param type for the Java action @@ -691,11 +691,11 @@ func (fb *flowBuilder) addRestCallAction(s *ast.RestCallStmt) model.ID { // looking at the JSON structure it references. If the root JSON element is // an Object, the mapping produces one object; if it is an Array, a list. singleObject := false - if fb.reader != nil { - if im, err := fb.reader.GetImportMappingByQualifiedName(s.Result.MappingName.Module, s.Result.MappingName.Name); err == nil && im.JsonStructure != "" { + if fb.backend != nil { + if im, err := fb.backend.GetImportMappingByQualifiedName(s.Result.MappingName.Module, s.Result.MappingName.Name); err == nil && im.JsonStructure != "" { // im.JsonStructure is "Module.Name" — split and look up the JSON structure. if parts := strings.SplitN(im.JsonStructure, ".", 2); len(parts) == 2 { - if js, err := fb.reader.GetJsonStructureByQualifiedName(parts[0], parts[1]); err == nil && len(js.Elements) > 0 { + if js, err := fb.backend.GetJsonStructureByQualifiedName(parts[0], parts[1]); err == nil && len(js.Elements) > 0 { singleObject = js.Elements[0].ElementType == "Object" } } @@ -990,12 +990,12 @@ func (fb *flowBuilder) addImportFromMappingAction(s *ast.ImportFromMappingStmt) } // Determine single vs list and result entity from the import mapping - if fb.reader != nil { - if im, err := fb.reader.GetImportMappingByQualifiedName(s.Mapping.Module, s.Mapping.Name); err == nil { + if fb.backend != nil { + if im, err := fb.backend.GetImportMappingByQualifiedName(s.Mapping.Module, s.Mapping.Name); err == nil { if im.JsonStructure != "" { parts := strings.SplitN(im.JsonStructure, ".", 2) if len(parts) == 2 { - if js, err := fb.reader.GetJsonStructureByQualifiedName(parts[0], parts[1]); err == nil && len(js.Elements) > 0 { + if js, err := fb.backend.GetJsonStructureByQualifiedName(parts[0], parts[1]); err == nil && len(js.Elements) > 0 { if js.Elements[0].ElementType == "Array" { resultHandling.SingleObject = false } diff --git a/mdl/executor/cmd_microflows_builder_control.go b/mdl/executor/cmd_microflows_builder_control.go index 98edcf49..8f4d20cd 100644 --- a/mdl/executor/cmd_microflows_builder_control.go +++ b/mdl/executor/cmd_microflows_builder_control.go @@ -7,9 +7,9 @@ import ( "strings" "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/mdl/types" ) // addIfStatement creates an IF/THEN/ELSE statement using ExclusiveSplit and ExclusiveMerge. @@ -285,7 +285,7 @@ func (fb *flowBuilder) addLoopStatement(s *ast.LoopStmt) model.ID { varTypes: fb.varTypes, // Share variable scope declaredVars: fb.declaredVars, // Share declared vars (fixes nil map panic) measurer: fb.measurer, // Share measurer - reader: fb.reader, // Share reader + backend: fb.backend, // Share backend hierarchy: fb.hierarchy, // Share hierarchy restServices: fb.restServices, // Share REST services for parameter classification } @@ -359,7 +359,7 @@ func (fb *flowBuilder) addWhileStatement(s *ast.WhileStmt) model.ID { varTypes: fb.varTypes, declaredVars: fb.declaredVars, measurer: fb.measurer, - reader: fb.reader, + backend: fb.backend, hierarchy: fb.hierarchy, restServices: fb.restServices, } diff --git a/mdl/executor/cmd_microflows_builder_flows.go b/mdl/executor/cmd_microflows_builder_flows.go index 78d96239..385a21bc 100644 --- a/mdl/executor/cmd_microflows_builder_flows.go +++ b/mdl/executor/cmd_microflows_builder_flows.go @@ -5,9 +5,9 @@ package executor import ( "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/mdl/types" ) // convertErrorHandlingType converts AST error handling type to SDK error handling type. @@ -64,7 +64,7 @@ func (fb *flowBuilder) addErrorHandlerFlow(sourceActivityID model.ID, sourceX in varTypes: fb.varTypes, declaredVars: fb.declaredVars, measurer: fb.measurer, - reader: fb.reader, + backend: fb.backend, hierarchy: fb.hierarchy, restServices: fb.restServices, } diff --git a/mdl/executor/cmd_microflows_builder_graph.go b/mdl/executor/cmd_microflows_builder_graph.go index 2b0737ba..3bc79346 100644 --- a/mdl/executor/cmd_microflows_builder_graph.go +++ b/mdl/executor/cmd_microflows_builder_graph.go @@ -5,9 +5,9 @@ package executor import ( "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/mdl/types" ) // buildFlowGraph converts AST statements to a Microflow flow graph. diff --git a/mdl/executor/cmd_microflows_builder_workflow.go b/mdl/executor/cmd_microflows_builder_workflow.go index aacf40e0..d29c1173 100644 --- a/mdl/executor/cmd_microflows_builder_workflow.go +++ b/mdl/executor/cmd_microflows_builder_workflow.go @@ -4,9 +4,9 @@ package executor import ( "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/mdl/types" ) // wrapAction wraps a MicroflowAction in an ActionActivity with standard positioning. diff --git a/mdl/executor/cmd_microflows_create.go b/mdl/executor/cmd_microflows_create.go index dd85ae1a..d8d10a2f 100644 --- a/mdl/executor/cmd_microflows_create.go +++ b/mdl/executor/cmd_microflows_create.go @@ -23,7 +23,7 @@ func isBuiltinModuleEntity(moduleName string) bool { } // execCreateMicroflow handles CREATE MICROFLOW statements. -// loadRestServices returns all consumed REST services, or nil if no reader. +// loadRestServices returns all consumed REST services, or nil if no backend. func loadRestServices(ctx *ExecContext) ([]*model.ConsumedRestService, error) { if !ctx.Connected() { return nil, nil @@ -213,7 +213,7 @@ func execCreateMicroflow(ctx *ExecContext, s *ast.CreateMicroflowStmt) error { varTypes: varTypes, declaredVars: declaredVars, measurer: &layoutMeasurer{varTypes: varTypes}, - reader: ctx.Backend, + backend: ctx.Backend, hierarchy: hierarchy, restServices: restServices, } diff --git a/mdl/executor/cmd_microflows_helpers.go b/mdl/executor/cmd_microflows_helpers.go index b9286b6a..a8fbfe8b 100644 --- a/mdl/executor/cmd_microflows_helpers.go +++ b/mdl/executor/cmd_microflows_helpers.go @@ -227,18 +227,18 @@ func (fb *flowBuilder) memberExpressionToString(expr ast.Expression, entityQN, a // for an attribute if it is an enumeration type. Returns "" if the attribute is not // an enumeration or if the domain model is not available. func (fb *flowBuilder) lookupEnumRef(entityQN, attrName string) string { - if fb.reader == nil || entityQN == "" || attrName == "" { + if fb.backend == nil || entityQN == "" || attrName == "" { return "" } parts := strings.SplitN(entityQN, ".", 2) if len(parts) != 2 { return "" } - mod, err := fb.reader.GetModuleByName(parts[0]) + mod, err := fb.backend.GetModuleByName(parts[0]) if err != nil || mod == nil { return "" } - dm, err := fb.reader.GetDomainModel(mod.ID) + dm, err := fb.backend.GetDomainModel(mod.ID) if err != nil || dm == nil { return "" } diff --git a/mdl/executor/cmd_pages_builder.go b/mdl/executor/cmd_pages_builder.go index 2da161f3..f9f6ae1c 100644 --- a/mdl/executor/cmd_pages_builder.go +++ b/mdl/executor/cmd_pages_builder.go @@ -311,14 +311,12 @@ func (pb *pageBuilder) resolveFolder(folderPath string) (model.ID, error) { if err != nil { return "", mdlerrors.NewBackend(fmt.Sprintf("create folder %s", part), err) } - currentContainerID = newFolderID - - // Add to cache pb.foldersCache = append(pb.foldersCache, &types.FolderInfo{ ID: newFolderID, ContainerID: currentContainerID, Name: part, }) + currentContainerID = newFolderID } } diff --git a/mdl/executor/cmd_pages_builder_input.go b/mdl/executor/cmd_pages_builder_input.go index 0b07d61e..b286b7cf 100644 --- a/mdl/executor/cmd_pages_builder_input.go +++ b/mdl/executor/cmd_pages_builder_input.go @@ -122,7 +122,7 @@ func (pb *pageBuilder) resolveMicroflow(qualifiedName string) (model.ID, error) } } - // Get microflows from reader cache + // Get microflows from backend mfs, err := pb.getMicroflows() if err != nil { return "", mdlerrors.NewBackend("list microflows", err) diff --git a/mdl/executor/cmd_pages_builder_v3.go b/mdl/executor/cmd_pages_builder_v3.go index 1d108b9b..2e2bc64b 100644 --- a/mdl/executor/cmd_pages_builder_v3.go +++ b/mdl/executor/cmd_pages_builder_v3.go @@ -4,14 +4,15 @@ package executor import ( "fmt" + "log" "strings" "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/domainmodel" "github.com/mendixlabs/mxcli/sdk/microflows" - "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/sdk/pages" ) @@ -60,7 +61,7 @@ func (pb *pageBuilder) buildPageV3(s *ast.CreatePageStmtV3) (*pages.Page, error) layoutID, err := pb.resolveLayout(s.Layout) if err != nil { // Layout not found is not fatal - page will work but may not render correctly - fmt.Printf("Warning: layout %s not found\n", s.Layout) + log.Printf("warning: layout %s not found", s.Layout) } else { page.LayoutID = layoutID @@ -740,7 +741,7 @@ func (pb *pageBuilder) moduleNameByID(moduleID model.ID) string { // getMicroflowReturnEntityName looks up a microflow and returns its return type entity name. // Returns empty string if the microflow doesn't return an entity or list of entities. func (pb *pageBuilder) getMicroflowReturnEntityName(qualifiedName string) string { - // First, check if the microflow was created during this session (not yet in reader cache) + // First, check if the microflow was created during this session (not yet in backend cache) if pb.execCache != nil && pb.execCache.createdMicroflows != nil { if info, ok := pb.execCache.createdMicroflows[qualifiedName]; ok { return info.ReturnEntityName @@ -755,7 +756,7 @@ func (pb *pageBuilder) getMicroflowReturnEntityName(qualifiedName string) string moduleName := parts[0] mfName := strings.Join(parts[1:], ".") - // Get microflows from reader cache + // Get microflows from backend mfs, err := pb.getMicroflows() if err != nil { return "" diff --git a/mdl/executor/cmd_pages_builder_v3_layout.go b/mdl/executor/cmd_pages_builder_v3_layout.go index a7dca672..1b49e223 100644 --- a/mdl/executor/cmd_pages_builder_v3_layout.go +++ b/mdl/executor/cmd_pages_builder_v3_layout.go @@ -5,11 +5,10 @@ package executor import ( "strings" - "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/pages" - - "github.com/mendixlabs/mxcli/mdl/ast" ) func (pb *pageBuilder) buildLayoutGridV3(w *ast.WidgetV3) (*pages.LayoutGrid, error) { diff --git a/mdl/executor/cmd_security_write.go b/mdl/executor/cmd_security_write.go index e97bf593..327d2345 100644 --- a/mdl/executor/cmd_security_write.go +++ b/mdl/executor/cmd_security_write.go @@ -10,8 +10,8 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/backend" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" - "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/security" ) @@ -781,7 +781,7 @@ func execRevokeWorkflowAccess(ctx *ExecContext, s *ast.RevokeWorkflowAccessStmt) func validateModuleRole(ctx *ExecContext, role ast.QualifiedName) error { module, err := findModule(ctx, role.Module) if err != nil { - return fmt.Errorf("module not found for role %s.%s: %w", role.Module, role.Name, err) + return mdlerrors.NewBackend(fmt.Sprintf("module not found for role %s.%s", role.Module, role.Name), err) } ms, err := ctx.Backend.GetModuleSecurity(module.ID) @@ -856,7 +856,7 @@ func execCreateDemoUser(ctx *ExecContext, s *ast.CreateDemoUserStmt) error { // Validate password against project password policy if err := ps.PasswordPolicy.ValidatePassword(s.Password); err != nil { - return fmt.Errorf("password policy violation for demo user '%s': %w\nhint: check your project's password policy with SHOW PROJECT SECURITY", s.UserName, err) + return mdlerrors.NewValidationf("password policy violation for demo user '%s': %v\nhint: check your project's password policy with SHOW PROJECT SECURITY", s.UserName, err) } // Check if user already exists diff --git a/mdl/executor/cmd_structure.go b/mdl/executor/cmd_structure.go index fa26ffac..20872700 100644 --- a/mdl/executor/cmd_structure.go +++ b/mdl/executor/cmd_structure.go @@ -75,8 +75,8 @@ func structureDepth1JSON(ctx *ExecContext, modules []structureModule) error { odataClientCounts := queryCountByModule(ctx, "odata_clients") odataServiceCounts := queryCountByModule(ctx, "odata_services") beServiceCounts := queryCountByModule(ctx, "business_event_services") - constantCounts := countByModuleFromReader(ctx, "constants") - scheduledEventCounts := countByModuleFromReader(ctx, "scheduled_events") + constantCounts := countByModuleFromBackend(ctx, "constants") + scheduledEventCounts := countByModuleFromBackend(ctx, "scheduled_events") tr := &TableResult{ Columns: []string{ @@ -195,9 +195,9 @@ func structureDepth1(ctx *ExecContext, modules []structureModule) error { odataServiceCounts := queryCountByModule(ctx, "odata_services") beServiceCounts := queryCountByModule(ctx, "business_event_services") - // Get constants and scheduled events from reader (no catalog tables) - constantCounts := countByModuleFromReader(ctx, "constants") - scheduledEventCounts := countByModuleFromReader(ctx, "scheduled_events") + // Get constants and scheduled events from backend (no catalog tables) + constantCounts := countByModuleFromBackend(ctx, "constants") + scheduledEventCounts := countByModuleFromBackend(ctx, "scheduled_events") // Calculate name column width for alignment nameWidth := 0 @@ -272,8 +272,8 @@ func queryCountByModule(ctx *ExecContext, tableAndWhere string) map[string]int { return counts } -// countByModuleFromReader counts elements per module using the reader (for types without catalog tables). -func countByModuleFromReader(ctx *ExecContext, kind string) map[string]int { +// countByModuleFromBackend counts elements per module using the backend (for types without catalog tables). +func countByModuleFromBackend(ctx *ExecContext, kind string) map[string]int { counts := make(map[string]int) h, err := getHierarchy(ctx) if err != nil { @@ -314,7 +314,7 @@ func pluralize(count int, singular, plural string) string { // ============================================================================ func structureDepth2(ctx *ExecContext, modules []structureModule) error { - // Pre-load data that needs the reader + // Pre-load data from the backend h, err := getHierarchy(ctx) if err != nil { return mdlerrors.NewBackend("build hierarchy", err) diff --git a/mdl/executor/cmd_workflows_write.go b/mdl/executor/cmd_workflows_write.go index ce2d116d..6a86e9a3 100644 --- a/mdl/executor/cmd_workflows_write.go +++ b/mdl/executor/cmd_workflows_write.go @@ -10,8 +10,8 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" - "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/workflows" ) diff --git a/mdl/executor/executor.go b/mdl/executor/executor.go index 5b76f689..f88c975d 100644 --- a/mdl/executor/executor.go +++ b/mdl/executor/executor.go @@ -303,6 +303,7 @@ func (e *Executor) Catalog() *catalog.Catalog { // Reader returns the MPR reader, or nil if not connected. // Deprecated: External callers should migrate to using Backend methods directly. +// TODO(shared-types): remove once all callers use Backend — target: v0.next milestone. func (e *Executor) Reader() *mpr.Reader { if e.backend == nil { return nil @@ -340,7 +341,7 @@ func (e *Executor) Close() error { // trackCreatedMicroflow registers a microflow that was created during this session. // This allows subsequent page creations to resolve references to the microflow -// even though the reader cache hasn't been updated. +// even though the backend cache hasn't been updated. func (e *Executor) trackCreatedMicroflow(moduleName, mfName string, id, containerID model.ID, returnEntityName string) { e.ensureCache() if e.cache.createdMicroflows == nil { @@ -358,7 +359,7 @@ func (e *Executor) trackCreatedMicroflow(moduleName, mfName string, id, containe // trackCreatedPage registers a page that was created during this session. // This allows subsequent page creations to resolve SHOW_PAGE references -// even though the reader cache hasn't been updated. +// even though the backend cache hasn't been updated. func (e *Executor) trackCreatedPage(moduleName, pageName string, id, containerID model.ID) { e.ensureCache() if e.cache.createdPages == nil { @@ -375,7 +376,7 @@ func (e *Executor) trackCreatedPage(moduleName, pageName string, id, containerID // trackCreatedSnippet registers a snippet that was created during this session. // This allows subsequent creations to resolve snippet references -// even though the reader cache hasn't been updated. +// even though the backend cache hasn't been updated. func (e *Executor) trackCreatedSnippet(moduleName, snippetName string, id, containerID model.ID) { e.ensureCache() if e.cache.createdSnippets == nil { diff --git a/mdl/executor/executor_connect.go b/mdl/executor/executor_connect.go index 6ae49507..2deb54f3 100644 --- a/mdl/executor/executor_connect.go +++ b/mdl/executor/executor_connect.go @@ -3,6 +3,7 @@ package executor import ( + "errors" "fmt" "github.com/mendixlabs/mxcli/mdl/ast" @@ -16,7 +17,7 @@ func execConnect(ctx *ExecContext, s *ast.ConnectStmt) error { } if e.backendFactory == nil { - return mdlerrors.NewBackend("connect", fmt.Errorf("no backend factory configured")) + return mdlerrors.NewBackend("connect", errors.New("no backend factory configured")) } b := e.backendFactory() if err := b.Connect(s.Path); err != nil { diff --git a/mdl/executor/widget_engine_test.go b/mdl/executor/widget_engine_test.go index f7be9510..a49f9ed1 100644 --- a/mdl/executor/widget_engine_test.go +++ b/mdl/executor/widget_engine_test.go @@ -214,17 +214,6 @@ func TestEvaluateCondition(t *testing.T) { } } -func TestEvaluateConditionUnknownReturnsFalse(t *testing.T) { - engine := &PluggableWidgetEngine{} - - w := &ast.WidgetV3{Properties: map[string]any{}} - result := engine.evaluateCondition("typoCondition", w) - - if result != false { - t.Errorf("expected false for unknown condition, got %v", result) - } -} - func TestSelectMappings_NoModes(t *testing.T) { engine := &PluggableWidgetEngine{} diff --git a/mdl/executor/widget_registry_test.go b/mdl/executor/widget_registry_test.go index b3e8608f..4f852963 100644 --- a/mdl/executor/widget_registry_test.go +++ b/mdl/executor/widget_registry_test.go @@ -308,7 +308,7 @@ func TestRegistryUserDefinitionOverrideLogsWarning(t *testing.T) { var buf bytes.Buffer log.SetOutput(&buf) - defer log.SetOutput(nil) + defer log.SetOutput(os.Stderr) projectPath := filepath.Join(tmpDir, "App.mpr") if err := reg.LoadUserDefinitions(projectPath); err != nil { diff --git a/mdl/types/asyncapi.go b/mdl/types/asyncapi.go index 692eaae6..ffd64227 100644 --- a/mdl/types/asyncapi.go +++ b/mdl/types/asyncapi.go @@ -4,6 +4,7 @@ package types import ( "fmt" + "sort" "strings" "gopkg.in/yaml.v3" @@ -61,8 +62,14 @@ func ParseAsyncAPI(yamlStr string) (*AsyncAPIDocument, error) { Description: raw.Info.Description, } - // Resolve messages from components - for name, msg := range raw.Components.Messages { + // Resolve messages from components (sorted for deterministic output) + messageNames := make([]string, 0, len(raw.Components.Messages)) + for name := range raw.Components.Messages { + messageNames = append(messageNames, name) + } + sort.Strings(messageNames) + for _, name := range messageNames { + msg := raw.Components.Messages[name] resolved := &AsyncAPIMessage{ Name: name, Title: msg.Title, @@ -88,8 +95,14 @@ func ParseAsyncAPI(yamlStr string) (*AsyncAPIDocument, error) { doc.Messages = append(doc.Messages, resolved) } - // Resolve channels - for channelName, channel := range raw.Channels { + // Resolve channels (sorted for deterministic output) + channelNames := make([]string, 0, len(raw.Channels)) + for name := range raw.Channels { + channelNames = append(channelNames, name) + } + sort.Strings(channelNames) + for _, channelName := range channelNames { + channel := raw.Channels[channelName] if channel.Subscribe != nil { msgName := "" if channel.Subscribe.Message.Ref != "" { @@ -138,8 +151,16 @@ func asyncRefName(ref string) string { } func resolveAsyncSchemaProperties(schema yamlAsyncSchema) []*AsyncAPIProperty { - var props []*AsyncAPIProperty - for name, prop := range schema.Properties { + // Sort property names for deterministic output + names := make([]string, 0, len(schema.Properties)) + for name := range schema.Properties { + names = append(names, name) + } + sort.Strings(names) + + props := make([]*AsyncAPIProperty, 0, len(names)) + for _, name := range names { + prop := schema.Properties[name] props = append(props, &AsyncAPIProperty{ Name: name, Type: prop.Type, diff --git a/mdl/types/doc.go b/mdl/types/doc.go index fe45c6ad..8d0d83f1 100644 --- a/mdl/types/doc.go +++ b/mdl/types/doc.go @@ -1,7 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 // Package types defines shared value types used in backend interfaces. -// These types are decoupled from sdk/mpr to enable WASM compilation of -// the mdl/ subtree. Conversion functions between these types and their -// sdk/mpr counterparts live in mdl/backend/mpr/. +// These types are decoupled from sdk/mpr to avoid pulling in CGO +// dependencies, keeping the mdl/ subtree dependency-light. +// Conversion functions between these types and their sdk/mpr +// counterparts live in mdl/backend/mpr/. package types diff --git a/mdl/types/edmx.go b/mdl/types/edmx.go index 17367b86..eb19c1d2 100644 --- a/mdl/types/edmx.go +++ b/mdl/types/edmx.go @@ -401,12 +401,12 @@ func ResolveNavType(t string) (typeName string, isMany bool) { // ============================================================================ type xmlEdmx struct { - XMLName xml.Name `xml:"Edmx"` - Version string `xml:"Version,attr"` - DataServices []xmlDataServices `xml:"DataServices"` + XMLName xml.Name `xml:"Edmx"` + Version string `xml:"Version,attr"` + DataServices []xmlDataService `xml:"DataServices"` } -type xmlDataServices struct { +type xmlDataService struct { Schemas []xmlSchema `xml:"Schema"` } diff --git a/mdl/types/id.go b/mdl/types/id.go index 528f145a..9c8ab021 100644 --- a/mdl/types/id.go +++ b/mdl/types/id.go @@ -13,7 +13,9 @@ import ( // GenerateID generates a new unique UUID v4 for model elements. func GenerateID() string { b := make([]byte, 16) - _, _ = rand.Read(b) + if _, err := rand.Read(b); err != nil { + panic("crypto/rand: " + err.Error()) + } b[6] = (b[6] & 0x0f) | 0x40 // Version 4 b[8] = (b[8] & 0x3f) | 0x80 // Variant is 10 @@ -25,15 +27,19 @@ func GenerateID() string { b[10], b[11], b[12], b[13], b[14], b[15]) } -// GenerateDeterministicID generates a stable UUID from a seed string. +// GenerateDeterministicID generates a stable UUID v4 from a seed string. // Used for System module entities that aren't in the MPR but need consistent IDs. func GenerateDeterministicID(seed string) string { h := sha256.Sum256([]byte(seed)) + // Set UUID version 4 and variant bits on the hash bytes + h[6] = (h[6] & 0x0f) | 0x40 // Version 4 + h[8] = (h[8] & 0x3f) | 0x80 // Variant is 10 return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x", h[0:4], h[4:6], h[6:8], h[8:10], h[10:16]) } -// BlobToUUID converts a 16-byte binary ID blob to a UUID string. +// BlobToUUID converts a 16-byte blob in Microsoft GUID format to a UUID string. +// For non-16-byte input, returns a hex-encoded string as a best-effort fallback. func BlobToUUID(data []byte) string { if len(data) != 16 { return hex.EncodeToString(data) @@ -51,13 +57,8 @@ func UUIDToBlob(uuid string) []byte { if uuid == "" { return nil } - var clean strings.Builder - for _, c := range uuid { - if c != '-' { - clean.WriteString(string(c)) - } - } - decoded, err := hex.DecodeString(clean.String()) + clean := strings.ReplaceAll(uuid, "-", "") + decoded, err := hex.DecodeString(clean) if err != nil || len(decoded) != 16 { return nil } @@ -93,11 +94,8 @@ func ValidateID(id string) bool { return true } -// Hash computes a hash for content (used for content deduplication). +// Hash computes a SHA-256 hash for content (used for content deduplication). func Hash(content []byte) string { - var sum uint64 - for i, b := range content { - sum += uint64(b) * uint64(i+1) - } - return fmt.Sprintf("%016x", sum) + h := sha256.Sum256(content) + return hex.EncodeToString(h[:]) } diff --git a/mdl/types/json_utils.go b/mdl/types/json_utils.go index 7a9d8da8..021ed5d0 100644 --- a/mdl/types/json_utils.go +++ b/mdl/types/json_utils.go @@ -34,9 +34,14 @@ func normalizeDateTimeValue(s string) string { // Find the decimal point after seconds dotIdx := strings.Index(s, ".") if dotIdx == -1 { - // No fractional part — insert .0000000 before timezone suffix - if idx := strings.IndexAny(s, "Z+-"); idx > 0 { - return s[:idx] + ".0000000" + s[idx:] + // No fractional part — insert .0000000 before timezone suffix. + // Search only after the time portion (index 19+ in "2015-05-22T14:56:29Z") + // to avoid matching '-' in the date portion. + if len(s) < 19 { + return s + } + if idx := strings.IndexAny(s[19:], "Z+-"); idx >= 0 { + return s[:19+idx] + ".0000000" + s[19+idx:] } return s } @@ -109,7 +114,7 @@ var reservedExposedNames = map[string]bool{ } // resolveExposedName returns the custom name if mapped, otherwise capitalizes the JSON key. -// Reserved names (Id, Type, Name) are prefixed with underscore to match Studio Pro behavior. +// Reserved names (Id, Type) are prefixed with underscore to match Studio Pro behavior. func (b *snippetBuilder) resolveExposedName(jsonKey string) string { if b.customNameMap != nil { if custom, ok := b.customNameMap[jsonKey]; ok { @@ -214,7 +219,9 @@ func (b *snippetBuilder) buildElementFromRawValue(exposedName, path, jsonKey str // Primitive — unmarshal to determine type var val any - json.Unmarshal(raw, &val) + if err := json.Unmarshal(raw, &val); err != nil { + return buildValueElement(exposedName, path, "Unknown", string(raw)) + } switch v := val.(type) { case string: @@ -226,7 +233,7 @@ func (b *snippetBuilder) buildElementFromRawValue(exposedName, path, jsonKey str return buildValueElement(exposedName, path, primitiveType, fmt.Sprintf("%q", v)) case float64: // Check the raw JSON text for a decimal point — Go's %v drops ".0" from 41850.0 - if v == math.Trunc(v) && !strings.Contains(trimmed, ".") { + if v == math.Trunc(v) && !strings.Contains(trimmed, ".") && v >= -(1<<53) && v <= (1<<53) { return buildValueElement(exposedName, path, "Integer", fmt.Sprintf("%v", int64(v))) } return buildValueElement(exposedName, path, "Decimal", fmt.Sprintf("%v", v)) @@ -257,10 +264,14 @@ func (b *snippetBuilder) buildElementFromRawRootArray(exposedName, path, rawJSON } dec := json.NewDecoder(strings.NewReader(rawJSON)) - dec.Token() // opening [ + if _, err := dec.Token(); err != nil { // opening [ + return arrayElem + } if dec.More() { var firstItem json.RawMessage - dec.Decode(&firstItem) + if err := dec.Decode(&firstItem); err != nil { + return arrayElem + } itemPath := path + "|(Object)" trimmed := strings.TrimSpace(string(firstItem)) @@ -300,10 +311,14 @@ func (b *snippetBuilder) buildElementFromRawArray(exposedName, path, jsonKey, ra // Decode array and get first element as raw JSON dec := json.NewDecoder(strings.NewReader(rawJSON)) - dec.Token() // opening [ + if _, err := dec.Token(); err != nil { // opening [ + return arrayElem + } if dec.More() { var firstItem json.RawMessage - dec.Decode(&firstItem) + if err := dec.Decode(&firstItem); err != nil { + return arrayElem + } trimmed := strings.TrimSpace(string(firstItem)) diff --git a/sdk/mpr/reader_types.go b/sdk/mpr/reader_types.go index 6b9099a9..f6e16d02 100644 --- a/sdk/mpr/reader_types.go +++ b/sdk/mpr/reader_types.go @@ -1,6 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 -// Package mpr - Additional types and utility methods for Reader. +// Package mpr - Reader methods for listing and querying model units. package mpr import ( diff --git a/sdk/mpr/utils.go b/sdk/mpr/utils.go index 1acc6739..9fc5a05d 100644 --- a/sdk/mpr/utils.go +++ b/sdk/mpr/utils.go @@ -3,8 +3,10 @@ package mpr import ( - "github.com/mendixlabs/mxcli/mdl/types" "go.mongodb.org/mongo-driver/bson/primitive" + + "github.com/mendixlabs/mxcli/mdl/bsonutil" + "github.com/mendixlabs/mxcli/mdl/types" ) // GenerateID generates a new unique ID for model elements. @@ -24,19 +26,12 @@ func BlobToUUID(data []byte) string { // IDToBsonBinary converts a UUID string to a BSON binary value. func IDToBsonBinary(id string) primitive.Binary { - blob := types.UUIDToBlob(id) - if blob == nil || len(blob) != 16 { - blob = types.UUIDToBlob(types.GenerateID()) - } - return primitive.Binary{ - Subtype: 0x00, - Data: blob, - } + return bsonutil.IDToBsonBinary(id) } // BsonBinaryToID converts a BSON binary value to a UUID string. func BsonBinaryToID(bin primitive.Binary) string { - return types.BlobToUUID(bin.Data) + return bsonutil.BsonBinaryToID(bin) } // Hash computes a hash for content (used for content deduplication). diff --git a/sdk/mpr/writer_agenteditor_agent.go b/sdk/mpr/writer_agenteditor_agent.go index 4d944e3c..22b548b7 100644 --- a/sdk/mpr/writer_agenteditor_agent.go +++ b/sdk/mpr/writer_agenteditor_agent.go @@ -82,19 +82,19 @@ func encodeAgentContents(a *agenteditor.Agent) (string, error) { MaxResults int `json:"maxResults,omitempty"` } type contentsShape struct { - Description string `json:"description"` - SystemPrompt string `json:"systemPrompt"` - UserPrompt string `json:"userPrompt"` - UsageType string `json:"usageType"` + Description string `json:"description"` + SystemPrompt string `json:"systemPrompt"` + UserPrompt string `json:"userPrompt"` + UsageType string `json:"usageType"` Variables []agenteditor.AgentVar `json:"variables"` - Tools []toolEntry `json:"tools"` - KnowledgebaseTools []kbToolEntry `json:"knowledgebaseTools"` - Model *agenteditor.DocRef `json:"model,omitempty"` - Entity *agenteditor.DocRef `json:"entity,omitempty"` - MaxTokens *int `json:"maxTokens,omitempty"` - ToolChoice string `json:"toolChoice,omitempty"` - Temperature *float64 `json:"temperature,omitempty"` - TopP *float64 `json:"topP,omitempty"` + Tools []toolEntry `json:"tools"` + KnowledgebaseTools []kbToolEntry `json:"knowledgebaseTools"` + Model *agenteditor.DocRef `json:"model,omitempty"` + Entity *agenteditor.DocRef `json:"entity,omitempty"` + MaxTokens *int `json:"maxTokens,omitempty"` + ToolChoice string `json:"toolChoice,omitempty"` + Temperature *float64 `json:"temperature,omitempty"` + TopP *float64 `json:"topP,omitempty"` } // Convert typed slices (ensure non-nil so JSON emits [] not null). diff --git a/sdk/mpr/writer_core.go b/sdk/mpr/writer_core.go index 2f191150..af63dc1b 100644 --- a/sdk/mpr/writer_core.go +++ b/sdk/mpr/writer_core.go @@ -10,21 +10,15 @@ import ( "os" "path/filepath" + "github.com/mendixlabs/mxcli/mdl/bsonutil" "github.com/mendixlabs/mxcli/mdl/types" "go.mongodb.org/mongo-driver/bson/primitive" ) // idToBsonBinary converts a UUID string to BSON Binary format. -// Mendix stores IDs as Binary with Subtype 0. +// Delegates to the canonical implementation in bsonutil. func idToBsonBinary(id string) primitive.Binary { - blob := types.UUIDToBlob(id) - if blob == nil || len(blob) != 16 { - blob = types.UUIDToBlob(types.GenerateID()) - } - return primitive.Binary{ - Subtype: 0x00, - Data: blob, - } + return bsonutil.IDToBsonBinary(id) } // Writer provides methods to write Mendix project files. diff --git a/sdk/mpr/writer_jsonstructure.go b/sdk/mpr/writer_jsonstructure.go index d21e6b47..e2687353 100644 --- a/sdk/mpr/writer_jsonstructure.go +++ b/sdk/mpr/writer_jsonstructure.go @@ -89,5 +89,3 @@ func serializeJsonElement(elem *types.JsonElement) bson.D { {Key: "WarningMessage", Value: ""}, } } - -