Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion cmd/mxcli/cmd_bson_dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ Examples:

# Save dump to file
mxcli bson dump -p app.mpr --type page --object "PgTest.MyPage" > mypage.json

# Extract raw BSON baseline for roundtrip testing
mxcli bson dump -p app.mpr --type page --object "PgTest.MyPage" --format bson > mypage.mxunit
`,
Run: func(cmd *cobra.Command, args []string) {
projectPath, _ := cmd.Flags().GetString("project")
Expand Down Expand Up @@ -136,6 +139,12 @@ Examples:
os.Exit(1)
}

if format == "bson" {
// Write raw BSON bytes to stdout (for baseline extraction)
os.Stdout.Write(obj.Contents)
return
}

if format == "ndsl" {
var doc bson.D
if err := bson.Unmarshal(obj.Contents, &doc); err != nil {
Expand Down Expand Up @@ -342,5 +351,5 @@ func init() {
bsonDumpCmd.Flags().StringP("object", "o", "", "Object qualified name to dump (e.g., Module.PageName)")
bsonDumpCmd.Flags().BoolP("list", "l", false, "List all objects of the specified type")
bsonDumpCmd.Flags().StringSliceP("compare", "c", nil, "Compare two objects: --compare Obj1,Obj2")
bsonDumpCmd.Flags().String("format", "json", "Output format: json, ndsl")
bsonDumpCmd.Flags().String("format", "json", "Output format: json, ndsl, bson (raw bytes)")
}
91 changes: 91 additions & 0 deletions cmd/mxcli/diag.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"time"

"github.com/mendixlabs/mxcli/mdl/diaglog"
"github.com/mendixlabs/mxcli/sdk/mpr"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -47,6 +48,18 @@ Examples:
return
}

checkUnits, _ := cmd.Flags().GetBool("check-units")
fix, _ := cmd.Flags().GetBool("fix")
if checkUnits {
projectPath, _ := cmd.Flags().GetString("project")
if projectPath == "" {
fmt.Fprintln(os.Stderr, "Error: --check-units requires -p <project.mpr>")
os.Exit(1)
}
runCheckUnits(projectPath, fix)
return
}

if tail > 0 {
runDiagTail(logDir, tail)
return
Expand All @@ -60,6 +73,8 @@ func init() {
diagCmd.Flags().Bool("log-path", false, "Print log directory path")
diagCmd.Flags().Bool("bundle", false, "Create tar.gz with logs for bug reports")
diagCmd.Flags().Int("tail", 0, "Show last N log entries")
diagCmd.Flags().Bool("check-units", false, "Check for orphan units and stale mxunit files (MPR v2)")
diagCmd.Flags().Bool("fix", false, "Auto-fix issues found by --check-units")
}

// runDiagInfo shows diagnostic summary.
Expand Down Expand Up @@ -252,3 +267,79 @@ func formatBytes(b int64) string {
}
return fmt.Sprintf("%d KB", b/1024)
}

// runCheckUnits checks for orphan units (Unit table entry without mxunit file)
// and stale mxunit files (file exists but no Unit table entry). MPR v2 only.
func runCheckUnits(mprPath string, fix bool) {
reader, err := mpr.Open(mprPath)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
defer reader.Close()

contentsDir := reader.ContentsDir()
if contentsDir == "" {
fmt.Println("Not an MPR v2 project (no mprcontents directory)")
return
}

// Build set of unit UUIDs from database
unitIDs, err := reader.ListAllUnitIDs()
if err != nil {
fmt.Fprintf(os.Stderr, "Error listing units: %v\n", err)
os.Exit(1)
}
unitSet := make(map[string]bool, len(unitIDs))
for _, id := range unitIDs {
unitSet[id] = true
}

// Scan mxunit files
files, err := filepath.Glob(filepath.Join(contentsDir, "*", "*", "*.mxunit"))
if err != nil {
fmt.Fprintf(os.Stderr, "Error scanning mxunit files: %v\n", err)
os.Exit(1)
}
fileSet := make(map[string]string, len(files)) // uuid → filepath
for _, f := range files {
uuid := strings.TrimSuffix(filepath.Base(f), ".mxunit")
fileSet[uuid] = f
}

// Check for orphan units (in DB but no file)
orphans := 0
for _, id := range unitIDs {
if _, ok := fileSet[id]; !ok {
fmt.Printf("ORPHAN UNIT: %s (in Unit table but no mxunit file)\n", id)
orphans++
}
}

// Check for stale files (file exists but not in DB)
stale := 0
for uuid, fpath := range fileSet {
if !unitSet[uuid] {
fmt.Printf("STALE FILE: %s\n", uuid)
stale++
if fix {
if err := os.Remove(fpath); err != nil {
fmt.Fprintf(os.Stderr, " ERROR removing: %v\n", err)
} else {
fmt.Printf(" REMOVED: %s\n", fpath)
// Clean empty parent dirs
dir2 := filepath.Dir(fpath)
os.Remove(dir2)
dir1 := filepath.Dir(dir2)
os.Remove(dir1)
}
}
}
}

fmt.Printf("\nSummary: %d units in DB, %d mxunit files, %d orphans, %d stale\n",
len(unitIDs), len(files), orphans, stale)
if stale > 0 && !fix {
fmt.Println("Run with --fix to auto-remove stale files")
}
}
25 changes: 16 additions & 9 deletions mdl/executor/cmd_pages_create_v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,16 +59,23 @@ func (e *Executor) execCreatePageV3(s *ast.CreatePageStmtV3) error {
return fmt.Errorf("failed to build page: %w", err)
}

// Delete old pages only after successful build
for _, id := range pagesToDelete {
if err := e.writer.DeletePage(id); err != nil {
return fmt.Errorf("failed to delete existing page: %w", err)
// Replace or create the page in the MPR
if len(pagesToDelete) > 0 {
// Reuse first existing page's UUID to avoid git delete+add (which crashes Studio Pro RevStatusCache)
page.ID = pagesToDelete[0]
if err := e.writer.UpdatePage(page); err != nil {
return fmt.Errorf("failed to update page: %w", err)
}
// Delete any additional duplicates
for _, id := range pagesToDelete[1:] {
if err := e.writer.DeletePage(id); err != nil {
return fmt.Errorf("failed to delete duplicate page: %w", err)
}
}
} else {
if err := e.writer.CreatePage(page); err != nil {
return fmt.Errorf("failed to create page: %w", err)
}
}

// Create the page in the MPR
if err := e.writer.CreatePage(page); err != nil {
return fmt.Errorf("failed to create page: %w", err)
}

// Track the created page so it can be resolved by subsequent page references
Expand Down
5 changes: 4 additions & 1 deletion mdl/grammar/MDLLexer.g4
Original file line number Diff line number Diff line change
Expand Up @@ -260,8 +260,9 @@ INPUTREFERENCESETSELECTOR: I N P U T R E F E R E N C E S E T S E L E C T O R;
FILEINPUT: F I L E I N P U T;
IMAGEINPUT: I M A G E I N P U T;

// Custom/Filter widgets
// Custom/Filter/Pluggable widgets
CUSTOMWIDGET: C U S T O M W I D G E T;
PLUGGABLEWIDGET: P L U G G A B L E W I D G E T;
TEXTFILTER: T E X T F I L T E R;
NUMBERFILTER: N U M B E R F I L T E R;
DROPDOWNFILTER: D R O P D O W N F I L T E R;
Expand Down Expand Up @@ -316,6 +317,8 @@ COLLECTION: C O L L E C T I O N;
STATICIMAGE: S T A T I C I M A G E;
DYNAMICIMAGE: D Y N A M I C I M A G E;
CUSTOMCONTAINER: C U S T O M C O N T A I N E R;
TABCONTAINER: T A B C O N T A I N E R;
TABPAGE: T A B P A G E;
GROUPBOX: G R O U P B O X;
VISIBLE: V I S I B L E;
SAVECHANGES: S A V E C H A N G E S;
Expand Down
28 changes: 20 additions & 8 deletions mdl/grammar/MDLParser.g4
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,8 @@ alterLayoutMapping
;

alterPageAssignment
: identifierOrKeyword EQUALS propertyValueV3 // Caption = 'Save'
: DATASOURCE EQUALS dataSourceExprV3 // DataSource = SELECTION widgetName
| identifierOrKeyword EQUALS propertyValueV3 // Caption = 'Save'
| STRING_LITERAL EQUALS propertyValueV3 // 'showLabel' = false
;

Expand Down Expand Up @@ -554,6 +555,7 @@ attributeName
: IDENTIFIER
| QUOTED_IDENTIFIER // Escape any reserved word ("Range", `Order`)
| commonNameKeyword
| ATTRIBUTE // Allow 'Attribute' as attribute name
;

attributeConstraint
Expand Down Expand Up @@ -596,7 +598,7 @@ attributeConstraint
* ```
*/
dataType
: STRING_TYPE (LPAREN NUMBER_LITERAL RPAREN)?
: STRING_TYPE (LPAREN (NUMBER_LITERAL | IDENTIFIER) RPAREN)?
| INTEGER_TYPE
| LONG_TYPE
| DECIMAL_TYPE
Expand Down Expand Up @@ -624,7 +626,7 @@ templateContext

