Extract shared types and mutation backends from sdk/mpr#233
Extract shared types and mutation backends from sdk/mpr#233retran wants to merge 6 commits intomendixlabs:mainfrom
Conversation
Phase 1-8: 85 read/write handler tests with mock backend infrastructure Error paths: 49 tests covering backend error propagation for all handler groups Not-connected: 29 tests verifying Connected/ConnectedForWrite guards JSON format: 26 tests validating JSON output for show/list handlers Production code changes: - Added Func fields to MockBackend for 8 agent-editor write methods - Fixed ContainerID parameter semantics in withContainer and all call sites
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
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
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
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
…naming consistency 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.
|
Splitting into stacked PRs for reviewability — diff exceeded GitHub's 20k line limit. |
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR decouples the MDL executor and backend interfaces from sdk/mpr by moving shared domain types into mdl/types and introducing backend mutation/builder interfaces so handlers no longer manipulate raw BSON directly.
Changes:
- Replace direct
sdk/mprtype usage across executor/catalog/backend withmdl/types. - Route page/workflow/widget mutations through backend mutator/builder interfaces (e.g.,
OpenPageForMutation,WidgetBuilderBackend). - Add extensive mock-backend based executor tests and introduce CGO-free BSON ID conversion helpers (
mdl/bsonutil).
Reviewed changes
Copilot reviewed 112 out of 155 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| mdl/executor/cmd_widgets.go | Switch widget property updates to PageMutator instead of raw BSON. |
| mdl/executor/cmd_structure.go | Read constants/scheduled events counts via backend instead of reader. |
| mdl/executor/cmd_settings_mock_test.go | Add mock-backend tests for settings output. |
| mdl/executor/cmd_security_write.go | Migrate security write path to mdl/types and adjust error wrapping. |
| mdl/executor/cmd_security_mock_test.go | Add mock-backend tests for security SHOW/DESCRIBE commands. |
| mdl/executor/cmd_rest_clients_mock_test.go | Add mock-backend tests for REST clients. |
| mdl/executor/cmd_rename.go | Replace mpr.RenameHit usage with types.RenameHit. |
| mdl/executor/cmd_published_rest_mock_test.go | Add mock-backend tests for published REST services. |
| mdl/executor/cmd_pages_mock_test.go | Add mock-backend tests for pages/snippets/layouts listing. |
| mdl/executor/cmd_pages_create_v3.go | Build pages/snippets via injected backend + widget backend. |
| mdl/executor/cmd_pages_builder_v3_pluggable.go | Remove BSON-heavy pluggable widget builder implementation. |
| mdl/executor/cmd_pages_builder_v3_layout.go | Generate IDs via types.GenerateID() instead of mpr.GenerateID(). |
| mdl/executor/cmd_pages_builder_input_filters.go | Remove BSON filter widget building (moved behind backend builder). |
| mdl/executor/cmd_pages_builder_input_cloning.go | Remove BSON deep-cloning helpers (moved behind backend). |
| mdl/executor/cmd_pages_builder_input.go | Replace reader-based listing with backend-based listing for snippets/microflows. |
| mdl/executor/cmd_pages_builder.go | Refactor pageBuilder to use backend.FullBackend + widget backend abstraction. |
| mdl/executor/cmd_odata_mock_test.go | Add mock-backend tests for OData clients/services. |
| mdl/executor/cmd_odata.go | Use types.GenerateID / types.ParseEdmx to avoid sdk/mpr dependency. |
| mdl/executor/cmd_notconnected_mock_test.go | Add mock-backend “not connected” tests for many handlers. |
| mdl/executor/cmd_navigation_mock_test.go | Add mock-backend tests for navigation output. |
| mdl/executor/cmd_navigation.go | Switch navigation types/specs from mpr to types. |
| mdl/executor/cmd_modules_mock_test.go | Add mock-backend test for SHOW MODULES. |
| mdl/executor/cmd_misc_mock_test.go | Add mock-backend test for SHOW VERSION via types.ProjectVersion. |
| mdl/executor/cmd_microflows_mock_test.go | Add mock-backend tests for microflows/nanoflows show/describe. |
| mdl/executor/cmd_microflows_helpers.go | Switch enum lookup to backend APIs. |
| mdl/executor/cmd_microflows_create.go | Generate IDs via types.GenerateID and pass backend to flow builder. |
| mdl/executor/cmd_microflows_builder_workflow.go | Use types.GenerateID for workflow-related microflow objects. |
| mdl/executor/cmd_microflows_builder_graph.go | Use types.GenerateID for microflow graph objects. |
| mdl/executor/cmd_microflows_builder_flows.go | Use types.GenerateID and pass backend across builder clones. |
| mdl/executor/cmd_microflows_builder_control.go | Use types.GenerateID and pass backend into nested builders. |
| mdl/executor/cmd_microflows_builder_annotations.go | Use types.GenerateID for annotation objects/flows. |
| mdl/executor/cmd_microflows_builder.go | Replace reader *mpr.Reader with backend backend.FullBackend. |
| mdl/executor/cmd_mermaid_mock_test.go | Add mock-backend test for Mermaid domain model output. |
| mdl/executor/cmd_lint.go | Set linter reader through e.Reader() accessor. |
| mdl/executor/cmd_jsonstructures_mock_test.go | Add mock-backend tests for JSON structures. |
| mdl/executor/cmd_jsonstructures.go | Move JSON helpers/element types to mdl/types. |
| mdl/executor/cmd_javascript_actions_mock_test.go | Add mock-backend tests for JavaScript actions. |
| mdl/executor/cmd_javaactions_mock_test.go | Add mock-backend tests for Java actions. |
| mdl/executor/cmd_javaactions.go | Generate action/type IDs via types.GenerateID. |
| mdl/executor/cmd_import_mappings_mock_test.go | Add mock-backend test for import mappings. |
| mdl/executor/cmd_import_mappings.go | Move JSON structure element types to types and pass backend into builders. |
| mdl/executor/cmd_imagecollections_mock_test.go | Add mock-backend tests for image collections. |
| mdl/executor/cmd_imagecollections.go | Switch image collection model types from mpr to types. |
| mdl/executor/cmd_fragments_mock_test.go | Add tests for fragment registry output. |
| mdl/executor/cmd_folders.go | Switch folder info types from mpr to types. |
| mdl/executor/cmd_features.go | Update comment to reflect “not connected” behavior. |
| mdl/executor/cmd_export_mappings_mock_test.go | Add mock-backend test for export mappings. |
| mdl/executor/cmd_export_mappings.go | Move JSON structure element types to types and pass backend into builders. |
| mdl/executor/cmd_enumerations_mock_test.go | Refactor enumeration tests to use mock ctx helpers and add describe tests. |
| mdl/executor/cmd_entities_mock_test.go | Add mock-backend tests for entities. |
| mdl/executor/cmd_entities.go | Use types.GenerateID and adjust error wrapping for calculated microflows. |
| mdl/executor/cmd_diff_local.go | Parse microflows from raw via backend rather than sdk/mpr. |
| mdl/executor/cmd_dbconnection_mock_test.go | Add mock-backend tests for database connections. |
| mdl/executor/cmd_datatransformer_mock_test.go | Add mock-backend tests for data transformers. |
| mdl/executor/cmd_contract.go | Move contract parsing/types to mdl/types and use types.GenerateID/ParseEdmx/ParseAsyncAPI. |
| mdl/executor/cmd_constants_mock_test.go | Add mock-backend tests for constants. |
| mdl/executor/cmd_catalog.go | Update parallel describe comments and wire local executor with backend. |
| mdl/executor/cmd_businessevents_mock_test.go | Add mock-backend tests for business event services. |
| mdl/executor/cmd_businessevents.go | Generate channel names via types.GenerateID. |
| mdl/executor/cmd_associations_mock_test.go | Add mock-backend tests for associations. |
| mdl/executor/cmd_agenteditor_mock_test.go | Add mock-backend tests for Agent Editor documents. |
| mdl/executor/bugfix_test.go | Update flowBuilder test to refer to nil backend instead of reader. |
| mdl/executor/bugfix_regression_test.go | Update comment to refer to “backend” instead of “reader”. |
| mdl/catalog/builder_references.go | Switch navigation menu item types from mpr to types. |
| mdl/catalog/builder_navigation.go | Switch navigation menu item types from mpr to types. |
| mdl/catalog/builder_contract.go | Use types.ParseEdmx/ParseAsyncAPI instead of sdk/mpr. |
| mdl/catalog/builder.go | Replace mpr-typed catalog reader payloads with types equivalents. |
| mdl/bsonutil/bsonutil.go | Add CGO-free UUID<->BSON binary conversion via mdl/types. |
| mdl/backend/workflow.go | Remove unrelated interfaces (moved to infrastructure.go) and drop sdk/mpr dependency. |
| mdl/backend/security.go | Replace mpr member access/revocation types with types. |
| mdl/backend/page.go | Clarify snippet listing comment (no GetSnippet). |
| mdl/backend/navigation.go | Switch navigation backend types/specs to types. |
| mdl/backend/mpr/datagrid_builder_test.go | Move test package to mprbackend and use bsonutil for IDs. |
| mdl/backend/mpr/convert.go | Add conversion helpers between sdk/mpr and mdl/types. |
| mdl/backend/mock/mock_workflow.go | Switch image collection types to types. |
| mdl/backend/mock/mock_security.go | Switch entity access revocation type to types. |
| mdl/backend/mock/mock_navigation.go | Switch navigation types/specs to types. |
| mdl/backend/mock/mock_mutation.go | Add mock implementations for mutation + widget builder/serialization backends. |
| mdl/backend/mock/mock_module.go | Switch folder info type to types. |
| mdl/backend/mock/mock_microflow.go | Add mock hook for ParseMicroflowFromRaw. |
| mdl/backend/mock/mock_mapping.go | Switch JSON structure types to types. |
| mdl/backend/mock/mock_java.go | Switch Java/JS action types to types. |
| mdl/backend/mock/mock_infrastructure.go | Switch raw unit/widget/rename payload types to types and flesh out AgentEditor create/delete hooks. |
| mdl/backend/mock/mock_connection.go | Switch version/project version types to types. |
| mdl/backend/mock/backend.go | Update MockBackend function signatures for types + add mutation/builder function hooks. |
| mdl/backend/microflow.go | Add backend method ParseMicroflowFromRaw. |
| mdl/backend/mapping.go | Switch JSON structure API to types and document ID string convention. |
| mdl/backend/java.go | Switch Java/JS action list/read types to types. |
| mdl/backend/infrastructure.go | Switch raw unit/rename/widget payload types to types and re-home settings/image/scheduled event interfaces. |
| mdl/backend/doc.go | Update package docs to reflect mdl/types decoupling + conversions in mpr backend. |
| mdl/backend/connection.go | Switch connection/module/folder interfaces to types and add clarifying docs. |
| mdl/backend/backend.go | Extend FullBackend to include mutation/serialization/builder interfaces. |
| examples/create_datagrid2_page/main.go | Wire executor to a backend factory (mpr backend). |
| cmd/mxcli/project_tree.go | Switch menu tree node builder to accept types.NavMenuItem. |
| cmd/mxcli/main.go | Wire CLI executor to a backend factory (mpr backend). |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| 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) | ||
| } |
There was a problem hiding this comment.
This refactor drops the explicit container-type dispatch (and the dedicated unsupported-container error). As a result, invalid/unknown ContainerType values will now surface as a generic backend 'open ... for mutation' error, which is less actionable than the prior NewUnsupported. Consider reintroducing a containerType := strings.ToLower(widgetRefs[0].ContainerType) guard here (e.g., allow only page/snippet/layout) and return mdlerrors.NewUnsupported(...) for anything else.
| // 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) | ||
| } |
There was a problem hiding this comment.
In dry-run mode, this still opens the container 'for mutation'. If OpenPageForMutation takes write locks or requires write credentials, dry-run could become unexpectedly disruptive or fail in environments where reads are allowed but writes are not. If possible, add a read-only open path for dry-run (or extend OpenPageForMutation to support a non-locking/dry-run option) so dryRun=true doesn't require mutation semantics.
| // getProjectPath returns the project directory path from the underlying reader. | ||
| func (pb *pageBuilder) getProjectPath() string { | ||
| if pb.backend != nil { | ||
| return pb.backend.Path() | ||
| } | ||
| return "" | ||
| } |
There was a problem hiding this comment.
The doc comment references the 'underlying reader', but the implementation now uses pb.backend. Update the comment to match the new abstraction (e.g., 'from the backend').
| 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" | ||
| } |
There was a problem hiding this comment.
resolveAttributeType calls ListDomainModels() every time it's invoked. When building large import/export mappings (deep element trees), this can become an O(N) backend roundtrip hotspot, especially now that 'backend' may be remote or more expensive than a local reader. Prefer listing domain models once in the top-level handler and passing a cached lookup (e.g., entityQN+attrName → type) down through the recursive builder.
| out := buf.String() | ||
|
|
||
| // Verify table contains our enumeration data. | ||
| if !strings.Contains(out, "MyModule.Color") { | ||
| t.Errorf("expected qualified name 'MyModule.Color' in output, got:\n%s", out) | ||
| } | ||
| if !strings.Contains(out, "3") { | ||
| t.Errorf("expected value count '3' in output, got:\n%s", out) | ||
| } | ||
| if !strings.Contains(out, "(1 enumerations)") { | ||
| t.Errorf("expected summary '(1 enumerations)' in output, got:\n%s", out) | ||
| } | ||
| assertContainsStr(t, out, "MyModule.Color") | ||
| assertContainsStr(t, out, "| 3") | ||
| assertContainsStr(t, out, "(1 enumerations)") |
There was a problem hiding this comment.
The assertion \"| 3\" is tightly coupled to the table renderer's spacing/column formatting; small formatting changes could break the test without any behavioral regression. Consider asserting on a more stable signal (e.g., enumeration name + a substring like "3" in the same row, or switch the test to JSON output for strict structure).
| 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, | ||
| } | ||
| } |
There was a problem hiding this comment.
This introduces new ID<->BSON conversion behavior (including 'invalid UUID → generate a new random ID' fallback and a fixed binary subtype). Add unit tests to pin the intended behavior: valid UUID round-trip, invalid UUID fallback producing a 16-byte blob, and subtype expectations, so callers don't observe silent behavioral drift later.
Summary
sdk/mprintomdl/types/so backend interfaces no longer depend on the SDK packagePageMutator,WorkflowMutator,WidgetObjectBuilder,DataGrid2Builder) that encapsulate all BSON manipulation behind themdl/backendabstractionsdk/mprimports — the executor now operates entirely through backend interfaces*mpr.Writer/*mpr.Readerfields fromExecutor, injectBackendFactoryinsteadWhy
The executor was tightly coupled to
sdk/mpr— every handler imported BSON types and manipulated raw documents directly. This made it impossible to swap storage backends (e.g., for WASM compilation where CGO/BSON drivers are unavailable) and made handler testing require real MPR files.What changed
New packages:
mdl/types/— shared domain types (ID utilities, navigation, java, mapping, EDMX, AsyncAPI, JSON helpers)mdl/bsonutil/— CGO-free BSON binary conversion utilitiesBackend interfaces (
mdl/backend/mutation.go):PageMutator— all page/widget BSON manipulation (1554 lines of implementation)WorkflowMutator— workflow activity/branch BSON manipulation (771 lines)WidgetObjectBuilder/WidgetBuilderBackend— widget object construction (1007 lines)DataGrid2Builder— datagrid2 column/filter widget construction (1260 lines)Executor simplification:
Commits
mdl/typessdk/mprDepends on
PR #232 (
feature/mock-handler-tests) — this branch includes those commits. Merge #232 first, then rebase this PR.