diff --git a/.claude/skills/design-mdl-syntax.md b/.claude/skills/design-mdl-syntax.md index 7a7f3259a..4e702f797 100644 --- a/.claude/skills/design-mdl-syntax.md +++ b/.claude/skills/design-mdl-syntax.md @@ -73,7 +73,7 @@ Does an existing pattern cover this? If yes, extend it. Don't invent new syntax. ``` New feature: "image collections" Existing pattern: create/alter/drop/list/describe -design: create image collection Module.Name (...) +design: create image collection Module.Name { image X ( File: '…' ) } describe image collection Module.Name list image COLLECTIONS [in module] ``` diff --git a/.claude/skills/fix-issue/findings/mdl-backend.jsonl b/.claude/skills/fix-issue/findings/mdl-backend.jsonl index 05b065e86..0fbc50353 100644 --- a/.claude/skills/fix-issue/findings/mdl-backend.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-backend.jsonl @@ -138,3 +138,6 @@ {"area": "mdl/backend", "date": "2026-09-26", "symptom": "describe → exec of a nanoflow detaches every annotation from its activity (AnnotationFlows 3 → 0, so the next describe prints the notes as free-floating) and deletes ExportLevel, UseListParameterByReference and ReturnVariableName; MarkAsUsed is cleared. Rules drop their annotation flows too", "cause": "nanoflowToGen and ruleToGen never wrote ObjectCollection.AnnotationFlows (microflowToGen did), and nanoflowToGen never set the three header keys; nanoflowFromGen did not read ReturnVariableName or MarkAsUsed and the builder hardcoded MarkAsUsed false", "file": "mdl/backend/modelsdk/nanoflow_write.go, rule_write.go, microflow.go; mdl/executor/cmd_microflows_build.go", "insight": "Three flow flavours share the object/flow converters but each has its own top-level writer, and each dropped a different subset — grep every *ToGen for the same list loop, not just the reported one. For keys with no MDL spelling and unknown version floors (Nanoflow.ExportLevel, UseListParameterByReference), carry ONLY the keys the stored unit has: nothing was ever written on create, Studio Pro fills an absent key, and an undeclared one makes the unit unopenable", "refs": ["ako/mxcli#705"]} {"area": "mdl/backend", "date": "2026-09-27", "symptom": "Running unchanged `describe user role` output (create or modify user role) rewrites Security$ProjectSecurity: /UserRoles[0] marker 2 -> 3; same for DemoUsers on a demo-user rewrite", "cause": "The encoder's default PartList marker is 3; Studio Pro writes 2 for ProjectSecurity.UserRoles and .DemoUsers (PedApp, TestApp, expr-checker agree). An untouched list passes through raw, so it only shows once an element in it is rewritten", "file": "mdl/backend/modelsdk/security_write.go", "insight": "A marker bug hides until the first rewrite of an element in that list; census the stored marker per (owner $Type, key) across Studio Pro fixtures and register it with RegisterPropertyListMarker", "refs": ["ako/mxcli#731"]} {"area": "mdl/backend", "date": "2026-09-27", "symptom": "Any write of a domain model (even rewriting a sibling entity) drops LocalizeDate from every DateTime attribute in the unit; external entity rewrites also rewrite Source/Key/Parts marker 2 -> 3", "cause": "attributeTypeToGen emitted DateTimeAttributeType without LocalizeDate and attributeTypeFromGen never read it; Rest$ODataKeyPart had no registered list marker so the encoder default 3 applied", "file": "mdl/backend/modelsdk/domainmodel.go, domainmodel_write.go", "insight": "A whole-unit rewrite re-serializes every sibling from the struct, so a property the struct cannot hold is lost on EVERY entity of the module, not just the edited one. Absent LocalizeDate means Mendix's default true (mxcli wrote it absent for years), so the reader must map absent to true or the fix would unlocalize every mxcli-authored DateTime. Only a Studio Pro fixture with DateTime attributes (TestApp) shows it", "refs": ["ako/mxcli#743"]} +{"area": "mdl/backend", "date": "2026-09-28", "symptom": "create or modify consumed odata service over a Studio Pro client: UseQuerySegment true -> false, Icon, CustomLocationTemplate and the empty ApplicationId/EndpointId/CatalogUrl/proxy/microflow keys deleted, MetadataReferences [3] -> [0], ValidatedEntities [1] -> [0]", "cause": "consumedODataServiceToGen writes the document wholesale from a model that holds none of these; placeholders (UseQuerySegment false, ExportLevel Hidden) and addStrIf-omitted empties replaced what Studio Pro stored", "file": "mdl/backend/modelsdk/odata_write.go (carryStoredConsumedODataService)", "fix": "UpdateConsumedODataService lays the fresh document over the stored one: placeholder keys keep the stored value, an omitted optional key is written \"\" when stored (so a cleared value stays cleared), lists keep the stored marker", "insight": "When the model is a strict subset of the stored document, overlay onto the stored bytes rather than growing the model field by field; but an optional key the writer omits must become \"\" rather than be carried, or clearing it through alter silently stops working", "refs": ["ako/mxcli#743"], "test": "TestConsumedODataServiceRewrite_CarriesWhatTheModelDoesNotHold"} +{"area": "mdl/backend", "date": "2026-09-28", "symptom": "Rewriting a Studio Pro workflow drops the empty Flow and the PersistentId of every outcome that leads nowhere, writes RelativeMiddlePoint/Size \"\" where Studio Pro stores \"0;0\", and deletes an empty EventSubProcesses [2]", "cause": "The writer added an outcome's Flow only when non-nil (Workflows$Outcome.flow is Required), gave condition outcomes no PersistentId, and omitted an empty EventSubProcesses list because the property is version-floored (11.8)", "file": "mdl/backend/modelsdk/workflow_write.go (encodeWorkflow, outcomeFlowToGen)", "fix": "Always write an outcome Flow; PersistentId on every outcome; \"0;0\" layout; empty EventSubProcesses via MandatoryListMarkers, suppressed through Encoder.OmitKeys below 11.8 only when the workflow has none (MandatoryListMarkers now honours OmitKeys)", "insight": "Encoder.OmitKeys suppresses a key whatever it holds, so version-floor only the empty default: suppressing the key outright would drop declared sub-processes on a project whose version could not be read", "refs": ["ako/mxcli#743"], "test": "TestWorkflowToGen_EveryOutcomeStoresAFlow"} +{"area": "mdl/backend", "date": "2026-09-28", "symptom": "Every domain-model rewrite deletes MaybeGeneralization HasOwnerAttr/HasChangedByAttr/HasCreatedDateAttr/HasChangedDateAttr = false on Studio Pro entities (#721 B)", "cause": "entityToGen set only the true flags, for parity with the legacy serializer", "file": "mdl/backend/modelsdk/domainmodel_write.go", "fix": "Write all four flags", "insight": "Legacy-writer parity is not Studio Pro parity; the flags were the last getput loss of a Studio Pro external entity, which is why it surfaced in #743", "refs": ["ako/mxcli#721", "ako/mxcli#743"], "test": "TestEntityToGen_WritesEveryGeneralizationFlag"} diff --git a/.claude/skills/fix-issue/findings/mdl-executor.jsonl b/.claude/skills/fix-issue/findings/mdl-executor.jsonl index d05e03d1d..d26296cf3 100644 --- a/.claude/skills/fix-issue/findings/mdl-executor.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-executor.jsonl @@ -736,3 +736,5 @@ {"area": "mdl/executor", "date": "2026-09-27", "symptom": "Running an external entity's describe output (create or modify external entity, either form) turns each attribute's Rest$ODataMappedValue into a DomainModels$StoredValue (RemoteName/RemoteType/Filterable... gone), re-mints attribute $IDs on the `from odata client` form, and rewrites String(36) as unlimited", "cause": "Both rewrite paths rebuilt attributes from the AST, which has no spelling for the OData mapping; the `from odata client` path minted fresh IDs; describe external entity printed GetTypeName() (`String`) instead of formatAttributeType", "file": "mdl/executor/cmd_entities.go (carryStoredAttributeState), cmd_odata.go", "insight": "Carry per attribute by name what the statement cannot spell, in the shared merge, and enumerate every path that rebuilds attributes: there were two statements for one document kind", "refs": ["ako/mxcli#743"]} {"area": "mdl/executor", "date": "2026-09-27", "symptom": "create or modify odata service over a Studio Pro service: EntitySet PageSize 10000 -> 0, entity sets reordered, member CanBeEmpty recomputed, ExportLevel deleted, AuthenticationTypes marker 1 -> 3", "cause": "The modify path rebuilt entity types/sets from the publish block (no spelling for PageSize without paging, set order, CanBeEmpty); the model had no ExportLevel; the writer used a marker-3 list copied from the legacy writer, not Studio Pro", "file": "mdl/executor/cmd_odata.go (carryPublishedEntityState), mdl/backend/modelsdk/odata_write.go", "insight": "Parity with the legacy writer is not evidence of what Studio Pro writes; measure markers on a Studio Pro fixture. A probe that runs describe output with the verb rewritten to create or modify (mdl/roundtrip TestTestAppCreateOrModifyProbe) shows what a verb switch would lose before switching it", "refs": ["ako/mxcli#743"]} {"area": "mdl/executor", "date": "2026-09-27", "symptom": "describe microflow printed `validation feedback $O/A message '{1} \u2026';` with no template arguments; describe -> exec stored the feedback with none", "cause": "formatAction's ValidationFeedbackAction case never read TemplateParameters, although the reader (textTemplateFromGen) and the writer both carried them", "file": "mdl/executor/cmd_microflows_format_action.go (templateArgsClause)", "insight": "A text template has two halves, the text and its parameters; check every describe case that prints one prints both. Found while switching the template form to `with ({1} = \u2026)` (ako/mxcli#751)."} +{"area": "mdl/executor", "date": "2026-09-28", "symptom": "create or modify workflow of its own describe output renames Studio Pro's implicit activities: start1 -> Start, end1 -> End, an event sub-process end2 -> End2", "cause": "describe prints no name for the implicit start/end, branch ends, jumps or end-of-path markers, and the rebuild names them from captions", "file": "mdl/executor/workflow_carry_names.go", "fix": "Pair declared and stored flows structurally (named activities by name, unnamed ones by type ordinal, nested flows by position, event sub-processes by name) and carry the stored names before deduplication", "insight": "Carry before the CE0495 deduplication, not after, so the carried names take part in it; pair nested flows with nil slots kept, because the rebuild leaves an empty outcome's flow nil where the stored one is an empty Flow", "refs": ["ako/mxcli#743"], "test": "TestCreateOrModifyWorkflow_CarriesNamesDescribeCannotPrint"} +{"area": "mdl/executor", "date": "2026-09-28", "symptom": "create or modify external entity declaring OrderId: String(20) on an Edm.Int64 property keeps RemoteType Edm.Int64; mx check CE6616", "cause": "carryStoredAttributeState carries RemoteType by name regardless of the declared type", "file": "mdl/executor/external_entity_remote_type.go", "fix": "Refuse under mdl 1 (MDL-V1-REMOTETYPE), warn under mdl 0", "insight": "CE6616 compares against the service's $metadata, not the stored RemoteType, so deriving RemoteType from the declared type would not fix it; measured with mx check on a TestApp copy", "refs": ["ako/mxcli#764"], "test": "TestCreateOrModifyExternalEntity_RemoteTypeMismatchRefusedUnderMdl1"} diff --git a/.claude/skills/fix-issue/findings/mdl-grammar.jsonl b/.claude/skills/fix-issue/findings/mdl-grammar.jsonl index fcafec92c..08c61bbc8 100644 --- a/.claude/skills/fix-issue/findings/mdl-grammar.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-grammar.jsonl @@ -63,3 +63,4 @@ {"area": "mdl/grammar", "date": "2026-09-23", "symptom": "`alter page M.P { set 'createFileAction' = microflow M.ACT_CreateFile on fileUploader1; };` (quoted or bare key) fails to parse: `line 2:37 extraneous input 'MyModule' expecting {DROP, ADD, SET, INSERT, REPLACE, '}'}` — a pluggable widget's NAMED action slot, writable on CREATE PAGE since #956, could only be retargeted by REPLACEing the whole widget", "cause": "`alterPageAssignment` special-cased `Action = actionExprV3` and sent every other key to `propertyValueV3`, which has no `microflow ` form. Below the grammar there was also no route: `SetWidgetProperty` would have stringified an action into `PrimitiveValue`, and `SetWidgetAction` writes the built-in click action, not a pluggable slot", "file": "`mdl/grammar/MDLParser.g4` (`alterPageAssignment`: `STRING_LITERAL|identifierOrKeyword EQUALS actionExprV3`), `mdl/visitor/visitor_alter_page.go` (keeps the author's key), `mdl/executor/cmd_alter_page.go` (`applySetPropertyMutator` routes any `*ast.ActionV3` not keyed `Action`), `mdl/backend/pagemutator/mutator.go` (`SetWidgetNamedAction`); tests `mdl/visitor/visitor_alter_page_named_action_test.go`, `mdl/backend/pagemutator/mutator_named_action_test.go`, `mdl/executor/alter_set_named_action_test.go`; example `mdl-examples/bug-tests/995-alter-page-set-named-action-slot.mdl`", "insight": "**Decide action-slot-ness by the stored PropertyType's `ValueType.Type == \"Action\"`, never by the presence of `Value.Action`** — every WidgetValue carries an Action (a NoAction by default) whatever its type, so a field-presence check would \"succeed\" writing into an Integer and change nothing. Unlike CREATE, ALTER has no datasource overlap to yield to (DataSource is its own alternative), so the action alternative goes before the scalar ones and `microflow M.X` parses straight to an action — no DataSourceV3 conversion. The check-time probe (validate_alter_set.go) dry-runs the same setter, so the wrong-type refusal surfaced at `check -p --references` for free. Measured on 11.12.1: set on a DataGrid 2 with Selection unset (slot hidden) also passed `mx check` at 0 errors — the CE0463 in #956's notes was 11.13.0, so ALTER does not yet run MDL-WIDGET10; don't assume either way without a build on the target version", "refs": ["mendixlabs/mxcli#995", "mendixlabs/mxcli#956"]} {"area": "mdl/grammar", "date": "2026-09-24", "symptom": "`DROP PAGE IF EXISTS FieldService.Stub;` -> `line 1:13 extraneous input 'EXISTS' expecting the start of a statement`. No document-level DROP (35 alternatives: entity through folder) accepted IF EXISTS, so any script that dropped something was one-shot and the drop had to be deleted after its first run (ako/mxcli#531, ChipCoV3, 11.14.0).", "cause": "`dropStatement` in MDLParser.g4 never applied the existing `ifExists` rule; only sub-document drops (attribute, index, enum value) and the two security drops had it, each added one statement at a time with its own AST field and handler branch.", "file": "`mdl/grammar/MDLParser.g4` (dropStatement); `mdl/ast/ast_drop.go` (DropGuard embedded in 35 Drop*Stmt); `mdl/visitor/visitor_entity.go` (ExitDropStatement sets it once, via defer); `mdl/executor/registry.go` (Dispatch turns NotFoundError into a skip); `mdl/executor/validate.go` (check --references skips guarded drops); tests `mdl/executor/drop_if_exists_test.go`; example `mdl-examples/bug-tests/531-drop-if-exists.mdl`", "insight": "The per-statement recipe the previous IF EXISTS fixes used (field + handler branch) would have been 35 copies, and the next doctype would miss it. What made one central guard safe was a measurement, not an assumption: a table test driving every bare DROP at a missing target through the registry showed all 35 handlers already return mdlerrors.NotFoundError, and only at lookup, before any mutation — so Dispatch can key on that type and let every other error through. Keep that bare-form table test: it is what breaks if a new drop handler reports 'not found' with fmt.Errorf. Mock pitfall on the way: MockBackend's Get*ByQualifiedName defaults to (nil, nil), which the real backend never returns for a missing document — mirror its 'not found' error or the handler dereferences nil. And check --references resolved a drop's MODULE, so a guarded drop in a missing module passed exec but failed check until validateWithContext skipped it too."} {"area": "mdl/grammar", "date": "2026-09-25", "symptom": "`create java action \u2026(EntityType: entity not null, \u2026)` failed with \"mismatched input 'String' expecting IDENTIFIER\"; Studio Pro accepts a type parameter named after a primitive, so such an action could not be authored, and DESCRIBE of a Studio Pro-authored one printed `returns String`, which re-parses as the primitive.", "cause": "dataType's `ENTITY LESS_THAN IDENTIFIER GREATER_THAN` excluded keyword tokens, and the describer emitted type-parameter names bare where a bare keyword means something else.", "file": "mdl/grammar/domains/MDLDomainModel.g4, mdl/executor/cmd_javaactions.go (formatCodeActionTypeParameterRef)", "insight": "Two halves of one round trip: the declaration slot needed identifierOrKeyword, and the *reference* slots needed no grammar change at all \u2014 a quoted `\"String\"` already parses as a bare qualifiedName, which isTypeParamRef resolves, while unquoted `String` stays the primitive. So the fix on the output side is mdlIdent (which lexes the name with the real lexer) on every type-parameter name DESCRIBE prints. Prove it with a describe \u2192 exec into a second module \u2192 describe diff, not by eyeballing one DESCRIBE.", "refs": ["mendixlabs/mxcli#1183"]} +{"area": "mdl/grammar", "date": "2026-09-28", "symptom": "A misspelt statement keyword was not an error. `craete module Foo;` and `dorp entity M.E;` parsed cleanly and did nothing (0 statements, exit 0); `craete persistent entity Shop.Note (Text: string(200));` reported `extraneous input '('` at column 35 instead of at the typo.", "cause": "`helpStatement: IDENTIFIER (helpTopicWord (DOT? helpTopicWord)*)?` is a utilityStatement alternative, so ANY statement starting with an IDENTIFIER matched it, the rest of the words as the topic; ExitHelpStatement then built nothing for a word other than help/?/exit/quit. The rule is the grammar's catch-all (the #1025 comment already warned it swallows from other statements); a typo is just the case where there is no other statement.", "file": "`mdl/grammar/MDLParser.g4` (`@parser::members` IsHelpWord), `mdl/grammar/domains/MDLSettings.g4` (semantic predicate on helpStatement), `mdl/visitor/visitor_unknown_statement.go` (\"unknown statement 'x' — did you mean 'y'?\" when the parser is still in statement/program and ANTLR names the one word), test `mdl/visitor/visitor_session_commands_test.go`", "insight": "A catch-all alternative that the visitor filters (`switch word { case \"help\": … }`) is a silent no-op for everything it does not list: move the filter INTO the grammar as a predicate (`{IsHelpWord(p.GetTokenStream().LT(1).GetText())}?`), so the parser, not the visitor, rejects. The words stay IDENTIFIERs — reserving HELP/EXIT/QUIT as tokens would take them away as names everywhere a bare IDENTIFIER is accepted. The generated parser imports no `strings`, so fold case by hand in @members. With the predicate the error lands at the word as `no viable alternative at input 'craete'`, and the listener can rewrite exactly that message when the recognizer's context is StatementContext/ProgramContext. Proof: without the predicate the test reports 3 of 4 inputs parsing to zero statements.", "refs": ["ako/mxcli#755", "mendixlabs/mxcli#1025"]} diff --git a/.claude/skills/implement-mdl-feature.md b/.claude/skills/implement-mdl-feature.md index e82ebbb96..6d5061c83 100644 --- a/.claude/skills/implement-mdl-feature.md +++ b/.claude/skills/implement-mdl-feature.md @@ -137,7 +137,7 @@ Based on the BSON structure, design MDL syntax that: Example design for REST call: ```sql -$response = rest call get 'http://api.example.com/data' +$response = call rest service get 'http://api.example.com/data' header 'Content-Type' = 'application/json' header Accept = 'application/json' auth basic $username password $password @@ -156,7 +156,7 @@ Add examples to `mdl-examples/doctype-tests/`: create microflow RestExamples.SimpleGet() returns string as $response begin - $response = rest call get 'https://api.example.com/data' + $response = call rest service get 'https://api.example.com/data' timeout 30 returns string; return $response; @@ -597,7 +597,7 @@ Expected: Valid MDL output that could be re-parsed. create microflow Test.RestExample() returns string as \$R begin - \$R = rest call get 'http://example.com' timeout 30 returns string; + \$R = call rest service get 'http://example.com' timeout 30 returns string; return \$R; end;" diff --git a/.claude/skills/mendix/agents/SKILL.md b/.claude/skills/mendix/agents/SKILL.md index 218f74ea3..f7538fd5a 100644 --- a/.claude/skills/mendix/agents/SKILL.md +++ b/.claude/skills/mendix/agents/SKILL.md @@ -39,7 +39,7 @@ register `ASU_AgentEditor` as an after-startup microflow. ```sql create model Module.MyModel ( Provider: MxCloudGenAI, -- default, can omit - key: Module.ApiKeyConst -- must be a String constant + key: @Module.ApiKeyConst -- must be a String constant ); ``` @@ -64,7 +64,7 @@ document Studio Pro created**, rather than from memory or from this file. ```sql create knowledge base Module.ProductDocs ( Provider: MxCloudGenAI, - key: Module.KBKeyConst + key: @Module.KBKeyConst ); ``` @@ -96,22 +96,22 @@ prompt here.$$, UserPrompt: 'Single line prompt.' ) { - mcp service Module.WebSearch { + mcp service Module.WebSearch ( Enabled: true - } + ) - knowledge base KBAlias { + knowledge base KBAlias ( source: Module.ProductDocs, collection: 'product-docs', MaxResults: 5, description: 'Product docs', Enabled: true - } + ) - tool MyMicroflowTool { + tool MyMicroflowTool ( description: 'Fetch customer data', Enabled: true - } + ) }; ``` @@ -131,9 +131,9 @@ alter consumed mcp service Module.MyMCP -- Agent: SET for scalars, ADD/DROP for tools, MCP services, knowledge bases alter agent Module.MyAgent set SystemPrompt = 'New prompt', Temperature = 0.7, MaxTokens = 4096 - add tool MyMicroflow { Description: '...', Enabled: true } - add mcp service Module.WeatherSvc { Description: '...', Enabled: true } - add knowledge base Docs { Source: Module.MyKB, Collection: 'docs', MaxResults: 5 } + add tool MyMicroflow ( Description: '...', Enabled: true ) + add mcp service Module.WeatherSvc ( Description: '...', Enabled: true ) + add knowledge base Docs ( Source: Module.MyKB, Collection: 'docs', MaxResults: 5 ) drop tool OldTool drop mcp service Module.OldSvc drop knowledge base OldKB; @@ -173,7 +173,7 @@ The feature uses `CustomBlobDocument` BSON type with a `Contents` field holding ### Minimal agent (no tools) ```sql -create model Module.M (Provider: MxCloudGenAI, key: Module.K); +create model Module.M (Provider: MxCloudGenAI, key: @Module.K); create agent Module.A ( UsageType: task, model: Module.M, diff --git a/.claude/skills/mendix/alter-page/SKILL.md b/.claude/skills/mendix/alter-page/SKILL.md index 861a4fa7a..f1733e50f 100644 --- a/.claude/skills/mendix/alter-page/SKILL.md +++ b/.claude/skills/mendix/alter-page/SKILL.md @@ -193,16 +193,16 @@ so a silent write would build cleanly and then fail to open. | `PopupResizable` | Page-level only (case-sensitive) | Boolean | `set (PopupResizable: true)` | | `Class` | Page-level (case-sensitive, no ON) | CSS class string | `set (Class: 'container-fluid bg-light')` | | `Style` | Page-level (case-sensitive, no ON) | Inline CSS string | `set (Style: 'min-height: 100vh')` | -| `Visible` (conditional) | Any widget | `[expression]` | `set (Visible: [Name != '']) on ctnDetails` | -| `Editable` (conditional) | Input widgets | `[expression]` | `set (Editable: [Active]) on txtName` | +| `Visible` (conditional) | Any widget | expression | `set (Visible: $currentObject/Name != '') on ctnDetails` | +| `Editable` (conditional) | Input widgets | expression | `set (Editable: $currentObject/Active) on txtName` | | `'quotedProp'` | Pluggable widgets | String, Boolean, Number | `set ('showLabel': false) on cbStatus` | -> **Conditional visibility/editability** — `set (Visible: [expr]) on widget` (and -> `Editable`) attach a per-object expression. Bare attributes are rooted in the -> widget data context automatically: `[Name != '']` becomes -> `$currentObject/Name != ''` (paths you write with `$currentObject/…`/`$Param/…` -> pass through). Setting `Editable` on a non-input widget is rejected. This mirrors -> CREATE PAGE's `visible: [...]` — see the create-page skill for enum-value rules. +> **Conditional visibility/editability** — `set (Visible: ) on widget` (and +> `Editable`) attach a per-object client expression, stored as written: name +> attributes as `$currentObject/Name`. The bracketed `set (Visible: [Name != ''])`, +> which roots a bare attribute for you, still works and warns MDL-DEPR081. Setting +> `Editable` on a non-input widget is rejected. This mirrors CREATE PAGE's +> `visible:` — see the create-page skill for enum-value rules. **Pluggable widget properties** use quoted names to set values in the widget's `Object.Properties[]`. Boolean values are stored as `"yes"`/`"no"` in BSON. @@ -542,7 +542,7 @@ adds. Both still fail at exec if they are genuinely wrong. | Missing `on widgetName` for widget SET | Add `on widgetName` (only page-level properties — `Title`, `Documentation`, `PopupWidth`, `PopupHeight`, `PopupResizable`, `Class`, `Style` — omit ON) | | `unsupported page-level property: title` | Page-level property names are case-sensitive — use `Title`, `PopupWidth`, `PopupHeight`, `PopupResizable`, `Class`, `Style` | | Using unquoted pluggable property names | Quote pluggable props: `set ('showLabel': false) on cb` | -| `pluggable property "X" not found` | The widget does not declare it — casing is not the problem (any casing resolves). The error lists the keys it does declare; `describe widget ` or `describe page` shows them in context. Run `mxcli check … --references` to get this before the script runs | +| `pluggable property "X" not found` | The widget does not declare it — casing is not the problem (any casing resolves). The error lists the keys it does declare; `describe widget type ` or `describe page` shows them in context. Run `mxcli check … --references` to get this before the script runs | | Wrong widget name | Use `describe page Module.Name` to see widget names | | SET on non-existent widget | Widget names are case-sensitive; check with DESCRIBE | | Missing semicolons between operations | Each operation inside `{ }` ends with `;` | diff --git a/.claude/skills/mendix/assess-quality/SKILL.md b/.claude/skills/mendix/assess-quality/SKILL.md index 774cf4116..53840e01f 100644 --- a/.claude/skills/mendix/assess-quality/SKILL.md +++ b/.claude/skills/mendix/assess-quality/SKILL.md @@ -40,7 +40,7 @@ Use catalog queries to understand the project's shape: ```sql -- Overview of project size and structure -show structure depth 2 +describe structure depth 2 -- Key metrics select module_name, count(*) from CATALOG.entities GROUP by module_name diff --git a/.claude/skills/mendix/atlas-design/reference/building-blocks.md b/.claude/skills/mendix/atlas-design/reference/building-blocks.md index ef4f318f1..544017308 100644 --- a/.claude/skills/mendix/atlas-design/reference/building-blocks.md +++ b/.claude/skills/mendix/atlas-design/reference/building-blocks.md @@ -49,7 +49,7 @@ is the fallback for hand-tuning or the modelsdk engine; the how-to is below. `DesignProperties:` — copy both. 3. **Reproduce the tree** on your page, binding real data where the block has placeholder text (`'Card title'` → your attribute/content). -4. **DRY it** — if the shape repeats, put it in a `define fragment` and `use` it. +4. **DRY it** — if the shape repeats, put it in a `create fragment` and `use` it. ### Worked example — `Card` @@ -80,7 +80,7 @@ the slot with each card's own content. This is the key idiom: one card wrapper, arbitrary bodies, no copy-paste of the wrapper markup. ```mdl -define fragment SectionCard as { +create fragment SectionCard as { container card1 (designproperties: ['Card style': on, 'Spacing': ['margin-bottom': 'Large']]) { container cardBody (class: 'card-body') { slot content -- each page's widgets land here @@ -119,7 +119,7 @@ inside; typed **parameters** vary *which entity* and *which microflow*. Declare `datasource` and/or `action` parameter and the card becomes a real component: ```mdl -define fragment EntityCard($data: datasource, $onOpen: action) as { +create fragment EntityCard($data: datasource, $onOpen: action) as { container card1 (designproperties: ['Card style': on]) { listview lv (datasource: $data) { slot content diff --git a/.claude/skills/mendix/bootstrap-app/SKILL.md b/.claude/skills/mendix/bootstrap-app/SKILL.md index 683b8e0e8..a2a4fca33 100644 --- a/.claude/skills/mendix/bootstrap-app/SKILL.md +++ b/.claude/skills/mendix/bootstrap-app/SKILL.md @@ -253,8 +253,9 @@ with these deltas: against the host name rather than the listen address: ```sql - alter settings configuration 'Default' - ApplicationRootUrl = 'http://backend.local:8080/'; + alter settings configuration 'Default' ( + ApplicationRootUrl: 'http://backend.local:8080/' + ); ``` `run --local` picks that up at boot and prints which configuration it came from. diff --git a/.claude/skills/mendix/connect-rapidminer-graph/SKILL.md b/.claude/skills/mendix/connect-rapidminer-graph/SKILL.md index 9cfd0b730..615f4d4dc 100644 --- a/.claude/skills/mendix/connect-rapidminer-graph/SKILL.md +++ b/.claude/skills/mendix/connect-rapidminer-graph/SKILL.md @@ -182,7 +182,7 @@ begin end loop; -- Inline REST CALL — NOT the REST Client (see notes) - $RawJson = rest call post 'https://graphstudio.mendixdemo.com/sparql/graphmart/http%3A%2F%2Fcambridgesemantics.com%2FGraphmart%2F3617250aca6a40d88972c1c0de38f86a' + $RawJson = call rest service post 'https://graphstudio.mendixdemo.com/sparql/graphmart/http%3A%2F%2Fcambridgesemantics.com%2FGraphmart%2F3617250aca6a40d88972c1c0de38f86a' header 'Accept' = 'application/sparql-results+json' header 'Content-Type' = 'application/sparql-query' auth basic '' password '' @@ -301,9 +301,7 @@ where { ## Credential management -For demos, literal credentials inline in the microflow are the simplest and most reliable. For anything else, put them in a project constant and reference it from the microflow via `$ConstantName` (requires a non-trivial amount of setup — see the project settings skill). - -**Do not** use `$ConstantName` in `create consumed rest service ... authentication: basic (username: $C, password: $C)` — the MDL parser rejects the `$` prefix there, and the skill files' claim of `rest$ConstantValue` serialization isn't reachable. +For demos, literal credentials inline in the microflow are the simplest and most reliable. For anything else, put them in a project constant and reference it as `@Module.ConstantName` — the one way MDL refers to a constant, in a microflow expression and in `create consumed rest service ... authentication: basic (username: @Module.User, password: @Module.Password)` alike. The older `$ConstantName` spelling in a REST credential still parses and warns MDL-DEPR083. ## Related skills diff --git a/.claude/skills/mendix/create-page/SKILL.md b/.claude/skills/mendix/create-page/SKILL.md index 3f944f080..23214d213 100644 --- a/.claude/skills/mendix/create-page/SKILL.md +++ b/.claude/skills/mendix/create-page/SKILL.md @@ -47,11 +47,11 @@ create [or replace] page Module.PageName page is opened in a pop-up. They are optional — omitting them uses the Mendix defaults (600 × 600, not resizable). Unlike the other header keywords, these property names are **case-sensitive** and must be written exactly as shown. They can also be changed later -with `alter page … { set PopupWidth = …; }` (see the alter-page skill). +with `alter page … { set (PopupWidth: …); }` (see the alter-page skill). **Page CSS class / style** (`Class` / `Style`) set the page's Appearance — a CSS class and inline style applied to the whole page (e.g. `Class: 'container-fluid bg-light'`). -Both are optional and can be changed later with `alter page … { set Class = '…'; }`. +Both are optional and can be changed later with `alter page … { set (Class: '…'); }`. **Page Variables**: Local variables at the page level for use in expressions (e.g., column visibility). - DataType: `boolean`, `string`, `integer`, `decimal`, `datetime` @@ -243,7 +243,7 @@ To make targeted changes to an existing page (change a label, add a field, remov ```sql -- Change a button caption and add a field alter page Module.Customer_Edit { - set caption = 'Save & Close' on btnSave; + set (caption: 'Save & Close') on btnSave; insert after txtEmail { textbox txtPhone (label: 'Phone', attribute: Phone) } @@ -259,43 +259,43 @@ See the dedicated skill file: [ALTER PAGE/SNIPPET](../alter-page/SKILL.md) ## Conditional Visibility and Editability -Any widget (including CONTAINER) can have conditional visibility. Input widgets can also have conditional editability. Use bracket syntax `[expression]`: +Any widget (including CONTAINER) can have conditional visibility. Input widgets can also have conditional editability. The condition is a Mendix client expression, written bare like every expression in MDL and stored exactly as written — so name the context object's attributes as `$currentObject/Attr`: ```sql -- Conditionally visible widget (boolean attribute) -textbox txtName (label: 'Name', attribute: Name, visible: [IsActive]) +textbox txtName (label: 'Name', attribute: Name, visible: $currentObject/IsActive) -- Conditionally visible container -container ctnDetails (visible: [Name != '']) { dynamictext t (content: '...') } +container ctnDetails (visible: $currentObject/Name != '') { dynamictext t (content: '...') } -- Conditionally editable input (boolean) -textbox txtStatus (label: 'Status', attribute: status, editable: [CanEdit]) +textbox txtStatus (label: 'Status', attribute: status, editable: $currentObject/CanEdit) --- Enum comparison: use the QUALIFIED enum value. Attributes are rooted for you, --- but a bare enum VALUE would be treated as an attribute — always qualify it. +-- Enum comparison: use the QUALIFIED enum value. textbox txtNotes (label: 'Notes', attribute: Notes, - visible: [Status = MES.EquipmentStatus.Running]) + visible: $currentObject/Status = MES.EquipmentStatus.Running) -- Combined textbox txtEmail (label: 'Email', attribute: Email, - visible: [ShowEmail], - editable: [CanEdit]) + visible: $currentObject/ShowEmail, + editable: $currentObject/CanEdit) --- Static values still work +-- Static values still work — a plain value is not a condition textbox txtReadOnly (label: 'Read Only', attribute: Name, editable: Never) textbox txtHidden (label: 'Hidden', attribute: Name, visible: false) --- A quoted-string expression is also accepted (CREATE and ALTER). Unlike the --- bracket form, it is NOT auto-rooted — write $currentObject/ yourself. -dynamictext ovChip (content: 'chip', visible: '$currentObject/Name != empty') - --- Function calls work in the bracket form, including functions whose name is --- also an MDL keyword (trim, length, find). Arguments are rooted like any other --- reference. -dynamictext tTrim (content: 'x', visible: [trim($currentObject/Slug) != '']) -textbox txtSlug (label: 'Slug', attribute: Slug, editable: [length(Slug) > 0]) +-- Function calls work, including functions whose name is also an MDL keyword +-- (trim, length, find). +dynamictext tTrim (content: 'x', visible: trim($currentObject/Slug) != '') +textbox txtSlug (label: 'Slug', attribute: Slug, editable: length($currentObject/Slug) > 0) ``` +The older bracketed form, `visible: [IsActive]`, still parses: it roots a bare +attribute in `$currentObject` for you, and warns **MDL-DEPR081**. `mxcli fmt +--upgrade` rewrites it to the expression it stores (`visible: $currentObject/IsActive`). +A constant condition (`editable: [false]`, stored as the condition `false`, not as +`Never`) has no bare spelling and keeps its brackets. + **Visible based on an attribute value** (Studio Pro's "Visible: based on attribute value") — list the Boolean/enumeration values that SHOW the widget; `empty` is "(empty)". Only an attribute of the enclosing data container's own entity: @@ -307,7 +307,7 @@ textbox txtPassword (label: 'Password', attribute: Password, visible: IsLocalUse > **`visible:`/`editable:` is a Mendix *expression*, not XPath** — a different -> function set from a datasource `where` clause, even though both use `[ ... ]`: +> function set from a datasource `where [ ... ]` clause: > > | | `visible:` / `editable:` (client expression) | `where [ … ]` (XPath) | > |---|---|---| @@ -323,23 +323,20 @@ textbox txtPassword (label: 'Password', attribute: Password, visible: IsLocalUse > [XPath keywords](https://docs.mendix.com/refguide/xpath-keywords-and-system-variables/). > **An unparseable conditional is an error, not a silent drop.** If the -> expression inside `visible: [ ... ]` / `editable: [ ... ]` can't be parsed, the +> expression in `visible:` / `editable:` can't be parsed, the > property has nowhere to go and would vanish on write — leaving the widget > unconditionally visible/editable, which looks identical to a specificity bug in > the running app. `mxcli check` reports this as **MDL-WIDGET19** and fails the > command instead. Until v0.16.x, `trim(…)` and `length(…)` hit exactly this path > and disappeared without a word (issue #852). -> **Attribute rooting is automatic** — a bare attribute in a widget -> visibility/editability expression (`[Name != '']`, `[IsActive]`) is rooted in the -> widget data context as `$currentObject/Name != ''` for you, so it no longer -> triggers CE0117. Paths you write with an explicit `$currentObject/…` or `$Param/…` -> prefix pass through unchanged. +> **Attributes are not rooted for you in the bare form** — the expression is +> stored as written, so a bare `Name` is CE0117. Write `$currentObject/Name` (or +> `$Param/…`). Only the deprecated bracketed form roots a bare attribute. > > **Enum comparison differs by context:** > - **Widget visibility/editability expression** (per-object): qualified enum value — -> `[Status = MES.EquipmentStatus.Running]` (the `Status` attribute is rooted for you; -> the *value* must stay qualified, or it would be mistaken for an attribute). +> `$currentObject/Status = MES.EquipmentStatus.Running`. > - **XPath datasource constraint** (`where […]`): the string key works — > `where [Status = 'Running']` (see [xpath-constraints](../xpath-constraints/SKILL.md)). > - **Microflow expression**: qualified value — diff --git a/.claude/skills/mendix/create-page/reference/widgets.md b/.claude/skills/mendix/create-page/reference/widgets.md index 8cfd2ee1b..7e75d8e76 100644 --- a/.claude/skills/mendix/create-page/reference/widgets.md +++ b/.claude/skills/mendix/create-page/reference/widgets.md @@ -842,7 +842,7 @@ conditional visibility — or put the condition in the microflow it calls: ```sql actionbutton btnSubmit ( caption: 'Submit', action: call microflow Mod.ACT_Submit, - visible: [$currentObject/Status = Mod.Status.Draft] + visible: $currentObject/Status = Mod.Status.Draft ) ``` diff --git a/.claude/skills/mendix/custom-widgets/SKILL.md b/.claude/skills/mendix/custom-widgets/SKILL.md index bf0bd3371..62526e3a4 100644 --- a/.claude/skills/mendix/custom-widgets/SKILL.md +++ b/.claude/skills/mendix/custom-widgets/SKILL.md @@ -25,7 +25,7 @@ the **properties** (the widget's own spelling — `tagName`, not `TagName`), and the **body containers** — `attribute` is an object list (one entry per repetition), `tagcontentcontainer` a child slot (holds widgets). -**Ask the widget rather than guessing.** `describe widget ` lists every +**Ask the widget rather than guessing.** `describe widget type ` lists every property with its type, default and enumeration members; every body container and whether MDL can express it; and a complete example that parses AND checks as written: @@ -94,7 +94,7 @@ form is reported only when the widget resolves, because without a definition matter because a required slot left empty is not a silent no-op at build time — it is `CE0642 "Property '…' is required."`, one per slot. -`describe widget -p ` lists a widget's container keywords +`describe widget type -p ` lists a widget's container keywords under **Body containers**, and — for an object list — the widgets-typed **slots inside one item**, with the widget types that route into each: diff --git a/.claude/skills/mendix/database-connections/SKILL.md b/.claude/skills/mendix/database-connections/SKILL.md index aa68ec641..d5ebc3fd6 100644 --- a/.claude/skills/mendix/database-connections/SKILL.md +++ b/.claude/skills/mendix/database-connections/SKILL.md @@ -47,21 +47,17 @@ create non-persistent entity MyModule.EmployeeRecord ( Connection credentials should be stored in constants: ```sql --- Connection string (JDBC URL) +/** JDBC connection string for external database */ create constant MyModule.DbConnectionString type string - default 'jdbc:oracle:thin:@//hostname:1521/SERVICENAME' - comment 'JDBC connection string for external database'; + default 'jdbc:oracle:thin:@//hostname:1521/SERVICENAME'; --- Username +/** Database username */ create constant MyModule.DbUsername type string - default 'app_user' - comment 'Database username'; + default 'app_user'; --- Password (use PRIVATE for local development) +/** Database password - inject via environment variable in production */ create constant MyModule.DbPassword type string - default '' - comment 'Database password - inject via environment variable in production' - PRIVATE; + default ''; ``` ## Database Connection Syntax @@ -234,7 +230,7 @@ create constant OracleDemo.OracleConnectionString type string create constant OracleDemo.OracleUser type string default 'scott'; -create constant OracleDemo.OraclePassword type string default 'tiger' PRIVATE; +create constant OracleDemo.OraclePassword type string default 'tiger'; -- Step 3: Create non-persistent entity for results create non-persistent entity OracleDemo.EmpRecord ( @@ -275,7 +271,7 @@ create constant Inventory.PgConnectionString type string default 'jdbc:postgresql://localhost:5432/inventory_db'; create constant Inventory.PgUser type string default 'inventory_app'; -create constant Inventory.PgPassword type string default '' PRIVATE; +create constant Inventory.PgPassword type string default ''; create non-persistent entity Inventory.ProductRecord ( ProductId: integer, @@ -333,7 +329,7 @@ describe database connection MyModule.MyDatabase; - Use `MX_Module_ConstantName` environment variables in production ### 2. Credential Security -- Use `PRIVATE` flag for password constants during development +- Leave a password constant's default empty (`default ''`) and set the value per environment; MDL has no private flag, and a trailing `PRIVATE` was never read - Never commit real passwords to version control - Inject credentials via CI/CD pipelines in production @@ -465,7 +461,7 @@ create non-persistent entity HR.EmployeeRecord ( create constant HR.DbUrl type string default 'jdbc:postgresql://localhost:5432/hrdb'; create constant HR.DbUser type string default 'app'; -create constant HR.DbPass type string default '' PRIVATE; +create constant HR.DbPass type string default ''; create database connection HR.MainDB type 'PostgreSQL' diff --git a/.claude/skills/mendix/demo-data/SKILL.md b/.claude/skills/mendix/demo-data/SKILL.md index faead4d4b..74975483b 100644 --- a/.claude/skills/mendix/demo-data/SKILL.md +++ b/.claude/skills/mendix/demo-data/SKILL.md @@ -41,13 +41,14 @@ For full credentials (username, password): Example output: ```sql -alter settings configuration 'Default' - DatabaseType = 'PostgreSql', - DatabaseUrl = 'localhost:5434', - DatabaseName = 'mxcli2-dev', - DatabaseUserName = 'mendix', - DatabasePassword = 'mendix', - HttpPortNumber = 8080; +alter settings configuration 'Default' ( + DatabaseType: 'PostgreSql', + DatabaseUrl: 'localhost:5434', + DatabaseName: 'mxcli2-dev', + DatabaseUserName: 'mendix', + DatabasePassword: 'mendix', + HttpPortNumber: 8080 +); ``` --- diff --git a/.claude/skills/mendix/download-marketplace-content/SKILL.md b/.claude/skills/mendix/download-marketplace-content/SKILL.md index e76434a26..3c592c8d7 100644 --- a/.claude/skills/mendix/download-marketplace-content/SKILL.md +++ b/.claude/skills/mendix/download-marketplace-content/SKILL.md @@ -300,7 +300,7 @@ DESCRIBE renders imperfectly — a snippet whose body comes out `{ }`, a buildin block under "Building blocks are read-only; they cannot be created via MDL" — and two imperfect renderings can differ for reasons that have nothing to do with you. Those are reported `unknown`, never `changed`, and `--save-edits` refuses to write -them: replaying `create or modify snippet X (Folder: 'Web') { }` would **empty** +them: replaying `create or modify snippet X folder 'Web' { }` would **empty** the snippet. **Read `verified`, not just `locallyModified`.** An element that cannot be described is diff --git a/.claude/skills/mendix/fragments/SKILL.md b/.claude/skills/mendix/fragments/SKILL.md index 5e592cf50..de133d3ca 100644 --- a/.claude/skills/mendix/fragments/SKILL.md +++ b/.claude/skills/mendix/fragments/SKILL.md @@ -1,6 +1,6 @@ --- name: fragments -description: "Define reusable widget groups with DEFINE FRAGMENT and place them with USE FRAGMENT. Use when the same widget pattern repeats across pages and should be written once." +description: "Define reusable widget groups with CREATE FRAGMENT and place them with USE FRAGMENT. Use when the same widget pattern repeats across pages and should be written once." --- # Mendix Fragments Skill @@ -8,7 +8,7 @@ description: "Define reusable widget groups with DEFINE FRAGMENT and place them ## When to Use This Skill Use this skill when: -- Defining reusable widget groups with `define fragment` +- Defining reusable widget groups with `create fragment` - Inserting fragments into pages or snippets with `use fragment` - Listing or inspecting fragments with `show fragments` / `describe fragment` - Building multiple pages that share common widget patterns (footers, form fields, buttons) @@ -24,10 +24,10 @@ Fragments are **script-scoped, transient** widget groups: ## Syntax Reference -### DEFINE FRAGMENT +### CREATE FRAGMENT ```mdl -define fragment SaveCancelFooter as { +create fragment SaveCancelFooter as { footer footer1 { actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: primary) actionbutton btnCancel (caption: 'Cancel', action: cancel changes) @@ -38,7 +38,7 @@ define fragment SaveCancelFooter as { Multiple top-level widgets: ```mdl -define fragment CustomerFields as { +create fragment CustomerFields as { textbox txtName (label: 'Name', attribute: Name) textbox txtEmail (label: 'Email', attribute: Email) textbox txtPhone (label: 'Phone', attribute: Phone) @@ -79,7 +79,7 @@ panel, or section) whose body varies per use. Declare a `slot` where the caller' widgets should land, then fill it with the `use fragment X { … }` payload form: ```mdl -define fragment Card as { +create fragment Card as { container cardWrap (class: 'card', designproperties: ['Card style': on]) { container cardBody (class: 'card-body') { slot content -- caller's widgets are spliced in here @@ -106,7 +106,7 @@ Rules (v1): (no slot marker), and there are no BSON/round-trip surprises. > For varying a leaf **value** (a label or attribute name) rather than wrapping a -> subtree, scalar params (`define fragment F($label, $attr) as …`) are a planned +> subtree, scalar params (`create fragment F($label, $attr) as …`) are a planned > v1.1 follow-up; today use a slot plus a one-line value fill. ### Parameter bindings — datasource & action (experimental) @@ -118,7 +118,7 @@ its value at the use site. This turns a shell into a real reusable component — one panel bound to a different entity and a different handler per use. ```mdl -define fragment DataPanel($data: datasource, $onEdit: action) as { +create fragment DataPanel($data: datasource, $onEdit: action) as { container panelWrap (class: 'card') { listview lvItems (datasource: $data) { slot content @@ -171,7 +171,7 @@ describe fragment SaveCancelFooter; ### Pattern 1: Standard CRUD Footer ```mdl -define fragment CrudFooter as { +create fragment CrudFooter as { footer footer1 { actionbutton btnSave (caption: 'Save', action: save changes, buttonstyle: primary) actionbutton btnCancel (caption: 'Cancel', action: cancel changes) @@ -197,7 +197,7 @@ create page Module.Order_Edit (...) { ### Pattern 2: Form Field Groups ```mdl -define fragment AddressFields as { +create fragment AddressFields as { textbox txtStreet (label: 'Street', attribute: Street) textbox txtCity (label: 'City', attribute: City) textbox txtZip (label: 'Zip Code', attribute: ZipCode) @@ -217,7 +217,7 @@ create page Module.Customer_Edit (...) { ### Pattern 3: Same Fragment with Prefix ```mdl -define fragment ActionButtons as { +create fragment ActionButtons as { actionbutton btnApprove (caption: 'Approve', action: save changes, buttonstyle: success) actionbutton btnReject (caption: 'Reject', action: cancel changes, buttonstyle: danger) }; @@ -242,8 +242,8 @@ create page Module.DualPanel (...) { ```mdl -- WRONG: Defining the same fragment name twice causes an error -define fragment footer as { ... }; -define fragment footer as { ... }; -- Error: fragment "Footer" already defined +create fragment footer as { ... }; +create fragment footer as { ... }; -- Error: fragment "Footer" already defined ``` ### Missing Fragment @@ -274,10 +274,10 @@ use fragment footer as second_ create page Module.MyPage (...) { use fragment footer -- Error: fragment "Footer" not found }; -define fragment footer as { ... }; +create fragment footer as { ... }; -- CORRECT: Define before use -define fragment footer as { ... }; +create fragment footer as { ... }; create page Module.MyPage (...) { use fragment footer -- OK }; @@ -285,7 +285,7 @@ create page Module.MyPage (...) { ## Validation Checklist -- [ ] All `define fragment` statements appear before their `use fragment` references +- [ ] All `create fragment` statements appear before their `use fragment` references - [ ] No duplicate fragment names in the script - [ ] Prefix used when the same fragment appears multiple times on one page - [ ] Fragment widget names don't conflict with other widgets on the page diff --git a/.claude/skills/mendix/generate-domain-model/reference/syntax.md b/.claude/skills/mendix/generate-domain-model/reference/syntax.md index 1c110a047..ae9f9fe01 100644 --- a/.claude/skills/mendix/generate-domain-model/reference/syntax.md +++ b/.claude/skills/mendix/generate-domain-model/reference/syntax.md @@ -474,8 +474,7 @@ create association Module.EntityWithFK_ReferencedEntity from Module.EntityWithFK to Module.ReferencedEntity type reference owner default -on delete set null -comment 'Additional documentation'; +on delete set null; ``` **Idempotency**: plain `create association` is **not** idempotent — re-running it diff --git a/.claude/skills/mendix/json-structures-and-mappings/SKILL.md b/.claude/skills/mendix/json-structures-and-mappings/SKILL.md index ca84f5b6c..e2304071e 100644 --- a/.claude/skills/mendix/json-structures-and-mappings/SKILL.md +++ b/.claude/skills/mendix/json-structures-and-mappings/SKILL.md @@ -163,12 +163,12 @@ in the demo corpus (22.6%), and the only non-JSON source MDL can create. It holds nothing external: it is a **selection over the domain model**. ```sql -create message definition collection Sales.MD_Order ( - definition OrderMessage for Sales.Order as 'Orders' ( +create message definition collection Sales.MD_Order { + definition OrderMessage for Sales.Order as 'Orders' { OrderId, - Sales.Order_Customer/Sales.Customer ( FirstName ) - ) -); + Sales.Order_Customer/Sales.Customer { FirstName } + } +}; ``` A bare name is an attribute; `Assoc/Module.Entity` is an association. **Name the diff --git a/.claude/skills/mendix/json-structures-and-mappings/reference/message-definitions.md b/.claude/skills/mendix/json-structures-and-mappings/reference/message-definitions.md index 7db587795..8a9fc0709 100644 --- a/.claude/skills/mendix/json-structures-and-mappings/reference/message-definitions.md +++ b/.claude/skills/mendix/json-structures-and-mappings/reference/message-definitions.md @@ -10,14 +10,14 @@ external: it is a **selection over the domain model**. ```sql create message definition collection Sales.MD_Order folder 'Messages' -( - definition OrderMessage for Sales.Order as 'Orders' ( +{ + definition OrderMessage for Sales.Order as 'Orders' { OrderId, Total as 'GrandTotal', - Sales.OrderLine_Order/Sales.OrderLine as 'Lines' ( Sku, Quantity ), - Sales.Order_Customer/Sales.Customer ( FirstName, Address example 'Kerstraat 5' ) - ) -); + Sales.OrderLine_Order/Sales.OrderLine as 'Lines' { Sku, Quantity }, + Sales.Order_Customer/Sales.Customer { FirstName, Address example 'Kerstraat 5' } + } +}; ``` A **bare name is an attribute**; `Assoc/Module.Entity` is an **association** with @@ -62,7 +62,7 @@ alter message definition Sales.MD_Order.OrderMessage add member LastName in Cust alter message definition Sales.MD_Order.OrderMessage set member Total as 'GrandTotal'; alter message definition Sales.MD_Order.OrderMessage drop member Sku in Lines; -alter message definition collection Sales.MD_Order add definition Line for Sales.Line ( Sku ); +alter message definition collection Sales.MD_Order add definition Line for Sales.Line { Sku }; alter message definition collection Sales.MD_Order rename definition Line to OrderLine; alter message definition collection Sales.MD_Order drop definition if exists OrderLine; ``` diff --git a/.claude/skills/mendix/json-structures-and-mappings/reference/rest-api-example.md b/.claude/skills/mendix/json-structures-and-mappings/reference/rest-api-example.md index d07efbbb1..4df48e735 100644 --- a/.claude/skills/mendix/json-structures-and-mappings/reference/rest-api-example.md +++ b/.claude/skills/mendix/json-structures-and-mappings/reference/rest-api-example.md @@ -139,7 +139,7 @@ create microflow Integration.GetCountryInfo () returns string as $json begin -- Fetch country data from REST API - $response = rest call get 'https://restcountries.com/v3.1/name/netherlands' + $response = call rest service get 'https://restcountries.com/v3.1/name/netherlands' header Accept = 'application/json' timeout 30 returns string diff --git a/.claude/skills/mendix/manage-navigation/SKILL.md b/.claude/skills/mendix/manage-navigation/SKILL.md index 8c1977cdd..07cb796d9 100644 --- a/.claude/skills/mendix/manage-navigation/SKILL.md +++ b/.claude/skills/mendix/manage-navigation/SKILL.md @@ -345,7 +345,7 @@ show references to MyModule.Home_Web; show impact of MyModule.Home_Web; -- Full context for a page (includes navigation references) -show context of MyModule.Home_Web; +describe context of MyModule.Home_Web; ``` ## Common Patterns diff --git a/.claude/skills/mendix/manage-security/SKILL.md b/.claude/skills/mendix/manage-security/SKILL.md index 2c0cafa6c..8a0df5735 100644 --- a/.claude/skills/mendix/manage-security/SKILL.md +++ b/.claude/skills/mendix/manage-security/SKILL.md @@ -97,7 +97,7 @@ then stops part-way through. ```sql -- Project-wide security overview -show project security; +describe app security; -- Module roles (all or filtered) show module roles; @@ -114,8 +114,8 @@ show access on entity MyModule.Customer; show access on MyModule.Customer; -- a bare name means the entity -- Full security matrix -show security matrix; -show security matrix in MyModule; +describe security matrix; +describe security matrix in MyModule; ``` ### Describe Commands @@ -262,16 +262,16 @@ grant read (Phone) on entity MyModule.Customer to MyModule.User; grant read *, write * on entity MyModule.Order to MyModule.User where [Status = 'Open']; -- Revoke entity access entirely -revoke MyModule.Viewer on MyModule.Customer; +revoke all on entity MyModule.Customer from MyModule.Viewer; -- Partial revoke: remove read on specific attribute -revoke MyModule.User on MyModule.Customer (read (Phone)); +revoke read (Phone) on entity MyModule.Customer from MyModule.User; -- Partial revoke: downgrade write to read-only -revoke MyModule.User on MyModule.Customer (write (Email)); +revoke write (Email) on entity MyModule.Customer from MyModule.User; -- Partial revoke: remove structural permission -revoke MyModule.User on MyModule.Customer (delete); +revoke delete on entity MyModule.Customer from MyModule.User; ``` #### Members added later @@ -390,9 +390,9 @@ create user role RegularUser (MyModule.User, OtherModule.Reader); -- Create with manage all roles permission create user role SuperAdmin (MyModule.Admin) manage all roles; --- Add/remove module roles +-- Add/drop module roles alter user role RegularUser add module roles (MyModule.Viewer); -alter user role RegularUser remove module roles (MyModule.Viewer); +alter user role RegularUser drop module roles (MyModule.Viewer); -- Remove user role drop user role RegularUser; @@ -506,7 +506,7 @@ create user role AppUser (Shop.User); create user role AppAdmin (Shop.Admin) manage all roles; -- 6. Verify -show security matrix in Shop; +describe security matrix in Shop; describe user role AppAdmin; ``` @@ -543,7 +543,7 @@ rule's default cover it, or change the default. After setting up security, verify with: ```bash # check security matrix -mxcli -p app.mpr -c "show security matrix in MyModule" +mxcli -p app.mpr -c "describe security matrix in MyModule" # Validate with Mendix mxcli docker check -p app.mpr diff --git a/.claude/skills/mendix/migrate-design-prototype/SKILL.md b/.claude/skills/mendix/migrate-design-prototype/SKILL.md index 423769afb..d270f7a2e 100644 --- a/.claude/skills/mendix/migrate-design-prototype/SKILL.md +++ b/.claude/skills/mendix/migrate-design-prototype/SKILL.md @@ -538,7 +538,7 @@ Use `alter page` to attach a class without rewriting the page (see `alter-page`) ```sql alter page ResourceScheduling.Approvals { - set Class = 'ss-appr-card ss-appr-card--conflict' on queueCard; + set (Class: 'ss-appr-card ss-appr-card--conflict') on queueCard; } ``` diff --git a/.claude/skills/mendix/mock-rest-apis/SKILL.md b/.claude/skills/mendix/mock-rest-apis/SKILL.md index 201a54949..6bea6c56d 100644 --- a/.claude/skills/mendix/mock-rest-apis/SKILL.md +++ b/.claude/skills/mendix/mock-rest-apis/SKILL.md @@ -49,7 +49,7 @@ create constant MyModule.ApiBaseUrl type String default 'https://api.example.com create microflow MyModule.CallApi() returns string begin - $response = rest call get @MyModule.ApiBaseUrl + '/rates' + $response = call rest service get @MyModule.ApiBaseUrl + '/rates' header Accept = 'application/json' returns string; return $response; diff --git a/.claude/skills/mendix/odata-data-sharing/SKILL.md b/.claude/skills/mendix/odata-data-sharing/SKILL.md index b38a891e7..156f3340f 100644 --- a/.claude/skills/mendix/odata-data-sharing/SKILL.md +++ b/.claude/skills/mendix/odata-data-sharing/SKILL.md @@ -81,9 +81,18 @@ Then re-import: `CREATE OR MODIFY EXTERNAL ENTITIES FROM F1Now.NowApi` maps the new entity sets. Do **not** `DROP CONSUMED ODATA SERVICE` and recreate it to force a refresh — that invalidates the client ID the existing external entities point at. -Note that `ALTER CONSUMED ODATA SERVICE SET MetadataUrl = …` does *not* re-fetch. Use +Note that `ALTER CONSUMED ODATA SERVICE … SET ( MetadataUrl: … )` does *not* re-fetch. Use `CREATE OR MODIFY` when the contract is what changed. +`describe` prints a client and an external entity as `create or modify …`, and +re-running that output changes nothing: the rewrite keeps what it cannot print +(the client's icon, UseQuerySegment, the catalog and proxy settings; each +attribute's OData mapping). An external entity's attribute **types** are the +service's, not the script's: declaring another (`OrderId: String(20)` for an +`Edm.Int64` property) is CE6616 in mx check. Under `mdl 1` that statement is +refused with nothing written; without the header it still writes, with warning +`MDL-V1-REMOTETYPE`. + **Use Cases for Local Metadata:** - **Offline development** — no network access required - **Testing and CI/CD** — reproducible builds with metadata snapshots @@ -202,31 +211,27 @@ Use the `Folder` property to organize OData documents within modules. ```sql -- Format 1: HTTP(S) URL -create consumed odata service ProductClient.ProductDataApiClient ( +create consumed odata service ProductClient.ProductDataApiClient folder 'Integration/ProductAPI' ( ODataVersion: OData4, - MetadataUrl: 'https://api.example.com/odata/v4/$metadata', - Folder: 'Integration/ProductAPI' + MetadataUrl: 'https://api.example.com/odata/v4/$metadata' ); -- Format 2: Absolute file:// URI -create consumed odata service ProductClient.ProductDataApiClient ( +create consumed odata service ProductClient.ProductDataApiClient folder 'Integration/ProductAPI' ( ODataVersion: OData4, - MetadataUrl: 'file:///Users/team/contracts/productdataapi.xml', - Folder: 'Integration/ProductAPI' + MetadataUrl: 'file:///Users/team/contracts/productdataapi.xml' ); -- Format 3a: Relative path with ./ -create consumed odata service ProductClient.ProductDataApiClient ( +create consumed odata service ProductClient.ProductDataApiClient folder 'Integration/ProductAPI' ( ODataVersion: OData4, - MetadataUrl: './metadata/productdataapi.xml', - Folder: 'Integration/ProductAPI' + MetadataUrl: './metadata/productdataapi.xml' ); -- Format 3b: Relative path without ./ -create consumed odata service ProductClient.ProductDataApiClient ( +create consumed odata service ProductClient.ProductDataApiClient folder 'Integration/ProductAPI' ( ODataVersion: OData4, - MetadataUrl: 'metadata/productdataapi.xml', - Folder: 'Integration/ProductAPI' + MetadataUrl: 'metadata/productdataapi.xml' ); create published odata service ProductApi.ProductDataApi ( diff --git a/.claude/skills/mendix/odata-data-sharing/reference/walkthroughs.md b/.claude/skills/mendix/odata-data-sharing/reference/walkthroughs.md index 54b37a4ed..661a110a2 100644 --- a/.claude/skills/mendix/odata-data-sharing/reference/walkthroughs.md +++ b/.claude/skills/mendix/odata-data-sharing/reference/walkthroughs.md @@ -309,8 +309,8 @@ from consumed odata service ShopClient.ShopApiClient ); -- Toggle the flag without recreating the entity. -alter entity ShopClient.Product set allow_create_change_locally = true; -alter entity ShopClient.Product set allow_create_change_locally = false; +alter entity ShopClient.Product set ( AllowCreateChangeLocally: true ); +alter entity ShopClient.Product set ( AllowCreateChangeLocally: false ); ``` ## Publishing a Non-Persistable Entity (no copy of the data) diff --git a/.claude/skills/mendix/organize-project/SKILL.md b/.claude/skills/mendix/organize-project/SKILL.md index 4797cb3b6..aa2a0a7f1 100644 --- a/.claude/skills/mendix/organize-project/SKILL.md +++ b/.claude/skills/mendix/organize-project/SKILL.md @@ -149,7 +149,7 @@ Three things about the output are deliberate: Use the CLI's `--json` flag for a row per document (`Module, Folder, Kind, Document`) when comparing against a checked-in layout. -Do **not** reach for `show structure` here: it groups by document type at every +Do **not** reach for `describe structure` here: it groups by document type at every depth and never shows which folder a document sits in. ## Moving Documents diff --git a/.claude/skills/mendix/project-settings/SKILL.md b/.claude/skills/mendix/project-settings/SKILL.md index bcf305d91..907322e1f 100644 --- a/.claude/skills/mendix/project-settings/SKILL.md +++ b/.claude/skills/mendix/project-settings/SKILL.md @@ -30,22 +30,22 @@ describe settings; ### Modify Model Settings ```sql -alter settings runtime AfterStartupMicroflow = 'Module.MF_Startup'; -- must return Boolean (CE0142) -alter settings runtime BeforeShutdownMicroflow = 'Module.MF_Shutdown'; -alter settings runtime HealthCheckMicroflow = 'Module.MF_HealthCheck'; -alter settings runtime HashAlgorithm = 'BCrypt'; -alter settings runtime BcryptCost = 12; -alter settings runtime JavaVersion = 'Java21'; -- or '21'; see note below -alter settings runtime RoundingMode = 'HalfUp'; -alter settings runtime AllowUserMultipleSessions = true; -alter settings runtime ScheduledEventTimeZoneCode = 'Etc/UTC'; -alter settings runtime DefaultTimeZoneCode = 'Europe/Amsterdam'; -alter settings runtime FirstDayOfWeek = 'Monday'; -- Default, Monday..Sunday -alter settings runtime DecimalScale = 8; -alter settings runtime EnableDataStorageOptimisticLocking = true; -alter settings runtime UseDatabaseForeignKeyConstraints = true; -alter settings runtime UseOQLVersion2 = true; -alter settings runtime SslCertificateAlgorithm = 'PKIX'; -- PKIX or SunX509 +alter settings runtime ( AfterStartupMicroflow: 'Module.MF_Startup' ); -- must return Boolean (CE0142) +alter settings runtime ( BeforeShutdownMicroflow: 'Module.MF_Shutdown' ); +alter settings runtime ( HealthCheckMicroflow: 'Module.MF_HealthCheck' ); +alter settings runtime ( HashAlgorithm: 'BCrypt' ); +alter settings runtime ( BcryptCost: 12 ); +alter settings runtime ( JavaVersion: 'Java21' ); -- or '21'; see note below +alter settings runtime ( RoundingMode: 'HalfUp' ); +alter settings runtime ( AllowUserMultipleSessions: true ); +alter settings runtime ( ScheduledEventTimeZoneCode: 'Etc/UTC' ); +alter settings runtime ( DefaultTimeZoneCode: 'Europe/Amsterdam' ); +alter settings runtime ( FirstDayOfWeek: 'Monday' ); -- Default, Monday..Sunday +alter settings runtime ( DecimalScale: 8 ); +alter settings runtime ( EnableDataStorageOptimisticLocking: true ); +alter settings runtime ( UseDatabaseForeignKeyConstraints: true ); +alter settings runtime ( UseOQLVersion2: true ); +alter settings runtime ( SslCertificateAlgorithm: 'PKIX' ); -- PKIX or SunX509 ``` **Not every project stores every setting.** Mendix adds model settings over time — @@ -97,18 +97,20 @@ configuration, add `DatabasePassword = '…'` yourself. ```sql -- Full database configuration -alter settings configuration 'Default' - DatabaseType = 'PostgreSql', - DatabaseUrl = 'localhost:5432', - DatabaseName = 'mydb', - DatabaseUserName = 'mendix', - DatabasePassword = 'mendix', - HttpPortNumber = 8080, - ServerPortNumber = 8090; +alter settings configuration 'Default' ( + DatabaseType: 'PostgreSql', + DatabaseUrl: 'localhost:5432', + DatabaseName: 'mydb', + DatabaseUserName: 'mendix', + DatabasePassword: 'mendix', + HttpPortNumber: 8080, + ServerPortNumber: 8090 +); -- Update a single field -alter settings configuration 'Default' - DatabaseUrl = 'newhost:5432'; +alter settings configuration 'Default' ( + DatabaseUrl: 'newhost:5432' +); ``` `HttpPortNumber`, `ServerPortNumber`, `BcryptCost`, `DefaultTaskParallelism` and @@ -131,14 +133,14 @@ show constant values; show constant values in MyModule; -- Filter by module -- Override a constant value in a configuration -alter settings constant 'BusinessEvents.ServerUrl' value 'kafka:9092' +alter settings constant @BusinessEvents.ServerUrl value 'kafka:9092' in configuration 'Default'; -- Without IN CONFIGURATION (uses first configuration) -alter settings constant 'MyModule.ApiKey' value 'abc123'; +alter settings constant @MyModule.ApiKey value 'abc123'; -- Remove a constant override (reset to default) -alter settings drop constant 'MyModule.ApiKey' in configuration 'Default'; +alter settings drop constant @MyModule.ApiKey in configuration 'Default'; ``` #### Shared vs private values @@ -169,10 +171,11 @@ on shared values only: create configuration 'Staging'; -- Create with properties -create configuration 'Production' - DatabaseType = 'PostgreSql', - DatabaseUrl = 'prod-db:5432', - HttpPortNumber = 8080; +create configuration 'Production' ( + DatabaseType: 'PostgreSql', + DatabaseUrl: 'prod-db:5432', + HttpPortNumber: 8080 +); -- Drop a configuration drop configuration 'Staging'; @@ -194,10 +197,10 @@ and produces nothing at build time — so enable the language first. alter settings LANGUAGE add 'de_DE'; alter settings LANGUAGE add 'ar_SD' (CheckCompleteness: true, CustomDateFormat: 'yyyy-MM-dd'); alter settings LANGUAGE modify 'de_DE' (CheckCompleteness: true); -alter settings LANGUAGE remove 'de_DE'; +alter settings Language drop 'de_DE'; -- the default must already be enabled -alter settings LANGUAGE DefaultLanguageCode = 'en_US'; +alter settings LANGUAGE ( DefaultLanguageCode: 'en_US' ); ``` A language is identified by its **code** — "Arabic, Sudan" is derived from @@ -213,7 +216,7 @@ A language is identified by its **code** — "Arabic, Sudan" is derived from **Set the default language BEFORE authoring content.** The default is not only a fallback — it is the language a new caption is stored under, because Mendix has no -language-neutral text. `alter settings LANGUAGE DefaultLanguageCode = 'nl_NL'` +language-neutral text. `alter settings LANGUAGE (DefaultLanguageCode: 'nl_NL')` *after* creating a page leaves that page's texts in the old language, and nothing reports it: `mx check` is 0 errors either way and the symptom shows up only in Studio Pro, as the empty-caption placeholder plus a "no translation" warning. @@ -231,9 +234,10 @@ list. ```sql -alter settings workflows - UserEntity = 'System.User', - DefaultTaskParallelism = 3; +alter settings workflows ( + UserEntity: 'System.User', + DefaultTaskParallelism: 3 +); ``` ### Workflow Groups (Mendix 11.2+) @@ -251,7 +255,7 @@ alter settings workflows add or modify group 'Approvers' (Description: 'Approves -- changes only the options it names alter settings workflows modify group 'Reviewers' (Description: 'Second-line review'); -alter settings workflows remove group 'Reviewers'; +alter settings workflows drop group 'Reviewers'; show workflow groups; ``` @@ -281,24 +285,26 @@ Four things worth knowing: ### PostgreSQL Configuration ```sql -alter settings configuration 'Default' - DatabaseType = 'PostgreSql', - DatabaseUrl = 'localhost:5432', - DatabaseName = 'myapp', - DatabaseUserName = 'mendix', - DatabasePassword = 'mendix', - HttpPortNumber = 8080; +alter settings configuration 'Default' ( + DatabaseType: 'PostgreSql', + DatabaseUrl: 'localhost:5432', + DatabaseName: 'myapp', + DatabaseUserName: 'mendix', + DatabasePassword: 'mendix', + HttpPortNumber: 8080 +); ``` ### SQL Server Configuration ```sql -alter settings configuration 'Default' - DatabaseType = 'SqlServer', - DatabaseUrl = 'localhost:1433', - DatabaseName = 'myapp', - DatabaseUserName = 'sa', - DatabasePassword = 'MyPassword', - HttpPortNumber = 8080; +alter settings configuration 'Default' ( + DatabaseType: 'SqlServer', + DatabaseUrl: 'localhost:1433', + DatabaseName: 'myapp', + DatabaseUserName: 'sa', + DatabasePassword: 'MyPassword', + HttpPortNumber: 8080 +); ``` ## Checklist diff --git a/.claude/skills/mendix/regular-expressions/SKILL.md b/.claude/skills/mendix/regular-expressions/SKILL.md index 0f70dd884..b579b453d 100644 --- a/.claude/skills/mendix/regular-expressions/SKILL.md +++ b/.claude/skills/mendix/regular-expressions/SKILL.md @@ -23,9 +23,9 @@ list regular expressions; list regular expressions in Val; describe regular expression Val.EmailAddress; -- re-executable MDL +/** A, not too restrictive, email address regular expression */ create regular expression Val.EmailAddress ( - Expression: '\w+((-|\+|\.)\w+)*@\w+([\.-]?\w+)*(\.\w{2,})+', - Documentation: 'A, not too restrictive, email address regular expression' + Expression: '\w+((-|\+|\.)\w+)*@\w+([\.-]?\w+)*(\.\w{2,})+' ); drop regular expression Val.EmailAddress; @@ -102,7 +102,7 @@ create entity Val.Person ( Code: String(20) unique error message 'Code must be unique' ); -alter entity Val.Person modify attribute Email String(200) +alter entity Val.Person modify attribute Email: String(200) not null error message 'Email is required'; ``` diff --git a/.claude/skills/mendix/rest-call-from-json/SKILL.md b/.claude/skills/mendix/rest-call-from-json/SKILL.md index 19aad2b57..2c9f8d71d 100644 --- a/.claude/skills/mendix/rest-call-from-json/SKILL.md +++ b/.claude/skills/mendix/rest-call-from-json/SKILL.md @@ -121,7 +121,7 @@ begin @position(185, 200) declare $endpoint string = $baseUrl + '/path'; @position(375, 200) - $Result = rest call get '{1}' with ({1} = $endpoint) + $Result = call rest service get '{1}' with ({1} = $endpoint) header 'Accept' = 'application/json' timeout 300 returns mapping Module.IMM_MyMapping as Module.MyRootObject on error rollback; @@ -139,7 +139,7 @@ end; **For list responses** (JSON root is an array): ```sql - $Results = rest call get '{1}' with ({1} = $endpoint) + $Results = call rest service get '{1}' with ({1} = $endpoint) header 'Accept' = 'application/json' timeout 300 returns mapping Module.IMM_MyMapping as Module.MyItem on error rollback; @@ -220,7 +220,7 @@ create or modify microflow Module.POST_Document_Upload ( returns boolean as $Ok begin declare $Ok boolean = false; - $Response = rest call post 'https://api.example.com/documents' + $Response = call rest service post 'https://api.example.com/documents' header 'ContentType' = 'application/pdf' body binary $Doc/Contents timeout 300 @@ -307,7 +307,7 @@ begin @position(185, 200) declare $endpoint string = $baseUrl + '/data/web/random'; @position(375, 200) - $Result = rest call get '{1}' with ({1} = $endpoint) + $Result = call rest service get '{1}' with ({1} = $endpoint) header 'Accept' = 'application/json' timeout 300 returns mapping Integrations.IMM_BibleVerse as Integrations.BibleApiResponse on error rollback; diff --git a/.claude/skills/mendix/rest-client/SKILL.md b/.claude/skills/mendix/rest-client/SKILL.md index 8e3658de8..f42788c55 100644 --- a/.claude/skills/mendix/rest-client/SKILL.md +++ b/.claude/skills/mendix/rest-client/SKILL.md @@ -81,22 +81,22 @@ create consumed rest service Module.OpenMeteoAPI ( authentication: none ) { - operation GetForecast { + operation GetForecast ( method: get, path: '/forecast', query: ($latitude: decimal, $longitude: decimal, $current: string), headers: ('Accept' = 'application/json'), timeout: 30, response: json as $WeatherJson - } + ) - operation PostData { + operation PostData ( method: post, path: '/submit', headers: ('Content-Type' = 'application/json'), body: json from $JsonPayload, response: none - } + ) }; ``` @@ -135,7 +135,7 @@ returns HTTP 200 with a 4-byte payload. Upload binary from a **microflow** instead, which does have a binary body: ```sql -rest call post 'https://api.example.com/upload' +call rest service post 'https://api.example.com/upload' header 'ContentType' = 'application/pdf' body binary $Doc/Contents returns response; @@ -260,40 +260,40 @@ Call an HTTP endpoint directly from a microflow — no REST client document need ```sql -- Simple GET returning a string -$response = rest call get 'https://api.example.com/data' +$response = call rest service get 'https://api.example.com/data' header Accept = 'application/json' timeout 30 returns string; -- GET with URL template parameters -$response = rest call get 'https://api.example.com/users/{1}' with ( +$response = call rest service get 'https://api.example.com/users/{1}' with ( {1} = toString($UserId) ) header Accept = 'application/json' returns string; -- POST with body -$response = rest call post 'https://api.example.com/items' +$response = call rest service post 'https://api.example.com/items' header 'Content-Type' = 'application/json' body '{"name": "test"}' returns string; -- With basic auth -$response = rest call get 'https://api.example.com/secure' +$response = call rest service get 'https://api.example.com/secure' auth basic 'username' password 'password' returns string; -- With import mapping (JSON → entity) -$item = rest call get 'https://api.example.com/item/1' +$item = call rest service get 'https://api.example.com/item/1' header Accept = 'application/json' returns mapping Module.IMM_Item as Module.Item; -- Fire and forget -rest call delete 'https://api.example.com/item/1' +call rest service delete 'https://api.example.com/item/1' returns nothing; -- Error handling -$response = rest call get 'https://api.example.com/data' +$response = call rest service get 'https://api.example.com/data' returns string on error continue; ``` @@ -417,13 +417,13 @@ create consumed rest service Module.WeatherAPI ( authentication: none ) { - operation GetCurrent { + operation GetCurrent ( method: get, path: '/forecast', query: ($latitude: decimal, $longitude: decimal, $current: string), headers: ('Accept' = 'application/json'), response: json as $Result - } + ) }; -- 5. Microflow (REST Client → Transform → Import) diff --git a/.claude/skills/mendix/write-layouts/SKILL.md b/.claude/skills/mendix/write-layouts/SKILL.md index 8a9055d5f..2e17bccd5 100644 --- a/.claude/skills/mendix/write-layouts/SKILL.md +++ b/.claude/skills/mendix/write-layouts/SKILL.md @@ -96,8 +96,8 @@ survive. Same operations as `ALTER PAGE`: ```sql alter layout MyModule.App_Default { insert into layoutContainer.top { snippetcall bar (snippet: MyModule.SNIPPET_ThemeBar) }; - set Content = 'My App' on brandText; - drop widget oldBanner; + set (Content: 'My App') on brandText; + drop oldBanner; }; ``` diff --git a/.claude/skills/mendix/write-microflows/reference/integration.md b/.claude/skills/mendix/write-microflows/reference/integration.md index 73ba8ba89..abc2e6a6b 100644 --- a/.claude/skills/mendix/write-microflows/reference/integration.md +++ b/.claude/skills/mendix/write-microflows/reference/integration.md @@ -133,13 +133,13 @@ Direct HTTP call with URL, headers, auth, body, and response handling specified ```mdl -- Simple GET returning string -$response = rest call get 'https://api.example.com/data' +$response = call rest service get 'https://api.example.com/data' header Accept = 'application/json' timeout 30 returns string; -- POST with JSON body -$response = rest call post 'https://api.example.com/items' +$response = call rest service post 'https://api.example.com/items' header 'Content-Type' = 'application/json' header Accept = 'application/json' body '{{"name": "{1}", "value": {2}}' with ( @@ -155,28 +155,28 @@ $response = rest call post 'https://api.example.com/items' -- itself, and the content type goes on a header. A consumed REST CLIENT -- document has no binary body — `Body: file from $Doc` there is refused as -- MDL-REST02 — so binary uploads belong here. -$response = rest call post 'https://api.example.com/upload' +$response = call rest service post 'https://api.example.com/upload' header 'ContentType' = 'application/pdf' body binary $Doc/Contents timeout 300 returns response; -- GET with URL template parameters -$response = rest call get 'https://api.example.com/users/{1}' with ( +$response = call rest service get 'https://api.example.com/users/{1}' with ( {1} = toString($UserId) ) header Accept = 'application/json' returns string; -- With basic authentication -$response = rest call get 'https://api.example.com/secure' +$response = call rest service get 'https://api.example.com/secure' header Accept = 'application/json' auth basic $username password $password timeout 30 returns string; -- DELETE (no response) -rest call delete 'https://api.example.com/items/{1}' with ( +call rest service delete 'https://api.example.com/items/{1}' with ( {1} = $ItemId ) returns nothing @@ -200,7 +200,7 @@ create persistent entity MyModule.MyFile extends System.FileDocument (); create microflow MyModule.ACT_Download ($Location: String) begin - $file = rest call get '{1}' with ({1} = $Location) + $file = call rest service get '{1}' with ({1} = $Location) header 'Accept' = 'application/octet-stream' timeout 300 returns MyModule.MyFile; diff --git a/.claude/skills/mendix/write-workflows/SKILL.md b/.claude/skills/mendix/write-workflows/SKILL.md index bce603c6d..aaaa351de 100644 --- a/.claude/skills/mendix/write-workflows/SKILL.md +++ b/.claude/skills/mendix/write-workflows/SKILL.md @@ -87,6 +87,11 @@ build with `CE0117 "Error(s) in expression."`. ## Activities +Expressions are bare, as everywhere in MDL: a decision's condition, a timer's +delay, a due date (`decision $WorkflowContext/Total > 1000`, `due date +addDays([%CurrentDateTime%], 3)`). The older string form (`decision '…'`) still +parses and warns `MDL-DEPR080`; `mxcli fmt --upgrade` rewrites it. + Every activity statement ends with `;`. Blocks `{ … }` nest a sub-flow. ```sql @@ -113,14 +118,14 @@ begin -- Decision: a boolean or enum exclusive split. The name is optional; give one -- when a `jump to` targets it. - decision decision1 '$WorkflowContext/Total > 1000' + decision decision1 $WorkflowContext/Total > 1000 outcomes true -> { call microflow Module.ACT_Escalate; } false -> { call microflow Module.ACT_AutoApprove; }; -- An enum decision: each outcome is a FULLY QUALIFIED enumeration value -- (Module.Enumeration.Value), plus one '' outcome for "none of the above". - decision decision2 '$WorkflowContext/Status' + decision decision2 $WorkflowContext/Status outcomes 'Module.ENUM_Status.Approved' -> { } 'Module.ENUM_Status.Rejected' -> { } @@ -132,14 +137,14 @@ begin path 2 { call microflow Module.ACT_Log; }; -- Wait for a timer, then continue (duration is a Mendix expression) - wait for timer timer1 'addHours([%CurrentDateTime%], 1)'; + wait for timer timer1 addHours([%CurrentDateTime%], 1); -- Wait for an external notification (e.g. an event) wait for notification waitForNotification1; -- An intermediate notification event (Mendix 11.11+): what `notify workflow` -- targets by name - notification DocumentsReceived comment 'Documents received'; + notification DocumentsReceived caption 'Documents received'; -- Loop back, or stop the whole workflow, from inside an outcome. A `jump to` -- and an `end workflow` must each END their path, so neither can close the @@ -148,11 +153,11 @@ begin page Module.ReviewPage outcomes 'Redo' { jump to Review; } - 'Cancel' { end workflow comment 'Cancelled'; } + 'Cancel' { end workflow caption 'Cancelled'; } 'Done' { }; -- Call a sub-workflow - call workflow Module.SubProcess as callWorkflow1 comment 'delegate'; + call workflow Module.SubProcess as callWorkflow1 caption 'delegate'; end workflow; ``` @@ -173,7 +178,7 @@ begin user task Review 'Review' page Module.ReviewPage outcomes 'Done' { } - boundary event interrupting timer 'addDays([%CurrentDateTime%], 3)' { + boundary event interrupting timer addDays([%CurrentDateTime%], 3) { call microflow Module.ACT_Escalate; }; end workflow; @@ -219,7 +224,7 @@ begin call microflow HR.ACT_LogCancel; }; event subprocess ESP_Reminder 'Daily reminder' - on non interrupting timer 'addDays([%CurrentDateTime%], 1)' as espReminderStart { + on non interrupting timer addDays([%CurrentDateTime%], 1) as espReminderStart { call microflow HR.ACT_Remind; }; end workflow; @@ -272,7 +277,7 @@ typed, and each op writes exactly one outcome type into it: | `insert outcome '' on X { }` | `UserTaskOutcome` | a user task | | `insert condition '' on X { }` | `…ConditionOutcome` | a decision, a call microflow | | `insert path on X { }` | `ParallelSplitOutcome` | a parallel split | -| `insert boundary event on X interrupting timer '' { }` | a boundary event | user task, call microflow, call workflow, wait for notification | +| `insert boundary event on X interrupting timer { }` | a boundary event | user task, call microflow, call workflow, wait for notification | Aim one at the wrong kind and the outcome lands in a list that cannot hold it, which is **not** a build error: the project stops **loading**, so Studio Pro will @@ -289,7 +294,11 @@ tasks, decisions, splits, jump-to targets, wait activities and boundary events all come back as statements (not comments). You can learn the exact syntax by describing a Studio-Pro-authored workflow, and `describe → drop → exec` reproduces a workflow that builds. (The implicit start/end activities are -omitted, as they are re-synthesised on create.) +omitted, as they are re-synthesised on create.) `describe` prints +`create or modify workflow`, and re-running it on the workflow it came from +changes nothing: the rewrite keeps the stored names of the activities MDL cannot +name (Studio Pro's `start1`, `end1`, …), the empty flow of an outcome that leads +nowhere, and the empty event sub-process list. That is for learning the syntax and for workflows your scripts own. **To change an existing Studio Pro workflow, use `alter workflow`**, never drop → exec: that @@ -444,7 +453,7 @@ values. The full list and the System **entities** are in `system-module`. reaches the end of the workflow needs no `end workflow`. - The main flow needs none: the body's closing `end workflow` is its End. An outcome left **empty** does not stop anything — it rejoins the main flow. - `comment '…'` sets the End's caption, as on every workflow activity. + `caption '…'` sets the End's caption, as on every workflow activity (`comment '…'` is its deprecated alias, MDL-DEPR104). - **A multi-user task says who must respond and how their outcomes decide**: `participants all | | percent`, `decide by …` and `await all users`, diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c0e773a2..40db61988 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,9 +8,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Changed +- **One way to refer to a constant: `@Module.Const`** (ako/mxcli#753, R5) — the spelling a Mendix expression already uses is now the only one: a consumed REST service's credentials (`Username: @Shop.ApiUser`), an agent-editor model's or knowledge base's `Key: @Shop.ApiKey` (also `alter model … set Key = @Shop.ApiKey`), and `alter settings [drop] constant @Shop.ApiUrl … in configuration 'Default'`. `describe model` and `describe knowledge base` print `Key: @…`. **Migrating a script:** nothing breaks — a REST credential's `$ApiUser` (a constant of the service's own module) warns **MDL-DEPR083**, a bare `Key: Shop.ApiKey` **MDL-DEPR084**, and `alter settings constant 'Shop.ApiUrl'` **MDL-DEPR085**; each builds the same statement and `mxcli fmt --upgrade` rewrites it. A settings string that is not a `Module.Name` is left in place and reported. +- **An entity revoke mirrors the grant** (ako/mxcli#753, R5) — `revoke write (Email), delete on entity Shop.Order from Shop.User;` takes rights away and keeps the rule, `revoke all on entity Shop.Order from Shop.User;` removes the rule. **Migrating a script:** nothing breaks — `revoke Shop.User on Shop.Order [(rights)]` still parses and builds the same statement, `check` / `exec` warn **MDL-DEPR082**, and `mxcli fmt --upgrade` rewrites it. +- **A page widget's conditional `Visible:` / `Editable:` is a bare expression** (ako/mxcli#753, R5) — `textbox t (Attribute: Name, Visible: $currentObject/IsActive, Editable: $currentObject/Status != 'Closed')`, and the same in `alter page … set (Visible: …) on t`. It is a client expression, not XPath, so it is written like every other expression and stored exactly as written: an attribute is `$currentObject/Attr`. A plain value keeps its meaning (`Visible: false`, `Editable: Never`). `describe page` prints the bare form. `Visible: $currentObject/Active` used to parse as a data source and fail check (MDL-WIDGET19); it now stores the condition. **Migrating a script:** nothing breaks — `Visible: [IsActive]` still parses, roots a bare attribute in `$currentObject` as before, and warns **MDL-DEPR081**; `mxcli fmt --upgrade` rewrites it to the expression it stores (`Visible: $currentObject/IsActive`). A constant condition (`Editable: [false]`, stored as the condition `false` rather than as `Never`) has no bare spelling, keeps its brackets and is not reported. +- **A workflow's expressions are written bare** (ako/mxcli#753, R5) — a decision's condition, `wait for timer`'s delay, a timer boundary event, a timer event sub-process's first execution time and a due date (workflow, user task, `alter workflow … set [activity …] due date`) are MDL expressions like every other: `decision $WorkflowContext/Total > 1000 comment 'Large order?'`, `due date addDays([%CurrentDateTime%], 3)`. The expression is stored as written. `describe workflow` prints the bare form; a stored expression that would not read back as itself (one the MDL expression grammar cannot parse, or with surrounding whitespace) keeps the string form, and a decision or `wait for timer` whose expression would otherwise read as the activity's name gets its name written. **Migrating a script:** nothing breaks — the string form still parses with the same meaning (its content is the expression; no slot takes a string-valued expression), `check` / `exec` warn **MDL-DEPR080**, and `mxcli fmt --upgrade` rewrites it. A string that does not read back as the same bare expression is left in place and reported. `set due date ''` still clears a due date and is not reported. +- **Documentation is a `/** … */` doc comment, the folder a `folder '…'` clause after the name, and a workflow activity's caption `caption '…'`** (ako/mxcli#755, R9) — on every document. A page, snippet, consumed REST service, consumed or published OData service and published REST service take `create page M.P folder 'Admin' (…)`; `describe` writes the clause, and for a consumed REST service it now writes the folder at all. `describe constant` and `describe json structure` write the documentation as a doc comment only. A workflow's `call microflow … caption 'Notify'`, `notification Ready caption 'Ready'` and `end workflow caption 'Done'` name what the clause always set. Measured on the Studio Pro-authored PedApp and TestApp fixtures: the round-trip harness stays green, and describe emits none of the old spellings. **Migrating a script:** nothing breaks — `comment '…'` on a constant, association, JSON structure or image collection (**MDL-DEPR100**), a workflow activity's `comment '…'` (**MDL-DEPR104**), `Folder: '…'` in those headers (**MDL-DEPR105**) and `Documentation: '…'` in a regular expression, task queue or scheduled event (**MDL-DEPR106**) still parse and build the same statement, `check` / `exec` warn, and `mxcli fmt --upgrade` moves the text; a statement that has both spellings keeps the one it stored. `create or modify` of a JSON structure, association, regular expression, task queue or scheduled event now updates its documentation when the statement states one; before, it kept the stored text. +- **Session commands belong to the REPL** (ako/mxcli#755, R7) — `connect`, `disconnect`, `use`, `set = …`, `status`, `check`, `build`, `lint`, `debug`, `execute script`, `execute runtime`, `help` and `introspect api` need a session, not a model. Under the `mdl 1;` preview header a script that holds one is refused; without the header it runs as before and `check` / `exec` warn **MDL-V1-SESSION**. The REPL keeps accepting them. Put the session on the command line instead: `mxcli exec script.mdl -p app.mpr --json`. `exit` / `quit` are not session commands. - **Document types are named as Studio Pro names them** (ako/mxcli#755, R10) — `consumed rest service` (was `rest client`), `consumed odata service` (was `odata client`), `published odata service` (was `odata service`), `task queue` (was `queue`), `alter app security` (was `alter project security`) and `alter settings runtime` (was `alter settings model`), in every statement that names the type: create, alter, drop, describe, list, move, grant/revoke on a published OData service, and an external entity's `from consumed odata service`. `describe` writes the new names. **Migrating a script:** nothing breaks — the old names still parse and build the identical statement, `check` / `exec` warn **MDL-DEPR550** to **MDL-DEPR555**, and `mxcli fmt --upgrade` rewrites them. `call microflow … in queue M.Q` is a call option, not the type name, and is unchanged. -- **An entity grant names the rights first, and every XPath is written in `[ ]`** (ako/mxcli#753, R5) — `grant read *, write (Email), create on entity Shop.Order to Shop.User where [Status = 'Open'];` takes the word order of every other grant, and the constraint is bracketed like a datasource or retrieve `where`, so the quotes inside it are written once instead of doubled. A workflow user task's targeting is the same: `targeting users xpath [System.UserRoles = '[%UserRole_Manager%]']`, and `alter workflow … set activity … targeting xpath [ … ]`. Sibling predicate groups (`[a][b]`) are one constraint, kept as written. `describe entity` and `describe workflow` print the new forms; a stored constraint the bracketed grammar does not read (none in the Studio Pro-authored PedApp and TestApp fixtures) is printed in the old quoted form so the output stays re-executable. **Migrating a script:** nothing breaks — `grant Shop.User on Shop.Order (read *) where '[…]'` and `targeting xpath '[…]'` still parse and build the same rule, and `check` / `exec` warn **MDL-DEPR030** and **MDL-DEPR031**; `mxcli fmt --upgrade` rewrites both. A quoted value that is not a bracketed XPath is left in place and reported. `revoke` keeps its current form for now. +- **Integration documents put properties in `( )` and children in `{ }`; an `alter microflow` fragment is `begin … end`** (ako/mxcli#754, R2) — a consumed REST service's operations are `operation GetUser ( Method: get, Path: '/u/{id}', Response: none )`; an agent's attachments are `tool X ( … )`, `mcp service M.S ( … )` and `knowledge base K ( … )`, in `create agent` and `alter agent … add`; an image collection is `create image collection M.Icons { image Logo ( File: 'logo.png' ) }`; a message definition collection is `{ definition D for M.E { A, M.E_B/M.B { C } } }`, and an association may select no member (`M.E_B/M.B { }`, which describe already printed as the unparseable `()`); and `alter microflow` / `alter nanoflow` take `insert after begin … end;` and `replace with begin … end;`, the fragment written as the body of `create microflow` is. `describe` prints the new forms. **Migrating a script:** nothing breaks — the old brackets still parse and build the identical statement, `check` / `exec` warn **MDL-DEPR070** (operation), **MDL-DEPR071** (agent attachment), **MDL-DEPR072** (image collection), **MDL-DEPR073** (message definitions) and **MDL-DEPR074** (alter fragment), once per statement, and `mxcli fmt --upgrade` rewrites them. +- **An entity grant names the rights first, and every XPath is written in `[ ]`** (ako/mxcli#753, R5) — `grant read *, write (Email), create on entity Shop.Order to Shop.User where [Status = 'Open'];` takes the word order of every other grant, and the constraint is bracketed like a datasource or retrieve `where`, so the quotes inside it are written once instead of doubled. A workflow user task's targeting is the same: `targeting users xpath [System.UserRoles = '[%UserRole_Manager%]']`, and `alter workflow … set activity … targeting xpath [ … ]`. Sibling predicate groups (`[a][b]`) are one constraint, kept as written. `describe entity` and `describe workflow` print the new forms; a stored constraint the bracketed grammar does not read (none in the Studio Pro-authored PedApp and TestApp fixtures) is printed in the old quoted form so the output stays re-executable. **Migrating a script:** nothing breaks — `grant Shop.User on Shop.Order (read *) where '[…]'` and `targeting xpath '[…]'` still parse and build the same rule, and `check` / `exec` warn **MDL-DEPR030** and **MDL-DEPR031**; `mxcli fmt --upgrade` rewrites both. A quoted value that is not a bracketed XPath is left in place and reported. - **A microflow error handler is `on error [without rollback] begin … end error`; `while` takes `begin … end while` like `loop`** (ako/mxcli#754, R2) — braces hold declarative children and imperative flow is `begin … end `, and the error handler was the only brace block inside a microflow. `describe microflow` / `describe nanoflow` now print `commit $O on error begin … end error;` (an empty handler `… on error without rollback begin end error;`), and so do the generated test flows of `mxcli test`. Under the `mdl 1;` preview header a `while` without `begin`, or ending in a bare `end`, is an error; without the header it keeps parsing and warns **MDL-V1-WHILE**, and `mxcli fmt --upgrade --header` inserts the missing words. Measured on the Studio Pro-authored PedApp and TestApp fixtures: the round-trip harness stays green. **Migrating a script:** nothing breaks — `on error { … }` still parses, builds the identical handler, and `check` / `exec` warn **MDL-DEPR540**; `mxcli fmt --upgrade` rewrites it. - **One argument form at every call site: `Param = expression`; one text-template form: `with ({1} = …)`** (ako/mxcli#751, R3/R4) — `call microflow`/`nanoflow`/`java action`/…, `execute database query`, `send rest request`, `show page`, and page/button actions and microflow/nanoflow data sources all bind an argument as `Order = $Order`, with no `$` on the parameter name and `=` rather than `:`. A workflow's `call microflow` / `call workflow` takes its arguments the same way, right after the callee, as bare expressions: `call microflow Shop.ACT_Process(Order = $WorkflowContext) as act1 comment 'Process'`. `show message` and `validation feedback` take `with ({1} = $a, {2} = $b)`, like `log` and the REST templates. `describe` writes these forms; it also writes a validation feedback's template arguments, which it used to drop, so describe → exec lost them. Measured on a copy of the Studio Pro-authored TestApp (Mendix 11.14.0): a script in the new forms and the same script in the old forms describe identically, and `mx check` reports 0 errors. **Migrating a script:** nothing breaks — `$Param = e` (**MDL-DEPR006**), `Param: e` (**MDL-DEPR007**), a workflow's `with (Param = '')` (**MDL-DEPR008**, same meaning: the string's content is the expression) and `objects [..]` / `parameters [..]` (**MDL-DEPR009**) still parse and build the same model, `check` / `exec` warn, and `mxcli fmt --upgrade` rewrites them. A workflow string whose content does not read back as the same bare expression is left in place and reported, and `describe` keeps the string form for such a stored expression. - **`describe page` no longer invents names Mendix does not store; `alter page` addresses a DataGrid 2 column by what it shows** (ako/mxcli#749, R12) — a layout grid's rows and columns, a data grid's columns and control bar, a gallery's template and filter, a chart's series and other object-list items have no Name in the model, and describe used to print one it made up (`row row1`, `column col3`, `controlbar controlBar1`, and a data-grid column named after its attribute, so two columns over `FullName` were both `column FullName`). They now print without one — `row { column (DesktopWidth: 6) { … } }`, `column (Attribute: FullName, Caption: 'Full name')` — and the widget name is optional in the grammar for exactly those elements. A column is addressed as `grid column(Attr)`, written as describe writes the attribute (`Owner/Name` over an association), or `grid column('Caption')`; an address two columns share is refused, listing the matches, unless `@n` picks one (`drop dg column(FullName)@2`). A widget Mendix does store a name for still needs one: `check` reports a missing one as **MDL-WIDGET35** and the page builder refuses it. Measured on the Studio Pro-authored PedApp fixture: the round-trip harness stays green, and `mx check` 11.13 shows no new error after `alter page … column(…)` edits and a create with nameless rows, grid columns and gallery slots. **Migrating a script:** nothing breaks — a name written on one of those elements still parses, is ignored as before, and `check` / `exec` warn **MDL-DEPR005**; `mxcli fmt --upgrade` removes it. The older `grid.Column` address keeps working. The info-level MDL-WIDGET16 note about dropped column names is gone, replaced by that warning. @@ -26,6 +33,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). ### Fixed +- **A misspelt statement keyword parsed as a help topic** (ako/mxcli#755, R7) — `helpStatement` accepted any word, so `craete module Foo;` parsed without error and did nothing, and `craete entity M.E (…)` reported its error at the `(`. Only `help`, `exit` and `quit` start that statement now, and a statement that starts with an unknown word is reported at the word: `unknown statement 'craete' — did you mean 'create'?`. - **Keyword operators were fused in some stored expressions** — a page action's microflow argument `Flag: $a and $b` was stored as `$aand$b`, `if $x then 'a' else 'b'` as `if$xthen'a'else'b'`; the same in `contentparams` values, `send rest request … with (…)` parameters and a dynamic `execute database query`. Those four places took the expression's text without its whitespace; literals, `+` and `$currentObject` were unaffected, which is why it went unnoticed, and `check --references` passed. Measured by decoding the stored units on a Mendix 11.14.0 project. The expression is now stored as written, with MDL comments removed, as the microflow expression sites already did. - **An input bound to an attribute outside any data container was written with no binding** — `textbox t (Attribute: FullName)` at the top of a page (or of a snippet, or inside a plain container) has no entity to qualify the name with, and the writer stored it as `AttributeRef: null`. Plain `check` passed, `exec --no-check` and `alter page … insert` at page level said success, and mxbuild 11.13.0 failed the page with CE0544 "This widget can only function inside a data context" + CE7005 (text box, text area, date picker, check box, radio buttons, drop-down), CE0402 (dynamic text `Attribute:`) or CE0642 (combo box). A qualified attribute there is stored and fails the same way (CE0544 / CE2421 / CE1365 / CE7247 "Move this widget into a data container"). `Attribute: $P/Name` and `Attribute: $currentObject/Name` never parsed as an attribute path and were dropped even inside a data view. `check` now reports all three as **MDL-WIDGET34** (no project needed), and the page builder refuses them with the widget named, so nothing is written; `alter page` refuses a bare name it has no entity for. Place the widget in a data view, list view, gallery or data grid and bind the attribute by name. - **An expression property written in brackets was silently dropped** (mendixlabs/mxcli#750) — `dynamicclasses: [ if $currentObject/Featured then 'a' else 'b' ]`, the spelling #750 proposes, parsed as a list that no writer reads: `check` was clean, `exec` said `Created page`, and the widget was stored with no dynamic class. `alter page … set DynamicClasses = [ … ]` said `Altered page` and changed nothing, and a column's `DynamicCellClass` stored the list's text — tokens fused, `[if$x/Ythen'a'else'b']` — as its expression. Measured on a copy of a Mendix 11.14.0 project with the pre-fix binary. `mxcli check` now reports **MDL-WIDGET32** for `DynamicClasses` and `DynamicCellClass` written as a list (no project needed), and ALTER refuses it, so `check -p` reports that too. Write the expression quoted. diff --git a/cmd/mxcli/cmd_describe.go b/cmd/mxcli/cmd_describe.go index 04b286999..6029b8bff 100644 --- a/cmd/mxcli/cmd_describe.go +++ b/cmd/mxcli/cmd_describe.go @@ -147,7 +147,7 @@ Example: case "USERROLE", "USER ROLE": mdlCmd = fmt.Sprintf("DESCRIBE USER ROLE '%s'", name) case "PROJECTSECURITY", "PROJECT SECURITY": - mdlCmd = "SHOW PROJECT SECURITY" + mdlCmd = "DESCRIBE APP SECURITY" case "SETTINGS": mdlCmd = "DESCRIBE SETTINGS" case "DEMOUSER", "DEMO USER": diff --git a/cmd/mxcli/cmd_new.go b/cmd/mxcli/cmd_new.go index 0304d1bb3..62b88cf40 100644 --- a/cmd/mxcli/cmd_new.go +++ b/cmd/mxcli/cmd_new.go @@ -266,7 +266,7 @@ Examples: if lowered, err := alignJavaVersion(mprPath, os.Stdout); err != nil { fmt.Fprintf(os.Stderr, " Warning: could not align the project's Java version: %v\n", err) fmt.Fprintln(os.Stderr, " If the first build fails with 'release version NN not supported',") - fmt.Fprintln(os.Stderr, " run: mxcli -p .mpr -c \"alter settings runtime JavaVersion = '21'\"") + fmt.Fprintln(os.Stderr, " run: mxcli -p .mpr -c \"alter settings runtime ( JavaVersion: '21' )\"") } else if lowered { fmt.Println() } diff --git a/cmd/mxcli/cmd_new_java.go b/cmd/mxcli/cmd_new_java.go index 608ac3efa..19ad99fce 100644 --- a/cmd/mxcli/cmd_new_java.go +++ b/cmd/mxcli/cmd_new_java.go @@ -56,13 +56,13 @@ func alignJavaVersion(projectPath string, out io.Writer) (bool, error) { return false, nil } - script := fmt.Sprintf("alter settings runtime JavaVersion = '%d';\n", fallback) + script := fmt.Sprintf("alter settings runtime ( JavaVersion: '%d' );\n", fallback) if err := runMDL(projectPath, script, io.Discard); err != nil { return false, fmt.Errorf("lowering JavaVersion from %d to %d: %w", major, fallback, err) } fmt.Fprintf(out, " Java %d → %d: no JDK %d on this machine, and the project would not build.\n", major, fallback, major) fmt.Fprintf(out, " Install a JDK %d and set JavaVersion back to %d to build for it:\n", major, major) - fmt.Fprintf(out, " mxcli -p .mpr -c \"alter settings runtime JavaVersion = '%d'\"\n", major) + fmt.Fprintf(out, " mxcli -p .mpr -c \"alter settings runtime ( JavaVersion: '%d' )\"\n", major) return true, nil } diff --git a/cmd/mxcli/cmd_query.go b/cmd/mxcli/cmd_query.go index 4de39e190..352f19230 100644 --- a/cmd/mxcli/cmd_query.go +++ b/cmd/mxcli/cmd_query.go @@ -157,7 +157,7 @@ Examples: os.Exit(1) } - mdlCmd := "SHOW STRUCTURE" + mdlCmd := "DESCRIBE STRUCTURE" if depth != 2 { mdlCmd += fmt.Sprintf(" DEPTH %d", depth) } @@ -211,7 +211,7 @@ Examples: os.Exit(1) } - mdlCmd := fmt.Sprintf("SHOW CONTEXT OF %s", args[0]) + mdlCmd := fmt.Sprintf("DESCRIBE CONTEXT OF %s", args[0]) if depth > 0 { mdlCmd += fmt.Sprintf(" DEPTH %d", depth) } diff --git a/cmd/mxcli/docker/detect.go b/cmd/mxcli/docker/detect.go index 723be4474..9bba77548 100644 --- a/cmd/mxcli/docker/detect.go +++ b/cmd/mxcli/docker/detect.go @@ -350,7 +350,7 @@ func resolveJDK(major int) (string, error) { // be noise. if major != DefaultJavaMajor { msg += fmt.Sprintf("\n This is the project's own Settings > Model > JavaVersion. To build for a"+ - "\n release you already have instead: mxcli -p .mpr -c \"alter settings runtime JavaVersion = '%d'\"", DefaultJavaMajor) + "\n release you already have instead: mxcli -p .mpr -c \"alter settings runtime ( JavaVersion: '%d' )\"", DefaultJavaMajor) } return "", fmt.Errorf("%s", msg) } diff --git a/cmd/mxcli/init_claudemd.go b/cmd/mxcli/init_claudemd.go index f65f4ef86..de9c780ca 100644 --- a/cmd/mxcli/init_claudemd.go +++ b/cmd/mxcli/init_claudemd.go @@ -161,7 +161,7 @@ func generateClaudeMD(projectName, mprFile string) string { w("## Running mxcli\n\n") w("The binary is in the **root of this project**, not on " + bt + "PATH" + bt + " — always " + bt + "./mxcli" + bt + ".\n\n") w(bt3 + "bash\n") - w("./mxcli -p " + mprPath + " -c \"SHOW STRUCTURE\" # one command\n") + w("./mxcli -p " + mprPath + " -c \"DESCRIBE STRUCTURE\" # one command\n") w("./mxcli exec script.mdl -p " + mprPath + " # a script\n") w("./mxcli # REPL\n") w(bt3 + "\n\n") @@ -201,7 +201,7 @@ func generateClaudeMD(projectName, mprFile string) string { w("| What MDL can say, and how | " + bt + "./mxcli syntax" + bt + " → " + bt + "./mxcli syntax [sub]" + bt + " (" + bt + "--json" + bt + " for bulk) |\n") w("| Which lint rules exist | " + bt + "./mxcli lint -p " + mprPath + " --list-rules" + bt + " |\n") w("| What a command takes | " + bt + "./mxcli help " + bt + " |\n") - w("| What this project contains | " + bt + "./mxcli -p " + mprPath + " -c \"SHOW STRUCTURE\"" + bt + " |\n") + w("| What this project contains | " + bt + "./mxcli -p " + mprPath + " -c \"DESCRIBE STRUCTURE\"" + bt + " |\n") w("| Why it was built this way | " + bt + "docs/brain/" + bt + " (above) |\n") w("\n") w("**Skills** are in " + bt + ".ai-context/skills//SKILL.md" + bt + " (and " + bt + ".claude/skills/" + bt + ", which is\n") diff --git a/cmd/mxcli/runconstants.go b/cmd/mxcli/runconstants.go index 05649256c..abb7e8f8b 100644 --- a/cmd/mxcli/runconstants.go +++ b/cmd/mxcli/runconstants.go @@ -17,7 +17,7 @@ import ( // value, and that map is what the standalone runtime is handed as // MicroflowConstants — the configuration's overrides are not in it. So // -// alter settings constant 'Encryption.EncryptionKey' value '…' in configuration 'Default'; +// alter settings constant @Encryption.EncryptionKey value '…' in configuration 'Default'; // // executed, reported success, survived a round-trip through `describe settings`, // and then did nothing: the app ran with the constant's default. Measured in diff --git a/cmd/mxcli/syntax/features_domain_model.go b/cmd/mxcli/syntax/features_domain_model.go index 8f094e842..d9b0fcc50 100644 --- a/cmd/mxcli/syntax/features_domain_model.go +++ b/cmd/mxcli/syntax/features_domain_model.go @@ -96,8 +96,8 @@ func init() { "if not exists", "if exists", "idempotent", "alter entities", "bulk", "every entity", "all entities", "where persistent", }, - Syntax: "ALTER ENTITY Module.Name ADD ATTRIBUTE [IF NOT EXISTS] AttrName: Type [constraints];\nALTER ENTITY Module.Name DROP ATTRIBUTE [IF EXISTS] AttrName;\nALTER ENTITY Module.Name RENAME ATTRIBUTE OldName TO NewName;\nALTER ENTITY Module.Name MODIFY ATTRIBUTE AttrName Type [DEFAULT val];\nALTER ENTITY Module.Name DROP DEFAULT ON ATTRIBUTE AttrName;\nALTER ENTITY Module.Name ADD INDEX [name] [ON] (attr1, attr2);\nALTER ENTITY Module.Name SET DOCUMENTATION 'text';\nALTER ENTITY Module.Name SET POSITION (x, y);\nALTER ENTITY Module.Name ADD EVENT HANDLER ON BEFORE COMMIT CALL Module.MF RAISE ERROR;\nALTER ENTITIES [IN Module] ADD ATTRIBUTE [IF NOT EXISTS] AttrName: Type [, ...]\n [WHERE PERSISTENT | WHERE NON-PERSISTENT];\n\nALTER ENTITIES is the bulk form: one statement applied to every entity in a\nmodule instead of one statement per entity. Only ADD ATTRIBUTE is offered --\nDROP and RENAME aimed at a set are destructive by a typo, and SET POSITION on\nevery entity is meaningless. Pair it with IF NOT EXISTS so the script re-runs.\n\nWHERE filters by persistence, using the same words CREATE ENTITY uses. A VIEW\nentity matches NEITHER: its rows come from an OQL query, so it is not the\npersistent/non-persistent distinction this filter means.\n\nWITHOUT IN, the sweep covers the whole project but SKIPS System and every\nMarketplace module, reporting which -- an upgrade replaces those modules and\nwould take the attribute with it. Naming a module with IN is taken as meaning\nit, so a deliberate edit there is still possible.\n\nSET POSITION places the entity in the domain-model editor, and CREATE ENTITY\ntakes the same thing as an @Position(x, y) annotation. Both are the box's\nCENTRE, not its top-left corner. An entity created without one takes the next\nslot in a wrapping grid, which is a default rather than a layout: to arrange a\nwhole module from its association graph, run 'mxcli layout -p app.mpr'\n(--dry-run first; it replaces positions you set by hand).\n\nMODIFY ATTRIBUTE always takes a type — restate it even when you are only\nchanging the default. There is no 'MODIFY ATTRIBUTE X SET DEFAULT v' form:\nSET would be read as the type name. Use DROP DEFAULT to clear one.\n\nIF NOT EXISTS / IF EXISTS make the add/drop a no-op (skipped, not an error)\nwhen the attribute is already present / already gone — so a domain script\nre-runs cleanly. For a whole script, 'mxcli exec --continue-on-error' reports\neach failed statement and keeps going instead of halting at the first.\n\nRENAME ATTRIBUTE also rewrites every reference to the attribute: the stored\nqualified names (microflow create/change members, page widgets, the entity's own\nvalidation and access rules) AND the bare steps inside XPath constraints, which\nare resolved to their owning entity first so another entity's identically-named\nattribute is left alone. A constraint that cannot be resolved is reported and\nleft unchanged, never guessed at. Uses inside microflow expressions ($obj/Attr)\nare free text and are NOT rewritten; mxbuild reports those as CE0117.", - Example: "ALTER ENTITY Shop.Customer ADD ATTRIBUTE Phone: String(20);\nALTER ENTITY Shop.Customer ADD ATTRIBUTE IF NOT EXISTS Phone: String(20); -- re-runnable\nALTER ENTITY Shop.Customer DROP ATTRIBUTE IF EXISTS OldField; -- re-runnable\nALTER ENTITY Shop.Customer RENAME ATTRIBUTE Email TO EmailAddress;\nALTER ENTITY Shop.Customer MODIFY ATTRIBUTE Phone String(30) DEFAULT ''; -- type restated\nALTER ENTITY Shop.Customer DROP DEFAULT ON ATTRIBUTE Phone; -- clear a default\nALTER ENTITY Shop.Customer ADD INDEX ON (EmailAddress);\nALTER ENTITY Shop.Customer\n ADD EVENT HANDLER ON BEFORE COMMIT CALL Shop.Validate($currentObject) RAISE ERROR;\n\n-- give every persistent entity in a module an audit trail, in one statement\nALTER ENTITIES IN Shop\n ADD ATTRIBUTE IF NOT EXISTS CreatedDate: AutoCreatedDate,\n ADD ATTRIBUTE IF NOT EXISTS ChangedDate: AutoChangedDate\n WHERE PERSISTENT;", + Syntax: "ALTER ENTITY Module.Name ADD ATTRIBUTE [IF NOT EXISTS] AttrName: Type [constraints];\nALTER ENTITY Module.Name DROP ATTRIBUTE [IF EXISTS] AttrName;\nALTER ENTITY Module.Name RENAME ATTRIBUTE OldName TO NewName;\nALTER ENTITY Module.Name MODIFY ATTRIBUTE AttrName: Type [DEFAULT val];\nALTER ENTITY Module.Name DROP DEFAULT ON ATTRIBUTE AttrName;\nALTER ENTITY Module.Name ADD INDEX [name] [ON] (attr1, attr2);\nALTER ENTITY Module.Name SET DOCUMENTATION 'text';\nALTER ENTITY Module.Name SET POSITION (x, y);\nALTER ENTITY Module.Name SET (AllowCreateChangeLocally: true|false); -- external entity\nALTER ENTITY Module.Name ADD EVENT HANDLER ON BEFORE COMMIT CALL Module.MF RAISE ERROR;\nALTER ENTITIES [IN Module] ADD ATTRIBUTE [IF NOT EXISTS] AttrName: Type [, ...]\n [WHERE PERSISTENT | WHERE NON-PERSISTENT];\n\nALTER ENTITIES is the bulk form: one statement applied to every entity in a\nmodule instead of one statement per entity. Only ADD ATTRIBUTE is offered --\nDROP and RENAME aimed at a set are destructive by a typo, and SET POSITION on\nevery entity is meaningless. Pair it with IF NOT EXISTS so the script re-runs.\n\nWHERE filters by persistence, using the same words CREATE ENTITY uses. A VIEW\nentity matches NEITHER: its rows come from an OQL query, so it is not the\npersistent/non-persistent distinction this filter means.\n\nWITHOUT IN, the sweep covers the whole project but SKIPS System and every\nMarketplace module, reporting which -- an upgrade replaces those modules and\nwould take the attribute with it. Naming a module with IN is taken as meaning\nit, so a deliberate edit there is still possible.\n\nSET POSITION places the entity in the domain-model editor, and CREATE ENTITY\ntakes the same thing as an @Position(x, y) annotation. Both are the box's\nCENTRE, not its top-left corner. An entity created without one takes the next\nslot in a wrapping grid, which is a default rather than a layout: to arrange a\nwhole module from its association graph, run 'mxcli layout -p app.mpr'\n(--dry-run first; it replaces positions you set by hand).\n\nMODIFY ATTRIBUTE always takes a type — restate it even when you are only\nchanging the default. There is no 'MODIFY ATTRIBUTE X SET DEFAULT v' form:\nSET would be read as the type name. Use DROP DEFAULT to clear one.\n\nIF NOT EXISTS / IF EXISTS make the add/drop a no-op (skipped, not an error)\nwhen the attribute is already present / already gone — so a domain script\nre-runs cleanly. For a whole script, 'mxcli exec --continue-on-error' reports\neach failed statement and keeps going instead of halting at the first.\n\nRENAME ATTRIBUTE also rewrites every reference to the attribute: the stored\nqualified names (microflow create/change members, page widgets, the entity's own\nvalidation and access rules) AND the bare steps inside XPath constraints, which\nare resolved to their owning entity first so another entity's identically-named\nattribute is left alone. A constraint that cannot be resolved is reported and\nleft unchanged, never guessed at. Uses inside microflow expressions ($obj/Attr)\nare free text and are NOT rewritten; mxbuild reports those as CE0117.", + Example: "ALTER ENTITY Shop.Customer ADD ATTRIBUTE Phone: String(20);\nALTER ENTITY Shop.Customer ADD ATTRIBUTE IF NOT EXISTS Phone: String(20); -- re-runnable\nALTER ENTITY Shop.Customer DROP ATTRIBUTE IF EXISTS OldField; -- re-runnable\nALTER ENTITY Shop.Customer RENAME ATTRIBUTE Email TO EmailAddress;\nALTER ENTITY Shop.Customer MODIFY ATTRIBUTE Phone: String(30) DEFAULT ''; -- type restated\nALTER ENTITY Shop.Customer DROP DEFAULT ON ATTRIBUTE Phone; -- clear a default\nALTER ENTITY Shop.Customer ADD INDEX ON (EmailAddress);\nALTER ENTITY Shop.Customer\n ADD EVENT HANDLER ON BEFORE COMMIT CALL Shop.Validate($currentObject) RAISE ERROR;\n\n-- give every persistent entity in a module an audit trail, in one statement\nALTER ENTITIES IN Shop\n ADD ATTRIBUTE IF NOT EXISTS CreatedDate: AutoCreatedDate,\n ADD ATTRIBUTE IF NOT EXISTS ChangedDate: AutoChangedDate\n WHERE PERSISTENT;", SeeAlso: []string{"domain-model.entity.create", "domain-model.entity.attributes"}, }) @@ -287,7 +287,7 @@ func init() { "many-to-one", "many-to-many", "foreign key", "owner", "delete behavior", }, - Syntax: "[@anchor(from: (x, y), to: (x, y))]\nCREATE [OR MODIFY] ASSOCIATION Module.Name\n FROM Module.FromEntity TO Module.ToEntity\n TYPE Reference|ReferenceSet\n [OWNER Default|Both]\n [ON DELETE CASCADE|RESTRICT|SET NULL [ERROR MESSAGE 'text']]\n [COMMENT 'text'];\nALTER ASSOCIATION Module.Name SET ANCHOR FROM (x, y) TO (x, y);\nDROP ASSOCIATION Module.Name;\n\nOR MODIFY: updates type/owner/delete behavior in-place, preserves UUID.\n\n@anchor sets the LINE ANCHORS — where the connector attaches to each entity box\nin the domain model editor — as a PERCENTAGE of the box (0..100, whole numbers).\n`from` is the FROM entity's box, `to` the TO entity's: (0, 50) is the middle of\nthe left edge, (100, 50) the right, (50, 100) the bottom centre. Omitting an end\nPRESERVES what is stored, so a CREATE OR MODIFY about something else never\nflattens a hand-tuned line. Cross-module associations have no anchors.\n\nDROP reconciles the entity access rules that named the association, and is\nREFUSED while a message definition still exposes it (that one cannot be\nreconciled -- removing the member would change a published contract). The\nrefusal prints the `alter message definition ... drop member` statement for\neach definition, ready to run.", + Syntax: "[@anchor(from: (x, y), to: (x, y))]\nCREATE [OR MODIFY] ASSOCIATION Module.Name\n FROM Module.FromEntity TO Module.ToEntity\n TYPE Reference|ReferenceSet\n [OWNER Default|Both]\n [ON DELETE CASCADE|RESTRICT|SET NULL [ERROR MESSAGE 'text']];\n-- Documentation: the /** … */ doc comment before the statement\n-- (COMMENT 'text' is its deprecated alias, MDL-DEPR100).\nALTER ASSOCIATION Module.Name SET ANCHOR FROM (x, y) TO (x, y);\nDROP ASSOCIATION Module.Name;\n\nOR MODIFY: updates type/owner/delete behavior in-place, preserves UUID.\n\n@anchor sets the LINE ANCHORS — where the connector attaches to each entity box\nin the domain model editor — as a PERCENTAGE of the box (0..100, whole numbers).\n`from` is the FROM entity's box, `to` the TO entity's: (0, 50) is the middle of\nthe left edge, (100, 50) the right, (50, 100) the bottom centre. Omitting an end\nPRESERVES what is stored, so a CREATE OR MODIFY about something else never\nflattens a hand-tuned line. Cross-module associations have no anchors.\n\nDROP reconciles the entity access rules that named the association, and is\nREFUSED while a message definition still exposes it (that one cannot be\nreconciled -- removing the member would change a published contract). The\nrefusal prints the `alter message definition ... drop member` statement for\neach definition, ready to run.", Example: "-- Many-to-one\nCREATE ASSOCIATION Shop.Order_Customer\n FROM Shop.Order TO Shop.Customer\n TYPE Reference\n OWNER Default\n ON DELETE SET NULL;\n\n-- Many-to-many\nCREATE ASSOCIATION Shop.Product_Tag\n FROM Shop.Product TO Shop.Tag\n TYPE ReferenceSet\n OWNER Both;\n\n-- Line leaving the bottom of Order and entering the top of Customer\n@anchor(from: (50, 100), to: (50, 0))\nCREATE ASSOCIATION Shop.Order_Customer\n FROM Shop.Order TO Shop.Customer;\n\n-- Retune the line without restating the association\nALTER ASSOCIATION Shop.Order_Customer SET ANCHOR FROM (0, 54) TO (100, 54);", SeeAlso: []string{"domain-model.association.create", "domain-model.association.anchor", "domain-model.association.delete-behavior"}, }) @@ -300,7 +300,7 @@ func init() { "reference set", "junction table", "foreign key", "owner default", "owner both", "storage column", "storage table", }, - Syntax: "CREATE [OR MODIFY] ASSOCIATION Module.AssociationName\n FROM Module.FromEntity TO Module.ToEntity\n TYPE Reference|ReferenceSet\n [OWNER Default|Both]\n [STORAGE COLUMN|TABLE]\n [ON DELETE CASCADE|RESTRICT|SET NULL [ERROR MESSAGE 'text']]\n [COMMENT 'text'];\n\nDirection:\n FROM = entity holding the FK (the \"many\" side)\n TO = entity being referenced (the \"one\" side)\n\nTypes:\n Reference = Many-to-one (FK column on FROM table)\n ReferenceSet = Many-to-many (junction table)\n\nOR MODIFY: updates in-place, preserves UUID. Safe to re-run.", + Syntax: "CREATE [OR MODIFY] ASSOCIATION Module.AssociationName\n FROM Module.FromEntity TO Module.ToEntity\n TYPE Reference|ReferenceSet\n [OWNER Default|Both]\n [STORAGE COLUMN|TABLE]\n [ON DELETE CASCADE|RESTRICT|SET NULL [ERROR MESSAGE 'text']];\n\nDocumentation: the /** … */ doc comment before the statement\n(COMMENT 'text' is its deprecated alias, MDL-DEPR100).\n\nDirection:\n FROM = entity holding the FK (the \"many\" side)\n TO = entity being referenced (the \"one\" side)\n\nTypes:\n Reference = Many-to-one (FK column on FROM table)\n ReferenceSet = Many-to-many (junction table)\n\nOR MODIFY: updates in-place, preserves UUID. Safe to re-run.", Example: "-- Many-to-one with delete behavior\nCREATE ASSOCIATION Shop.Order_Customer\n FROM Shop.Order TO Shop.Customer\n TYPE Reference\n OWNER Default\n ON DELETE RESTRICT;\n\n-- Many-to-many\nCREATE ASSOCIATION Shop.Product_Tag\n FROM Shop.Product TO Shop.Tag\n TYPE ReferenceSet\n OWNER Both;\n\n-- Idempotent update\nCREATE OR MODIFY ASSOCIATION Shop.Order_Customer\n FROM Shop.Order TO Shop.Customer\n TYPE Reference\n OWNER Default\n ON DELETE CASCADE;", SeeAlso: []string{"domain-model.association.delete-behavior", "domain-model.entity.create"}, }) @@ -405,8 +405,8 @@ func init() { "constant", "configuration", "config value", "create constant", "setting", }, - Syntax: "CREATE CONSTANT Module.Name TYPE DataType DEFAULT value [COMMENT 'text'];\nCREATE OR MODIFY CONSTANT Module.Name TYPE DataType DEFAULT value;\n\nSHOW CONSTANTS;\nDESCRIBE CONSTANT Module.Name;\nDROP CONSTANT Module.Name;", - Example: "CREATE CONSTANT MyModule.ApiBaseUrl\n TYPE String\n DEFAULT 'https://api.example.com/v1';\n\nCREATE CONSTANT MyModule.MaxRetries\n TYPE Integer\n DEFAULT 3\n COMMENT 'Maximum API retry attempts';", + Syntax: "[/** documentation */]\nCREATE CONSTANT Module.Name TYPE DataType DEFAULT value;\nCREATE OR MODIFY CONSTANT Module.Name TYPE DataType DEFAULT value;\n\nSHOW CONSTANTS;\nDESCRIBE CONSTANT Module.Name;\nDROP CONSTANT Module.Name;", + Example: "CREATE CONSTANT MyModule.ApiBaseUrl\n TYPE String\n DEFAULT 'https://api.example.com/v1';\n\n/** Maximum API retry attempts */\nCREATE CONSTANT MyModule.MaxRetries\n TYPE Integer\n DEFAULT 3;", SeeAlso: []string{"domain-model.constant.create"}, }) @@ -418,7 +418,7 @@ func init() { "show constants", "constant values", "modify constant", "string constant", "integer constant", "boolean constant", }, - Syntax: "CREATE CONSTANT Module.Name\n TYPE String|Integer|Long|Decimal|Boolean|DateTime\n DEFAULT value\n [COMMENT 'description'];\n\nCREATE OR MODIFY CONSTANT Module.Name\n TYPE DataType DEFAULT value [COMMENT 'text'];\n\nSHOW CONSTANTS;\nSHOW CONSTANTS IN ;\nSHOW CONSTANT VALUES;\nDESCRIBE CONSTANT Module.Name;\nDROP CONSTANT Module.Name;\n\nRemove override:\n ALTER SETTINGS DROP CONSTANT 'Module.Name' IN CONFIGURATION 'cfg';\n\n" + + Syntax: "[/** description */]\nCREATE CONSTANT Module.Name\n TYPE String|Integer|Long|Decimal|Boolean|DateTime\n DEFAULT value;\n\nCREATE OR MODIFY CONSTANT Module.Name\n TYPE DataType DEFAULT value;\n\n-- Documentation is the /** … */ doc comment; COMMENT 'text' is its\n-- deprecated alias (MDL-DEPR100).\n\nSHOW CONSTANTS;\nSHOW CONSTANTS IN ;\nSHOW CONSTANT VALUES;\nDESCRIBE CONSTANT Module.Name;\nDROP CONSTANT Module.Name;\n\nRemove override:\n ALTER SETTINGS DROP CONSTANT @Module.Name IN CONFIGURATION 'cfg';\n\n" + "Shared vs private values:\n" + " A per-configuration override holds either a SHARED value (stored in the\n" + " model, so in version control — every developer gets it) or a PRIVATE one\n" + @@ -430,7 +430,7 @@ func init() { " SHOW CONSTANT VALUES reports it as (private); DESCRIBE SETTINGS reports it\n" + " as a comment, not a re-executable statement. DROP CONSTANT still works.\n" + " Change a constant to a shared value in Studio Pro.", - Example: "CREATE CONSTANT MyModule.ApiBaseUrl\n TYPE String\n DEFAULT 'https://api.example.com/v1';\n\nCREATE CONSTANT MyModule.MaxRetries\n TYPE Integer DEFAULT 3\n COMMENT 'Maximum number of API retry attempts';\n\nCREATE CONSTANT MyModule.EnableDebug\n TYPE Boolean DEFAULT false;\n\nCREATE OR MODIFY CONSTANT MyModule.ApiBaseUrl\n TYPE String\n DEFAULT 'https://api.staging.example.com/v2';", + Example: "CREATE CONSTANT MyModule.ApiBaseUrl\n TYPE String\n DEFAULT 'https://api.example.com/v1';\n\n/** Maximum number of API retry attempts */\nCREATE CONSTANT MyModule.MaxRetries\n TYPE Integer DEFAULT 3;\n\nCREATE CONSTANT MyModule.EnableDebug\n TYPE Boolean DEFAULT false;\n\nCREATE OR MODIFY CONSTANT MyModule.ApiBaseUrl\n TYPE String\n DEFAULT 'https://api.staging.example.com/v2';", SeeAlso: []string{"domain-model.constant"}, }) diff --git a/cmd/mxcli/syntax/features_integration.go b/cmd/mxcli/syntax/features_integration.go index e1a662c6e..ee3a1699e 100644 --- a/cmd/mxcli/syntax/features_integration.go +++ b/cmd/mxcli/syntax/features_integration.go @@ -55,12 +55,17 @@ func init() { " ErrorHandlingMicroflow: microflow Module.HandleError\n" + ")\n" + "[HEADERS ('Key': 'Value')];\n\n" + + "ALTER CONSUMED ODATA SERVICE Module.Name SET (Key: value, ...); -- CREATE's keys\n" + + "ALTER PUBLISHED ODATA SERVICE Module.Name SET (Key: value, ...);\n\n" + "CREATE EXTERNAL ENTITY Module.Name\n" + " FROM CONSUMED ODATA SERVICE Module.Client\n" + " (EntitySet: 'Name', RemoteName: 'Name')\n" + " (Attr: Type, ...);\n\n" + "CREATE EXTERNAL ENTITIES FROM Module.Client\n" + - " [INTO Module] [ENTITIES (Name1, Name2)];", + " [INTO Module] [ENTITIES (Name1, Name2)];\n\n" + + "ALTER ENTITY Module.Name SET (AllowCreateChangeLocally: true);\n\n" + + "-- `SET Key = value, ...` (no parentheses) still runs and warns MDL-DEPR061;\n" + + "-- `SET allow_create_change_locally = true` warns MDL-DEPR063.", Example: "CREATE CONSTANT MyModule.SvcUrl TYPE String DEFAULT 'https://api.example.com/odata/v4/';\n\nCREATE CONSUMED ODATA SERVICE MyModule.SalesforceAPI (\n Version: '1.0',\n ODataVersion: OData4,\n MetadataUrl: 'https://api.example.com/odata/$metadata',\n Timeout: 300,\n ServiceUrl: MyModule.SvcUrl\n);\n\nCREATE EXTERNAL ENTITIES FROM MyModule.SalesforceAPI INTO Integration;", SeeAlso: []string{"odata", "odata.publish", "odata.show"}, }) @@ -199,14 +204,14 @@ func init() { Register(SyntaxFeature{ Path: "rest.call", - Summary: "REST CALL activity inside a microflow, and its five RETURNS forms", + Summary: "CALL REST SERVICE activity inside a microflow, and its five RETURNS forms", Keywords: []string{ "rest call", "call rest service", "http get", "http post", "returns response", "returns string", "returns mapping", "file document", "filedocument", "download", "httpresponse", "body binary", "binary", "upload", "post binary", }, - Syntax: "[$Var =] REST CALL GET|POST|PUT|PATCH|DELETE '' [WITH ({1} = expr, ...)]\n" + + Syntax: "[$Var =] CALL REST SERVICE GET|POST|PUT|PATCH|DELETE '' [WITH ({1} = expr, ...)]\n" + " [HEADER 'Name' = expr]\n" + " [AUTH BASIC $user PASSWORD $pass]\n" + " [BODY '