// Non-list data type - used for createObjectStatement to avoid matching "CREATE LIST OF"
nonListDataType
: STRING_TYPE (LPAREN NUMBER_LITERAL RPAREN)?
: STRING_TYPE (LPAREN (NUMBER_LITERAL | IDENTIFIER) RPAREN)?
| INTEGER_TYPE
| LONG_TYPE
| DECIMAL_TYPE
Expand Down Expand Up @@ -665,16 +667,20 @@ createAssociationStatement
FROM qualifiedName
TO qualifiedName
associationOptions?
| ASSOCIATION qualifiedName LPAREN
FROM qualifiedName TO qualifiedName
(COMMA associationOption)*
RPAREN
;

associationOptions
: associationOption+
;

associationOption
: TYPE (REFERENCE | REFERENCE_SET)
| OWNER (DEFAULT | BOTH)
| STORAGE (COLUMN | TABLE)
: TYPE COLON? (REFERENCE | REFERENCE_SET)
| OWNER COLON? (DEFAULT | BOTH)
| STORAGE COLON? (COLUMN | TABLE)
| DELETE_BEHAVIOR deleteBehavior
| COMMENT STRING_LITERAL
;
Expand Down Expand Up @@ -771,6 +777,7 @@ enumValueName
| SERVICE | SERVICES // OData/auth keywords used as enum values
| GUEST | SESSION | BASIC | CLIENT | CLIENTS
| PUBLISH | EXPOSE | EXTERNAL | PAGING | HEADERS
| DISPLAY | STRUCTURE // Layout/structure keywords used as enum values
;

