diff --git a/.claude/commands/mxcli-dev/review.md b/.claude/commands/mxcli-dev/review.md index 596868287a..0d51e89447 100644 --- a/.claude/commands/mxcli-dev/review.md +++ b/.claude/commands/mxcli-dev/review.md @@ -60,6 +60,8 @@ proactively. Add a row after every review that surfaces something new. | 29 | A predicate that names ONE cause of a build error is read as if it named the error (`mem.IsCalculated` for CE6592, which an autonumber also triggers) — the half that is covered works, so every test passes and the gap is invisible until a user hits the other half | Code correctness | When a guard cites a CE number, enumerate what the PLATFORM rejects, not what the current code checks. Put the rule in one named place (`types.WriteRightsForbidden`) rather than a bare boolean at each site, so the second cause has somewhere to go. And fix every pass that can re-derive the value — a reconcile running after every program re-broke a grant the user had corrected by hand | | 30 | Two commands compute the same thing from two copies of the setup (`report` re-implementing `lint`'s rule list and skipping its config), so they disagree about a project — and a SCORE carries no provenance, so neither number looks wrong | Code correctness | Extract the shared setup and route both through it. A value test cannot guard this when the copies live inside cobra `RunE` bodies: use a structural check on the source, with a positive control asserted FIRST so it cannot pass vacuously | | 31 | A test helper that needs a heavyweight object only to satisfy a signature (`NewLintContext(nil, nil)`, which panics) invites a nil-guard added purely to make the test compile — behaviour nothing in production needs, defended forever | Test coverage | Narrow the signature instead: if the helper does not use the parameter, drop it and let the caller apply the part it owns. A test that cannot construct an argument is usually telling you the argument does not belong | +| 32 | A fix adds a diagnostic for a capability the model lacks while leaving in place the code that asserts the capability EXISTS — MDL042 telling the author a loop's `@caption` is dropped, while `cmd_microflows_builder_annotations.go` still ran `case *microflows.LoopedActivity: activity.Caption = ann.Caption` under the comment "LOOP / WHILE activities can carry a caption just like splits", and the describer still emitted one. Nothing read either back. The next reader trusts the code over the warning, deletes the check, and reopens the bug from the other side. The reason it survives is that it usually has TESTS — three here asserted the caption was carried, all of them against the semantic object and none against storage, so they passed throughout and failed only on the correct fix | Code correctness | `generated/metamodel` is the arbiter: a field on the semantic type it does not declare cannot survive a write, so an assignment to it is dead by construction. Grep the writer, the describer and the semantic struct and delete (or re-comment) whatever sets it. MEASURE before deleting — `exec` then `describe` on a real project, with the UNMODIFIED build, so the deletion rests on the stored document rather than on reading the codec. Invert the tests that defended it rather than deleting them, keeping any half still true (escaping coverage belongs on a type that can carry a caption), and check the inverted test fails when the assignment is put back | +| 33 | A column added to `createTables` without bumping `CatalogSchemaVersion` — the version guard only drops tables when it CHANGES, and `CREATE TABLE IF NOT EXISTS` never adds a column, so every user with a cached catalog keeps a table the new SELECT cannot read. Measured on #1181: `activities_for` yielded 302 on `main` and **0** on the branch against the same cache, `no such column: UseRequestTimeout`, `mxcli lint` exit 1. `mxcli report` runs the same rules and never checks `QueryErrors()`, so there it would score silently | Code correctness | Bump the constant in the same commit — its doc comment says so and `62913741` is the precedent (four columns + 11→12 + a builder test). To REPRODUCE, the cache must actually be reused: build it with the old binary and run the new one with the **same spelling of `-p`**, because a relative-vs-absolute path invalidates on "MPR path changed" and hides the bug; confirm the run says "Loading cached catalog … (from cache)" before believing a green result | --- diff --git a/.claude/skills/fix-issue/findings/mdl-executor.jsonl b/.claude/skills/fix-issue/findings/mdl-executor.jsonl index ed70b75f46..0502b60639 100644 --- a/.claude/skills/fix-issue/findings/mdl-executor.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-executor.jsonl @@ -689,6 +689,7 @@ {"area": "mdl/executor", "date": "2026-09-23", "symptom": "`CALL MICROFLOW M.F(…) IN QUEUE M.Q` where `F` returns **Boolean**: `mxcli check -p --references` says `Check passed!`, `mx check` says **CE7033** \"A microflow used for background execution must have a Microflow return type of 'Nothing'.\" (at Call microflow activity 'F'). Reported with the CE0142 after-startup sibling, which MDL073 had already closed.", "cause": "The --references pass resolved the call target and the queue name separately, and both resolve. Nothing compared the binding (`in queue`) against the signature of the flow it names. Added MDL088: a project-less pass (ValidateQueuedCallReturnType) for a target the script creates, and validateQueuedMicroflowTargets on the --references path for a stored target, which skips script-defined targets so the fault is not printed twice. Stored void microflows read back as ReturnType \"Void\", not \"\" — both must mean Nothing.", "file": "`mdl/executor/validate_queued_call_return.go` (queuedMicroflowCalls, checkQueuedMicroflowReturnsNothing, validateQueuedMicroflowTargets, ValidateQueuedCallReturnType), wired in `validate_program.go` and `validate.go` (validateFlowBodyReferences); examples `mdl-examples/bug-tests/1064-queued-microflow-must-return-nothing{,.fail}.mdl`", "insight": "Same class as MDL073 (\"the reference resolves\" ≠ \"the reference is usable\"): any binding that names a flow carries a constraint on that flow's signature, and a resolver checks only the name. When one such check lands, sweep for its siblings at other binding sites. The queued CALL JAVA ACTION twin (CE7038) is still unchecked and was deliberately left out of scope. Two things that cost time: (1) `mxcli exec` of a script that CREATEs a queue and then binds a call to it refuses with 'task queue not found' — validateFlowBodyReferences checks queues against the project only, not the script context — so the repro has to create the queue in a separate exec; (2) walk call statements by reflection, not by the flowRefCollector switch, which does not descend into WHILE bodies. Measured on mxbuild 11.12.0 with two projects: Boolean target → CE7033, void target → 0 errors.", "refs": ["mendixlabs/mxcli#1064"], "ce": ["CE7033"], "rules": ["MDL088"]} {"area": "mdl/executor/microflow-layout", "date": "2026-09-23", "symptom": "MPR011 fires on EVERY `while` loop mxcli writes \u2014 'first activity at (50,80) lies outside the loop box' \u2014 single-level loops included. `mx check` passes and the app runs; the flow just renders wrong in Studio Pro. Reported from a real project as 'looks like an mxcli layout issue', with 3 MPR011 warnings still in its final lint run. mxcli's own lint rule was correctly flagging mxcli's own output.", "cause": "One missing term in the WHILE builder. addWhileStatement had `innerStartX := LoopPadding` (50) where addLoopStatement has `LoopPadding + iteratorSpace + ActivityWidth/2` (210). A microflow object's Position is its CENTRE \u2014 the builder says so itself ('Position is the CENTER point (RelativeMiddlePoint in Mendix)') \u2014 so a centre at x=50 with ActivityWidth=120 puts the left edge at -10. The doc comment says the while layout 'matches addLoopStatement but without iterator icon space': dropping the iterator space (100) was right, taking ActivityWidth/2 with it was not, because that term is not iterator space, it is what converts a centre to a left edge. The very next line, `innerStartY := LoopPadding + ActivityHeight/2`, adds the half-height for exactly this reason \u2014 so the omission was accidental, not a choice. Reported (50,80) matches term for term: 50 = LoopPadding, 80 = LoopPadding + ActivityHeight/2.", "file": "`mdl/executor/cmd_microflows_builder_control.go` (addWhileStatement: `innerStartX := LoopPadding + ActivityWidth/2`), tests `mdl/executor/loop_containment_test.go` (TestWhileLoopBox_ContainsDefaultLaidOutChildren, TestWhileLoopFirstChildLeftEdgeIsInsideTheBox)", "insight": "The containment invariant WAS already tested \u2014 loop_containment_test.go exists from #884 and asserts exactly this \u2014 but every fixture in it built a FOREACH loop. There are two loop builders; one was covered and the uncovered one shipped the violation into every project that writes a `while`. An invariant is worth what its COVERAGE is, and a file named for an invariant reads as if it covers the invariant, which is how a second code path goes unexamined for months. When a rule flags the tool's own output, believe the rule first: the reporter hedged with 'looks like an mxcli layout issue' and was exactly right. Cheap tell for this class: a term present on one axis and absent on the other in adjacent lines (`+ ActivityHeight/2` on Y, nothing on X) is almost always an omission rather than a decision. Failing test written first; it reproduced the reported geometry to the pixel, x[-10,...] at 1, 2, 4 and 7 activities. Still uncovered: addManualWhileTrueStatement, the third loop builder.", "refs": ["ako/mxcli#884", "ako/mxcli#645"]} {"date": "2026-09-23", "area": "mdl-executor", "symptom": "upstream #1176: DESCRIBE prints `all` on an import activity that returns ONE object — `$objectResponse = import from mapping M.IMM($s) all;` — which reads as a list import. Reported on v0.23.0 / Studio Pro 11.12.3, after #881 was believed to have settled import ranges", "cause": "#881 made `formatImportMappingRange` always emit a range keyword, because at the time a missing keyword let the range fall back to the variable's cardinality and store First. The later runtime fix (unauthored range written as All explicitly) made bare and `all` build the same activity, but the describe side was never revisited, so `all` kept printing where it was only noise", "file": "`mdl/executor/cmd_microflows_format_action.go` (`formatImportMappingRange`: return \"\" for All against SingleObject); tests `mdl/executor/cmd_microflows_import_range_test.go` (`TestImportRange_ObjectResultDescribesWithoutAll`); example `mdl-examples/bug-tests/1176-import-mapping-object-describes-without-all.mdl`", "insight": "**This was not #881 regressing — it was #881's own workaround outliving its reason.** 'DESCRIBE must never emit nothing' was a guard against the builder's then-broken default; once the builder wrote a missing keyword as All explicitly, the guard became pure noise, and nothing linked the two sites. When a formatter emits something 'because the builder would otherwise infer X', put that reason in a test that asserts the builder equivalence (bare vs keyword build the same activity), so fixing the builder flags the formatter. Proving the omission safe needs that equivalence on a real project, not just the unit test: on 11.12.3 both spellings store byte-identical ResultHandling (ConstantRange{SingleObject:false} + ObjectType), `mx check` 0 errors, and exec'ing the described text reports 'Unchanged microflow'. Wrong turn to skip: a JSON diff of two EMPTY extractions prints 'IDENTICAL' — `bson dump` emits ordered Key/Value lists, not objects; check the extraction is non-empty before trusting a diff", "refs": ["#881", "#1176"]} +{"area": "mdl/executor", "date": "2026-09-24", "symptom": "`@caption 'Are there months left?'` above a `while` passed `mxcli check` with no warning, `exec` created the microflow, and `describe` showed the while with no caption. The same caption on a `loop` was reported as MDL042.", "cause": "MDL042 lived in the `*ast.LoopStmt` case of validate_microflow.go only. `addWhileStatement` builds the same Microflows$LoopedActivity as a for-each loop -- a WhileLoopCondition instead of an iterator -- and LoopedActivity has no Caption property, so the caption had nowhere to go and nothing said so.", "file": "mdl/executor/validate_microflow.go", "insight": "A diagnostic keyed on one AST statement misses every other statement that builds the same model element. When a check exists because the MODEL lacks a property, key it on what the builder writes (here: every LoopedActivity) rather than on the MDL keyword that led there.", "refs": "mendixlabs/mxcli#1187"} {"area": "mdl-executor", "date": "2026-09-24", "refs": ["#1173"], "symptom": "`ALTER ENTITY ADD ATTRIBUTE Region: String(200)` passed `mxcli check -p --references`, exec printed \"Added attribute 'Region' to entity MyFirstModule.SaleStats\" with exit 0, and `mx check` then failed CE6770 \"View Entity is out of sync with the OQL Query.\" The attribute was written as DomainModels$StoredValue with no OQL column behind it", "cause": "execAlterEntity's ADD/DROP ATTRIBUTE branches treat every entity as a table: nothing asked isViewEntity, although CREATE ASSOCIATION and bulk ALTER ENTITIES already did. The check-time AlterEntityStmt case only resolved the module and enumerations", "file": "`mdl/executor/cmd_entities.go` (viewEntityAttributeSetRefusal, AlterEntityAddAttribute/DropAttribute guards), `mdl/executor/validate.go` (validateViewEntityAttributeSet); test `mdl/executor/alter_entity_view_test.go`; bug-test `mdl-examples/bug-tests/1173-alter-view-entity-attribute.mdl`", "insight": "**Measure every ALTER op on a view before choosing the fix, not just the reported one** — one mxbuild per op on 11.12.1: ADD → CE6770, DROP → CE6770, MODIFY to the wrong type → CE6770 but MODIFY to the matching type → 0 errors (so MODIFY's failure is a type mismatch, a different gap, not this one), RENAME → 0 errors (the OqlViewValue binds the column by its Reference/alias, not the attribute name), SET COMMENT → 0. Refusing all four attribute ALTERs would have blocked a working RENAME. **Refuse rather than bind**: the issue offers \"create an OqlViewValue bound to the matching alias\", but for ADD there is no matching alias — the query has no such column — so any write stays CE6770; the query is the declaration, so the refusal points at `create or modify view entity`, verified to build clean with the added column. Guard both layers: check must see a view the SCRIPT creates (sc.viewEntities) as well as a stored one (findEntity + isViewEntity), or check passes a script exec stops halfway. Bulk ALTER ENTITIES already excluded views (e.Source/OqlQuery), so the single-entity path was the only entry. Control: stubbing both guards fails all four refusal tests with \"accepted — mxbuild reports CE6770\""} {"date": "2026-09-24", "area": "mdl-executor", "symptom": "upstream #1175: a `--` comment inside a view entity's select list produced false MDL030 — `select column 1 has no as alias: '-- the customer's running total'` plus a second one for the text after the comment's comma. Reported on v0.23.0 as an apostrophe bug", "cause": "Every static OQL check (ValidateOQLSyntax, ValidateOQLTypes, inferOQLTypes, viewAssociationColumns) works on `Query.RawQuery`, which is stored verbatim and so keeps the author's comments. parseSelectColumns splits on top-level commas with no notion of a comment, so the comment became a column and each comma in it another", "file": "`mdl/executor/oql_comments.go` (`stripOQLComments`, called at the top of the four entry points in `oql_type_inference.go` / `oql_view_associations.go`); tests `mdl/executor/validate_oql_comments_test.go`; example `mdl-examples/bug-tests/1175-oql-comment-is-not-a-select-column.mdl`", "insight": "**The apostrophe was a red herring: a comment with no apostrophe fails the same way** — measured before theorising, and it moved the fix from the quote-skipping in topLevelKeywordIndex to the comment itself. Strip at the entry points, not inside the helpers: the checks also run regexes over the whole query (division, association-path, reserved word), and a comment containing `from`, `/` or `a.B.C_D` would trip those too. Blank comments to spaces of the same length rather than deleting them, so any offset computed on the stripped text still indexes the original. Do NOT strip in the visitor — the stored query keeps the comments, which is the author's documentation. A type-check test using the same query passed without the fix (comment columns infer no type), so it was dropped rather than kept as a test that detects nothing", "refs": ["mendixlabs/mxcli#1175"], "rules": ["MDL030"]} {"area":"mdl/executor","date":"2026-09-24","symptom":"`mxcli check -p … --references` passes a view entity whose association-path join is written `join s/Mod.A_B/System.UserRole AS r` (uppercase AS) even when a pass-through column from `r` declares the wrong string length; mxbuild then fails with CE6770 \"View Entity is out of sync with the OQL Query.\" Lowercase `as` reports MDL031 correctly.","cause":"extractAliasMap matched the path join case-insensitively ((?i)…(?:as\\s+)?) but then recovered the path from match[0] with strings.TrimSuffix(path, \"as\") — case-sensitive — so with `AS` the path kept a trailing ` AS`, the end-anchored lastEntity regex failed, and the alias was never mapped. Every column from that alias silently went without type inference.","file":"mdl/executor/oql_type_inference.go","insight":"A (?i) regex followed by string surgery on the whole match reintroduces case sensitivity by the back door: capture every piece you need as its own group instead of trimming it back out. The tell is a single control table varying only the case of one keyword — every other keyword in upper case was harmless, which points straight at code that handles that one token outside the regex. An unresolved alias is silent (the checker skips unknown types rather than reporting), so the symptom is a check that PASSES; test at extractAliasMap directly, and control with the unfixed binary on a real project (it printed `Check passed!` for AS, the error for as). DESCRIBE prints AS in upper case, so round-tripped OQL hits this by default.","refs":["#652"],"ce":["CE6770"],"rules":["MDL031"]} diff --git a/.claude/skills/mendix/write-lint-rules/SKILL.md b/.claude/skills/mendix/write-lint-rules/SKILL.md index 3e6bf7d842..3241920a64 100644 --- a/.claude/skills/mendix/write-lint-rules/SKILL.md +++ b/.claude/skills/mendix/write-lint-rules/SKILL.md @@ -351,6 +351,8 @@ def count_not(node): | `entity_ref` | string | Referenced entity qualified name | | `service_ref` | string | Called service document (REST / web service / OData client); empty when the activity calls none | | `action_ref` | string | Operation or action within that service; empty when the activity calls none | +| `use_request_timeout` | bool | Call REST service: whether "Use a timeout" is enabled. False for other action types | +| `timeout_expression` | string | Call REST service: the timeout in seconds, stored as an expression, e.g. `"300"` | ### rest_client | Property | Type | Example | @@ -395,6 +397,7 @@ Returned by `permissions()` (all types) or `permissions_for()` (entity-specific) | `member_name` | string | Attribute name (for MEMBER_READ/MEMBER_WRITE) | | `xpath_constraint` | string | XPath constraint or empty | | `is_constrained` | bool | True if XPath constraint is set | +| `default_member_access_rights` | string | The rule's "default rights for new members": `"None"`, `"ReadOnly"` or `"ReadWrite"`. Empty for non-entity permissions | ### user_role | Property | Type | Example | diff --git a/.claude/skills/mendix/write-microflows/reference/control-flow.md b/.claude/skills/mendix/write-microflows/reference/control-flow.md index bc39d74ee3..3ebf37138a 100644 --- a/.claude/skills/mendix/write-microflows/reference/control-flow.md +++ b/.claude/skills/mendix/write-microflows/reference/control-flow.md @@ -206,10 +206,11 @@ begin end loop; ``` -> **`@caption` does nothing on a loop.** Mendix for-loops have no caption -> property, so `@caption` on a `loop` is silently dropped (`mxcli check` flags -> it as **MDL042**). To label a loop, use `@annotation 'text'` — it attaches a -> note, exactly like drawing one onto the loop in Studio Pro. +> **`@caption` does nothing on a loop or a while loop.** Both are the same loop +> activity, which has no caption property, so `@caption` on a `loop` or a `while` +> is dropped (`mxcli check` flags it as **MDL042**). To label either, use +> `@annotation 'text'` — it attaches a note, exactly like drawing one onto the loop +> in Studio Pro. **Note**: - Loop variable (`$Product`) is scoped to the loop body diff --git a/CHANGELOG.md b/CHANGELOG.md index 3ac4a90451..e230f2bd51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). - **An OData client's proxy constant written `@Module.Const` was stored with the `@`** — `ProxyHost` / `ProxyPort` / `ProxyUsername` / `ProxyPassword` are by-name references to a constant, and Studio Pro stores the bare name (with `ProxyType: Override`). `"@Module.Const"` named no constant, so the proxy resolved to nothing. `create`, `create or modify` and `alter` now store the bare name for the bare, `@` and quoted-`@` spellings. The constant may be a String or an Integer. - **The design-time `$metadata` fetch sent a literal credential with its quotes** — `HttpUsername: '''MxAdmin'''`, the spelling that stores Studio Pro's `'MxAdmin'`, was sent as `'MxAdmin'`: a 401 and an empty client. A credential that is a single string literal now sends its content; a compound expression (`'Key ' + @M.C`) is reported unresolved instead of sent as text. - **The `odata-data-sharing` skill taught credentials that store the wrong value** — its walkthrough wrote `HttpUsername: 'MxAdmin'`, which stores the identifier `MxAdmin`. It now writes `'''MxAdmin'''` and explains why. +- **`@caption` on a `while` loop was dropped without a word** (mendixlabs/mxcli#1187) — `check` passed, `exec` wrote the loop, and `describe` showed it without the caption, while the same caption on a `loop` was reported as **MDL042**. Both build a `Microflows$LoopedActivity`, which has no Caption property; only the for-each case was checked. MDL042 now covers a `while` too, pointing to `@annotation`, which round-trips through `describe`. ## [0.24.0] - 2026-09-24 diff --git a/cmd/mxcli/syntax/capability_docs_drift_test.go b/cmd/mxcli/syntax/capability_docs_drift_test.go index 2c78ffe9dd..c1950bd90a 100644 --- a/cmd/mxcli/syntax/capability_docs_drift_test.go +++ b/cmd/mxcli/syntax/capability_docs_drift_test.go @@ -50,6 +50,29 @@ func readDoc(t *testing.T, name string) string { // `mxcli syntax` topic is NOT sitting in the docs' "no MDL surface at all" // table. The syntax registry is populated from the code, so it cannot claim a // topic for something that does not exist. +// shippedCapabilities pairs a capability with the syntax topic that PROVES it +// ships. A topic is registered from Go code, so the pairing cannot go stale in +// the direction that matters: delete the feature and the topic goes with it. +// +// The last three were added after an audit found the matrix claiming all three +// were unavailable: "Microflow rules" and "Message definitions" sat under "Not +// Yet Implemented" (i.e. "no MDL surface at all") while both were authorable, +// and Layouts were described as "Read-only, no syntax topic" in three separate +// gap lists. None of it was caught, because this table did not name them — a +// hand-maintained guard only guards what someone remembered to add. +var shippedCapabilities = []struct{ topic, claim string }{ + {"database-connection", "Ext. DB connector"}, + {"queue", "Task queue"}, + {"scheduled-event", "Scheduled events"}, + {"regular-expression", "Regular expressions"}, + {"image-collection", "Image collection"}, + {"navigation.menu-document", "Menus"}, + {"workflow", "Workflows"}, + {"layout", "Layouts"}, + {"microflow.rule", "Microflow rules"}, + {"message-definition", "Message definitions"}, +} + func TestCapabilityDocsDoNotClaimShippedFeaturesAreMissing(t *testing.T) { matrix := readDoc(t, "MDL_FEATURE_MATRIX.md") @@ -66,15 +89,7 @@ func TestCapabilityDocsDoNotClaimShippedFeaturesAreMissing(t *testing.T) { // Each capability that must not appear as unimplemented, keyed by the syntax // topic that proves it ships. A topic is registered from Go code, so this // pairing cannot go stale in the direction that matters. - for _, c := range []struct{ topic, claim string }{ - {"database-connection", "Ext. DB connector"}, - {"queue", "Task queue"}, - {"scheduled-event", "Scheduled events"}, - {"regular-expression", "Regular expressions"}, - {"image-collection", "Image collection"}, - {"navigation.menu-document", "Menus"}, - {"workflow", "Workflows"}, - } { + for _, c := range shippedCapabilities { if ByPath(c.topic) == nil { t.Errorf("no `mxcli syntax %s` topic — either the feature was removed "+ "(then drop this row) or the topic is missing (then add it)", c.topic) @@ -122,3 +137,36 @@ func TestMissingCapabilitiesIsMarkedAsDated(t *testing.T) { } } } + +// TestMissingSyntaxTopicsAreActuallyMissing closes the hole that let Layouts be +// listed as "Read-only, no syntax topic" while `mxcli syntax layout` answered. +// +// This is a direct contradiction rather than a judgement call, which is why it +// can be asserted mechanically: the section states a topic does not exist, and +// the registry says it does. Deliberately narrower than the Skills/Examples gap +// lists, where an entry can be true at the same time as a syntax topic exists — +// Regular Expressions has a topic AND genuinely has no skill. +func TestMissingSyntaxTopicsAreActuallyMissing(t *testing.T) { + matrix := readDoc(t, "MDL_FEATURE_MATRIX.md") + + start := strings.Index(matrix, "### Missing Syntax Topics") + if start < 0 { + t.Fatal(`MDL_FEATURE_MATRIX.md has no "### Missing Syntax Topics" section — ` + + `if it was renamed, update this guard rather than deleting it`) + } + section := matrix[start:] + if end := strings.Index(section, "\n### "); end > 0 { + section = section[:end] + } + + for _, c := range shippedCapabilities { + if ByPath(c.topic) == nil { + continue // covered by the sibling test, which reports it there + } + if strings.Contains(section, c.claim) { + t.Errorf("MDL_FEATURE_MATRIX.md lists %q under \"Missing Syntax Topics\", "+ + "but `mxcli syntax %s` resolves — the doc tells a reader to look for "+ + "syntax that is already published", c.claim, c.topic) + } + } +} diff --git a/docs-site/src/appendixes/quick-reference.md b/docs-site/src/appendixes/quick-reference.md index 30f8116625..564e37586a 100644 --- a/docs-site/src/appendixes/quick-reference.md +++ b/docs-site/src/appendixes/quick-reference.md @@ -541,7 +541,7 @@ Cross-reference commands require `REFRESH CATALOG FULL` to populate reference da | Stdin piping | `echo "CMD" \| mxcli -p app.mpr` | Quiet mode, pipe-friendly | | Check syntax | `mxcli check script.mdl` | Parse-only validation | | Check references | `mxcli check script.mdl -p app.mpr --references` | With reference validation | -| Lint project | `mxcli lint -p app.mpr [--format json\|sarif]` | 14 built-in + 27 Starlark rules | +| Lint project | `mxcli lint -p app.mpr [--format json\|sarif]` | 19 built-in + 31 Starlark rules | | Report | `mxcli report -p app.mpr [--format markdown\|json\|html]` | Best practices report | | Test | `mxcli test tests/ -p app.mpr` | `.test.mdl` / `.test.md` files | | Diff script | `mxcli diff -p app.mpr changes.mdl` | Compare script vs project | diff --git a/docs-site/src/migration/validation.md b/docs-site/src/migration/validation.md index 2df058ccfe..0c173d2ac8 100644 --- a/docs-site/src/migration/validation.md +++ b/docs-site/src/migration/validation.md @@ -11,7 +11,7 @@ mxcli check script.mdl # 2. Reference validation (checks entity/microflow names exist) mxcli check script.mdl -p app.mpr --references -# 3. Lint the full project (41 built-in + 27 Starlark rules) +# 3. Lint the full project (19 built-in + 31 Starlark rules) mxcli lint -p app.mpr # 4. Quality report (scored 0-100 per category) diff --git a/docs-site/src/reference/capabilities.md b/docs-site/src/reference/capabilities.md index 5bf56c2307..f8a8bef35b 100644 --- a/docs-site/src/reference/capabilities.md +++ b/docs-site/src/reference/capabilities.md @@ -110,7 +110,7 @@ Everything mxcli can do, organized by use case. | Cross-references | `LIST CALLERS/CALLEES OF` | Who calls what | | Impact analysis | `LIST IMPACT OF Module.Entity` | What breaks if I change this | | Transitive callers | `LIST CALLERS OF ... TRANSITIVE` | Full call chain | -| Linting | `mxcli lint -p app.mpr` | 14 built-in + 27 Starlark rules | +| Linting | `mxcli lint -p app.mpr` | 19 built-in + 31 Starlark rules | | Best practices report | `mxcli report -p app.mpr` | Scored report with categories | | Missing translations | QUAL005 linter rule | Detects incomplete translations | | Catalog queries | `SELECT ... FROM CATALOG.tables` | SQL over project metadata | diff --git a/docs-site/src/tutorial/validation.md b/docs-site/src/tutorial/validation.md index 3aad848373..af79093f85 100644 --- a/docs-site/src/tutorial/validation.md +++ b/docs-site/src/tutorial/validation.md @@ -193,7 +193,7 @@ For a broader set of checks across the entire project (not just a single script) mxcli lint -p app.mpr ``` -This runs 14 built-in rules plus 29 Starlark rules covering security, architecture, quality, and naming conventions. See `mxcli lint --list-rules` for the full list. +This runs 19 built-in rules plus 31 Starlark rules covering security, architecture, quality, and naming conventions. See `mxcli lint --list-rules` for the full list. For CI/CD integration, output in SARIF format: diff --git a/docs/01-project/MDL_FEATURE_MATRIX.md b/docs/01-project/MDL_FEATURE_MATRIX.md index e1e3bc5f3e..9d2c96df67 100644 --- a/docs/01-project/MDL_FEATURE_MATRIX.md +++ b/docs/01-project/MDL_FEATURE_MATRIX.md @@ -166,10 +166,11 @@ live distinction is **MPR vs MCP**. | **Associations** | Y | Y | Y | N | Y | Y | 01 | Y | N | Y | Y | Y | Y | Y | Y | Y | N | | **Enumerations** | Y | Y | Y | Y | Y | Y | 01 | Y | Y | N | Y | Y | Y | N | Y | Y | Y | | **Microflows** | Y | Y | Y | Y | Y | N | 02 | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | -| **Nanoflows** | Y | Y | Y | Y | Y | N | Y | Y | Y | Y | Y | Y | Y | Y | P | N | N | +| **Nanoflows** | Y | Y | Y | Y | Y | N | 02b | Y | Y | Y | Y | Y | Y | Y | P | N | N | +| **Rules** | Y | Y | Y | Y | Y | N | Y | Y | Y | Y | N | Y | Y | N | N | Y | N | | **Pages** | Y | Y | Y | N | Y | Y | 03 | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | | **Snippets** | Y | Y | Y | N | Y | Y | 03 | Y | Y | Y | Y | Y | Y | N | Y | Y | Y | -| **Layouts** | Y | Y | N | N | N | N | N | N | Y | Y | Y | N | Y | N | Y | N | N | +| **Layouts** | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | Y | N | Y | Y | N | | **Java Actions** | Y | Y | Y | N | Y | N | 07 | Y | Y | Y | Y | Y | Y | N | Y | Y | N | | **Constants** | Y | Y | Y | Y | Y | N | 09 | Y | N | P | Y | N | Y | N | P | N | N | | **OData Clients** | Y | Y | Y | Y | Y | Y | 10 | Y | Y | P | Y | Y | Y | N | Y | Y | N | @@ -178,24 +179,25 @@ live distinction is **MPR vs MCP**. | **Modules** | Y | Y | Y | N | Y | N | all | Y | Y | Y | Y | Y | Y | N | Y | N | N | | **Navigation** | Y | Y | Y | - | - | Y | 11 | N | Y | Y | Y | Y | Y | N | N | Y | N | | **Business Events** | Y | Y | Y | N | Y | N | 13 | N | Y | N | Y | N | Y | N | Y | Y | N | -| **Project Settings** | Y | Y | - | - | - | Y | N | N | Y | Y | Y | N | Y | N | N | Y | P | -| **Task Queues** | Y | Y | Y | Y | Y | N | 21 | Y | Y | N | N | Y | Y | N | Y | Y | N | -| **Scheduled Events** | Y | Y | Y | Y | Y | N | 21 | Y | Y | Y | N | Y | Y | N | Y | Y | N | +| **Project Settings** | Y | Y | - | - | - | Y | 14 | N | Y | Y | Y | N | Y | N | N | Y | P | +| **Task Queues** | Y | Y | Y | Y | Y | N | Y | Y | Y | N | N | Y | Y | N | Y | Y | N | +| **Scheduled Events** | Y | Y | Y | Y | Y | N | Y | Y | Y | Y | N | Y | Y | N | Y | Y | N | | **Database Connections** | Y | Y | Y | Y | Y | N | 05 | Y | Y | N | N | Y | Y | N | Y | Y | N | -| **Regular Expressions** | Y | Y | Y | Y | Y | N | N | Y | Y | Y | N | N | Y | N | Y | Y | N | -| **Validation Rules** | - | Y | Y | - | - | Y | N | Y | N | Y | N | N | Y | N | N | Y | N | -| **Menus** | - | Y | Y | Y | Y | N | N | Y | N | N | N | N | Y | N | N | Y | N | -| **Image Collections** | Y | Y | Y | N | Y | N | N | Y | N | N | N | N | Y | N | Y | Y | N | -| **JavaScript Actions** | Y | Y | Y | N | Y | N | N | Y | Y | Y | N | N | Y | N | Y | N | N | -| **Published REST Services** | Y | Y | Y | Y | Y | N | N | N | Y | N | P | N | Y | N | N | Y | N | +| **Regular Expressions** | Y | Y | Y | Y | Y | N | Y | Y | Y | Y | N | N | Y | N | Y | Y | N | +| **Validation Rules** | - | Y | Y | - | - | Y | Y | Y | N | Y | N | N | Y | N | N | Y | N | +| **Menus** | - | Y | Y | Y | Y | N | 26 | Y | N | N | N | N | Y | N | N | Y | N | +| **Image Collections** | Y | Y | Y | N | Y | N | 19 | Y | N | N | N | N | Y | N | Y | Y | N | +| **JavaScript Actions** | Y | Y | Y | N | Y | N | 07b | Y | Y | Y | N | N | Y | N | Y | N | N | +| **Published REST Services** | Y | Y | Y | Y | Y | N | 22 | N | Y | N | P | N | Y | N | N | Y | N | | **REST Clients** | Y | Y | Y | Y | Y | Y | 06 | Y | Y | P | Y | Y | Y | N | Y | Y | N | -| **Import Mappings** | Y | Y | Y | N | Y | N | 06 | Y | N | N | P | Y | N | N | N | Y | N | -| **Export Mappings** | Y | Y | Y | N | Y | N | 06 | Y | N | N | P | Y | N | N | N | Y | N | +| **Import Mappings** | Y | Y | Y | N | Y | N | 21 | Y | N | N | P | Y | N | N | N | Y | N | +| **Export Mappings** | Y | Y | Y | N | Y | N | 21 | Y | N | N | P | Y | N | N | N | Y | N | | **JSON Structures** | Y | Y | Y | Y | Y | N | 20 | Y | N | N | P | N | N | N | N | N | N | -| **Workflows** | Y | Y | Y | N | Y | Y | N | Y | Y | Y | N | Y | Y | N | N | Y | N | -| **AI Agent documents** | Y | Y | Y | N | Y | N | N | Y | N | N | N | Y | Y | N | Y | Y | N | -| **Pluggable widgets** | Y | Y | Y | - | Y | Y | 03 | Y | N | N | P | Y | Y | N | N | Y | N | -| **Data Transformers** | Y | Y | Y | N | Y | N | N | Y | N | N | N | N | Y | N | Y | Y | N | +| **Message Definitions** | Y | Y | Y | Y | Y | Y | 40 | Y | N | N | N | N | Y | N | N | Y | N | +| **Workflows** | Y | Y | Y | N | Y | Y | 24 | Y | Y | Y | N | Y | Y | N | N | Y | N | +| **AI Agent documents** | Y | Y | Y | N | Y | N | 27 | Y | N | N | N | Y | Y | N | Y | Y | N | +| **Pluggable widgets** | Y | Y | Y | - | Y | Y | 30 | Y | N | N | P | Y | Y | N | N | Y | N | +| **Data Transformers** | Y | Y | Y | N | Y | N | 23 | Y | N | N | N | N | Y | N | Y | Y | N | ## Security Features @@ -214,7 +216,7 @@ live distinction is **MPR vs MCP**. | Feature | SHOW | DESCRIBE | CREATE | OR MODIFY | DROP | ALTER | Examples | Tests | Catalog | REFS | LSP | Skills | Help | Viz | REPL | Syntax | Starlark | |---------|------|----------|--------|-----------|------|-------|----------|-------|---------|------|-----|--------|------|-----|------|--------|----------| -| **Folders** | N | N | P | N | N | N | N | P | N | N | P | Y | Y | - | N | N | N | +| **Folders** | N | N | P | N | N | N | 18 | P | N | N | P | Y | Y | - | N | N | N | | **MOVE** | - | - | - | - | - | - | N | P | N | N | P | Y | Y | - | N | Y | N | ## External SQL & Data @@ -236,7 +238,7 @@ live distinction is **MPR vs MCP**. | **Catalog Query** | `select ... from CATALOG.` | Y | Y | SQL against project metadata | | **Cross-References** | `show callers/callees/references/impact/context of` | Y | Y | Requires `refresh catalog full` | | **Full-Text Search** | `search ''` | Y | Y | Across all strings and source | -| **Linting** | `mxcli lint -p app.mpr` | Y | Y | 14 built-in + 27 Starlark rules | +| **Linting** | `mxcli lint -p app.mpr` | Y | Y | 19 built-in + 31 Starlark rules | | **Report** | `mxcli report -p app.mpr` | Y | Y | Scored best practices report | | **Widget Discovery** | `show widgets [in module] [where ...]` | Y | Y | Experimental | | **Widget Update** | `update widgets set ... where ...` | Y | Y | Bulk pluggable widget updates | @@ -294,7 +296,6 @@ These types are not covered in `help.go` output: ### Missing Skills -- **Layouts** — Read-only, no skill needed - **Constants** — No dedicated skill ### Missing Tests @@ -303,7 +304,6 @@ These types are not covered in `help.go` output: ### Missing Examples -- **Layouts** — Read-only, no example needed - **Folders / MOVE** — No dedicated example file ### Missing REPL Autocomplete @@ -318,7 +318,6 @@ These types are not covered in `help.go` output: - **Constants** — No `mxcli syntax constant` topic - **Nanoflows** — No dedicated syntax topic (covered by microflow topic) -- **Layouts** — Read-only, no syntax topic - **Modules** — No dedicated syntax topic ### Missing Starlark APIs @@ -368,8 +367,6 @@ Document types that exist in Mendix and have **no** MDL surface at all. | Feature | Notes | |---------|-------| -| **Microflow rules** (`Microflows$Rule`) | Reusable decision logic called from a microflow. Not to be confused with `CREATE VALIDATION RULE`, which is an attribute constraint and *is* supported | -| **Message definitions** (`MessageDefinitions$MessageDefinitionCollection`) | Message definition documents | | **XML schemas** | Imported XSD documents | | **Web service publish / consume** | SOAP. `CALL WEB SERVICE` exists in microflows for a stored service; the service documents themselves are not authorable | | **Data importer** | Excel/CSV import documents | diff --git a/docs/01-project/MDL_QUICK_REFERENCE.md b/docs/01-project/MDL_QUICK_REFERENCE.md index 6bff72d057..4482937093 100644 --- a/docs/01-project/MDL_QUICK_REFERENCE.md +++ b/docs/01-project/MDL_QUICK_REFERENCE.md @@ -1867,7 +1867,7 @@ Name the widget the way you write it in a page body. The target is stored as the | Execute script | `mxcli exec script.mdl -p app.mpr` | Script file | | Check syntax | `mxcli check script.mdl` | Parse-only validation | | Check references | `mxcli check script.mdl -p app.mpr --references` | With reference validation | -| Lint project | `mxcli lint -p app.mpr [--format json\|sarif]` | 15 built-in + 27 Starlark rules | +| Lint project | `mxcli lint -p app.mpr [--format json\|sarif]` | 19 built-in + 31 Starlark rules | | Report | `mxcli report -p app.mpr [--format markdown\|json\|html]` | Best practices report | | Test | `mxcli test tests/ -p app.mpr` | `.test.mdl` / `.test.md` files | | Diff script | `mxcli diff -p app.mpr changes.mdl` | Compare script vs project | diff --git a/mdl-examples/bug-tests/microflow-1187-while-caption.mdl b/mdl-examples/bug-tests/microflow-1187-while-caption.mdl new file mode 100644 index 0000000000..89f18741f6 --- /dev/null +++ b/mdl-examples/bug-tests/microflow-1187-while-caption.mdl @@ -0,0 +1,77 @@ +-- mendixlabs/mxcli#1187 — @caption on a while loop was dropped with no diagnostic. +-- +-- A `while` builds the same Microflows$LoopedActivity as a for-each `loop`, and +-- generated/metamodel declares no Caption on it, so the caption has nowhere to go. +-- MDL042 was raised only for `loop`, so a `while` passed `check`, was written by +-- `exec`, and came back from `describe` with the caption gone. +-- +-- EXPECTED after the fix — `mxcli check` reports MDL042 twice, once per loop: +-- ⚠ @caption on a while loop has no effect … [MDL042] +-- ⚠ @caption on a loop has no effect … [MDL042] +-- and NOT for the @annotation forms, which are stored and round-trip. +-- +-- In Studio Pro: neither loop shows a caption; both show the attached note. + +create or modify entity MyFirstModule.LoopCaptionProbe ( + Name: string(100) +); +/ + +-- The reporter's repro: @caption on a while. +create or modify microflow MyFirstModule.MF_1187_WhileCaption () +returns integer as $Months +begin + declare $Months integer = 0; + @caption 'Are there months left to count?' + while $Months < 3 + begin + set $Months = $Months + 1; + end while; + return $Months; +end; +/ + +-- The case that was already reported, kept beside it so a regression in either +-- direction shows up in the same run. +create or modify microflow MyFirstModule.MF_1187_LoopCaption () +returns integer as $Count +begin + declare $Count integer = 0; + retrieve $Probes from MyFirstModule.LoopCaptionProbe; + @caption 'Count the probes' + loop $Probe in $Probes + begin + set $Count = $Count + 1; + end loop; + return $Count; +end; +/ + +-- The supported spelling for both: @annotation is stored and comes back from +-- describe, so neither of these may report MDL042. +create or modify microflow MyFirstModule.MF_1187_WhileAnnotation () +returns integer as $Months +begin + declare $Months integer = 0; + @annotation 'Counts up to three months' + while $Months < 3 + begin + set $Months = $Months + 1; + end while; + return $Months; +end; +/ + +create or modify microflow MyFirstModule.MF_1187_LoopAnnotation () +returns integer as $Count +begin + declare $Count integer = 0; + retrieve $Probes from MyFirstModule.LoopCaptionProbe; + @annotation 'Counts every probe' + loop $Probe in $Probes + begin + set $Count = $Count + 1; + end loop; + return $Count; +end; +/ diff --git a/mdl/backend/modelsdk/microflow_read_actions.go b/mdl/backend/modelsdk/microflow_read_actions.go index bc7d9193c1..70f1aefce0 100644 --- a/mdl/backend/modelsdk/microflow_read_actions.go +++ b/mdl/backend/modelsdk/microflow_read_actions.go @@ -331,6 +331,9 @@ func actionFromGen(el element.Element) microflows.MicroflowAction { ErrorHandlingType: microflows.ErrorHandlingType(rawStr(raw, "ErrorHandlingType")), TimeoutExpression: rawStr(raw, "TimeOutExpression"), } + if b, ok := raw.Lookup("UseRequestTimeOut").BooleanOK(); ok { + out.UseRequestTimeOut = b + } out.ID = model.ID(a.ID()) if hc, ok := raw.Lookup("HttpConfiguration").DocumentOK(); ok { out.HttpConfiguration = httpConfigFromRaw(hc) diff --git a/mdl/catalog/builder_microflows.go b/mdl/catalog/builder_microflows.go index d9cf1596d6..1bbc97022d 100644 --- a/mdl/catalog/builder_microflows.go +++ b/mdl/catalog/builder_microflows.go @@ -58,9 +58,10 @@ func (b *Builder) buildMicroflows() error { if b.fullMode { actStmt, err = b.tx.Prepare(` INSERT INTO activities_data (Id, Name, Caption, ActivityType, Sequence, MicroflowId, MicroflowQualifiedName, - ModuleName, Folder, EntityRef, ActionType, ServiceRef, ActionRef, Description, + ModuleName, Folder, EntityRef, ActionType, ServiceRef, ActionRef, + UseRequestTimeout, TimeoutExpression, Description, ProjectId, SnapshotId) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `) if err != nil { return err @@ -158,6 +159,8 @@ func (b *Builder) buildMicroflows() error { actionType := "" serviceRef := "" actionRef := "" + useRequestTimeout := 0 + timeoutExpression := "" if act, ok := obj.(*microflows.ActionActivity); ok { if act.Action != nil { @@ -170,6 +173,13 @@ func (b *Builder) buildMicroflows() error { case *microflows.CallExternalAction: serviceRef = a.ConsumedODataService actionRef = a.Name + case *microflows.RestCallAction: + // "Use a timeout" plus the seconds, which Studio Pro + // stores as an expression string (e.g. "300"). + if a.UseRequestTimeOut { + useRequestTimeout = 1 + } + timeoutExpression = a.TimeoutExpression } } } @@ -188,6 +198,8 @@ func (b *Builder) buildMicroflows() error { actionType, serviceRef, actionRef, + useRequestTimeout, + timeoutExpression, "", projectID, snapshotID, ) @@ -250,6 +262,8 @@ func (b *Builder) buildMicroflows() error { actionType := "" serviceRef := "" actionRef := "" + useRequestTimeout := 0 + timeoutExpression := "" if act, ok := obj.(*microflows.ActionActivity); ok { if act.Action != nil { @@ -262,6 +276,13 @@ func (b *Builder) buildMicroflows() error { case *microflows.CallExternalAction: serviceRef = a.ConsumedODataService actionRef = a.Name + case *microflows.RestCallAction: + // "Use a timeout" plus the seconds, which Studio Pro + // stores as an expression string (e.g. "300"). + if a.UseRequestTimeOut { + useRequestTimeout = 1 + } + timeoutExpression = a.TimeoutExpression } } } @@ -280,6 +301,8 @@ func (b *Builder) buildMicroflows() error { actionType, serviceRef, actionRef, + useRequestTimeout, + timeoutExpression, "", projectID, snapshotID, ) @@ -341,7 +364,7 @@ func (b *Builder) buildMicroflows() error { if _, err := actStmt.Exec( string(obj.GetID()), activityName, "Activity", activityType, seq+1, string(rule.ID), qualifiedName, moduleName, moduleName, - "", actionType, "", "", "", + "", actionType, "", "", 0, "", "", projectID, snapshotID, ); err != nil { return err diff --git a/mdl/catalog/builder_permissions.go b/mdl/catalog/builder_permissions.go index e2d53beb66..6ac3dd7d40 100644 --- a/mdl/catalog/builder_permissions.go +++ b/mdl/catalog/builder_permissions.go @@ -61,8 +61,8 @@ func (b *Builder) buildPermissions() error { } stmt, err := b.tx.Prepare(` - INSERT INTO permissions (ModuleRoleName, ElementType, ElementName, MemberName, AccessType, XPathConstraint, ModuleName, ProjectId, SnapshotId) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO permissions (ModuleRoleName, ElementType, ElementName, MemberName, AccessType, XPathConstraint, DefaultMemberAccessRights, ModuleName, ProjectId, SnapshotId) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `) if err != nil { return err @@ -112,27 +112,33 @@ func (b *Builder) buildEntityPermissions(stmt *sql.Stmt, projectID, snapshotID s // and MemberAccesses, not by AllowRead/AllowWrite flags. hasRead, hasWrite := entityAccessFromMemberRights(rule) + // The rule's "default rights for new members" setting. Stored + // alongside every row this rule produces, the same way + // XPathConstraint is: it is a property of the rule, not of the + // individual access type. + defaultRights := string(rule.DefaultMemberAccessRights) + for _, roleName := range roleNames { // Entity-level permissions if rule.AllowCreate { - stmt.Exec(roleName, PermissionElementEntity, entityQN, nil, AccessTypeCreate, xpath, moduleName, projectID, snapshotID) + stmt.Exec(roleName, PermissionElementEntity, entityQN, nil, AccessTypeCreate, xpath, defaultRights, moduleName, projectID, snapshotID) count++ } if hasRead { - stmt.Exec(roleName, PermissionElementEntity, entityQN, nil, AccessTypeRead, xpath, moduleName, projectID, snapshotID) + stmt.Exec(roleName, PermissionElementEntity, entityQN, nil, AccessTypeRead, xpath, defaultRights, moduleName, projectID, snapshotID) count++ } if hasWrite { - stmt.Exec(roleName, PermissionElementEntity, entityQN, nil, AccessTypeWrite, xpath, moduleName, projectID, snapshotID) + stmt.Exec(roleName, PermissionElementEntity, entityQN, nil, AccessTypeWrite, xpath, defaultRights, moduleName, projectID, snapshotID) count++ } if rule.AllowDelete { - stmt.Exec(roleName, PermissionElementEntity, entityQN, nil, AccessTypeDelete, xpath, moduleName, projectID, snapshotID) + stmt.Exec(roleName, PermissionElementEntity, entityQN, nil, AccessTypeDelete, xpath, defaultRights, moduleName, projectID, snapshotID) count++ } // Member-level permissions - count += b.emitMemberPermissions(stmt, rule, ent, roleName, entityQN, xpath, moduleName, projectID, snapshotID) + count += b.emitMemberPermissions(stmt, rule, ent, roleName, entityQN, xpath, defaultRights, moduleName, projectID, snapshotID) } } } @@ -172,7 +178,7 @@ func entityAccessFromMemberRights(rule *domainmodel.AccessRule) (hasRead, hasWri // When MemberAccesses is non-empty, use explicit per-member rights. // When MemberAccesses is empty, expand DefaultMemberAccessRights to all attributes. func (b *Builder) emitMemberPermissions(stmt *sql.Stmt, rule *domainmodel.AccessRule, ent *domainmodel.Entity, - roleName, entityQN, xpath, moduleName, projectID, snapshotID string) int { + roleName, entityQN, xpath, defaultRights, moduleName, projectID, snapshotID string) int { count := 0 if len(rule.MemberAccesses) > 0 { @@ -187,11 +193,11 @@ func (b *Builder) emitMemberPermissions(stmt *sql.Stmt, rule *domainmodel.Access } if ma.AccessRights == domainmodel.MemberAccessRightsReadOnly || ma.AccessRights == domainmodel.MemberAccessRightsReadWrite { - stmt.Exec(roleName, PermissionElementEntity, entityQN, memberName, AccessTypeMemberRead, xpath, moduleName, projectID, snapshotID) + stmt.Exec(roleName, PermissionElementEntity, entityQN, memberName, AccessTypeMemberRead, xpath, defaultRights, moduleName, projectID, snapshotID) count++ } if ma.AccessRights == domainmodel.MemberAccessRightsReadWrite { - stmt.Exec(roleName, PermissionElementEntity, entityQN, memberName, AccessTypeMemberWrite, xpath, moduleName, projectID, snapshotID) + stmt.Exec(roleName, PermissionElementEntity, entityQN, memberName, AccessTypeMemberWrite, xpath, defaultRights, moduleName, projectID, snapshotID) count++ } } @@ -199,11 +205,11 @@ func (b *Builder) emitMemberPermissions(stmt *sql.Stmt, rule *domainmodel.Access // Expand default to all attributes for _, attr := range ent.Attributes { if rule.DefaultMemberAccessRights == domainmodel.MemberAccessRightsReadOnly || rule.DefaultMemberAccessRights == domainmodel.MemberAccessRightsReadWrite { - stmt.Exec(roleName, PermissionElementEntity, entityQN, attr.Name, AccessTypeMemberRead, xpath, moduleName, projectID, snapshotID) + stmt.Exec(roleName, PermissionElementEntity, entityQN, attr.Name, AccessTypeMemberRead, xpath, defaultRights, moduleName, projectID, snapshotID) count++ } if rule.DefaultMemberAccessRights == domainmodel.MemberAccessRightsReadWrite { - stmt.Exec(roleName, PermissionElementEntity, entityQN, attr.Name, AccessTypeMemberWrite, xpath, moduleName, projectID, snapshotID) + stmt.Exec(roleName, PermissionElementEntity, entityQN, attr.Name, AccessTypeMemberWrite, xpath, defaultRights, moduleName, projectID, snapshotID) count++ } } @@ -233,7 +239,7 @@ func (b *Builder) buildMicroflowPermissions(stmt *sql.Stmt, projectID, snapshotI for _, roleID := range mf.AllowedModuleRoles { // AllowedModuleRoles are BY_NAME strings stored as model.ID roleName := string(roleID) - stmt.Exec(roleName, PermissionElementMicroflow, mfQN, nil, AccessTypeExecute, nil, moduleName, projectID, snapshotID) + stmt.Exec(roleName, PermissionElementMicroflow, mfQN, nil, AccessTypeExecute, nil, nil, moduleName, projectID, snapshotID) count++ } } @@ -262,7 +268,7 @@ func (b *Builder) buildPagePermissions(stmt *sql.Stmt, projectID, snapshotID str for _, roleID := range pg.AllowedRoles { // AllowedRoles are BY_NAME strings stored as model.ID roleName := string(roleID) - stmt.Exec(roleName, PermissionElementPage, pgQN, nil, AccessTypeView, nil, moduleName, projectID, snapshotID) + stmt.Exec(roleName, PermissionElementPage, pgQN, nil, AccessTypeView, nil, nil, moduleName, projectID, snapshotID) count++ } } @@ -289,7 +295,7 @@ func (b *Builder) buildODataServicePermissions(stmt *sql.Stmt, projectID, snapsh svcQN := moduleName + "." + svc.Name for _, roleName := range svc.AllowedModuleRoles { - stmt.Exec(roleName, PermissionElementODataService, svcQN, nil, AccessTypeAccess, nil, moduleName, projectID, snapshotID) + stmt.Exec(roleName, PermissionElementODataService, svcQN, nil, AccessTypeAccess, nil, nil, moduleName, projectID, snapshotID) count++ } } diff --git a/mdl/catalog/tables.go b/mdl/catalog/tables.go index 84aca3aee0..d9a8ddb3bc 100644 --- a/mdl/catalog/tables.go +++ b/mdl/catalog/tables.go @@ -545,6 +545,8 @@ func (c *Catalog) createTables() error { ActionType TEXT, ServiceRef TEXT, ActionRef TEXT, + UseRequestTimeout INTEGER DEFAULT 0, + TimeoutExpression TEXT, Description TEXT, ProjectId TEXT, SnapshotId TEXT @@ -1026,6 +1028,7 @@ func (c *Catalog) createTables() error { MemberName TEXT, AccessType TEXT NOT NULL, XPathConstraint TEXT, + DefaultMemberAccessRights TEXT, ModuleName TEXT, ProjectId TEXT, SnapshotId TEXT diff --git a/mdl/executor/cmd_microflows_builder_annotations.go b/mdl/executor/cmd_microflows_builder_annotations.go index 591a203b7b..f6aa20b0b4 100644 --- a/mdl/executor/cmd_microflows_builder_annotations.go +++ b/mdl/executor/cmd_microflows_builder_annotations.go @@ -117,12 +117,13 @@ func (fb *flowBuilder) applyAnnotations(activityID model.ID, ann *ast.ActivityAn if ann.Caption != "" { activity.Caption = ann.Caption } - case *microflows.LoopedActivity: - // LOOP / WHILE activities can carry a caption just like - // splits and action activities. - if ann.Caption != "" { - activity.Caption = ann.Caption - } + // No case for *microflows.LoopedActivity. A loop and a while build + // one, and generated/metamodel -- the arbiter -- declares no Caption + // on Microflows$LoopedActivity, so there is nowhere for the value to + // go: the gen writer emits none and the reader can never populate + // one. Assigning it here wrote a field nothing reads and contradicted + // MDL042, which tells the author the caption is dropped. The + // diagnostic is the whole of the support (mendixlabs/mxcli#1187). } break diff --git a/mdl/executor/cmd_microflows_builder_annotations_test.go b/mdl/executor/cmd_microflows_builder_annotations_test.go index c8adc52599..860303ac41 100644 --- a/mdl/executor/cmd_microflows_builder_annotations_test.go +++ b/mdl/executor/cmd_microflows_builder_annotations_test.go @@ -271,7 +271,7 @@ func TestLoopBodyIfAnnotationPromotedToParentFlows(t *testing.T) { // TestLoopCaptionPreserved covers the loop caption case — previously untested // per PR review. The fix for the outer-IF caption contamination bug also applied // the same snapshot/restore pattern to addLoopStatement and addWhileStatement. -func TestLoopCaptionPreserved(t *testing.T) { +func TestLoopCaptionNotStorable(t *testing.T) { innerReturn := &ast.ReturnStmt{Value: &ast.LiteralExpr{Value: true, Kind: ast.LiteralBoolean}} loop := &ast.LoopStmt{ LoopVariable: "item", @@ -299,13 +299,18 @@ func TestLoopCaptionPreserved(t *testing.T) { if len(loops) != 1 { t.Fatalf("expected 1 LoopedActivity, got %d", len(loops)) } - if loops[0].Caption != "Process each item" { - t.Errorf("loop caption: got %q, want %q", loops[0].Caption, "Process each item") + // The builder must NOT carry a caption onto a LoopedActivity. generated/metamodel + // declares no Caption on Microflows$LoopedActivity, so the value cannot reach + // storage: measured on 11.6.6, `@caption` on a loop is absent from `describe` + // after `exec`, while `@annotation` round-trips. Setting it here wrote a field + // nothing reads and contradicted MDL042 (mendixlabs/mxcli#1187). + if loops[0].Caption != "" { + t.Errorf("loop caption: got %q, want %q — a loop caption is not storable, MDL042 reports it", loops[0].Caption, "") } } -// TestWhileLoopCaptionPreserved — same coverage for the WHILE shape. -func TestWhileLoopCaptionPreserved(t *testing.T) { +// TestWhileLoopCaptionNotStorable — same coverage for the WHILE shape. +func TestWhileLoopCaptionNotStorable(t *testing.T) { whileStmt := &ast.WhileStmt{ Condition: &ast.BinaryExpr{ Left: &ast.VariableExpr{Name: "n"}, @@ -337,8 +342,10 @@ func TestWhileLoopCaptionPreserved(t *testing.T) { if len(loops) != 1 { t.Fatalf("expected 1 LoopedActivity (WHILE), got %d", len(loops)) } - if loops[0].Caption != "Until n >= 10" { - t.Errorf("while caption: got %q, want %q", loops[0].Caption, "Until n >= 10") + // Same as the for-each case above: a while builds the same LoopedActivity, so + // its caption is equally unstorable and equally reported as MDL042. + if loops[0].Caption != "" { + t.Errorf("while caption: got %q, want %q — a while caption is not storable, MDL042 reports it", loops[0].Caption, "") } } diff --git a/mdl/executor/cmd_microflows_show_helpers.go b/mdl/executor/cmd_microflows_show_helpers.go index 07274f92ea..38c18d4f08 100644 --- a/mdl/executor/cmd_microflows_show_helpers.go +++ b/mdl/executor/cmd_microflows_show_helpers.go @@ -797,9 +797,9 @@ func emitObjectAnnotations( if split, ok := obj.(*microflows.InheritanceSplit); ok && split.Caption != "" { *lines = append(*lines, indentStr+fmt.Sprintf("@caption %s", mdlQuote(split.Caption))) } - if loop, ok := obj.(*microflows.LoopedActivity); ok && loop.Caption != "" { - *lines = append(*lines, indentStr+fmt.Sprintf("@caption %s", mdlQuote(loop.Caption))) - } + // No @caption for a LoopedActivity: the metamodel declares none on it, so a + // stored loop never carries one and this only ever emitted MDL that check + // would then report as MDL042 (mendixlabs/mxcli#1187). // @annotation (attached Annotation objects) *lines = append(*lines, annotationsByTarget.lines(currentID, pos, objectHeight(obj), indentStr)...) diff --git a/mdl/executor/cmd_microflows_show_helpers_test.go b/mdl/executor/cmd_microflows_show_helpers_test.go index 2cf8817846..d2169bca9b 100644 --- a/mdl/executor/cmd_microflows_show_helpers_test.go +++ b/mdl/executor/cmd_microflows_show_helpers_test.go @@ -162,7 +162,14 @@ func TestEmitObjectAnnotations_EscapesMultilineText(t *testing.T) { } } -func TestEmitObjectAnnotations_LoopCaption(t *testing.T) { +// A LoopedActivity must emit NO @caption. generated/metamodel declares no Caption +// on Microflows$LoopedActivity, so a stored loop can never carry one — emitting it +// produced MDL that `check` then reports as MDL042 (mendixlabs/mxcli#1187). The +// in-memory field is set here deliberately: even then, nothing may be emitted. +// mdlQuote's escaping is covered directly by TestMdlQuote_* in +// cmd_microflows_annotation_escape_test.go, so nothing is lost by not asserting it +// on a caption that cannot exist. +func TestEmitObjectAnnotations_LoopCaptionNotEmitted(t *testing.T) { obj := µflows.LoopedActivity{ BaseMicroflowObject: microflows.BaseMicroflowObject{ BaseElement: model.BaseElement{ID: mkID("loop")}, @@ -176,8 +183,8 @@ func TestEmitObjectAnnotations_LoopCaption(t *testing.T) { emitObjectAnnotations(obj, &lines, "", nil, nil, nil, nil) got := strings.Join(lines, "\n") - if !strings.Contains(got, "@caption 'Loop owner''s\\ncaption'") { - t.Fatalf("expected escaped loop caption, got:\n%s", got) + if strings.Contains(got, "@caption") { + t.Fatalf("a loop must not describe a @caption (it is not storable), got:\n%s", got) } } diff --git a/mdl/executor/validate_microflow.go b/mdl/executor/validate_microflow.go index 981b8c93b4..5902bbfec8 100644 --- a/mdl/executor/validate_microflow.go +++ b/mdl/executor/validate_microflow.go @@ -374,6 +374,10 @@ func (v *microflowValidator) walkBody(body []ast.MicroflowStatement) { // never legal. The body was not walked at all before, so nothing inside // a while was checked. v.checkQualifiedCallInExpression("while condition", stmt.Condition) + // A while loop is the same Microflows$LoopedActivity as a for-each loop, + // with a WhileLoopCondition instead of an iterator, so a @caption on it is + // dropped exactly the same way. + v.checkCaptionOnLoop(stmt.Annotations, "a while loop") v.walkBody(stmt.Body) case *ast.CallMicroflowStmt: v.checkAssociationObjectArgs("microflow "+stmt.MicroflowName.String(), stmt.Arguments) @@ -387,17 +391,7 @@ func (v *microflowValidator) walkBody(body []ast.MicroflowStatement) { // mapping are alternatives. Same function exec calls. v.checkWebServiceRequestBody(stmt) case *ast.LoopStmt: - // Check: @caption on a loop is silently dropped — Mendix for-loops - // have no caption (Microflows$LoopedActivity has no Caption - // property; Studio Pro auto-labels them from the iterator). The - // supported way to label a loop is an annotation note. - if stmt.Annotations != nil && stmt.Annotations.Caption != "" { - v.addViolation("MDL042", linter.SeverityWarning, - "@caption on a loop has no effect — Mendix loops have no caption "+ - "(the loop activity has no Caption property, so it is dropped). "+ - "Use @annotation to attach a note to the loop instead.", - "Replace @caption with @annotation to label the loop") - } + v.checkCaptionOnLoop(stmt.Annotations, "a loop") // Check: nested loop anti-pattern. This is a heuristic — a nested loop is // only wasteful when the inner loop is a key LOOKUP (find one matching // item). Intentional aggregation that must visit every element (group × @@ -1573,3 +1567,19 @@ func (v *microflowValidator) checkAnnotationLabels(body []ast.MicroflowStatement } walk(body) } + +// checkCaptionOnLoop raises MDL042 for a @caption on a loop or a while loop. Both +// build a Microflows$LoopedActivity, which has no Caption property -- Studio Pro +// labels a for-each loop from its iterator and a while loop from its condition -- +// so the caption is dropped on write. The supported way to label either is an +// annotation note, which round-trips through DESCRIBE. +func (v *microflowValidator) checkCaptionOnLoop(ann *ast.ActivityAnnotations, what string) { + if ann == nil || ann.Caption == "" { + return + } + v.addViolation("MDL042", linter.SeverityWarning, + "@caption on "+what+" has no effect — Mendix loops have no caption "+ + "(the loop activity has no Caption property, so it is dropped). "+ + "Use @annotation to attach a note to the loop instead.", + "Replace @caption with @annotation to label the loop") +} diff --git a/mdl/executor/validate_microflow_loop_caption_test.go b/mdl/executor/validate_microflow_loop_caption_test.go index d66e27e2aa..f402b3e9df 100644 --- a/mdl/executor/validate_microflow_loop_caption_test.go +++ b/mdl/executor/validate_microflow_loop_caption_test.go @@ -52,3 +52,32 @@ func TestValidateMicroflow_PlainLoopNoWarn(t *testing.T) { t.Error("MDL042 must not fire for a plain loop") } } + +func mfWithWhileAnnotations(ann *ast.ActivityAnnotations) *ast.CreateMicroflowStmt { + return &ast.CreateMicroflowStmt{ + Name: ast.QualifiedName{Module: "Sample", Name: "MF"}, + Body: []ast.MicroflowStatement{ + &ast.WhileStmt{ + Condition: &ast.LiteralExpr{Value: true, Kind: ast.LiteralBoolean}, + Annotations: ann, + Body: []ast.MicroflowStatement{}, + }, + }, + } +} + +// A while loop builds the same LoopedActivity as a for-each loop, so its @caption +// is dropped the same way and must be reported the same way (mendixlabs/mxcli#1187). +// Before the fix a while loop's caption passed check silently and vanished on exec. +func TestValidateMicroflow_CaptionOnWhileWarns(t *testing.T) { + if !loopHasMDL042(mfWithWhileAnnotations(&ast.ActivityAnnotations{Caption: "Months left?"})) { + t.Error("expected MDL042 warning for @caption on a while loop") + } +} + +// @annotation is the supported label for a while loop too, and must not warn. +func TestValidateMicroflow_AnnotationOnWhileNoWarn(t *testing.T) { + if loopHasMDL042(mfWithWhileAnnotations(&ast.ActivityAnnotations{Notes: []ast.MicroflowAnnotation{{Text: "Count the months"}}})) { + t.Error("MDL042 must not fire for @annotation on a while loop") + } +} diff --git a/mdl/linter/context.go b/mdl/linter/context.go index a9ff0e9347..e27ef4fbf1 100644 --- a/mdl/linter/context.go +++ b/mdl/linter/context.go @@ -388,13 +388,17 @@ type Permission struct { MemberName string // populated for MEMBER_READ/MEMBER_WRITE, empty for entity-level XPathConstraint string // empty means unconstrained IsConstrained bool // convenience: XPathConstraint != "" + // DefaultMemberAccessRights is the rule's "default rights for new members" + // setting: None, ReadOnly or ReadWrite. Empty when not applicable. + DefaultMemberAccessRights string } // PermissionsFor returns an iterator over all permissions for a given entity. func (ctx *LintContext) PermissionsFor(entityQualifiedName string) iter.Seq[Permission] { return func(yield func(Permission) bool) { rows, err := ctx.db.Query(` - SELECT ModuleRoleName, ElementName, MemberName, AccessType, XPathConstraint, ModuleName + SELECT ModuleRoleName, ElementName, MemberName, AccessType, XPathConstraint, + COALESCE(DefaultMemberAccessRights, ''), ModuleName FROM permissions WHERE ElementType = 'ENTITY' AND ElementName = ? ORDER BY ModuleRoleName, AccessType @@ -408,7 +412,8 @@ func (ctx *LintContext) PermissionsFor(entityQualifiedName string) iter.Seq[Perm for rows.Next() { var p Permission var memberName, xpathConstraint, moduleName sql.NullString - err := rows.Scan(&p.ModuleRoleName, &p.EntityName, &memberName, &p.AccessType, &xpathConstraint, &moduleName) + err := rows.Scan(&p.ModuleRoleName, &p.EntityName, &memberName, &p.AccessType, &xpathConstraint, + &p.DefaultMemberAccessRights, &moduleName) if err != nil { ctx.recordQueryError("PermissionsFor (row scan)", err) continue @@ -435,6 +440,9 @@ type AllPermission struct { XPathConstraint string IsConstrained bool ModuleName string + // DefaultMemberAccessRights is the rule's "default rights for new members" + // setting: None, ReadOnly or ReadWrite. Empty for non-entity permissions. + DefaultMemberAccessRights string } // Permissions returns an iterator over all permissions in the catalog. @@ -446,7 +454,8 @@ func (ctx *LintContext) Permissions() iter.Seq[AllPermission] { rows, err := ctx.db.Query(` SELECT ModuleRoleName, ElementType, ElementName, COALESCE(MemberName, ''), AccessType, - COALESCE(XPathConstraint, ''), COALESCE(ModuleName, '') + COALESCE(XPathConstraint, ''), COALESCE(DefaultMemberAccessRights, ''), + COALESCE(ModuleName, '') FROM permissions ORDER BY ElementType, ElementName, ModuleRoleName, AccessType `) @@ -459,7 +468,8 @@ func (ctx *LintContext) Permissions() iter.Seq[AllPermission] { for rows.Next() { var p AllPermission if err := rows.Scan(&p.ModuleRoleName, &p.ElementType, &p.ElementName, - &p.MemberName, &p.AccessType, &p.XPathConstraint, &p.ModuleName); err != nil { + &p.MemberName, &p.AccessType, &p.XPathConstraint, + &p.DefaultMemberAccessRights, &p.ModuleName); err != nil { continue } p.IsConstrained = p.XPathConstraint != "" @@ -1368,6 +1378,12 @@ type Activity struct { // by the catalog builder and are empty for activities that call neither. ServiceRef string ActionRef string + // UseRequestTimeout mirrors "Use a timeout" on a Call REST service + // activity; TimeoutExpression is the number of seconds, which Studio Pro + // stores as an expression string (e.g. "300"). Both are zero for other + // action types. + UseRequestTimeout bool + TimeoutExpression string } // ActivitiesFor returns an iterator over all activities for a given microflow. @@ -1376,7 +1392,8 @@ func (ctx *LintContext) ActivitiesFor(microflowQualifiedName string) iter.Seq[Ac rows, err := ctx.db.Query(` SELECT Id, Name, Caption, ActivityType, ActionType, MicroflowId, MicroflowQualifiedName, ModuleName, EntityRef, - ServiceRef, ActionRef + ServiceRef, ActionRef, + COALESCE(UseRequestTimeout, 0), COALESCE(TimeoutExpression, '') FROM activities WHERE MicroflowQualifiedName = ? ORDER BY Sequence @@ -1391,9 +1408,11 @@ func (ctx *LintContext) ActivitiesFor(microflowQualifiedName string) iter.Seq[Ac var a Activity var name, caption, actionType, entityRef sql.NullString var serviceRef, actionRef sql.NullString + var useRequestTimeout int err := rows.Scan(&a.ID, &name, &caption, &a.ActivityType, &actionType, &a.MicroflowID, &a.MicroflowQualifiedName, &a.ModuleName, &entityRef, - &serviceRef, &actionRef) + &serviceRef, &actionRef, + &useRequestTimeout, &a.TimeoutExpression) if err != nil { ctx.recordQueryError("ActivitiesFor (row scan)", err) continue @@ -1404,6 +1423,7 @@ func (ctx *LintContext) ActivitiesFor(microflowQualifiedName string) iter.Seq[Ac a.EntityRef = entityRef.String a.ServiceRef = serviceRef.String a.ActionRef = actionRef.String + a.UseRequestTimeout = useRequestTimeout != 0 if !yield(a) { return diff --git a/mdl/linter/starlark.go b/mdl/linter/starlark.go index 40e2d48155..84d84be701 100644 --- a/mdl/linter/starlark.go +++ b/mdl/linter/starlark.go @@ -1008,27 +1008,29 @@ func referenceToStarlark(r Reference) starlark.Value { // allPermissionToStarlark converts an AllPermission to a Starlark struct. func allPermissionToStarlark(p AllPermission) starlark.Value { return starlarkstruct.FromStringDict(starlark.String("permission"), starlark.StringDict{ - "module_role_name": starlark.String(p.ModuleRoleName), - "element_type": starlark.String(p.ElementType), - "element_name": starlark.String(p.ElementName), - "member_name": starlark.String(p.MemberName), - "access_type": starlark.String(p.AccessType), - "xpath_constraint": starlark.String(p.XPathConstraint), - "is_constrained": starlark.Bool(p.IsConstrained), - "module_name": starlark.String(p.ModuleName), + "module_role_name": starlark.String(p.ModuleRoleName), + "element_type": starlark.String(p.ElementType), + "element_name": starlark.String(p.ElementName), + "member_name": starlark.String(p.MemberName), + "access_type": starlark.String(p.AccessType), + "xpath_constraint": starlark.String(p.XPathConstraint), + "is_constrained": starlark.Bool(p.IsConstrained), + "module_name": starlark.String(p.ModuleName), + "default_member_access_rights": starlark.String(p.DefaultMemberAccessRights), }) } // permissionToStarlark converts a Permission to a Starlark struct. func permissionToStarlark(p Permission) starlark.Value { return starlarkstruct.FromStringDict(starlark.String("entity_permission"), starlark.StringDict{ - "module_role_name": starlark.String(p.ModuleRoleName), - "module_name": starlark.String(p.ModuleName), - "entity_name": starlark.String(p.EntityName), - "access_type": starlark.String(p.AccessType), - "member_name": starlark.String(p.MemberName), - "xpath_constraint": starlark.String(p.XPathConstraint), - "is_constrained": starlark.Bool(p.IsConstrained), + "module_role_name": starlark.String(p.ModuleRoleName), + "module_name": starlark.String(p.ModuleName), + "entity_name": starlark.String(p.EntityName), + "access_type": starlark.String(p.AccessType), + "member_name": starlark.String(p.MemberName), + "xpath_constraint": starlark.String(p.XPathConstraint), + "is_constrained": starlark.Bool(p.IsConstrained), + "default_member_access_rights": starlark.String(p.DefaultMemberAccessRights), }) } @@ -1089,6 +1091,8 @@ func activityToStarlark(a Activity) starlark.Value { "entity_ref": starlark.String(a.EntityRef), "service_ref": starlark.String(a.ServiceRef), "action_ref": starlark.String(a.ActionRef), + "use_request_timeout": starlark.Bool(a.UseRequestTimeout), + "timeout_expression": starlark.String(a.TimeoutExpression), }) } diff --git a/sdk/microflows/microflows.go b/sdk/microflows/microflows.go index 5f3497dcb4..ddce64bfad 100644 --- a/sdk/microflows/microflows.go +++ b/sdk/microflows/microflows.go @@ -455,6 +455,9 @@ type LoopSource interface { // LoopedActivity represents a loop construct (FOR EACH or WHILE). type LoopedActivity struct { BaseMicroflowObject + // Caption is NOT storable: generated/metamodel declares no Caption on + // Microflows$LoopedActivity, so a value here is dropped at the gen boundary. + // Nothing sets it; `mxcli check` reports MDL042 instead (mendixlabs/mxcli#1187). Caption string `json:"caption,omitempty"` Documentation string `json:"documentation,omitempty"` LoopSource LoopSource `json:"loopSource,omitempty"` diff --git a/sdk/microflows/microflows_actions.go b/sdk/microflows/microflows_actions.go index d2e77b44d5..c338a2dcb0 100644 --- a/sdk/microflows/microflows_actions.go +++ b/sdk/microflows/microflows_actions.go @@ -846,7 +846,11 @@ type RestCallAction struct { ErrorHandlingType ErrorHandlingType `json:"errorHandlingType,omitempty"` OutputVariable string `json:"outputVariable,omitempty"` UseReturnVariable bool `json:"useReturnVariable"` - TimeoutExpression string `json:"timeoutExpression,omitempty"` + // UseRequestTimeOut is Studio Pro's "Use a timeout" toggle. Stored as + // UseRequestTimeOut; TimeoutExpression (stored TimeOutExpression) holds the + // number of seconds as an expression, e.g. "300". + UseRequestTimeOut bool `json:"useRequestTimeOut,omitempty"` + TimeoutExpression string `json:"timeoutExpression,omitempty"` } func (RestCallAction) isMicroflowAction() {}