Skip to content
Merged
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
1 change: 1 addition & 0 deletions .claude/skills/fix-issue.md
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ cases for these three BSON types — they fell to `default: return nil`.
| `sdk/mpr/parser_listoperation_test.go` | New file, 4 parser tests |
| `UPDATE SECURITY`, `CREATE ASSOCIATION` or any `GRANT` silently strips **inherited** members from a specialized entity's access rules; `GRANT` naming an inherited member reports success and persists nothing, so REVOKE+GRANT cannot repair it. `mx check` shows only CE0066, hiding the CE2729 "No read access to attribute" cascade until Studio Pro's Update security is clicked | Mendix inheritance is multi-table: all of a parent's attributes are members of the child, so an access rule needs a MemberAccess entry for every member, own **and** inherited, each qualified against the entity that **declares** it. Both the GRANT builder and `ReconcileMemberAccesses` enumerated only `entity.Attributes`, so an inherited reference matched nothing and was deleted as stale — and reconciliation runs **immediately after every GRANT**, deleting what the grant had just written correctly | `mdl/executor/entity_hierarchy.go` (`EntityMembers`), `mdl/executor/cmd_security_write.go` (`execGrantEntityAccess`), `mdl/backend/modelsdk/domainmodel_security_write.go` (`ReconcileMemberAccesses`, `attrRefBelongsTo`), `sdk/mpr/writer_security.go` (legacy engine) | Walk the generalization chain and qualify each member against its declaring entity; in the reconciler, only strip a reference qualified to **this** entity — an ancestor may live in another module or System, neither loaded there, so preserve what cannot be validated. **Two facts must be established against `mx check`, never inferred**: (a) the child-qualified form is CE1613 "attribute no longer exists" while the declaring-entity form validates clean; (b) `System.User`'s members are the exception — entities specialising it are *user entities* whose platform members Mendix manages, and listing them turns a clean rule into CE0066, while omitting `System.FileDocument`'s six members is CE0066 until all are present. **Generalisable**: when a post-write reconcile pass validates against a narrower model than the writer used, it will quietly undo correct writes — check what runs *after* a write before concluding the writer is at fault. Repro `mdl-examples/bug-tests/758-inherited-member-access.mdl`. Issues #758, #765 (umbrella; #451 is the same declaring-entity rule in the change-object writer) |
| `describe` (and `context` / `diff-local`) renders a Retrieve's XPath with only its **first** predicate group — `where A/B[EndDate = $X];` when the BSON holds `[A/B[EndDate = $X]][Status != 'Completed'][CompletionDate = empty]`. No warning; the output reads as a complete but materially *less restrictive* query, so correct defensive code looks buggy | The grammar's `xpathConstraint` rule matches ONE bracket group, and Mendix concatenates siblings. `ParseXPathConstraint` removes the error listeners, so ANTLR parsed group 1, left the rest on the token stream, and **still returned ok=true**; `enrichXPathConstraintForDescribe` treated that as a full parse and re-rendered only what came back. The `if !ok { return original }` fallback never fired | `mdl/visitor/visitor_xpath_public.go` (`ParseXPathConstraint`), `mdl/visitor/xpath_groups.go` (`SplitXPathPredicateGroups`), `mdl/executor/cmd_microflows_format_action.go` (`enrichXPathGroups`, and the render-path split) | Two layers. (1) Reject a partial parse — after the rule, require `stream.LA(1) == antlr.TokenEOF`; that alone stops the loss, since the caller then falls back to the stored string. (2) Split into top-level groups and enrich each, so enrichment still reaches groups after the first. The splitter must track **nesting depth and quoting**: a naive `][` split mangles a nested `[A/B[x = 1]]` and a literal containing `]`. **Generalisable**: a parser that silently accepts a prefix is worse than one that fails — any `ok` returned by a rule that can match less than its input must be checked against EOF before callers treat it as lossless. Repro `mdl-examples/bug-tests/772-xpath-constraint-groups.mdl`; A/B against a pre-fix binary on the same project shows the two dropped groups. Issue #772 |
| An import/export mapping over an entity created with `EXTENDS` maps only its **own** attributes; every inherited field shows unmapped in Studio Pro, and `mx check` reports CE1613 "The selected attribute 'Mod.Child.Attr' no longer exists". An inherited Boolean/DateTime element also gets `DataType=String` | The mapping builder prefixed the entity being mapped unconditionally (`attr = parentEntity + "." + attr`), but a member reference is qualified against the entity that **declares** it — the same rule as entity access rules (#758) and the change-object writer (#451). Separately `resolveAttributeType` scanned only the entity's own attributes and fell through to its `"String"` default | `mdl/executor/cmd_import_mappings.go` and `cmd_export_mappings.go` (both carry the same two lines), `mdl/executor/entity_hierarchy.go` (`ResolveMemberRef`, `ResolveMemberType`) | Route both sites through the generalization walk added for #758: `ResolveMemberRef` returns the declaring-entity reference and `ResolveMemberType` finds the type up the chain, each falling back to the old behaviour when the member cannot be resolved. **Watch for the sibling defect**: the old `resolveAttributeType` matched entities **by name across every domain model**, so a same-named entity in another module could win — resolve the module by name instead. **Generalisable**: when one rule has several call sites, a fix at one of them proves nothing about the others; grep for the *pattern* (`range entity.Attributes`, `parentEntity + "."`) rather than the reported symptom. Repro `mdl-examples/bug-tests/703-mapping-inherited-attributes.mdl`; A/B on the same project shows `Map703.Contract.DocName` (CE1613) become `Map703.DocumentBase.DocName`. Issue #703, umbrella #765 |

**Key insight:** `microflows$ListRange` stores offset/limit inside a nested
`CustomRange` map — must cast `raw["CustomRange"].(map[string]any)` before
Expand Down
35 changes: 34 additions & 1 deletion .claude/skills/mendix/json-structures-and-mappings.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,40 @@ A JSON structure defines the schema of a JSON payload. It stores a JSON snippet
### Import Mappings
An import mapping converts a JSON string into Mendix entity objects. It maps JSON fields to entity attributes.

### Export Mappings
#### Inherited attributes

Mendix inheritance is multi-table: all of a parent's attributes are members of the
child, so an entity created with `extends` can map them. Name an inherited
attribute exactly like one of the entity's own — mxcli resolves each to the entity
that **declares** it, which is the reference Studio Pro needs to show the field
mapped.

```sql
create persistent entity Docs.DocumentBase (
DocName: String(200),
Confidential: Boolean
);

create persistent entity Docs.Contract extends Docs.DocumentBase (
ContractNumber: String(50)
);

create import mapping Docs.IMM_Contract
with json structure Docs.JSON_Contract
{
create Docs.Contract {
ContractNumber = contractNumber, -- own
DocName = docName, -- inherited
Confidential = confidential -- inherited
}
};
```

Qualifying an inherited attribute against the entity being mapped instead of its
declaring entity is Mendix **CE1613** "The selected attribute ... no longer
exists", and the field shows unmapped in Studio Pro.

## Export Mappings
An export mapping converts Mendix entity objects into a JSON string. It maps entity attributes to JSON fields.

### Critical: Import and Export Need Different Domain Models
Expand Down
10 changes: 9 additions & 1 deletion cmd/mxcli/syntax/features_integration.go
Original file line number Diff line number Diff line change
Expand Up @@ -393,7 +393,15 @@ func init() {
"show import mappings", "describe import mapping",
"with json structure", "find or create", "object handling",
},
Syntax: "SHOW IMPORT MAPPINGS [IN Module];\nDESCRIBE IMPORT MAPPING Module.Name;\nCREATE [OR MODIFY] IMPORT MAPPING Module.Name\n WITH JSON STRUCTURE Module.JsonStruct\n{\n create|find|find or create Module.Entity {\n Attr = jsonField [KEY],\n Assoc/Module.Child = nestedKey { ... }\n }\n};\nDROP IMPORT MAPPING Module.Name;\n\nOR MODIFY: updates mapping in-place, preserves UUID.",
Syntax: "SHOW IMPORT MAPPINGS [IN Module];\nDESCRIBE IMPORT MAPPING Module.Name;\n" +
"CREATE [OR MODIFY] IMPORT MAPPING Module.Name\n WITH JSON STRUCTURE Module.JsonStruct\n{\n" +
" create|find|find or create Module.Entity {\n Attr = jsonField [KEY],\n" +
" Assoc/Module.Child = nestedKey { ... }\n }\n};\nDROP IMPORT MAPPING Module.Name;\n\n" +
"OR MODIFY: updates mapping in-place, preserves UUID.\n\n" +
"Inherited attributes:\n" +
" An entity mapped with EXTENDS can map its inherited attributes too —\n" +
" name them exactly like its own. mxcli resolves each to the entity that\n" +
" declares it, which is what Studio Pro needs to show the field mapped.",
Example: "CREATE IMPORT MAPPING Shop.IMM_Order\n WITH JSON STRUCTURE Shop.JSON_Order\n{\n create Shop.Order {\n OrderId = orderId KEY,\n TotalAmount = total\n }\n};\n\n-- Idempotent update\nCREATE OR MODIFY IMPORT MAPPING Shop.IMM_Order\n WITH JSON STRUCTURE Shop.JSON_Order\n{\n find or create Shop.Order {\n OrderId = orderId KEY,\n TotalAmount = total,\n Status = status\n }\n};",
SeeAlso: []string{"export-mapping", "json-structure"},
})
Expand Down
26 changes: 26 additions & 0 deletions docs-site/src/reference/integration/create-import-mapping.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,32 @@ CREATE OR MODIFY IMPORT MAPPING MyModule.IMM_Pet
- Arrays in the JSON sample map directly to child entity objects — there is no intermediate container entity (unlike export mappings).
- Import and export of the same JSON typically require different entity structures because of the FK direction difference.

