@@ -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 .
8787func (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 .
108108func (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.
242244func (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
0 commit comments