Skip to content

Commit b8499d5

Browse files
committed
fix: address PR mendixlabs#335 review feedback
- Remove [perf] log noise from Store.ListUnits (#1) - Fix FindByQualifiedName docs: clarify it matches simple Name, not qualified (#2) - Fix concurrency docs: "not safe for concurrent mutation" (#3) - Add TODO for WriteTransaction.Commit partial-failure window (#4) - Add TODO for PatchEncoded escape-hatch methods (#5) - Add path traversal guard in DeleteModuleWithCleanup (#6) - Add package doc.go with architecture overview and migration guidance (#7)
1 parent 637565e commit b8499d5

4 files changed

Lines changed: 59 additions & 16 deletions

File tree

modelsdk/codec/store.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package codec
22

33
import (
44
"fmt"
5-
"log"
65
"time"
76

87
"github.com/mendixlabs/mxcli/modelsdk/element"
@@ -87,7 +86,7 @@ func (s *Store) ListUnits() []UnitInfo {
8786
Name: name,
8887
})
8988
}
90-
log.Printf("[perf] Store.ListUnits: %s (units=%d)", time.Since(listStart), len(units))
89+
_ = listStart // reserved for future debug logging
9190
return units
9291
}
9392

modelsdk/doc.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Package modelsdk provides auto-generated, type-safe access to Mendix MPR files.
2+
//
3+
// modelsdk is designed as a replacement for the hand-written sdk/ package.
4+
// It coexists with sdk/ during migration — no existing code is modified.
5+
//
6+
// # Choosing between modelsdk/ and sdk/
7+
//
8+
// Use modelsdk/ for new code that needs:
9+
// - Dirty tracking (know what changed before writing)
10+
// - BSON roundtrip safety (unimplemented fields survive read/write)
11+
// - Coverage beyond the 5-10 domains in sdk/ (53 domains here)
12+
//
13+
// Use sdk/ for existing code that already works — migrate incrementally.
14+
//
15+
// # Architecture
16+
//
17+
// The package has three layers:
18+
//
19+
// - element.Base: identity, raw BSON bytes, dirty bitmap with container chain propagation
20+
// - property.Primitive/Part/PartList/Enum/ByNameRef: lazy-decoded fields with per-bit dirty tracking
21+
// - codec.Decoder/Encoder: type registry dispatch with three-branch encoding (clean/child-dirty/self-dirty)
22+
//
23+
// # Generated code
24+
//
25+
// The modelsdk/gen/ subdirectory contains auto-generated types for all 53 Mendix
26+
// metamodel domains. To regenerate after updating the TypeScript SDK reference:
27+
//
28+
// npm install mendixmodelsdk --prefix reference/mendixmodelsdk
29+
// go run ./cmd/modelsdk-codegen
30+
//
31+
// Each generated domain package contains:
32+
// - types.go: struct definitions, getters/setters, InitFromRaw, registry init()
33+
// - enums.go: enumeration type aliases and constants
34+
// - refs.go: cross-domain reference metadata registration
35+
// - version.go: per-property introduced/deleted/public version info
36+
package modelsdk

modelsdk/model.go

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func (m *Model) Units() []codec.UnitInfo {
8383
}
8484