## Inherited attributes

Mendix inheritance is multi-table: all of a parent's attributes are members of the
child, so an entity declared with `EXTENDS` can map them. Name an inherited
attribute exactly like one of the entity's own; mxcli resolves each to the entity
that **declares** it.

```sql
CREATE PERSISTENT ENTITY Docs.DocumentBase (DocName: String(200), Confidential: Boolean);
CREATE PERSISTENT ENTITY Docs.Contract EXTENDS Docs.DocumentBase (ContractNumber: String(50));

CREATE IMPORT MAPPING Docs.IMM_Contract
WITH JSON STRUCTURE Docs.JSON_Contract
{
create Docs.Contract {
ContractNumber = contractNumber,
DocName = docName,
Confidential = confidential
}
};
```

Referencing an inherited attribute against the entity being mapped rather than its
declaring entity is Mendix **CE1613** *"The selected attribute ... no longer
exists"*, and Studio Pro shows the field unmapped.

## See Also

[CREATE JSON STRUCTURE](create-json-structure.md), [CREATE EXPORT MAPPING](create-export-mapping.md)
73 changes: 73 additions & 0 deletions mdl-examples/bug-tests/703-mapping-inherited-attributes.mdl
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
-- Bug #703: import/export mappings silently skipped inherited attributes
--
-- Mendix inheritance is multi-table: a child adds attributes to its parent's, and
-- all the parent's are members of the child. A mapping element bound to one of
-- them must reference the entity that DECLARES it — `Map703.DocumentBase.DocName`,
-- not `Map703.Contract.DocName`.
--
-- mxcli prefixed the entity being mapped, unconditionally:
--
-- attr := def.Attribute
-- if parentEntity != "" && !strings.Contains(attr, ".") {
-- attr = parentEntity + "." + attr // always the CHILD
-- }
--
-- so every inherited field produced a reference to an attribute that does not
-- exist on that entity. In Studio Pro the field shows as unmapped — the reported
-- symptom — and `mx check` reports CE1613 "The selected attribute ... no longer
-- exists".
--
-- A second, quieter defect sat alongside it: resolveAttributeType scanned only the
-- entity's own attributes and fell through to a "String" default, so an inherited
-- Boolean or DateTime element got the wrong DataType even once the reference was
-- right. (That function also matched entities by name across every domain model,
-- ignoring the module, so a same-named entity elsewhere could win; it now resolves
-- the module by name.)
--
-- This is the mapping half of the #765 umbrella. The same declaring-entity rule
-- governs entity access rules (#758) and the change-object writer (#451).
--
-- Verify:
-- 1. mxcli exec 703-mapping-inherited-attributes.mdl -p app.mpr
-- 2. mxcli docker check -p app.mpr -> 0 errors (no CE1613)
-- 3. Dump the mapping unit: the inherited elements must read
-- Attribute=Map703.DocumentBase.DocName DataType=DataTypes$StringType
-- Attribute=Map703.DocumentBase.Confidential DataType=DataTypes$BooleanType
-- Before the fix both read Map703.Contract.* and both were StringType.
-- 4. Open the mapping in Studio Pro: all three fields are mapped.

create module Map703;

create json structure Map703.JSON_Contract
snippet '{"docName": "NDA", "confidential": true, "contractNumber": "C-1"}';

-- Parent supplies DocName (String) and Confidential (Boolean).
create persistent entity Map703.DocumentBase (
DocName: String(200),
Confidential: Boolean
);

-- Child adds its own; inherits the two above.
create persistent entity Map703.Contract extends Map703.DocumentBase (
ContractNumber: String(50)
);

create import mapping Map703.IMM_Contract
with json structure Map703.JSON_Contract
{
create Map703.Contract {
ContractNumber = contractNumber,
DocName = docName,
Confidential = confidential
}
};

create export mapping Map703.EMM_Contract
with json structure Map703.JSON_Contract
{
Map703.Contract {
contractNumber = ContractNumber,
docName = DocName,
confidential = Confidential
}
};
3 changes: 2 additions & 1 deletion mdl-examples/doctype-tests/08-security-examples.mdl
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,8 @@ create module SecTest;
create persistent entity SecTest.Customer (
Name: string(200) not null,
Email: string(200),
IsActive: boolean default true
IsActive: boolean default true,
Notes: string(500)
);

@position(300,100)
Expand Down
11 changes: 10 additions & 1 deletion mdl/executor/cmd_export_mappings.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,9 +365,18 @@ func buildExportMappingElementModel(moduleName string, def *ast.ExportMappingEle
elem.Kind = "Value"
elem.TypeName = "ExportMappings$ValueMappingElement"
elem.DataType = resolveAttributeType(parentEntity, def.Attribute, b)
// A member reference is qualified against the entity that DECLARES it, so
// an inherited attribute carries an ancestor's name. Prefixing the entity
// being mapped produced CE1613 "The selected attribute no longer exists"
// and left the field unmapped in Studio Pro (mendixlabs/mxcli#703) — the
// same rule as entity access rules (#758), both under the #765 umbrella.
attr := def.Attribute
if parentEntity != "" && !strings.Contains(attr, ".") {
attr = parentEntity + "." + attr
if ref, ok := ResolveMemberRef(b, parentEntity, attr); ok {
attr = ref
} else {
attr = parentEntity + "." + attr
}
}
elem.Attribute = attr
// JsonPath already set from JSON structure clone above
Expand Down
23 changes: 22 additions & 1 deletion mdl/executor/cmd_import_mappings.go
Original file line number Diff line number Diff line change
Expand Up @@ -341,9 +341,18 @@ func buildImportMappingElementModel(moduleName string, def *ast.ImportMappingEle
elem.TypeName = "ImportMappings$ValueMappingElement"
elem.DataType = resolveAttributeType(parentEntity, def.Attribute, b)
elem.IsKey = def.IsKey
// A member reference is qualified against the entity that DECLARES it, so
// an inherited attribute carries an ancestor's name. Prefixing the entity
// being mapped produced CE1613 "The selected attribute no longer exists"
// and left the field unmapped in Studio Pro (mendixlabs/mxcli#703) — the
// same rule as entity access rules (#758), both under the #765 umbrella.
attr := def.Attribute
if parentEntity != "" && !strings.Contains(attr, ".") {
attr = parentEntity + "." + attr
if ref, ok := ResolveMemberRef(b, parentEntity, attr); ok {
attr = ref
} else {
attr = parentEntity + "." + attr
}
}
elem.Attribute = attr
}
Expand Down Expand Up @@ -372,6 +381,18 @@ func resolveAttributeType(entityQN, attrName string, b backend.DomainModelBacken
if len(parts) != 2 {
return "String"
}
// Follow the generalization chain: an inherited attribute is not in the
// entity's own list, and defaulting it to String gave a mapping element the
// wrong DataType (mendixlabs/mxcli#703). Resolving by module name also stops a
// same-named entity in another module being picked up, which the previous
// scan-every-domain-model loop did.
if hb, ok := b.(entityLookupBackend); ok {
if t := ResolveMemberType(hb, entityQN, attrName); t != "" {
return t
}
return "String"
}

dms, err := b.ListDomainModels()
if err != nil {
return "String"
Expand Down
Loading
Loading