Skip to content

Commit bc18c2b

Browse files
committed
refactor: decouple executor from storage layer, extract remaining BSON
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
1 parent 41cde57 commit bc18c2b

37 files changed

+1607
-3179
lines changed

cmd/mxcli/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99
"os"
1010
"strings"
1111

12+
"github.com/mendixlabs/mxcli/mdl/backend"
13+
mprbackend "github.com/mendixlabs/mxcli/mdl/backend/mpr"
1214
"github.com/mendixlabs/mxcli/mdl/diaglog"
1315
"github.com/mendixlabs/mxcli/mdl/executor"
1416
"github.com/mendixlabs/mxcli/mdl/repl"
@@ -194,6 +196,7 @@ func resolveFormat(cmd *cobra.Command, defaultFormat string) string {
194196
func newLoggedExecutor(mode string) (*executor.Executor, *diaglog.Logger) {
195197
logger := diaglog.Init(version, mode)
196198
exec := executor.New(os.Stdout)
199+
exec.SetBackendFactory(func() backend.FullBackend { return mprbackend.New() })
197200
exec.SetLogger(logger)
198201
if globalJSONFlag {
199202
exec.SetFormat(executor.FormatJSON)

examples/create_datagrid2_page/main.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import (
2222
"os"
2323
"strings"
2424

25+
"github.com/mendixlabs/mxcli/mdl/backend"
26+
mprbackend "github.com/mendixlabs/mxcli/mdl/backend/mpr"
2527
"github.com/mendixlabs/mxcli/mdl/executor"
2628
"github.com/mendixlabs/mxcli/mdl/visitor"
2729
)
@@ -51,6 +53,7 @@ func main() {
5153

5254
// Create the MDL executor with stdout for output
5355
exec := executor.New(os.Stdout)
56+
exec.SetBackendFactory(func() backend.FullBackend { return mprbackend.New() })
5457

5558
// Define the MDL script to create a page with DataGrid2
5659
// Note: Adjust module name, entity name, and attributes to match your project

mdl/backend/mock/backend.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,8 @@ type MockBackend struct {
276276
SerializeWidgetToOpaqueFunc func(w pages.Widget) any
277277
SerializeDataSourceToOpaqueFunc func(ds pages.DataSource) any
278278
BuildCreateAttributeObjectFunc func(attributePath string, objectTypeID, propertyTypeID, valueTypeID string) (any, error)
279+
BuildDataGrid2WidgetFunc func(id model.ID, name string, spec backend.DataGridSpec, projectPath string) (*pages.CustomWidget, error)
280+
BuildFilterWidgetFunc func(spec backend.FilterWidgetSpec, projectPath string) (pages.Widget, error)
279281

280282
// AgentEditorBackend
281283
ListAgentEditorModelsFunc func() ([]*agenteditor.Model, error)

mdl/backend/mock/mock_mutation.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
package mock
44

55
import (
6+
"fmt"
7+
68
"github.com/mendixlabs/mxcli/mdl/backend"
79
"github.com/mendixlabs/mxcli/model"
810
"github.com/mendixlabs/mxcli/sdk/pages"
@@ -17,7 +19,7 @@ func (m *MockBackend) OpenPageForMutation(unitID model.ID) (backend.PageMutator,
1719
if m.OpenPageForMutationFunc != nil {
1820
return m.OpenPageForMutationFunc(unitID)
1921
}
20-
return nil, nil
22+
return nil, fmt.Errorf("MockBackend.OpenPageForMutation not configured")
2123
}
2224

2325
// ---------------------------------------------------------------------------
@@ -28,7 +30,7 @@ func (m *MockBackend) OpenWorkflowForMutation(unitID model.ID) (backend.Workflow
2830
if m.OpenWorkflowForMutationFunc != nil {
2931
return m.OpenWorkflowForMutationFunc(unitID)
3032
}
31-
return nil, nil
33+
return nil, fmt.Errorf("MockBackend.OpenWorkflowForMutation not configured")
3234
}
3335

3436
// ---------------------------------------------------------------------------
@@ -94,3 +96,17 @@ func (m *MockBackend) BuildCreateAttributeObject(attributePath string, objectTyp
9496
}
9597
return nil, nil
9698
}
99+
100+
func (m *MockBackend) BuildDataGrid2Widget(id model.ID, name string, spec backend.DataGridSpec, projectPath string) (*pages.CustomWidget, error) {
101+
if m.BuildDataGrid2WidgetFunc != nil {
102+
return m.BuildDataGrid2WidgetFunc(id, name, spec, projectPath)
103+
}
104+
return nil, fmt.Errorf("MockBackend.BuildDataGrid2Widget not configured")
105+
}
106+
107+
func (m *MockBackend) BuildFilterWidget(spec backend.FilterWidgetSpec, projectPath string) (pages.Widget, error) {
108+
if m.BuildFilterWidgetFunc != nil {
109+
return m.BuildFilterWidgetFunc(spec, projectPath)
110+
}
111+
return nil, fmt.Errorf("MockBackend.BuildFilterWidget not configured")
112+
}

mdl/backend/mpr/backend.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,11 @@ type MprBackend struct {
3434
path string
3535
}
3636

37+
// New creates a new unconnected MprBackend. Call Connect(path) to open a project.
38+
func New() *MprBackend {
39+
return &MprBackend{}
40+
}
41+
3742
// Wrap creates an MprBackend that wraps an existing Writer (and its Reader).
3843
// This is used during migration when the Executor already owns the Writer
3944
// and we want to expose it through the Backend interface without opening
@@ -75,6 +80,11 @@ func (b *MprBackend) Disconnect() error {
7580
func (b *MprBackend) IsConnected() bool { return b.writer != nil }
7681
func (b *MprBackend) Path() string { return b.path }
7782

83+
// MprReader returns the underlying *mpr.Reader for callers that still
84+
// require direct SDK access (e.g. linter rules). Prefer Backend methods
85+
// for new code.
86+
func (b *MprBackend) MprReader() *mpr.Reader { return b.reader }
87+
7888
func (b *MprBackend) Version() types.MPRVersion { return convertMPRVersion(b.reader.Version()) }
7989
func (b *MprBackend) ProjectVersion() *types.ProjectVersion { return convertProjectVersion(b.reader.ProjectVersion()) }
8090
func (b *MprBackend) GetMendixVersion() (string, error) { return b.reader.GetMendixVersion() }

mdl/backend/mpr/convert.go

Lines changed: 3 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -263,36 +263,10 @@ func convertNavMenuItem(in *mpr.NavMenuItem) *types.NavMenuItem {
263263
// Conversion helpers: mdl/types -> sdk/mpr (for write methods)
264264
// ---------------------------------------------------------------------------
265265

266+
// unconvertNavProfileSpec is now a pass-through since mpr.NavigationProfileSpec
267+
// is aliased to types.NavigationProfileSpec.
266268
func unconvertNavProfileSpec(s types.NavigationProfileSpec) mpr.NavigationProfileSpec {
267-
out := mpr.NavigationProfileSpec{
268-
LoginPage: s.LoginPage,
269-
NotFoundPage: s.NotFoundPage,
270-
HasMenu: s.HasMenu,
271-
}
272-
if s.HomePages != nil {
273-
out.HomePages = make([]mpr.NavHomePageSpec, len(s.HomePages))
274-
for i, hp := range s.HomePages {
275-
out.HomePages[i] = mpr.NavHomePageSpec{IsPage: hp.IsPage, Target: hp.Target, ForRole: hp.ForRole}
276-
}
277-
}
278-
if s.MenuItems != nil {
279-
out.MenuItems = make([]mpr.NavMenuItemSpec, len(s.MenuItems))
280-
for i, mi := range s.MenuItems {
281-
out.MenuItems[i] = unconvertNavMenuItemSpec(mi)
282-
}
283-
}
284-
return out
285-
}
286-
287-
func unconvertNavMenuItemSpec(in types.NavMenuItemSpec) mpr.NavMenuItemSpec {
288-
out := mpr.NavMenuItemSpec{Caption: in.Caption, Page: in.Page, Microflow: in.Microflow}
289-
if in.Items != nil {
290-
out.Items = make([]mpr.NavMenuItemSpec, len(in.Items))
291-
for i, sub := range in.Items {
292-
out.Items[i] = unconvertNavMenuItemSpec(sub)
293-
}
294-
}
295-
return out
269+
return s
296270
}
297271

298272
func unconvertEntityMemberAccessSlice(in []types.EntityMemberAccess) []mpr.EntityMemberAccess {

mdl/backend/mpr/convert_roundtrip_test.go

Lines changed: 53 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package mprbackend
77

88
import (
99
"errors"
10+
"reflect"
1011
"testing"
1112

1213
"github.com/mendixlabs/mxcli/mdl/types"
@@ -492,7 +493,9 @@ func TestUnconvertNavMenuItemSpec_Isolated(t *testing.T) {
492493
{Caption: "Child", Microflow: "MF2"},
493494
},
494495
}
495-
out := unconvertNavMenuItemSpec(in)
496+
// Since mpr.NavMenuItemSpec is aliased to types.NavMenuItemSpec,
497+
// unconvert is now a pass-through. Verify the alias holds.
498+
var out mpr.NavMenuItemSpec = in
496499
if out.Caption != "Parent" || out.Page != "Page1" || out.Microflow != "MF1" {
497500
t.Errorf("field mismatch: %+v", out)
498501
}
@@ -503,7 +506,7 @@ func TestUnconvertNavMenuItemSpec_Isolated(t *testing.T) {
503506

504507
func TestUnconvertNavMenuItemSpec_NilItems(t *testing.T) {
505508
in := types.NavMenuItemSpec{Caption: "Leaf"}
506-
out := unconvertNavMenuItemSpec(in)
509+
var out mpr.NavMenuItemSpec = in
507510
if out.Items != nil {
508511
t.Errorf("expected nil Items for leaf: %+v", out.Items)
509512
}
@@ -598,3 +601,51 @@ func TestUnconvertImageCollection(t *testing.T) {
598601
}
599602
}
600603

604+
// ============================================================================
605+
// Field-count drift assertions
606+
// ============================================================================
607+
//
608+
// These tests catch silent field drift: if a struct gains a new field but
609+
// the convert/unconvert function is not updated, the test fails.
610+
611+
func assertFieldCount(t *testing.T, name string, v any, expected int) {
612+
t.Helper()
613+
actual := reflect.TypeOf(v).NumField()
614+
if actual != expected {
615+
t.Errorf("%s field count changed: expected %d, got %d — update convert.go and this test", name, expected, actual)
616+
}
617+
}
618+
619+
func TestFieldCountDrift(t *testing.T) {
620+
// mpr → types pairs (manually copied in convert.go).
621+
// If a struct gains a field, update the convert function AND this count.
622+
assertFieldCount(t, "mpr.FolderInfo", mpr.FolderInfo{}, 3)
623+
assertFieldCount(t, "types.FolderInfo", types.FolderInfo{}, 3)
624+
assertFieldCount(t, "mpr.UnitInfo", mpr.UnitInfo{}, 4)
625+
assertFieldCount(t, "types.UnitInfo", types.UnitInfo{}, 4)
626+
assertFieldCount(t, "mpr.RenameHit", mpr.RenameHit{}, 4)
627+
assertFieldCount(t, "types.RenameHit", types.RenameHit{}, 4)
628+
assertFieldCount(t, "mpr.RawUnit", mpr.RawUnit{}, 4)
629+
assertFieldCount(t, "types.RawUnit", types.RawUnit{}, 4)
630+
assertFieldCount(t, "mpr.RawUnitInfo", mpr.RawUnitInfo{}, 5)
631+
assertFieldCount(t, "types.RawUnitInfo", types.RawUnitInfo{}, 5)
632+
assertFieldCount(t, "mpr.RawCustomWidgetType", mpr.RawCustomWidgetType{}, 6)
633+
assertFieldCount(t, "types.RawCustomWidgetType", types.RawCustomWidgetType{}, 6)
634+
assertFieldCount(t, "mpr.JavaAction", mpr.JavaAction{}, 4)
635+
assertFieldCount(t, "types.JavaAction", types.JavaAction{}, 4)
636+
assertFieldCount(t, "mpr.JavaScriptAction", mpr.JavaScriptAction{}, 12)
637+
assertFieldCount(t, "types.JavaScriptAction", types.JavaScriptAction{}, 12)
638+
assertFieldCount(t, "mpr.NavigationDocument", mpr.NavigationDocument{}, 4)
639+
assertFieldCount(t, "types.NavigationDocument", types.NavigationDocument{}, 4)
640+
assertFieldCount(t, "mpr.JsonStructure", mpr.JsonStructure{}, 8)
641+
assertFieldCount(t, "types.JsonStructure", types.JsonStructure{}, 8)
642+
assertFieldCount(t, "mpr.JsonElement", mpr.JsonElement{}, 14)
643+
assertFieldCount(t, "types.JsonElement", types.JsonElement{}, 14)
644+
assertFieldCount(t, "mpr.ImageCollection", mpr.ImageCollection{}, 6)
645+
assertFieldCount(t, "types.ImageCollection", types.ImageCollection{}, 6)
646+
assertFieldCount(t, "mpr.EntityMemberAccess", mpr.EntityMemberAccess{}, 3)
647+
assertFieldCount(t, "types.EntityMemberAccess", types.EntityMemberAccess{}, 3)
648+
assertFieldCount(t, "mpr.EntityAccessRevocation", mpr.EntityAccessRevocation{}, 6)
649+
assertFieldCount(t, "types.EntityAccessRevocation", types.EntityAccessRevocation{}, 6)
650+
}
651+

0 commit comments

Comments
 (0)