enumerationOptions
Expand Down Expand Up @@ -1780,6 +1787,8 @@ useFragmentRef
// V3 Widget: WIDGET name (Props) { children }
widgetV3
: widgetTypeV3 IDENTIFIER widgetPropertiesV3? widgetBodyV3?
| PLUGGABLEWIDGET STRING_LITERAL IDENTIFIER widgetPropertiesV3? widgetBodyV3? // PLUGGABLEWIDGET 'widget.id' name
| CUSTOMWIDGET STRING_LITERAL IDENTIFIER widgetPropertiesV3? widgetBodyV3? // CUSTOMWIDGET 'widget.id' name (legacy)
;

// V3 Widget types (same as V2)
Expand Down Expand Up @@ -1822,6 +1831,8 @@ widgetTypeV3
| STATICIMAGE
| DYNAMICIMAGE
| CUSTOMCONTAINER
| TABCONTAINER
| TABPAGE
| GROUPBOX
;

Expand Down Expand Up @@ -1862,6 +1873,7 @@ widgetPropertyV3
| EDITABLE COLON propertyValueV3 // Editable: Never | Always
| TOOLTIP COLON propertyValueV3 // Tooltip: 'text'
| IDENTIFIER COLON propertyValueV3 // Generic: any other property
| keyword COLON propertyValueV3 // Generic: keyword as property name (for pluggable widgets)
;

// Filter type values - handle keywords like CONTAINS that are also filter types
Expand Down Expand Up @@ -2573,7 +2585,7 @@ widgetTypeKeyword
| COMBOBOX | DYNAMICTEXT | ACTIONBUTTON | LINKBUTTON | DATAVIEW
| LISTVIEW | DATAGRID | GALLERY | LAYOUTGRID | IMAGE | STATICIMAGE
| DYNAMICIMAGE | HEADER | FOOTER | SNIPPETCALL | NAVIGATIONLIST
| CUSTOMCONTAINER | DROPDOWN | REFERENCESELECTOR | GROUPBOX
| CUSTOMCONTAINER | TABCONTAINER | TABPAGE | DROPDOWN | REFERENCESELECTOR | GROUPBOX
| IDENTIFIER
;

Expand Down Expand Up @@ -3228,7 +3240,7 @@ keyword
| ACTIONBUTTON | CHECKBOX | COMBOBOX | CONTROLBAR | DATAGRID | DATAVIEW // Widget keywords
| DATEPICKER | DYNAMICTEXT | GALLERY | LAYOUTGRID | LINKBUTTON | LISTVIEW
| NAVIGATIONLIST | RADIOBUTTONS | SEARCHBAR | SNIPPETCALL | TEXTAREA | TEXTBOX
| IMAGE | STATICIMAGE | DYNAMICIMAGE | CUSTOMCONTAINER | GROUPBOX
| IMAGE | STATICIMAGE | DYNAMICIMAGE | CUSTOMCONTAINER | TABCONTAINER | TABPAGE | GROUPBOX
| HEADER | FOOTER | IMAGEINPUT
| VERSION | TIMEOUT | PATH | PUBLISH | PUBLISHED | EXPOSE | NAMESPACE_KW | SOURCE_KW | CONTRACT | CHANNELS | MESSAGES // OData/AsyncAPI keywords
| SESSION | GUEST | BASIC | AUTHENTICATION | ODATA | SERVICE | CLIENT | CLIENTS | SERVICES
Expand Down
11 changes: 10 additions & 1 deletion mdl/grammar/parser/MDLLexer.interp

Large diffs are not rendered by default.

Loading