8585
// LoadUnit loads and decodes a single unit by ID. Results are cached.
86-
// Safe for concurrent use.
86+
// Not safe for concurrent mutation — use from a single goroutine.
8787
func (m *Model) LoadUnit(id element.ID) (element.Element, error) {
8888
if elem, ok := m.uc.Get(id); ok {
8989
return elem, nil
@@ -104,7 +104,7 @@ func (m *Model) LoadUnit(id element.ID) (element.Element, error) {
104104

105105
// AllOfType loads and returns all units whose $Type matches the given type name.
106106
// Uses UnitInfo.Type metadata to pre-filter, avoiding BSON I/O for non-matching units.
107-
// Safe for concurrent use.
107+
// Not safe for concurrent mutation — use from a single goroutine.
108108
func (m *Model) AllOfType(typeName string) []element.Element {
109109
var result []element.Element
110110
currentTypeGen := m.uc.TypeGen(typeName)
@@ -154,12 +154,13 @@ func (m *Model) AllOfType(typeName string) []element.Element {
154154
return result
155155
}
156156

157-
// FindByQualifiedName searches for a unit of the given type whose
158-
// "Name" property matches qualifiedName. Uses the name index for O(1)
159-
// lookup, falling back to a linear scan if the index misses.
160-
func (m *Model) FindByQualifiedName(typeName, qualifiedName string) (element.Element, error) {
157+
// FindByQualifiedName searches for a unit of the given type whose BSON
158+
// "Name" field matches name. Note: Mendix stores the module-local simple
159+
// name (e.g. "ACT_GetUser"), not the dot-qualified name ("MyModule.ACT_GetUser").
160+
// Uses the name index for O(1) lookup, falling back to a linear scan if the index misses.
161+
func (m *Model) FindByQualifiedName(typeName, name string) (element.Element, error) {
161162
// Fast path: index lookup.
162-
if id, ok := m.uc.FindByName(typeName + ":" + qualifiedName); ok {
163+
if id, ok := m.uc.FindByName(typeName + ":" + name); ok {
163164
return m.LoadUnit(id)
164165
}
165166

@@ -175,8 +176,8 @@ func (m *Model) FindByQualifiedName(typeName, qualifiedName string) (element.Ele
175176
if u.Type == "" && decodeTypeField(raw) != typeName {
176177
continue
177178
}
178-
name, _ := raw.LookupErr("Name")
179-
if s, ok := name.StringValueOK(); ok && s == qualifiedName {
179+
bsonName, _ := raw.LookupErr("Name")
180+
if s, ok := bsonName.StringValueOK(); ok && s == name {
180181
elem, err := m.decoder.Decode(raw)
181182
if err != nil {
182183
return nil, fmt.Errorf("decode unit %s: %w", u.ID, err)
@@ -185,7 +186,7 @@ func (m *Model) FindByQualifiedName(typeName, qualifiedName string) (element.Ele
185186
return elem, nil
186187
}
187188
}
188-
return nil, fmt.Errorf("element %s with name %q not found", typeName, qualifiedName)
189+
return nil, fmt.Errorf("element %s with name %q not found", typeName, name)
189190
}
190191

191192
// Encode serializes an element to BSON bytes.
@@ -238,7 +239,8 @@ func (m *Model) Store() *codec.Store { return m.store }
238239
// PatchEncodedField sets a top-level field on already-encoded BSON bytes.
239240
// Use this only when the SDK type's setter has a different type than the
240241
// BSON storage format (e.g., SDK uses Part[element.Element] but BSON stores
241-
// a plain string). This is a last-resort escape hatch.
242+
// a plain string).
243+
// TODO: remove when generated setters cover all BSON storage type mismatches.
242244
func (m *Model) PatchEncodedField(data []byte, key string, value any) ([]byte, error) {
243245
return codec.PatchBSONField(data, key, value)
244246
}
@@ -342,9 +344,13 @@ func (m *Model) DeleteModuleWithCleanup(moduleID element.ID, moduleName string)
342344

343345
// Clean up themesource directory.
344346
projectDir := filepath.Dir(m.store.Path())
345-
themesourceDir := filepath.Join(projectDir, "themesource", strings.ToLower(moduleName))
346-
if stat, err := os.Stat(themesourceDir); err == nil && stat.IsDir() {
347-
os.RemoveAll(themesourceDir)
347+
themesourceBase := filepath.Join(projectDir, "themesource")
348+
themesourceDir := filepath.Clean(filepath.Join(themesourceBase, strings.ToLower(moduleName)))
349+
// Guard against path traversal: the resolved path must be under themesource/.
350+
if strings.HasPrefix(themesourceDir, themesourceBase+string(filepath.Separator)) {
351+
if stat, err := os.Stat(themesourceDir); err == nil && stat.IsDir() {
352+
os.RemoveAll(themesourceDir)
353+
}
348354
}
349355

350356
return nil

modelsdk/mpr/writer_core.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ func (wt *WriteTransaction) WriteUnit(unitID string, contents []byte) error {
157157

158158
// Commit commits the transaction.
159159
// For v2, this first commits the database, then finalizes file writes.
160+
// TODO: adopt two-phase approach (rename files first, then commit DB) to
161+
// eliminate the partial-failure window where DB is committed but files are stale.
160162
func (wt *WriteTransaction) Commit() error {
161163
if wt.committed {
162164
return fmt.Errorf("transaction already committed")

0 commit comments

Comments
 (0)