diff --git a/.claude/commands/mxcli-dev/review.md b/.claude/commands/mxcli-dev/review.md index 9d14c8227d..53edca9946 100644 --- a/.claude/commands/mxcli-dev/review.md +++ b/.claude/commands/mxcli-dev/review.md @@ -56,6 +56,9 @@ proactively. Add a row after every review that surfaces something new. | 26 | A clause added to a SHARED grammar rule (a datasource, a widget-property list) is written by only ONE of the constructs that rule serves — the others parse it, `exec` reports success, and DESCRIBE does not echo it back. A silent drop, often shipped by the very change that was fixing silent drops | Code correctness | Enumerate the other constructs the rule serves and RUN one. The round-trip that proved the feature on its intended target says nothing about them. Refuse it where it cannot be stored, naming the construct that can — an error, not a warning, when the metamodel decides it and no future package can make it valid | | 27 | A metamodel-sync or list-coverage test asserts that a GAP still exists (`clickCapableInMendix["listview"]`, "a template for the list view's own entity is the base case Mendix permits") — so it passes throughout and FAILS on the correct fix, and the belief it encodes was never measured | Test coverage | Invert such a test rather than deleting it: keep the half that is still true (the metamodel really does carry the field) and flip the half that is not. When a test justifies itself by what a helper returns rather than by a measurement, treat it as a claim to check, not as evidence | | 28 | A describe emitter added beside a shared property formatter duplicates a field the formatter already prints (`Editable: true` twice on one widget) — invisible when the round-trip only covers the page the change was written against | DESCRIBE roundtrip | Round-trip a page OTHER than the one under test, and assert occurrence COUNT (`strings.Count(out, x) != 1`), not presence. `Unchanged page` on re-exec of the describe output is the evidence that the emitted MDL rebuilds the stored document; `Check passed!` is not | +| 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 | --- diff --git a/.claude/lint-rules/sec_strict_mode.star b/.claude/lint-rules/sec_strict_mode.star index c9f18c05f2..d324b9f60c 100644 --- a/.claude/lint-rules/sec_strict_mode.star +++ b/.claude/lint-rules/sec_strict_mode.star @@ -27,6 +27,5 @@ def check(): return [violation( message="Strict mode is disabled. This weakens XPath constraint enforcement and is relevant to CVE-2023-23835.", location=location(module="", document_type="security", document_name="ProjectSecurity"), - # mxcli/MDL cannot toggle strict mode — it is a Studio Pro-only setting. - suggestion="Enable strict mode in Studio Pro: Project Security > Enable 'Check security' and turn on strict-mode XPath validation (not settable via MDL).", + suggestion="Enable it with: ALTER PROJECT SECURITY STRICT MODE ON; (or in Studio Pro under Project Security).", )] diff --git a/.claude/skills/fix-issue/findings/cmd-mxcli.jsonl b/.claude/skills/fix-issue/findings/cmd-mxcli.jsonl index 8790ffec32..d782636722 100644 --- a/.claude/skills/fix-issue/findings/cmd-mxcli.jsonl +++ b/.claude/skills/fix-issue/findings/cmd-mxcli.jsonl @@ -116,3 +116,4 @@ {"area": "cmd/mxcli", "date": "2026-09-17", "symptom": "`mxcli run --local` on a project with Settings > Web UI > OptimizedClient = No exits 1 after the cold build with `no rollup.config.mjs and no bundle at .../web/dist/index.js ... the build did not produce a client` \u2014 about a deployment whose client is sitting in the same directory. `mxcli docker run` works", "cause": "The client gate tested for exactly two shapes (a rollup config to run, or a bundle mxbuild already wrote) and treated everything else as a failed build. A classic (Dojo) deployment is a legitimate third shape with NEITHER: there is no bundling step for the classic client in any Mendix version. Nothing in the local loop read UseOptimizedClient, so the mode was invisible", "file": "`cmd/mxcli/docker/webclient_plan.go` (new: `planWebClient`, `isClassicWebClient`, `noWebClientError`) + `webclient.go` (`BuildWebClient`, `ensureWebClientBundle`) + `webclient_watch.go` (`StartWebClientWatch`) + `runlocal.go` (`ensureClientServed`)", "insight": "Measured on one blank 11.12.2 app built twice, changing only the setting: **mxbuild swaps which client lands in `deployment/web/` and parks the other beside it** \u2014 OptimizedClient=Yes gives `web/` the React client (+`rollup.config.mjs`) and parks Dojo in `dojo-web/`; =No gives `web/` the Dojo client (`index.html` loading `mxclientsystem/mxui/mxui.js`, no rollup config, no dist) and parks React in `react-web/` WITH its own rollup config. So detect from the DEPLOYMENT, not from the model's setting: the deployment is what gets served, the two disagree exactly when the setting has just changed, it needs no plumbing through the five call sites, and it covers MigrationMode without predicting what that mode emits. Detect on POSITIVE evidence (the entry point names its client) \u2014 inferring classic from the absence of the React shapes would make every genuinely broken deployment look classic and silently skip the bundle, which is the black screen the gate exists to prevent; keep a control test that a clientless deployment still fails. **The gate had FIVE consumers, not one**: boot, the `--watch` bundler, the post-boot re-bundle guard, and `ensureClientServed`, which probes that `/dist/index.js` is *served* \u2014 measured 404 on a classic app, so fixing only the boot moves the failure to every applied change under `--watch`. Two of them carried hand-copied duplicates of the same gate and had already drifted once (the 11.14 fix, ako/mxcli-ledger #146, landed on `BuildWebClient` only, so `run --local` worked on 11.14 and `run --local --watch` did not) \u2014 so the fix collapses them into one `planWebClient` rather than adding a third copy. Repro from Linux with no Mac and no Studio Pro: `mxcli new` an 11.12.2 app, flip `UseOptimizedClient` to `No` on `Forms$WebUIProjectSettingsPart` (note the `Forms$` prefix, not `Settings$`), build, and keep the unflipped copy as the control. Patching that BSON with a Go `map` corrupts the file \u2014 mxbuild refuses it with `Expected '$ID' as the first property of a storage object` \u2014 because map iteration loses key order; use `bson.D` throughout. Verified in a browser, not just at the gate: `mx` global present, real page content, zero console errors. Issue #1123", "refs": ["#1123", "ako/mxcli-ledger#146"]} {"area": "cmd-mxcli", "date": "2026-09-17", "symptom": "`mxcli new --version 10.24.25` (and `mxcli setup mxbuild --version 10.24.25`) dies with `HTTP 404 from https://cdn.mendix.com/runtime/mxbuild-10.24.25.tar.gz`. Every 9.x and 10.x version probed 404s while 11.6.0/11.12.1/11.13.0 return 200 from the same host and path, which reads as 'Mendix 10 is no longer on the CDN'.", "cause": "Mendix 9 and 10 publish FOUR-part artifact names carrying a build number the release notes never mention: the release called 10.24.25 is `mxbuild-10.24.25.122571.tar.gz`. Mendix 11 publishes three parts. `MxBuildCDNURL` interpolates whatever string it is handed and nothing resolved a partial version, so a hand-typed 10.x version named no artifact at all. Project-driven paths were never affected — the MPR's `_ProductVersion` already carries all four parts (`10.24.25.122571`) and `parseVersion` takes the first three for major/minor/patch while the full string goes to the URL.", "file": "`cmd/mxcli/docker/version_resolve.go` (ResolveCDNVersion, highestBuild, CDNReleasesFor); wired at the two entry points where a user types a version, `cmd/mxcli/cmd_new.go` and `cmd/mxcli/setup.go`. Tests `cmd/mxcli/docker/version_resolve_test.go`.", "insight": "**A uniform 404 across a whole major version is evidence about the NAME, not about availability.** The conclusion drawn from it — 'Mendix 10 cannot be downloaded here' — blocked a verification for an entire session, and the fix was one listing call: the CDN is an S3 bucket that answers ListObjectsV2 (`?list-type=2&prefix=runtime/mxbuild-10.24.`), so what exists is enumerable rather than guessable. When a probe fails identically for every input in a class, question the query before concluding the class is empty. Three traps in the resolution itself, each a test: the `.sha256` sidecar beside every archive must not be picked as an artifact; the prefix needs its trailing dot or `10.24.2` swallows `10.24.20`..`10.24.26`; and build numbers are not zero-padded, so a text sort puts 99999 above 122571 and 10.24.9 above 10.24.26. Resolve at the entry point and thread the RESOLVED string onward — `mxcli new` checks the created project's stamp against the requested version, and `mx create-project` stamps four parts, so resolving late would fail that postcondition.", "refs": ["#1121"]} {"area": "cmd/mxcli", "date": "2026-09-18", "symptom": "mendixlabs/mxcli#1025: `mxcli syntax` advertises `mxcli syntax workflow user-task targeting` in its own help and answers `Unknown topic: workflow user-task targeting`. Same for `workflow user-task` and `workflow parallel-split`, all of which `mxcli syntax workflow` lists as sub-topics; `--json` was the only route that reached them.", "cause": "The CLI built its path with `strings.Join(args, \".\")` and never split an argument, so a topic handed over as ONE string — a quoted copy-paste, a tool wrapper, `sh -c` — became the path `workflow user-task targeting`, which matches nothing. The REPL's `help` had resolved multi-word topics since it was written (`resolveHelpPath`, greedy hyphen-joining): one question, two answers, and the CLI held the weaker copy. The #955 segment-match fallback could not save it either — it passed the DOTTED path to `BySegmentMatch`, and no segment contains a '.', so that fallback was silently dead for every multi-word query.", "file": "cmd/mxcli/syntax/topic.go (new: Lookup, topicWords, resolvePath), cmd/mxcli/help.go, mdl/executor/cmd_misc.go (resolveHelpPath deleted), mdl/grammar/domains/MDLSettings.g4 (helpStatement, helpTopicWord), mdl/visitor/visitor_query.go (ExitHelpStatement); tests cmd/mxcli/cmd_syntax_test.go, cmd/mxcli/syntax/topic_test.go, mdl/executor/cmd_misc_test.go, mdl/visitor/visitor_help_topic_test.go; example mdl-examples/bug-tests/syntax-1025-topic-drilldown.mdl", "insight": "**The spaces in the reported error message were the whole diagnosis, and reading them as a paraphrase cost an hour.** The command prints the path it built, and the CLI joins on '.', so `Unknown topic: workflow user-task targeting` cannot come from the command as documented — it can only come from the topic arriving as a single argument. Every line of the report follows from that and nothing else does: `syntax workflow` works (one word), `--json` works (the flag is not part of the topic), the three multi-word forms fail. Take a quoted error message literally, character for character, before assuming the reporter retyped it. **The reported version is downloadable and settles it in one run**: `mxcli setup mxcli`'s own URL shape (`releases/download//mxcli-linux-amd64`, NOT the goreleaser `_Linux_x86_64.tar.gz` that 404s) fetched v0.20.0, where the unquoted command works and the quoted one reproduces the message verbatim — so 'fixed since' and 'never broken' were both wrong. **The guard that matters is not the three cases from the report** but `TestEveryRegisteredPathIsReachableBySpelling`: every registered path, tried dotted, as separate arguments, and as one string. The registry prints dotted paths and then tells the reader to drill down with words, so a spelling that does not resolve is the command contradicting its own output; a per-case test would have passed the day someone added a topic with a new shape. Control: stub the whitespace split in `topicWords` and it fails with the reported path, spaces and all. **The grammar half has a trap the CLI half does not, and only the EXISTING suite caught it.** `helpStatement: IDENTIFIER (identifierOrKeyword)*` is the grammar's catch-all — a statement that is just an identifier and some words — so whatever it can swallow, it swallows from the statement that should have had it. Widening it to `(DOT? helpTopicWord)*` to take `help workflow.user-task` made `Sec.ApiUser` a complete statement of its own, and `create module role Sec.ApiUser` then parsed, WITH NO PARSE ERROR, as CREATE MODULE (named \"role\") followed by a help topic — two statements, wrong types, six unrelated security tests red. `(helpTopicWord (DOT? helpTopicWord)*)?` — a topic word before any dot — leaves `.ApiUser` unconsumable and restores the old disambiguation. Bisect a grammar regression by SHAPE, not by reading the ATN: adding the unused rule alone was clean, the hyphen alone was clean, the leading optional DOT was the whole of it, and three regenerations said so in about a minute. **When widening a permissive rule, the test to add is not for the new spelling but for what the rule must still NOT swallow** (TestHelpRuleDoesNotSwallowATrailingQualifiedName).", "refs": ["mendixlabs/mxcli#1025", "#955"]} +{"area": "cmd/mxcli", "date": "2026-09-18", "symptom": "`mxcli report` scores a project against rules the team disabled in `lint-config.yaml`. `mxcli lint` honours the config, the report's SCORE does not move, so the score cannot be calibrated at all. Reported at 66/100 against a 99/100 blank-app baseline, where 61 of 86 findings were two deliberately-accepted rules", "cause": "`cmd_report.go` never called `linter.FindConfigFile`/`LoadConfig` — it went straight from `linter.New` to `BuildReport`. Separately it carried its own INLINE copy of the built-in rule list, one rule behind `builtinLintRules()` (missing MDL-FLOW01), so the two commands scored one project against two rule sets. One root cause: report re-implemented lint's setup instead of sharing it", "file": "`cmd/mxcli/cmd_report.go`, `cmd/mxcli/cmd_lint.go`, new `cmd/mxcli/lint_setup.go` (`projectLintRules`, `applyLintConfig`), `mdl/linter/linter.go` (`RuleEnabled`)", "insight": "Same class as #904 in the opposite direction: there a silently reduced rule set made the score falsely HIGH, here an unread config makes it falsely LOW — and both are invisible because a score carries no provenance. **A value test cannot guard the inline copy**: both commands build rules inside a cobra RunE, so nothing a unit test can call notices a second list being re-added. The guard is therefore structural — grep `cmd_report.go` for `lint.AddRule(rules.New` — with a POSITIVE CONTROL first (assert `builtinLintRules` still constructs rules) so it cannot pass vacuously, the same shape as `scripts/check-tunnel-deps.sh`. Take the LintContext out of `applyLintConfig`'s signature: `NewLintContext(nil, nil)` panics, and a nil-guard added only to make a test compile is how a helper acquires behaviour nothing needs", "refs": ["#525", "#904"]} diff --git a/.claude/skills/fix-issue/findings/mdl-backend.jsonl b/.claude/skills/fix-issue/findings/mdl-backend.jsonl index e448128210..50ff6a767b 100644 --- a/.claude/skills/fix-issue/findings/mdl-backend.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-backend.jsonl @@ -116,3 +116,4 @@ {"area": "mdl/backend", "date": "2026-09-17", "symptom": "A microflow's URL (the deep link Studio Pro shows on the microflow's properties, Mendix 10.6+, e.g. `item/{Key}`) disappears after any `CREATE OR MODIFY MICROFLOW` \u2014 including one that only edits the body. `mxcli check`, `mx check` and mxbuild all report success before and after; the loss is visible only in Studio Pro (#1120)", "cause": "`microflowToGen` wrote `out.SetUrl(\"\")` and `out.SetUrlSearchParametersQualifiedNames(nil)` unconditionally in its `major >= 10` block, `microflowFromGen` never read either back, and `sdk/microflows.Microflow` had no field to hold them \u2014 so the value had no path across a rewrite at any of the three layers", "file": "`mdl/backend/modelsdk/microflow_write.go` (microflowToGen), `mdl/backend/modelsdk/microflow.go` (microflowFromGen), `mdl/executor/cmd_microflows_build.go`, `sdk/microflows/microflows.go`", "fix": "Carry `Url`/`UrlSearchParameters` the way AllowConcurrentExecution/MarkAsUsed/ApplyEntityAccess already are: field on the semantic microflow, read in microflowFromGen, written from the model in microflowToGen, and seeded from the stored microflow in the executor's rewrite path. DESCRIBE emits a `-- URL: \u2026` note, because a describe -> rename -> exec COPY still has nothing to preserve from", "insight": "This is the fifth property in `microflows.Microflow` lost this way and the first with NO checker behind it, which is what made it a user report rather than an internal find. The earlier four were all caught by a build error eventually (CE4899 for the concurrency flags, CE0122 for Excluded) or by a security review (ApplyEntityAccess); a microflow with no URL is simply a valid microflow, so every gate stays green and only a human opening Studio Pro can see it. **Generalisable**: when auditing a rebuild for guard-don't-drop, rank the constants it writes by whether a checker would notice their absence \u2014 the ones nothing checks are the ones that reach users, and they are exactly the ones a 'does this look like configuration?' audit skips. The mechanical version is to diff a Studio Pro document key by key against the writer's output; here `grep 'out.Set.*(\"\")\\|(nil)' microflow_write.go` finds the whole remaining set in one line (`ExportLevel` pinned to \"Hidden\", `ConcurrencyErrorMicroflow`/`ConcurrencyErrorMessage` emptied \u2014 both still unguarded, though CE4899 makes the concurrency pair loud). Measured control: reverting either half (SetUrl or the read) alone fails TestMicroflowRoundTrip_DeepLinkURL with the reported symptom, so both halves are load-bearing"} {"area": "mdl/backend", "date": "2026-09-17", "symptom": "A microflow's **export level** (Studio Pro's Hidden/API switch \u2014 whether it is part of the module's public surface when the module is exported as a package) is reset to `Hidden` by any `CREATE OR MODIFY MICROFLOW`. Every checker stays green, because a hidden microflow is a valid microflow; the module's API is simply smaller", "cause": "`microflowToGen` wrote `out.SetExportLevel(\"Hidden\")` unconditionally, `microflowFromGen` never read it back, and `sdk/microflows.Microflow` had no field \u2014 the identical three-layer gap as the deep-link URL in the same function", "file": "`mdl/backend/modelsdk/microflow_write.go` (microflowToGen), `mdl/backend/modelsdk/microflow.go` (microflowFromGen), `mdl/executor/cmd_microflows_build.go`, `sdk/microflows/microflows.go`", "fix": "Same carry as the URL, plus a DEFAULT: `\"\"` is not a member of `MicroflowsExportLevel`, so an empty model value is written as `Hidden` rather than passed through (the precedent is `json_write.go`). DESCRIBE emits `-- Export level:` only when the value is not `Hidden`", "insight": "Found by running the mechanical audit the URL fix prompted \u2014 `grep 'out.Set.*(\"\\|(nil)' microflow_write.go` over the one function \u2014 which is the cheap move after any instance of this class and turned up three more constants in one line. **The measurement that shaped the fix**: three real marketplace modules (Business Events 3.12.0, External Database Connector 6.2.3/6.3.0) store `Hidden` on 3 of 3 microflows and 55 of 55 documents overall, all three exporting at module level `Source` \u2014 so the hardcoded value was not wrong, it was a default masquerading as a constant. That is the shape of the trap: the audit finds the constant, but only a reference document tells you whether to carry it, default it, or leave it alone. A marketplace `.mpk` is a free source of these \u2014 `unzip -o pkg.mpk project.mpr` gives a real Studio Pro-authored MPR to query, no Studio Pro and no network needed (`mx-modules/` holds three). **Never carry an enum-valued property straight through without a default**: a stored document that says nothing reads as `\"\"`, and writing `\"\"` back is precisely the unloadable-model write CLAUDE.md warns about \u2014 mxbuild tolerates it and Studio Pro throws at MprProperty.cs. Controls: pinning the writer back, stubbing the reader, and neutralising the executor carry each fail a different test with the reported symptom"} {"area": "mdl/backend", "date": "2026-09-17", "symptom": "Unit tests for a carried microflow property (URL, export level, concurrency) all pass, and the end-to-end behaviour against a real project is still unverified \u2014 the integration gate that would have caught it, `TestMxCheck_DoctypeScripts`, `t.Skip`s whenever `mx` is absent, which is every run in a fresh container", "cause": "Two separate measurement errors, both invisible to `go test`. (1) The test fixture paired `Url: \"item/{Key}\"` with `UrlSearchParameters: [\"\u2026.Key\"]` \u2014 the SAME parameter \u2014 which mxbuild rejects as **CE5612**: a parameter used in the URL path may not also be a search parameter. Nothing in a unit test validates the model, so the fixture described a document Mendix refuses to build. (2) `bin/mxcli` was stale: `go build ./mdl/...` and `make test` had been run after each fix, but not `make build`, so the end-to-end run exercised a binary predating two of the three commits", "file": "`mdl/backend/modelsdk/microflow_roundtrip_flags_test.go`, `mdl/executor/microflow_carried_properties_test.go`, `mdl/executor/roundtrip_doctype_test.go` (the skipping gate)", "fix": "Fixture uses a distinct `Filter` parameter and says why. End-to-end procedure that actually measures it: `mxcli setup mxbuild -p ` (~719 MB, works through the session proxy), copy `testdata/expr-checker` as the fixture, create the microflow with mxcli, seed the unauthorable properties straight into the stored unit with `mpr.NewWriter` + `UpdateRawUnit`, then `mx check` BEFORE (the fixture must be a document Mendix accepts, or it proves nothing), `mxcli exec` a body-only rewrite, read the unit back, `mx check` after", "insight": "**A skipping integration gate is worse than no gate**: `mxCheckAvailable()` + `t.Skip` means a green `make test` says nothing about mxbuild, and reading the CLAUDE.md line about #808 is not the same as checking whether it applies to your own run \u2014 `ls ~/.mxcli/mxbuild` is. **Rebuild the binary before any end-to-end run**, and check its mtime against the last commit: a stale `bin/mxcli` produced a result (URL survived, export level did not) that looked exactly like a genuine second-read-path defect, and sent me hunting for a duplicate resolver that does not exist. **Seed the fixture through the writer, not by hand-editing BSON**, and always `mx check` the seeded state first: the CE5612 error came from the seed, not from mxcli, and without the before-check it would have been misattributed to the fix. Measured, mxbuild 11.6.6: pre-fix binary rewrites the microflow to `Url=\"\"`, empty search params, `ExportLevel=\"Hidden\"`; post-fix keeps all three; `mx check` 0 errors on both the seeded control and the rewritten project"} +{"area": "mdl/backend", "date": "2026-09-18", "symptom": "`ALTER PAGE M.P { SET Documentation = '...' }` is refused: \"unsupported page-level property: Documentation\". Documenting an existing page therefore means re-running its CREATE, because the `/** … */` doc comment on the create statement is the only other source", "cause": "`applyPageLevelSetMut` handled Title/Url/Popup*/Class/Style and had no Documentation case. The field was otherwise fully understood — `pageToGen` has always written it on CREATE via `out.SetDocumentation`, and gen binds it as `property.NewPrimitive[string](\"Documentation\")`. The grammar already parsed it (`identifierOrKeyword EQUALS propertyValueV3`) and the executor has no allowlist, so it was one missing case in the mutator, not a syntax gap", "file": "`mdl/backend/pagemutator/mutator.go` (`applyPageLevelSetMut`)", "insight": "One case covers **Page, Layout AND Snippet** — all three declare Documentation and all three reach this function through `SetWidgetProperty(\"\")`. Store an empty string rather than rejecting it: removing a doc comment from a script has to be expressible, and the property is a bare string with no unset value. **Update the unsupported-property message in the same change** — that list is the only guidance a reader gets, and a stale one sends them back to the CREATE workaround the fix exists to remove (a test asserts the message names Documentation). Note the re-run workaround is worse than it sounds: describe → exec is only as complete as what MDL can spell, so restating a page to document it can silently lose widgets", "refs": ["#527"]} diff --git a/.claude/skills/fix-issue/findings/mdl-executor.jsonl b/.claude/skills/fix-issue/findings/mdl-executor.jsonl index ca0570ddd7..e21645d673 100644 --- a/.claude/skills/fix-issue/findings/mdl-executor.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-executor.jsonl @@ -648,4 +648,13 @@ {"area":"mdl/executor","date":"2026-09-18","symptom":"CE7252 \"The parameters for remote action '' have changed\" (and CE7269 on the return) survives DROP + CREATE OR REPLACE MICROFLOW and CREATE OR MODIFY EXTERNAL ENTITIES, with no MDL that clears it. Reported as a stale 'parameter fingerprint / BSON hash' in the OData client and a request for REFRESH ODATA CLIENT Module.Client ACTIONS. Third report of this CE code after #1020 and #1073.","cause":"Two defects the same code was hiding. (1) edmReturnTypeToKind did not map Edm.TimeOfDay, so the call was written with no ParameterType / no VariableDataType - CE7252 on a parameter, CE7269 on a return - and neither field is reachable from MDL, both being derived from the contract. (2) For the types Mendix itself refuses (Edm.Duration/Stream/Binary/Geography*, a ComplexType, a TypeDefinition, Collection(Edm.*)) mxcli accepted the statement silently and left an unbuildable project; Mendix answers CE7255 'Action of service is not supported', which no BSON can change. A third case sat between them: an entity-typed PARAMETER whose external entity was never imported was unreported, though the identical case on the RETURN type already named the import statement.","file":"mdl/executor/external_action_types.go (new: classifyExternalActionType, checkExternalActionTypes); mdl/executor/cmd_microflows_builder_calls.go (edmReturnTypeToKind + refuseUntypableExternalAction); mdl/executor/validate_external_action_calls.go; mdl/types/edmx.go (FindEnumType)","insight":"**Enumerate the type space against mxbuild instead of theorising about the CE code.** One action per EDM shape, written by mxcli into a real 11.12.0 app and checked, produced a truth table in two runs, and the table is what separates OUR bug from the platform's: Edm.TimeOfDay is the only type that failed WITHOUT Mendix also calling it unsupported (CE7253/CE7255). Reasoning could not have reached that, and neither could a deny-list written from documentation. **The same table kills the obvious over-fix**: an ENUM-typed parameter builds at 0 errors with no type written at all, so 'mxcli could not name a Mendix type' is not on its own grounds to refuse - a refusal keyed on that would have rejected calls that build. **A rule wired only into the reference pass would have missed the reported workflow**: `mxcli exec` runs ValidateProgram(prog, projectPath) (the no-project linter), NOT Executor.ValidateProgram, so only `check --references` sees it; the refusal is applied at the writer too, from the same function, as CheckLayoutPlaceholderNames already does. Verified by building the faulty binary and watching exec write the call anyway. **There is no fingerprint** - the third reporter in a row believed one was stored; Mendix re-derives alignment from the cached contract every build, so 'recreate the microflow' really does rewrite everything and its failure means the rewrite is equally wrong, or the action is uncallable. **Controls**: reverting the TimeOfDay mapping alone takes the probe app 0 -> 2 errors (CE7252 + CE7269, the reported symptom verbatim) and, because the two halves are coupled, ALSO makes the refusal misfire on a call that builds - which is what the accepts-what-builds test catches. Stubbing checkExternalActionTypes to return nil restores the silent write. Repro mdl-examples/bug-tests/odata-1089-external-action-unmappable-types.mdl","refs":["mendixlabs/mxcli#1089","mendixlabs/mxcli#1073","mendixlabs/mxcli#1020"],"ce":["CE7252","CE7269","CE7255","CE7253"]} {"area":"mdl/executor","date":"2026-09-18","symptom":"`Action:`/`OnClick:` on a `listview`, `staticimage` or `dynamicimage` is parsed, accepted, and dropped on write — the widget does nothing. Reported honestly as MDL-WIDGET23 with a workaround (wrap it in a `container`), so it read as a real capability gap","cause":"Only the BUILDERS were missing. `pages.ListView.ClickAction` and `pages.StaticImage/DynamicImage.OnClickAction` already existed, and the writers already serialised them (`widget_write.go` calls `clientActionToGen(x.ClickAction)`; `widget_write_legacy_gaps.go` does the same for both images). `buildListViewV3`/`buildStaticImageV3`/`buildDynamicImageV3` never called `w.GetAction()`, so the field was always nil","file":"`mdl/executor/cmd_pages_builder_v3_widgets.go` (three builders); `cmd_pages_describe_parse.go` + `cmd_pages_describe_output.go` (the read half); `validate_widget_onclick.go` (`clickCapableInMendix` now empty)","insight":"**Check the writer before believing a 'no writer' warning.** MDL-WIDGET23's own message said \"mxcli has no writer for it\" and that was wrong for all three — the field and the serializer were both already there, so the fix was one `if action := w.GetAction()` per builder, not a feature. A warning that names a remedy can outlive the gap it describes; `TestClickCapableInMendix_NoLongerNamesWhatIsWritten` now fails if a name is left behind. **The write half alone is not the fix**: it landed first, mxbuild reported 0 errors, and the action vanished on the next `describe -> exec` — valid BSON, clean build, construct gone. Prove it with a re-exec of the describe output and look for **`Unchanged page`**, which shows the emitted MDL rebuilds the stored document exactly; `Check passed!` does not. **Round-trip a DIFFERENT page than the one under test**: adding the emitter also printed `Editable: true` twice on every list view, because the shared property formatter already prints it from the same field — the parse side's own comment predicted exactly that and the new-page test could not see it. Two metamodel-sync tests asserted the gap still existed (`TestClickCapableTypesCarryClickActionInMetamodel`, and a case inside `TestMDLWIDGET23_ReportsTheDroppedAction`); invert such a test rather than deleting it, so the metamodel half keeps its guard","refs":["ako/mxcli#512"],"rules":["MDL-WIDGET23"]} {"area": "mdl/executor", "date": "2026-09-18", "symptom": "A List View's \"Search attributes\" (its search bar) had no MDL spelling — `SearchAttributes: [Name]` on the widget was MDL-WIDGET07 \"not recognized and will be silently dropped on write\", so the search bar could only be set in Studio Pro", "cause": "Not modelled anywhere: `sdk/pages.DatabaseSource` had `XPathConstraint` and `Sorting` but no search, and `listViewSourceToGen` wrote an EMPTY `Forms$ListViewSearch` unconditionally", "file": "`mdl/grammar/MDLLexer.g4` (`SEARCH_BY`) + `domains/MDLPage.g4` + `domains/MDLSettings.g4` (keyword rule); `mdl/ast/ast_page_v3.go`; `mdl/visitor/visitor_page_v3.go`; `sdk/pages/pages_datasources.go`; `mdl/executor/cmd_pages_builder_v3.go`; `mdl/backend/modelsdk/widget_write.go`; `cmd_pages_describe_datasource.go`", "insight": "**No marketplace module ships a populated `ListViewSearch` — all 18 in a blank 11.12.2 app are empty — so pin the shape on the SIBLING instead.** `SearchRefs` is `[]*DomainModelsAttributeRef` and a Studio Pro `Forms$GridSortItem` carries the identical element, which gives a real reference without a Studio Pro session. Reuse the existing `attributeRefToGen` rather than writing a second builder: it means the new list inherits exactly what `sort by` has always written, including two deviations from Studio Pro that are then NOT regressions — typed-array marker **3** where Studio Pro writes **2**, and no `EntityRef: null` key. Verify that claim by dumping mxcli's own sort bars from the same project; both markers appear side by side (Studio Pro's 2, mxcli's 3). **A new lexer token needs the keyword rule too**: `TestKeywordRuleCoverage` fails otherwise, because every non-structural token must stay usable as an identifier — `SORT_BY` was already there and `SEARCH_BY` had to join it. Round-trip evidence to insist on is **`Unchanged page` on re-exec of the describe output**, not `Check passed!`: it proves the emitted MDL rebuilds the stored document byte-for-byte after canon.Reconcile. Measured gap left alone deliberately: neither `sort by` nor `search by` is reference-checked, so a nonexistent attribute passes `check --references` and surfaces as CE1613 at build time — checked on both clauses before concluding the new one was no worse **Review caught a silent drop in this very change**: `search by` hangs off the SHARED database-source rule, so the grammar accepts it on a gallery or a grid, and only the list view writer emits it — on a gallery, check passed, exec said \"Created page\", and DESCRIBE did not echo it back. **When a clause is added to a shared grammar rule, enumerate the OTHER widgets that rule serves and run one of them**; the round-trip proving it works on a list view says nothing about them. Now refused at build with the widget named, as an error not a warning: unlike an unrecognised property key, which a newer widget package might define, this is decided by Mendix's metamodel and cannot become valid later.", "refs": ["ako/mxcli#512"], "ce": ["CE1613"]} +{"area": "mdl/executor", "date": "2026-09-18", "symptom": "A page-level `actionbutton` (outside any dataview) with `Action: show_page Page(Param: $Var)` silently drops the argument: `mxcli check --references` says \"All references valid\", `exec` reports success, `DESCRIBE PAGE` then prints `(Item: $currentObject)` on a page where $currentObject is unbound, and mxbuild 11.13.0 fails **CE1571** \"No argument has been selected for parameter 'Item'\" per parameter of the target page. Every variant fails the same way — colon or `$P =` form, a literal, a datagrid control bar or a bare page-level button, a single parameter. MDL-PAGEARG01, shipped for the adjacent shape, does not fire", "cause": "MDL-PAGEARG01 had TWO context states where it needed three. `contextKnown` was set only on entering a data-bound widget, so the root of a CREATE PAGE walk was indistinguishable from ALTER PAGE's genuinely-unknown context and the guard stood down. But a full-page walk DOES know: nothing encloses the root, so there is no context object, and the empty `ParameterMappings` mxcli writes (deliberate, #296 — an explicit one is CE0115) is not an inferred mapping but a missing one", "file": "`mdl/executor/cmd_pages_showpage_args.go` (`pageArgContext` replacing the `(contextVar, contextKnown)` pair; `atDocumentRoot`, `enteringDataWidget`, `argContextForChildren`, `argContextForSubtreeOf`, `contextFreeContainers`), `cmd_pages_builder_v3.go` (`buildWidgetV3` + the `showPage` guard), `validate_widgets.go` (`validateWidgetTree` vs new `validateWidgetSubtree`), `cmd_pages_create_v3.go`, `cmd_pages_layout_v3.go`", "insight": "**A two-valued \"do we know?\" flag hides a third state, and the missing one is the bug.** known/unknown collapsed \"there is no context object\" into \"we cannot say\", and the guard's own stand-down rule then protected the defect it was written to catch. Split it into known+present and the root of every full-document walk becomes a *provable* absence. **With no context object, refuse EVERY argument form, not just a mismatched $variable** — a literal and an association path are left alone elsewhere only because they cannot be compared to a context object; where there is none the mapping is empty either way, which is why the report's literal-value bisection also failed. **The trap is the opposite direction: concluding \"no context\" needs an allow-list of containers, never a deny-list of data widgets.** `datagrid dg (DataSource: Mod.Entity)` — the bare-entity shorthand — leaves a plain string in `Properties[\"DataSource\"]`, not a parsed `*ast.DataSourceV3`, so a walker keyed on `GetDataSource() != nil` calls a row-scoped column button context-free and refuses correct code; `mdl-examples/bug-tests/295-showpage-null-variable.mdl` is that case and caught it on the first `make check-mdl`. Descend through a named list of context-free containers (container/layoutgrid/row/column/tab*/groupbox/…) and degrade anything else to UNKNOWN. **Guard the check-time walker's ROOT, not just its recursion**: `validateWidgetTree` served CREATE PAGE and ALTER PAGE INSERT alike, so the two roots had to be split (`validateWidgetSubtree`) or ALTER regressed. Measured on mxbuild 11.13.0 both ways: fault forced back in → check passes, exec writes, DESCRIBE shows `$currentObject`, 1 CE1571; fixed → refused at check and at exec, and a project carrying $currentObject-in-a-dataview, arg-by-name, a listview row button and a zero-arg page-level button builds at 0 errors. **A snippet parameter does NOT supply one either** — probed, not assumed: a button at a snippet's root gets its own CE1571, which is why snippet roots are treated like page roots. Tests `cmd_pages_showpage_args_test.go`, examples `mdl-examples/bug-tests/1029-showpage-arg-{without,with}-context*.mdl`", "refs": ["#1029", "#296"], "ce": ["CE1571", "CE0115"]} {"area": "mdl/executor", "date": "2026-09-18", "symptom": "`describe page` emits `-- Forms$StaticImageViewer (imgAll) -- NOT re-executable: mxcli cannot author this widget, so re-running this script would drop it` for the Studio Pro-authored static images in a Selection helper's three mandatory custom slots. Replaying the description empties all three and mxbuild answers CE0642 \"Property 'All selected' is required.\" x3. The reporter's workaround is an empty `dynamictext` per slot: structurally valid, icons gone", "cause": "Two independent halves, both absent. (1) `Forms$StaticImageViewer` had no case in cmd_pages_describe_parse.go OR cmd_pages_describe_output.go, so it fell through to the unknown-type note \u2014 even though `staticimage` has been a keyword the executor dispatches and a widget both writers serialise, for years. (2) MDL had no spelling for WHICH image it shows: staticImageToGen wrote `SetImageQualifiedName(\"\")` under a comment saying `MDL cannot name an image (the builder never fills ImageID)`, and pages.StaticImage carried a dead `ImageID model.ID` that nothing ever set and that named the wrong thing \u2014 Forms$StaticImageViewer.Image is a ByNameRef to Images$Image, so a three-part NAME (Module.Collection.Image) is what is stored. A third, smaller drop sat beside them: the generic pluggable-widget emit branch was gated on explicit properties / object lists / actions and NOT on ChildSlots, so a widget whose only content is a populated slot described as a bare head", "file": "`mdl/executor/cmd_pages_describe_parse.go` + `cmd_pages_describe_output.go` (read and emit), `mdl/executor/cmd_pages_builder_v3_widgets.go` (buildStaticImageV3), `mdl/backend/modelsdk/widget_write_legacy_gaps.go` (staticImageToGen), `sdk/pages/pages_widgets_display.go` (ImageName replaces the dead ImageID), `mdl/executor/validate_widgets.go` (MDL-WIDGET07 allow-list)", "insight": "**A widget that is authorable is not therefore describable, and the two gaps hide each other.** `staticimage` had a keyword, a grammar token, a builder and two writers \u2014 everything a coverage scan looks for \u2014 and no describer at all, which is why the note read as a capability gap rather than a missing 20-line case. Grep the describe switch for the `$Type`, not the keyword. **Emitting the keyword alone would have been worse than the bug**: a visible `NOT re-executable` note becomes a silent drop. That is the third time this exact trap is recorded on this exact widget (ako/mxcli#512 for `Action:`, mxcli-formula1 FINDINGS \u00a7142 for the pluggable `image`), so treat 'describe emits it' and 'the builder reads it' as one change, never two. **Prove the round trip with `Unchanged page`, not with `Check passed`** \u2014 re-exec of the describe output printing `Unchanged` is the only signal that the emitted MDL rebuilds the stored document exactly; the pre-fix binary printed `Replaced page` on the same input. **The decisive control was a second binary, not a stubbed function**: `git stash` the fixed files, build, and run describe->exec on the SAME stored page \u2014 it reproduced CE0642 x3 verbatim, which no unit test could have claimed. Reverting only the writer (`Image` back to \"\") gives CE0436 \"No image selected.\", which is what proves the three-part name RESOLVES rather than merely being accepted. **CE0582 is not evidence of a defect here**: mxbuild 11.12.1 reports it for a static image anywhere, slots included \u2014 Mendix's own React-client deprecation, and the reason `image` is the right widget on a new page **Making a widget describable puts its OTHER properties at risk**, and it is the same failure class one level down: the writer hardcoded WidthUnit/HeightUnit to \"Auto\" and the builder hardcoded Responsive to true, harmlessly while nothing described the widget \u2014 and the moment describe emits re-executable MDL those become a SILENT normalisation on every replay, worse than the loud note they replaced. When you make something round-trip, enumerate what the type stores and decide each one; the defaults still stay unemitted, or each round trip accumulates a clause the author never wrote", "refs": ["mendixlabs/mxcli#1057", "ako/mxcli#512"], "ce": ["CE0642", "CE0436", "CE0582", "CE1613"], "rules": ["MDL-WIDGET07"]} +{"area": "mdl/executor", "date": "2026-09-19", "symptom": "Adding a DESCRIBE clause for a microflow header property shows up in `diff-local` but NOT in `describe microflow`, or vice versa \u2014 the same property renders differently depending on which command asked", "cause": "`cmd_microflows_show.go` contains TWO copies of the microflow header renderer: `renderMicroflowMDL` (used by describe-to-string, diff and diff-local) and an inline block inside `describeMicroflowMode` (used by `describe microflow` itself). They independently emit documentation, @excluded, @applyentityaccess, the signature, the return type, the folder and the expose clauses", "file": "`mdl/executor/cmd_microflows_show.go` (renderMicroflowMDL vs describeMicroflowMode)", "fix": "Emit the new properties from one helper (`microflowDocumentPropertyLines`) and call it from both. The duplication itself is untouched \u2014 removing it is a refactor of its own \u2014 but every property added from here on goes through a shared helper", "insight": "**Duplicate-resolver drift inside a single file, found by the feature not working rather than by reading.** The clauses were added to `renderMicroflowMDL` first; unit tests passed (they call that function), and the end-to-end `describe microflow` printed nothing. Grepping for the call site is not enough \u2014 `grep -c 'exposeClauseLines'` returns 2 and that is the tell. **Generalisable**: before adding to a renderer, count how many functions in the file already emit the same construct; if more than one, the addition goes in a helper both call, because the next person will hit the same trap and the test that would catch it is an end-to-end one nobody writes for a describe line"} +{"area": "mdl/executor", "date": "2026-09-19", "symptom": "`allow concurrent execution` sets AllowConcurrentExecution=true but the stored ConcurrencyErrorMessage stays on disk, byte-identical and with its original $ID, no matter what the writer sets the model field to. A second run reports `Unchanged`", "cause": "NOT a writer bug. `canon.Reconcile` calls `CarryTranslations`, which copies a stored `Texts$Text`'s translations onto the rebuilt document, because a rebuild states one language and drops the rest. It cannot distinguish 'cleared on purpose' from 'the statement had no way to say it', so an emptied text is refilled. Only a targeted patch may pass `canon.ContentsOwnTranslations()`, and a microflow rebuild is not one", "file": "`modelsdk/canon/translations.go` (CarryTranslations), `modelsdk/canon/identity.go` (ContentsOwnTranslations), `mdl/executor/microflow_document_properties.go`", "fix": "Stop trying to clear it. A clause sets what it states and leaves the sibling alone \u2014 which also matches Studio Pro, where allowing concurrency greys the error fields rather than erasing them, so re-disallowing restores the message. An inert stored message breaks nothing: Mendix reads it only when execution is disallowed", "insight": "**A text field cannot be cleared through a rebuild, by design \u2014 check `canon` before writing a guard for translations.** This feature shipped with a restate-or-refuse guard refusing to overwrite a multi-language message, on the reasoning that MDL states one string and would drop the others. That reasoning was wrong and the guard was deleted: measured on 11.6.6, restating an English concurrency message left its Dutch translation untouched, because CarryTranslations pairs the texts by containment path and carries the rest. **Two lessons**: the platform may already solve the problem your guard is for, so read `canon`'s carries before adding one; and 'the write did not take effect' on a text field is a carry, not a broken writer \u2014 the tell is that the stored subtree keeps its original `$ID`"} +{"area": "mdl/executor", "date": "2026-09-19", "symptom": "Making a previously-unauthorable microflow property authorable produces a build error nothing warned about: two microflows sharing a deep link fail CE0570, and the very first thing anyone does with the feature \u2014 `describe` -> rename -> `exec` \u2014 produces exactly that", "cause": "Mendix requires deep-link URLs to be unique across the app. The rule is not statement-local, so neither the grammar nor the shared statement checks can see it; it needs the project's other microflows", "file": "`mdl/executor/microflow_document_properties.go` (checkURLNotTaken)", "fix": "Check it at the write path, where `ctx.Backend.ListMicroflows()` is available, skipping the microflow's own ID and excluded twins. Refused by name with the CE number rather than left to the build", "insight": "**When a property becomes authorable, enumerate the platform rules that only a build enforced \u2014 and ask specifically what the COPY path now produces.** Three rules surfaced for one small feature, and every one of them was found by running mxbuild rather than by reading the metamodel: CE5612 (path and search parameters are disjoint), CE4899 (disallow needs a handler), CE0570 (URLs are unique). The last is the one a design review would miss, because it only exists once DESCRIBE emits the property and copying becomes possible \u2014 the feature created its own failure mode. Statement-local rules belong in `mdl/types` shared by check and exec; a rule needing the project belongs at the write path, and `mxcli check -p` can call it later"} +{"area":"mdl/executor","date":"2026-09-19","symptom":"Two defects on `dynamicimage`, found while closing the sibling of mendixlabs/mxcli#1057. (1) EVERY dynamic image mxcli wrote failed the build: `[error] [CE0489] \"Select an entity for the data source of this dynamic image.\" at Dynamic image 'imgPhoto'`. (2) `describe page` emitted `-- Forms$ImageViewer (imgPhoto) -- NOT re-executable` and the replay DELETED the widget","cause":"(1) `dynamicImageToGen` called `imageViewerSourceToGen()` with NO ARGUMENTS — a zero-arg constructor for a property whose whole content is the entity binding — so the stored Forms$ImageViewerSource carried only its own $ID and `EntityRef: null`. `pages.DynamicImage` had no DataSource field at all to pass. (2) No case for `Forms$ImageViewer` in cmd_pages_describe_parse.go or cmd_pages_describe_output.go, same as the static image. Also hardcoded in the writer beside the source: DefaultImage always \"\" (with a dead `DefaultImage model.ID` field, wrongly typed — it is a by-name ref to Images$Image), both size units always \"Auto\", ShowAsThumbnail and OnClickEnlarge always false","file":"`mdl/backend/modelsdk/widget_write_legacy_gaps.go` (dynamicImageToGen, imageViewerSourceToGen now takes the source), `mdl/executor/cmd_pages_describe_parse.go` + `cmd_pages_describe_output.go`, `mdl/executor/cmd_pages_builder_v3_widgets.go` (buildDynamicImageV3), `sdk/pages/pages_widgets_display.go`, `mdl/executor/validate_widgets.go` (MDL-WIDGET07 allow-list)","insight":"**A zero-argument constructor for a property that has content is the tell.** `imageViewerSourceToGen()` took no parameters and nobody noticed for as long as the widget existed, because the call site read as complete — Go gives you no warning that a builder cannot express what it is building. Grep the `*ToGen` helpers for an empty parameter list and check each against what the metamodel says the type holds. **A round trip that makes the build QUIETER is the worst evidence shape there is**: the pre-fix describe->exec dropped the widget, so its own CE0582 vanished too and mx check went 4 errors -> 2. Anyone bisecting on the error COUNT would have read the deletion as the fix. Compare error SETS, never counts. **Reverting the writer to a bare source is the control that proves the entity ref resolves** (CE0489 returns, 4 errors), which no unit test can claim — the test only shows a DirectEntityRef was written, not that Mendix accepts it. **Look for the Studio Pro reference, then say so when there is none**: a blank 11.12.1 app has 0 Studio Pro dynamic images (the 1 instance found was mxcli's own), so the widget shape is metamodel-derived and flagged as such; what IS pinned is `DomainModels$DirectEntityRef{Entity}` at 20/20. My first fixture scanner reported 0 instances of EVERYTHING including its own control — mongo-driver decodes a nested BSON document into `bson.D`, not `map[string]any`, so a map-only walk silently matches nothing. A uniform zero across a whole class is evidence about the query","refs":["mendixlabs/mxcli#1057"],"ce":["CE0489","CE0582"],"rules":["MDL-WIDGET07"]} +{"area": "mdl/executor", "date": "2026-09-18", "symptom": "`grant read *, write *` on an entity carrying an **autonumber** writes ReadWrite on it and the build fails **CE6592**. The user narrows the grant by hand with `revoke (write (RequestNumber))`. A calculated attribute on the same entity is downgraded correctly", "cause": "The CE6592 downgrade asked only whether an attribute was CALCULATED (`attr.Value.Type == \"CalculatedValue\"`). An autonumber carries no `DomainModels$CalculatedValue` — its value comes from the database on insert, not from a microflow on read — so it failed the test and kept its write. Mendix forbids write on BOTH for the same reason, so the predicate covered exactly half the rule. Found by reading the predicate, not from a repro: the reporter had already worked around it", "file": "`mdl/executor/entity_hierarchy.go` (`EntityMembers`), `mdl/executor/cmd_security_write.go` (grant), `mdl/backend/modelsdk/domainmodel_security_write.go` (`ReconcileMemberAccesses`, two sites), rule in `mdl/types/member_write_rights.go`", "insight": "**The GRANT is half the fix.** `ReconcileMemberAccesses` runs on the executor's finalize step after EVERY program, so a grant corrected by hand was re-broken by the next write touching the module — fixing only the grant path would have passed a repro and left the defect reachable. Reconcile needed BOTH of its sites: the existing-entry downgrade (keyed on a set) and the missing-entry add (which otherwise inherits the rule's ReadWrite default). The rule lives once in `types.WriteRightsForbidden(isCalculated, isAutoNumber)` because the two callers speak different currencies (sdk/domainmodel vs modelsdk/gen) and neither may import the other. **Every test needs a plain attribute as a second control** — asserting only that the autonumber becomes ReadOnly passes against a blanket downgrade that would strip write from the whole model", "refs": ["#524"], "ce": ["CE6592"]} +{"area": "mdl/executor", "date": "2026-09-20", "symptom": "An input widget cannot bind an attribute over an association. `textbox t (attribute: Assoc/Attr)` builds a FLAT path and mxbuild fails **CE1613** \"The selected attribute 'Rules.RuleAction.RuleAction_BusinessRule/Name' no longer exists.\" — note the shape: the association segment is pasted onto the CONTEXT entity instead of being navigated. The same syntax works on a DataGrid2 column, so one page could bind an associated attribute in a grid column and fail on the text box beside it. Read half: DESCRIBE emitted a bare `Attribute: Name`, so describe → exec over a Studio Pro page REBOUND the widget to an attribute the context entity does not have, with `mxcli check` clean", "cause": "All six input builders (textbox/textarea/datepicker/dropdown/checkbox/radiobuttons) called `resolveAttributePath`, which knows nothing about associations; `resolveAssociationAttributePath` existed and was wired only into DataGrid2 columns, DynamicText params and the widget engine. On the read side `extractAttributeRef` returned the last segment of AttributeRef.Attribute and ignored AttributeRef.EntityRef entirely, while `columnAttributeFromRef` already handled it correctly — two readers of one BSON shape", "file": "`mdl/executor/cmd_pages_builder_v3.go` (`resolveInputAttribute`), `cmd_pages_builder_v3_widgets.go` (six builders), `cmd_pages_describe_parse.go` (`extractAttributeRef`), `mdl/backend/modelsdk/widget_write.go` (`inputAttributeRefToGen`), `sdk/pages/pages_widgets_input.go` (AttributeRefSteps on six structs)", "insight": "**Mendix DOES permit this on a plain text box** — the question that blocked the fix, settled by a Studio Pro reference rather than reasoning: ako/TestApp `Rules.RuleAction_NewEdit` textBox4 stores Attribute \"Rules.BusinessRule.Name\" + IndirectEntityRef over Rules.RuleAction_BusinessRule. Without it the plausible fix was the opposite one (refuse it, hint at a nested dataview). **Fix both halves together or the round trip just changes failure mode** — a describer emitting a path the builder cannot consume turns silent corruption into a build error. The build-level control is the evidence that counts: same script, same mxbuild 11.14.0, pre-fix binary → CE1613, post-fix → 0 errors. An entity with NO attribute of that name is what makes the control sharp, since a same-named attribute would merely bind somewhere else", "refs": ["#529"], "ce": ["CE1613"]} +{"area": "mdl/executor", "date": "2026-09-20", "symptom": "`CREATE OR MODIFY EXTERNAL ENTITIES FROM` marks an external entity's KEY attribute Updatable=true when the entity set's UpdateRestrictions say the set is updatable. Mendix computes a key as non-updatable, so `mx check` reports CE6630 \"'DefinitionId' is marked Updatable=False in the OData service, but True in the app.\" at Attribute 'MyFirstModule.Definition.DefinitionId'. Independent of the ComplexType flattening fixed in #1118 — reproduces on a contract with no complex type at all", "cause": "createExternalEntities' attribute loop started `updatable` from the entity set's UpdateRestrictions/Updatable and cleared it only for NonUpdatableProperties, Core.Computed, Core.Immutable or a flattened complex leaf. Key membership was already in the loop as keyPropSet[p.Name] — passed to edmToDomainModelAttrType, never consulted for updatability. The rule is gated on isTopLevel: only the key of an entity that HAS an entity set is non-updatable", "file": "`mdl/executor/cmd_contract.go` (createExternalEntities attribute loop; `isKey` now drives both the attribute type and Updatable)", "insight": "**The rule is `isKey && isTopLevel`, and the `isTopLevel` half cost a red CI run to learn.** The first fix was the blanket 'a key is never updatable' — every unit test green, the reported CE6630 gone, a real 11.12.1 build confirming it, and it turned ONE error into SEVEN of its inverse on the live TripPin contract: \"'TripId' is marked Updatable=True in the OData service, but False in the app.\" over Trip, PlanItem, Event, Flight, PublicTransportation, Employee and Manager — every one a derived or contained type with NO entity set, mutated through its parent's write flow — while Person/Airline/Airport (the entity sets) stayed silent at false. **`UserName` is the two-sided control inside one document**: expected False on Person and True on Employee and Manager, so the split is the entity set and cannot be inheritance or the property. **#1118's finding said this in advance and it was not heeded: 'Do not stop at a synthetic fixture. TripPin is the fixture to reach for.'** Seven synthetic annotation shapes agreed with each other and were all top-level, so the probe could not see the variable that mattered — a negative that is uniform across a whole class is evidence about the query. Cheapest guard for next time: `go test -tags integration -run 'TestMxCheck_DoctypeScripts/10-odata-examples'` is ~36s locally against cached mxbuild, versus a 12-minute CI round. **Clear Updatable on a key, NOT Creatable** — a key is written once at creation, so Insertable still applies; the report's build flagged it Updatable=False only, and clearing both is CE6630 inverted on the key of any insertable set. **The report's own error count was wrong**, which is why the real build mattered: it promises \"exactly one CE6630, naming the key attribute\"; the identical contract gives TWO — the key AND the non-key `Label`. The fix takes 2 to 1, not to 0, so a regression test asserting `mx check` clean would have failed against a correct fix. **The Label half is a SECOND defect, still open**: mxbuild answered Updatable=False for the non-key attribute of all seven TOP-LEVEL shapes (inline record, ``, UpdateMethod=PATCH, +NonUpdatableProperties/+DeleteRestrictions, unannotated, external ``, property-level Core.Permissions/ReadWrite). The evidence now points at `Updatable == !isTopLevel` for ALL attributes — the aliased run is the discriminator: with mxbuild reading an Updatable=true set and the app at false on every attribute, there were Creatable errors and NO Updatable error — but it was not broadened, because no top-level set has yet been seen that mxbuild treats as having an updatable attribute, and §48 measured CE6630 firing both ways. Two wrong turns ruled out by measurement, each cheap and each tempting: (1) the stored $metadata is NOT filtered — `grep -rc UpdateRestrictions mprcontents/` matches the InsertRestrictions count exactly, so mxbuild reads both from the same document and applies an extra rule to updatability only; (2) there is no entity-level updatable to set — `generated/metamodel` (the arbiter) gives Rest$ODataRemoteEntitySource Creatable/Deletable/Countable/Skip/Top and **no Updatable**, confirming the existing code comment, and mxbuild checks entity-level Creatable (it reported one) but never entity-level Updatable. Also measured: mxcli matches capability terms by fully-qualified name only, so the **aliased** spelling (`Capabilities.UpdateRestrictions` with an `/`) — what most real services emit — parses in mxbuild and not in mxcli, silently flipping every capability to the conservative default; the external `` form DOES parse. And `describe external entity` does not round-trip any per-attribute capability: it emits `Name: Type` only (losing Updatable, Creatable, Filterable, Sortable and the String length), so describe->exec into a fresh project rebuilds them from defaults. Repro `mdl-examples/bug-tests/odata-key-attribute-updatable.mdl`; tests `mdl/executor/cmd_contract_key_updatable_test.go` (the top-level and derived-type pair is the two-sided control — either alone passes against a fix wrong in the other direction)", "file_refs": ["mdl/executor/cmd_contract.go"], "ce": ["CE6630"]} +{"area":"mdl/executor","date":"2026-09-20","symptom":"`CREATE OR MODIFY EXTERNAL ENTITIES FROM` marks NON-KEY attributes of a top-level external entity Updatable=true when the entity set's UpdateRestrictions say the set is updatable. `mx check` reports one CE6630 per attribute: \"'Label' is marked Updatable=False in the OData service, but True in the app.\" The sibling of the key-attribute defect fixed the same day; fixing only the key took the reported repro from 2 errors to 1, not to 0","cause":"`createExternalEntities` derived `defaultUpdatable` from `entitySet.Updatable`. Mendix does not: it computes Updatable as a function of whether the entity has an entity set at all — never for a top-level entity, always for a non-top-level one, which is written through its parent's flow. The annotation is irrelevant to it","file":"`mdl/executor/cmd_contract.go` (`defaultUpdatable := !isTopLevel`, replacing the UpdateRestrictions override; the key-specific guard added earlier becomes subsumed)","insight":"**The positive control that closes this is a contract whose `NonUpdatableProperties` names ONLY the key.** That service is asserting, by name, that every other property IS updatable — and mxbuild still answers False. Ten top-level shapes were probed in three rounds and all answered False (inline record, typed ``, UpdateMethod=PATCH, +NonUpdatableProperties +DeleteRestrictions, unannotated, external ``, Core.Permissions/ReadWrite, Core.OptimisticConcurrency/ETag, DeepUpdateSupport, and the key-only exclusion list); the first seven were NOT enough to act on, because 'no shape produces True' is the uniform-negative shape that means the probe is wrong — what made it actionable was a shape that states the opposite explicitly and is still refused. **Two false leads, each cheap and each worth skipping.** (1) ETag/optimistic concurrency, suggested by a comment in this very function about the service that motivated #729 — no effect. (2) The model's `AllowCreateChangeLocally`: the intuition is that Mendix would permit attribute changes once local changes are allowed, and it is wrong — setting it Yes on a top-level entity left the expectation at False. **That second control is the one that explains the rule rather than just fitting it**: an external object CAN be changed in memory and handed to an external OData action, and that is what the local-change flag governs; the attribute's `Updatable` mirrors only what the endpoint itself accepts on a PATCH. Domain knowledge from the maintainer, not derivable from the metamodel — worth asking for before probing an eleventh contract shape. The Insert/Update asymmetry that makes this look like a parser bug is real and is not one: mxbuild reads `InsertRestrictions` from the same document, in the same shapes, and honours it — so `Creatable` follows the contract and `Updatable` does not, which is also why 'the entity is read-only' is the wrong summary and why every test here asserts Creatable as its control. **A stale test encoded the old belief and had to be corrected, not worked around**: #1118's `TestCreateExternalEntities_FlattenedAttributesAreReadOnly` asserted `Label` was Creatable AND Updatable as its control; the Updatable half had been assumed from the contract rather than measured, while the flattened-attribute half it was controlling for HAD been. The control still works on Creatable alone. Verified end to end on a real 11.12.1 project: 2 errors before any fix, 1 after the key-only fix, **0 errors** now; TripPin (`-run 'TestMxCheck_DoctypeScripts/10-odata-examples'`, ~26s locally) is the other-direction control and stays green, since every entity it flags is non-top-level. Repro `mdl-examples/bug-tests/odata-key-attribute-updatable.mdl`; tests `mdl/executor/cmd_contract_key_updatable_test.go`","file_refs":["mdl/executor/cmd_contract.go"],"ce":["CE6630"]} diff --git a/.claude/skills/fix-issue/findings/mdl-grammar.jsonl b/.claude/skills/fix-issue/findings/mdl-grammar.jsonl index 6c570cbd99..fd5e9c69d4 100644 --- a/.claude/skills/fix-issue/findings/mdl-grammar.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-grammar.jsonl @@ -56,3 +56,4 @@ {"area": "mdl/grammar", "date": "2026-09-07", "symptom": "`drop demo user` and `drop user role` had no idempotent form, so a one-time cleanup either broke every later run of its slice script or had to be commented out (ako/CapTrackV4 R5, 024).", "cause": "Neither statement accepted IF EXISTS, though the grammar already had an ifExists rule used by ALTER ENTITY's DROP ATTRIBUTE / DROP INDEX / DROP VALUE. Added `ifExists?` to both statements, an IfExists field to each AST node, and a skip-with-message branch in each handler.", "file": "`mdl/grammar/domains/MDLSecurity.g4`; `mdl/ast/ast_security.go`; `mdl/visitor/visitor_security.go`; `mdl/executor/cmd_security_write.go`; tests `mdl/visitor/drop_if_exists_test.go`; example `mdl-examples/bug-tests/drop-security-if-exists.mdl`", "insight": "The check that finds this whole class is one loop — `for f in $(ls mdlsource/*.mdl | sort); do mxcli exec \"$f\" -p app.mpr; done` — which is exactly what a fresh clone does and what nobody runs. It found five non-idempotent statement forms in the reporting project; four already had a `create or modify` spelling, and the two with no idempotent form at all are the ones that ended up commented out. When adding a DROP statement, check whether the grammar's existing ifExists rule should apply rather than deciding idempotence is the author's problem."} {"area": "mdl/grammar", "date": "2026-09-08", "symptom": "Adding lexer tokens for a new feature broke an unrelated, previously-passing MDL example: `editable: never` on a list view stopped parsing after NEVER became a keyword", "cause": "A new lexer token steals every existing use of that word as an identifier or property value unless it is also added to the `keyword` rule in MDLSettings.g4. NEVER, ONLINE, SYNC and PRESERVE were added for offline sync; NEVER was already a real page property value", "file": "`mdl/grammar/domains/MDLSettings.g4` (keyword rule)", "insight": "Before adding a lexer token, grep the examples for that word as a value or name — the collision is with EXISTING scripts, so nothing in the new feature's own tests can find it. TestKeywordRuleCoverage catches the omission but only asserts the rule LISTS the token; add a test that the word still parses as an identifier, which is the property that actually matters. Here the two failures had one cause: the coverage test named the tokens and check-mdl named the victim file, and the file name (maint2-editable-never-create-page.mdl) said which word", "refs": ["PROPOSAL_offline_sync_configuration.md"]} {"area": "mdl/grammar", "date": "2026-09-15", "symptom": "Re-executing `describe workflow` output failed with `mismatched input 'boundary' expecting ';'` for any user task, call microflow or wait for notification that has two or more boundary events.", "cause": "formatBoundaryEvents emits `boundary event timer '…' { … }` per event (boundaryEventKeyword includes the prefix), and the syntax topic documents that per-clause form, but MDLWorkflow.g4 had `(BOUNDARY EVENT workflowBoundaryEventClause+)?` — one keyword, then clauses.", "fix": "All four sites accept `(BOUNDARY EVENT workflowBoundaryEventClause ((BOUNDARY EVENT)? workflowBoundaryEventClause)*)?`, so both the per-clause and the shared form parse; the visitor is unchanged.", "file": "mdl/grammar/domains/MDLWorkflow.g4", "insight": "A round trip that ends in `diff describe-1 describe-2` is vacuous when the exec in between fails: the second describe reads the unchanged document and matches. It reported IDENTICAL here while the exec had died on a parse error that a grep filter hid. Assert the exec itself — zero parse errors and a rewrite verb — before diffing. The integration round-trip tests had the same blind spot: they compare describe output but never feed it back to the parser, so a grammar/describer disagreement on a construct with more than one instance could not be seen. A test that re-parses describe output (TestWorkflowDescribe_TwoBoundaryEventsReparse) is the cheap guard."} +{"area": "mdl/grammar", "date": "2026-09-18", "symptom": "Lint rule SEC005 reports \"strict mode is disabled\" and MDL has no statement that turns it on — the rule's own suggestion said \"not settable via MDL\". A lint rule with no remedy, recorded on the reporting project as the one finding left Open", "cause": "StrictMode was read everywhere and written nowhere: `security_read.go` reads it, `show security` prints it, the Starlark rule lints it, and `ProjectSecurity.SetStrictMode` existed in gen and was never called. `alterProjectSecurityStatement` had three variants (LEVEL, DEMO USERS, GUEST ACCESS) and no fourth", "file": "`mdl/grammar/MDLLexer.g4` + `domains/MDLSecurity.g4` + `domains/MDLSettings.g4` (keyword rule), `mdl/ast/ast_security.go`, `mdl/visitor/visitor_security.go`, `mdl/executor/cmd_security_write.go`, `mdl/backend/security.go`, `mdl/backend/modelsdk/security_write.go`, `mdl/backend/mock/mock_security.go`, `.claude/lint-rules/sec_strict_mode.star`", "insight": "**Writing a property gen merely offers is the trap; this is not one.** StrictMode is declared by BOTH generated sources and mxcli already reads it from real projects, which is the evidence that separates it from the Layout placeholder properties that make a document Studio Pro cannot open. **The AST field must be a POINTER** — a bare bool would disable strict mode on every DEMO USERS toggle, since \"said nothing\" and \"asked for off\" would be the same value (a test pins this). New tokens STRICT and MODE both go in the parser's `keyword` rule: `mode` is an entirely plausible attribute name and a keyword left out of that rule silently breaks every model already using the word (`TestKeywordRuleCoverage` catches it; a parse test pins it too). **Update the lint rule's suggestion in the same change** — a remedy that still says \"Studio Pro only\" leaves the finding exactly as unhelpful as before. No level-dependent refusal was added: the model stores StrictMode independently of SecurityLevel, and the rule already scopes its own advice to Production", "refs": ["#526"], "rules": ["SEC005"]} diff --git a/.claude/skills/fix-issue/findings/mdl-other.jsonl b/.claude/skills/fix-issue/findings/mdl-other.jsonl index 24638a84ef..c7ea61b165 100644 --- a/.claude/skills/fix-issue/findings/mdl-other.jsonl +++ b/.claude/skills/fix-issue/findings/mdl-other.jsonl @@ -67,3 +67,4 @@ {"area": "mdl/catalog", "date": "2026-09-17", "symptom": "Every microflow behind a published REST endpoint reads as dead: `CATALOG.GRAPH_DEAD_ASSETS` lists it, `SHOW CALLERS OF` says \"(no callers found)\", `SHOW REFERENCES TO` and `impact` report nothing, and QUAL004 says \"is not called from anywhere. Remove if unused.\" On the reporter's model, 92 of 93 published operations name a microflow and all 92 were listed dead \u2014 15 percent of its dead-microflow list, pointed at the most exposed code in the app (mendixlabs/mxcli#1126)", "cause": "`buildPublishedRestServices` wrote `published_rest_operations_data.Microflow` and returned \u2014 it appended to no slice that `buildReferences` drains, and there was no RefKind for the edge. The binding was in the catalog; the edge was not. Fourth instance of one class (widget actions #773, scheduled events, project settings, this)", "file": "`mdl/catalog/builder.go` (`publishedRestRefs`), `mdl/catalog/builder_rest.go` (`publishedRestRef`, `publishedRestOpName`, collection in the operation loop), `mdl/catalog/builder_references.go` (`RefKindPublish`, `extractPublishedRestRefs`), `mdl/catalog/builder_graph.go` (`graphRefKinds`), `mdl/executor/cmd_search.go` (`callerRefKinds`), `.claude/lint-rules/orphaned_elements.star` (`MICROFLOW_ENTRY_KINDS`); tests `mdl/catalog/builder_rest_refs_test.go`, `mdl/catalog/lint_rule_vocabulary_test.go`, `mdl/executor/cmd_search_callers_test.go`", "insight": "**`GRAPH_DEAD_ASSETS` is kind-AGNOSTIC \u2014 the comment beside `schedule` in `graphRefKinds` says otherwise and is wrong.** The view is `NOT EXISTS (SELECT 1 FROM refs WHERE TargetName = \u2026)`; `git log -L` shows it has never filtered on RefKind. That false comment sent the issue's root-cause analysis down the wrong path, and it would have sent the fix there too: `graphRefKinds` matters for the ANALYSIS graph (communities/layers/cycles/centrality), not for the dead list. Measured with a one-kind insert: a `publish` row in neither `graphRefKinds` nor `callerRefKinds` still took the microflow from dead=1 to dead=0. **Check which consumers actually filter before assuming all four do**: `impact` and `SHOW REFERENCES TO` select every kind, so the refs row alone fixes them; only `SHOW CALLERS` and QUAL004 need a vocabulary edit. **The three vocabularies drift independently and nothing tests the union** \u2014 `settings` shipped in v0.22.0 into refs and into QUAL004 but NOT into `callerRefKinds`, so `show callers of ` was still blind two releases later; found only by auditing the lists while adding a fourth kind, and fixed here alongside. **`sync` looks like an entry point and is not**: it targets an ENTITY, so it belongs with `datasource`/`retrieve` in the excluded set \u2014 the test now pins it there, because the next person adding a kind will read the list, not the builder. **Carry the source's own id on the edge**: the operation's synthetic `opID` was already computed for `published_rest_operations_data`, so passing it as `SourceId` (the scheduled-event precedent passes \"\") makes 'who calls this microflow' one join from the endpoint's path and summary. **Controls**: stubbing the extractor to emit nothing reproduces \"reported dead\" verbatim, and dropping the empty-microflow guard fails the two-operation test \u2014 the suite is green against neither. Adjacent and NOT fixed: `business_events_data.PublishMicroflow`/`SubscribeMicroflow` emit no edge either, and `PublishedRestService.AuthenticationMicroflow` (and its OData sibling) is on gen but never read into the semantic model, so a REST auth handler is invisible to mxcli entirely", "refs": ["mendixlabs/mxcli#1126", "mendixlabs/mxcli#773"], "rules": ["QUAL004"]} {"area": "mdl/catalog", "date": "2026-09-17", "symptom": "A microflow that runs only as an **entity event handler** is reported as unused from three directions at once: `show callers of Mod.ACT_Order_Validate` says `(no callers found)`, `CATALOG.GRAPH_DEAD_ASSETS` lists it, and `mxcli lint` emits `[QUAL004] ... is not called from anywhere.` with the suggestion **\"Remove if unused\"** \u2014 on a microflow that runs on every commit. Reported as 32 dead of 36 handlers across 24 entities", "cause": "`mdl/catalog` touched `Entity.EventHandlers` in exactly one place and threw the list away: `hasEventHandlers = 1` in `builder_modules.go`. No `refs` row was ever emitted, and no table held the handlers, so the reference graph had no ENTITY -> MICROFLOW edge for them. The `calculate` edge two lines below in `buildReferences` is the same shape and was already there, which is why the infrastructure looked complete", "file": "`mdl/catalog/builder_entity_events.go` (new), `mdl/catalog/builder_references.go` (`RefKindEvent` + `extractEventHandlerRefs`), `mdl/catalog/tables.go` (`entity_event_handlers_data` + view, `CatalogSchemaVersion` 11->12), `mdl/catalog/catalog.go` (`Tables()`), `mdl/catalog/builder.go` (field + build step), `mdl/catalog/builder_graph.go` (`graphRefKinds`), `mdl/executor/cmd_search.go` (`callerRefKinds`), `.claude/lint-rules/orphaned_elements.star`", "insight": "**The third consumer of a new RefKind is a schema version, not a list.** Beyond the three kind lists the scheduled-event fix named (`callerRefKinds`, `graphRefKinds`, the QUAL004 rule), a new edge needs `CatalogSchemaVersion` bumped: refs are only written by REFRESH CATALOG FULL and `NewFromFile` applies the schema with CREATE TABLE IF NOT EXISTS, so without the bump an existing `.mxcli/catalog.db` gains the empty table and keeps serving the pre-fix edge set \u2014 the wrong answer, from a cache, after the fix shipped. `migrateIfSchemaMismatch` drops and rebuilds on a mismatch (verified by hand-editing catalog_meta back to '11'). **A flag is a missing table wearing a value**: `HasEventHandlers` and `NavigationProfile.OfflineEntityCount` are the same defect, and the fix is the same pair \u2014 rows for what it does, an edge for whether it is reachable. Do not encode the detail in the kind: eight kinds (`before_commit`, `after_delete`, ...) would enter every consumer's list to say one thing, so the moment/event go in the table and the edge stays one `event`. **The control has to be the binary, not the test**: stubbing `extractEventHandlerRefs` to emit nothing and rebuilding reproduced `(no callers found)` + 2 dead microflows on the same project, which is what proves the assertion detects something. mendixlabs/mxcli#1127; repro `mdl-examples/bug-tests/catalog-1127-entity-event-handler-refs.mdl`", "refs": ["mendixlabs/mxcli#1127"]} {"area":"mdl/catalog","date":"2026-09-18","symptom":"A Starlark lint rule written from the bundled write-lint-rules skill matches zero rows and reports a clean pass — or, with an allowlist, inverts into flagging everything (138 of 282 ACT_ microflows on one real project, 49% false positives)","cause":"The skill's example tables are the only documentation of the lint API and nothing tied them to the values the catalog emits. action_type listed Mendix BSON *storage* names (CreateChangeAction, CommitAction, ShowFormAction, CloseFormAction, ShowHomeFormAction) against a catalog that labels an action with its SDK name via getMicroflowActionType; source_type/target_type/element_type/access_type were lower-cased against an upper-case vocabulary; data_type was lower-cased against TitleCase from AttributeType.GetTypeName","file":"`.claude/skills/mendix/write-lint-rules/SKILL.md` (six rows), `mdl/catalog/builder_references.go` (RefObject* constants + RefSourceObjectTypes/RefTargetObjectTypes), `mdl/catalog/builder_permissions.go` (PermissionElement*/AccessType* constants), test `mdl/catalog/lint_rule_doc_vocabulary_test.go`","insight":"**Fix the documentation the rule author reads, not just the rule that was reported.** The identical defect was found and fixed in CONV010's allowlist a month earlier (finding 2026-08-17, pinned by lint_rule_vocabulary_test.go) — and recurred, because the *source* the author copied from was never corrected. A rule pinned to the labeller and a doc that is not is one fix, not two. **Check the sibling rows before believing the report's scope**: element_type, access_type and data_type had the same lower-casing and nobody had reported them; data_type ('string' vs 'String') is the most-used filter in a lint rule, so it was the most expensive one. **A doc value is only pinnable against a named vocabulary**, so the fix is half refactor: the emitters' scattered ALL-CAPS literals became RefObject*/PermissionElement*/AccessType* constants with published lists, mirroring the SourceObjectTypes precedent already in this package. action_type needs no list — the label IS the Go type name (%T), so the test reads the isMicroflowAction marker methods out of sdk/microflows/microflows_actions.go with go/ast. **Three legs, not one, when proving a filter fix**: old values -> 7/7 flagged, corrected -> 0, corrected-minus-one -> exactly that one. Leg C is the control; without it a silent rule and a correct rule both report zero, which is the bug itself","refs":["mendixlabs/mxcli#1027"],"rules":["CONV010"]} +{"area":"mdl/executor","date":"2026-09-18","symptom":"A page parameter passed as an argument to a nanoflow/microflow BUTTON action is not wired — Studio Pro reports CE1571 \"No argument has been selected for parameter 'X' and no default is available\" on opening the page, while `mx check`, `mxcli check --references` and `mxcli lint` are all clean. Reported as an asymmetry: of two arguments, the one matching the enclosing dataview's DataSource 'works' and the other does not","cause":"Mendix stores a flow argument in one of TWO slots of Forms$MicroflowParameterMapping / Forms$NanoflowParameterMapping: a reference to a page parameter, snippet parameter or page variable goes in `Variable` as a Forms$PageVariable; a literal or expression goes in `Expression`. mxcli only ever wrote `Expression: \"$Name\"`, which binds nothing. The read side was wrong in the mirror image — the three action describers and flowSourceArgs looked for a `Name` key on that sub-document, which Forms$PageVariable does not have","file":"`sdk/pages/pages_widgets_action.go` (VariableKind on both mapping types), `mdl/executor/cmd_pages_flow_args.go` (new: classifyFlowArgValue + pageVariableArgValue), `mdl/executor/cmd_pages_builder_v3.go` (3 of the 4 copies of the $-rule), `mdl/backend/modelsdk/widget_write.go` (bindParameterMappingValue), `mdl/executor/cmd_pages_describe_output.go` + `cmd_pages_describe_datasource.go` (read)","insight":"**The reported asymmetry is a red herring — both arguments were written identically and NEITHER was bound.** Studio Pro supplies a default for the one that is the dataview's object and reports the other; 'and no default is available' in CE1571 says exactly that. Time spent on why $Dto worked is wasted. **mxbuild is not a detector here**: `mx check` on the reported project is 0 errors before AND after the fix, so the usual two-copies-of-a-real-project run proves nothing and the reporter is right that it only shows in Studio Pro. **Get the reference from a Marketplace .mpk — it contains a whole Studio Pro-authored `project.mpr`**: `mxcli marketplace download --output x.mpk && unzip -o x.mpk project.mpr`, then `mxcli bson dump` it. A blank app is useless for this (every mapping list in it is empty); Workflow Commons 4.11.0 gave 101 flow parameter mappings, of which 95 bind through Variable and 6 through Expression — and all 6 of those are Boolean literals, so the $-prefixed Expression mxcli wrote occurs ZERO times. `marketplace install` refuses that package (javasource path guard), so extract rather than install. **The PageVariable slot follows what the name refers to** (PageParameter 20, SnippetParameter 58, Widget 17) — a snippet is the COMMON case, not the corner, and `paramScope` is the right oracle because it holds only entity-typed parameters, which is the same set Mendix binds this way. **Leave $currentObject alone**: no reference for the bare form was measured and show_page already depends on the context object being inferred (MDL-PAGEARG01), so changing it on a guess risks the case that works. **The read bug hid the write bug**: describe printed `Action: microflow M.F` with no arguments for Studio Pro content, so a round-trip looked lossless and the missing binding never showed up as a diff","refs":["mendixlabs/mxcli#1140","mendixlabs/mxcli#835"],"ce":["CE1571"]} diff --git a/.claude/skills/mendix/alter-page/SKILL.md b/.claude/skills/mendix/alter-page/SKILL.md index ca76c6cdb7..c4898bb5c5 100644 --- a/.claude/skills/mendix/alter-page/SKILL.md +++ b/.claude/skills/mendix/alter-page/SKILL.md @@ -117,6 +117,7 @@ set Title = 'New Page Title' set PopupWidth = 800 set PopupHeight = 480 set PopupResizable = true +set Documentation = 'What this page is for.' -- Retarget a button's on-click action. Any form `create page` accepts works -- here, including the combined ones. @@ -154,6 +155,7 @@ so a silent write would build cleanly and then fail to open. | `visible` | Any widget | String or Boolean | `set visible = false on txtHidden` | | `Name` | Any widget | String | `set Name = 'newName' on oldName` | | `Title` | Page-level only (case-sensitive) | String | `set Title = 'Edit Customer'` | +| `Documentation` | Page-level only (case-sensitive) | String (`''` clears) | `set Documentation = 'Coordinator triage step.'` | | `layout` | Page-level only | Qualified name | `set layout = Atlas_Core.Atlas_Default` | | `PopupWidth` | Page-level only (case-sensitive) | Positive integer (pixels) | `set PopupWidth = 800` | | `PopupHeight` | Page-level only (case-sensitive) | Positive integer (pixels) | `set PopupHeight = 480` | @@ -491,7 +493,7 @@ adds. Both still fail at exec if they are genuinely wrong. | Mistake | Fix | |---------|-----| -| Missing `on widgetName` for widget SET | Add `on widgetName` (only page-level properties — `Title`, `PopupWidth`, `PopupHeight`, `PopupResizable`, `Class`, `Style` — omit ON) | +| 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 | diff --git a/.claude/skills/mendix/create-page/SKILL.md b/.claude/skills/mendix/create-page/SKILL.md index 9495c15c59..9538aac4a1 100644 --- a/.claude/skills/mendix/create-page/SKILL.md +++ b/.claude/skills/mendix/create-page/SKILL.md @@ -66,6 +66,7 @@ Both are optional and can be changed later with `alter page … { set Class = ' | Properties | `(key: value, ...)` | `(title: 'Edit', layout: Atlas_Core.Atlas_Default)` | | Widget name | Required after type | `textbox txtName (...)` | | Attribute binding | `attribute: AttrName` | `textbox txt (label: 'Name', attribute: Name)` | +| Attribute over an association | `attribute: Assoc/Attr` (bare association name, multi-hop OK) | `textbox txt (label: 'Rule', attribute: RuleAction_BusinessRule/Name)` | | Variable binding | `datasource: $Var` | `dataview dv (datasource: $Product) { ... }` | | Action binding | `action: type` | `actionbutton btn (caption: 'Save', action: save_changes)` | | Database source | `datasource: database entity` | `datagrid dg (datasource: database Module.Entity)` | @@ -432,6 +433,14 @@ DATAVIEW dv (DataSource: $Issue) { A bare association name is qualified with the module of the entity the widget sits on. On a ComboBox that matters: its `DataSource:` is the *option list*, but +An input widget can also *traverse* an association to show a value from the +other side: `attribute: Assoc/Attr` binds the far attribute and stores the hops, +which is what Studio Pro does. It works on textbox, textarea, datepicker, +dropdown, checkbox and radiobuttons, and on data grid columns, with the same +bare-association spelling in each. Note what it is NOT: this shows a value from +the associated object, it does not make it editable through the association — +for editing the other object, nest a dataview over the association instead. + `Association:` names a reference on the containing entity, so `Association: Issue_Assignee` resolves against the dataview's entity, not the option list's module. diff --git a/.claude/skills/mendix/create-page/reference/widgets.md b/.claude/skills/mendix/create-page/reference/widgets.md index ef2a700073..4b7ff4f05b 100644 --- a/.claude/skills/mendix/create-page/reference/widgets.md +++ b/.claude/skills/mendix/create-page/reference/widgets.md @@ -122,13 +122,21 @@ describe icon collection Atlas_Core.Atlas_Filled -- every icon + its reference - `action: show_page Module.PageName(Param: $value)` - Navigate with parameters - `action: show_page Module.PageName($Param = $value)` - Also accepted (microflow-style) - `action: create_object Module.Entity then show_page Module.PageName` - Create and navigate -- **A `show_page` argument must be the context object.** Mendix takes the page - argument from the enclosing data widget, so the only spellings that mean - anything are `$currentObject` or the name of the variable that widget is bound - to (`datasource: $Customer` → `(Customer: $Customer)` is fine). Naming any other - variable is refused as **MDL-PAGEARG01** — it used to be accepted and silently - opened the page with the context object anyway. To open a page with something - else, call a microflow that shows it. +- **A `show_page` argument must be the context object, and there has to BE one.** + Mendix takes the page argument from the enclosing data widget, so the only + spellings that mean anything are `$currentObject` or the name of the variable + that widget is bound to (`datasource: $Customer` → `(Customer: $Customer)` is + fine). Naming any other variable is refused as **MDL-PAGEARG01** — it used to be + accepted and silently opened the page with the context object anyway. +- **Outside a data widget the same rule leaves nothing at all**, so a button sitting + on the page itself (or in a plain `container`/`layoutgrid`) may pass **no** + argument — not a page parameter, not `$currentObject`, not a literal. There is no + context object there for Mendix to infer, and the page opens with nothing: + mxbuild reports **CE1571** per parameter of the target page, and a page whose + parameters are optional would simply show the wrong data. MDL-PAGEARG01 refuses + that too (mendixlabs/mxcli#1029). To open a parameterised page from such a + button, call a microflow that does `show page Module.Page(Param: $value)` — + that path wires the arguments properly. - **The list above is the whole vocabulary, and a keyword without its argument is not in it.** `action: open_link` with no URL, `action: show_page` with no page, `action: microflow` with no name — each is **MDL-WIDGET28**. Until @@ -695,6 +703,35 @@ appears — it is deprecated in favour of the pluggable `image` widget, which takes the same `Image:`. mxcli still writes it, because round-tripping a model that already contains one is the point; prefer `image` on a new page. +#### `DataSource:` — which object a DYNAMICIMAGE shows + +A dynamic image shows the image held by an **object**, so it needs the entity +that object belongs to — reachable from the widget's context, which in practice +means the enclosing data container's entity: + +```sql +listview lvPhoto (DataSource: database from MyModule.Photo) { + dynamicimage imgPhoto ( + DataSource: database from MyModule.Photo, + DefaultImage: 'MyModule.Images.placeholder', + Width: 200, Height: 200 + ) +} +``` + +**Without `DataSource:` the build fails with CE0489** ("Select an entity for the +data source of this dynamic image"). Every `dynamicimage` mxcli wrote before this +was missing it, so the widget could not build at all. + +`DefaultImage:` is the fallback shown when the object carries no image, named the +same three-part way as `staticimage`'s `Image:`. `WidthUnit:`/`HeightUnit:`, +`Responsive: false`, `DisplayAs: thumbnail` and `OnClickType: enlarge` are all +written; leave them out for Mendix's defaults (auto, responsive, full size, no +enlarge), which `describe page` also omits. + +CE0582 applies here too — `dynamicimage` is deprecated alongside `staticimage`, +and the pluggable `image` widget is the replacement for both. + #### Setting Image Source (PLUGGABLEWIDGET syntax) The IMAGE shorthand creates a pluggable Image widget. For advanced properties like image source, use PLUGGABLEWIDGET syntax: diff --git a/.claude/skills/mendix/overview-pages/SKILL.md b/.claude/skills/mendix/overview-pages/SKILL.md index db6417e6e7..664d7bcb4c 100644 --- a/.claude/skills/mendix/overview-pages/SKILL.md +++ b/.claude/skills/mendix/overview-pages/SKILL.md @@ -561,13 +561,21 @@ navigationlist widgetName { - `action: microflow Module.MicroflowName(Param: $value)` - Call microflow with parameters - `action: show_page Module.PageName` - Navigate to page - `action: show_page Module.PageName(Param: $value)` - Navigate with parameters -- **A `show_page` argument must be the context object.** Mendix takes the page - argument from the enclosing data widget, so the only spellings that mean - anything are `$currentObject` or the name of the variable that widget is bound - to (`datasource: $Customer` → `(Customer: $Customer)` is fine). Naming any other - variable is refused as **MDL-PAGEARG01** — it used to be accepted and silently - opened the page with the context object anyway. To open a page with something - else, call a microflow that shows it. +- **A `show_page` argument must be the context object, and there has to BE one.** + Mendix takes the page argument from the enclosing data widget, so the only + spellings that mean anything are `$currentObject` or the name of the variable + that widget is bound to (`datasource: $Customer` → `(Customer: $Customer)` is + fine). Naming any other variable is refused as **MDL-PAGEARG01** — it used to be + accepted and silently opened the page with the context object anyway. +- **Outside a data widget the same rule leaves nothing at all**, so a button sitting + on the page itself (or in a plain `container`/`layoutgrid`) may pass **no** + argument — not a page parameter, not `$currentObject`, not a literal. There is no + context object there for Mendix to infer, and the page opens with nothing: + mxbuild reports **CE1571** per parameter of the target page, and a page whose + parameters are optional would simply show the wrong data. MDL-PAGEARG01 refuses + that too (mendixlabs/mxcli#1029). To open a parameterised page from such a + button, call a microflow that does `show page Module.Page(Param: $value)` — + that path wires the arguments properly. ## Handling Circular Dependencies diff --git a/.claude/skills/mendix/write-microflows/reference/pitfalls.md b/.claude/skills/mendix/write-microflows/reference/pitfalls.md index fe7046d49a..be632b05ed 100644 --- a/.claude/skills/mendix/write-microflows/reference/pitfalls.md +++ b/.claude/skills/mendix/write-microflows/reference/pitfalls.md @@ -539,65 +539,72 @@ It is a **security** setting and it only ever narrows, so the rules mirror the same rule that catches `@applyentityacces` and any other annotation the document does not read. The message names what that document does accept. -## Concurrency settings are preserved, not authorable - -Studio Pro's **"Disallow concurrent execution"**, its error message and error -microflow, and **"Mark as used"** have no MDL syntax. All four now survive a -`create or modify microflow`; before, the rebuild wrote its own values over -every one of them. - -The concurrency one is worth knowing about even though it is fixed, because of -which way it failed. The rebuild hardcoded *allow*, so a microflow that -**disallowed** concurrent execution came back allowing it — the running app's -concurrency protection silently removed. **CE4899 only fires on -disallow-without-a-message**, never on allow, so the one check that exists in -this area could not see it, and the error message went the same way, -translations included. - -There is nothing to write in a script. What matters is the same rule as below: -use `create or modify` to edit such a microflow, never `drop` + `create`. - -## Export level is preserved, not authorable - -A microflow carries an **export level** — Studio Pro's `Hidden` or `API` — which -decides whether it is part of the module's public surface when the module is -exported as a package. Like the URL below, MDL cannot write it, and like the URL -it now **survives a `create or modify microflow`**; before, every rewrite pinned -it to `Hidden`, quietly removing the microflow from a protected module's API. - -`Hidden` is the normal value by a wide margin — measured across Business Events -3.12.0 and External Database Connector 6.2.3/6.3.0, every document of every type -stores it — so `describe microflow` mentions the export level **only when it is -not `Hidden`**, as a `-- Export level:` comment. The copy caveat below applies to -it identically. - -## The deep-link URL is preserved, not authorable - -A microflow can carry a **URL** (Mendix 10.6+) — Studio Pro's "URL" field, e.g. -`item/{Key}` — which makes it reachable as a deep link. MDL has **no syntax for -it**, so there is no annotation to write and nothing to check. - -What matters is that it **survives**: a `create or modify microflow` that -rewrites the body keeps the stored URL and its search parameters. It did not -before #1120, and this one was harder to notice than the flags above, because a -microflow *without* a URL is a valid microflow — `mxcli check`, `mx check` and -mxbuild all reported success, and the deep link was simply gone the next time -someone opened Studio Pro. - -**A parameter in the URL path may not also be a search parameter.** mxbuild -rejects that combination with **CE5612** ("The Microflow parameter … cannot be -used as a URL parameter if it is already a URL search parameter"). Path -parameters and query parameters are disjoint sets. mxcli cannot author either, -so this only matters when reading a describe comment or reasoning about a -project — but it is the rule that decides whether a stored pair is valid. - -Two consequences for scripts: - -- **`describe microflow` emits it as a `-- URL:` comment**, not as executable - MDL, because there is nothing to execute. That comment is a warning, not - decoration: a **describe → rename → exec copy has nothing to preserve from**, - so the new microflow has no URL. Set it in Studio Pro after copying. -- **`drop microflow` followed by `create microflow` loses it** for the same - reason. Use `create or modify` to edit a microflow that has a deep link — or a - non-default export level or any concurrency setting, which the drop path loses - the same way. +## Document properties: authorable, and omitted still preserves + +Four microflow properties live in the header rather than the body: + +```mdl +create or modify microflow Shop.ACT_ShowOrder ($Order: Shop.Order, $Tab: String) +url 'order/{Order}' +url search parameters ($Tab) +export level api +disallow concurrent execution error message 'This order is already being processed' +begin + ... +end; +``` + +- **`url`** is the deep link (Mendix **10.6+**), Studio Pro's URL field. `url search + parameters (...)` names the parameters passed as query arguments; `drop url` + removes both. +- **`export level api | hidden`** decides whether the microflow is part of the + module's public surface when the module is exported. +- **`disallow concurrent execution`** takes `error message 'text'` or + `error microflow Mod.Name`; `allow concurrent execution` is the default. + +**An omitted clause preserves what is stored.** Same rule as `@excluded`, +`@applyentityaccess` and `EXPOSED AS`: a `create or modify` that only edits the +body leaves all of them alone. That is the fix for #1120, and the clauses are the +way to opt out of it deliberately — `drop url`, `export level hidden`, +`allow concurrent execution`. + +### Four rules that used to surface only at build time + +| | Rule | Mendix reports | +|---|---|---| +| **MDL-MF01** | every `{Name}` must name a parameter of this microflow | — | +| **MDL-MF02** | a parameter in the PATH may **not** also be a search parameter | CE5612 | +| **MDL-MF03** | `disallow` needs an error message or microflow | CE4899 | +| — | a URL another microflow already owns (needs `-p`) | CE0570 | + +MF02 is the one to remember: path parameters and query parameters are disjoint +sets. `url 'item/{Key}'` with `url search parameters ($Key)` is rejected — use a +different parameter for the query argument. + +A segment may carry an attribute path — `{Customer/Name}` binds the **Customer** +parameter by one of its attributes — so the leading identifier is what must match. + +### Two things that do NOT get cleared + +- **`allow concurrent execution` leaves a stored error message.** Studio Pro greys + those fields rather than erasing them, so re-disallowing restores the message — + and `canon.CarryTranslations` would put it back regardless, because a rebuild + cannot distinguish "cleared on purpose" from "the statement could not say it". + An inert stored message breaks nothing: Mendix reads it only when execution is + disallowed. +- **Other languages of an error message.** MDL states one string, but a rewrite + keeps the rest: measured, restating an English message left its Dutch + translation untouched. `describe` flags the languages a **copy** would not carry. + +### `Mark as used` still has no clause + +It is carried across a rewrite like the others were, and there is no way to set +it from MDL. Nothing is lost by that — it only suppresses an editor warning. + +## `drop` + `create` is still a new document + +`drop microflow` followed by `create microflow` starts from nothing, so it keeps +none of these unless the script restates them. Use `create or modify` to edit a +microflow that carries any of them — and note that `describe` now emits all +four clauses, so **describe → rename → exec copies them faithfully**. Give the +copy a different `url`, though: two microflows may not share one (CE0570). diff --git a/CLAUDE.md b/CLAUDE.md index 66abbfdb44..63a1ff71fb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -533,7 +533,7 @@ The `api/` package provides a simplified, fluent API inspired by Mendix Web Exte a, err := api.Open("/path/to/project.mpr") // or api.New(b) over any backend defer a.Close() -module, _ := a.Modules.GetModule("MyModule") +module, _ := a.Modules.Get("MyModule") a.SetModule(module) entity, _ := a.DomainModels.CreateEntity("Customer"). diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e5784708dd..6eb43244b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -437,7 +437,8 @@ If you're contributing from a fork, this is the full cycle: | Directory | Purpose | |-----------|---------| | `cmd/mxcli/` | CLI commands (Cobra) | -| `sdk/mpr/` | MPR file reading/writing, BSON parsing | +| `modelsdk/` | MPR file reading/writing, BSON codec, canonical form | +| `mdl/backend/modelsdk/` | Backend implementation: semantic model ↔ BSON | | `sdk/microflows/`, `sdk/pages/`, etc. | Domain types | | `mdl/grammar/` | ANTLR4 grammar (`MDLParser.g4`, `MDLLexer.g4`) | | `mdl/ast/` | AST node types | diff --git a/cmd/mxcli/cmd_lint.go b/cmd/mxcli/cmd_lint.go index f3d3d3cb4c..c46382750d 100644 --- a/cmd/mxcli/cmd_lint.go +++ b/cmd/mxcli/cmd_lint.go @@ -125,25 +125,8 @@ Examples: // so we can compute the catalog depth they need BEFORE building it. A rule // that needs the refs (full) or graph_* (communities) tables then gets them // automatically instead of silently returning empty results (issue #721). - lintRules := builtinLintRules() - // Search upward from the project for .claude/lint-rules/, so one - // directory at the repo root serves an app in a subfolder (#904). + lintRules := projectLintRules(projectDir, os.Stderr) lintRulesDir := linter.FindLintRulesDir(projectDir) - starlarkRules, loadFailures, err := linter.LoadStarlarkRulesFromDir(lintRulesDir) - if err != nil { - // Previously discarded, which made an unreadable directory look - // exactly like a project with no custom rules. - fmt.Fprintf(os.Stderr, "Warning: could not read %s: %v\n", lintRulesDir, err) - } - for _, f := range loadFailures { - fmt.Fprintf(os.Stderr, "Warning: rule file skipped: %s: %s\n", f.Path, f.Reason) - } - if len(loadFailures) > 0 { - fmt.Fprintf(os.Stderr, "Warning: %d rule file(s) skipped — those rules did not run.\n", len(loadFailures)) - } - for _, rule := range starlarkRules { - lintRules = append(lintRules, rule) - } // Build catalog at the depth the rules need (fast / full / communities). catalogMode := linter.RequiredCatalogMode(lintRules) @@ -184,8 +167,7 @@ Examples: // Load lint config file and apply (excludedModules, rule severity/enabled overrides). // Config ExcludeModules merges with --exclude flag values. - configPath := linter.FindConfigFile(projectDir) - if cfg, err := linter.LoadConfig(configPath); err == nil { + if cfg, configPath := applyLintConfig(lint, projectDir, os.Stderr); cfg != nil { if len(cfg.ExcludeModules) > 0 { merged := append(excludeModules, cfg.ExcludeModules...) ctx.SetExcludedModules(merged) @@ -204,9 +186,6 @@ Examples: pluralItThem(len(shadowed))) } } - cfg.ApplyConfig(lint) - } else { - fmt.Fprintf(os.Stderr, "Warning: failed to load lint config: %v\n", err) } // If --rules is specified, disable every rule not in the allowlist. diff --git a/cmd/mxcli/cmd_report.go b/cmd/mxcli/cmd_report.go index 9138d444a0..3e1b826456 100644 --- a/cmd/mxcli/cmd_report.go +++ b/cmd/mxcli/cmd_report.go @@ -10,7 +10,6 @@ import ( "time" "github.com/mendixlabs/mxcli/mdl/linter" - "github.com/mendixlabs/mxcli/mdl/linter/rules" "github.com/mendixlabs/mxcli/mdl/visitor" "github.com/spf13/cobra" ) @@ -88,53 +87,22 @@ Examples: ctx := linter.NewLintContext(cat, exec.Backend()) ctx.SetExcludedModules(excludeModules) - // Create linter and register all rules - lint := linter.New(ctx) - - // Built-in Go rules - lint.AddRule(rules.NewNamingConventionRule()) - lint.AddRule(rules.NewEmptyMicroflowRule()) - lint.AddRule(rules.NewDomainModelSizeRule()) - lint.AddRule(rules.NewValidationFeedbackRule()) - lint.AddRule(rules.NewImageSourceRule()) - lint.AddRule(rules.NewEmptyContainerRule()) - lint.AddRule(rules.NewGallerySelectionListenerRule()) - lint.AddRule(rules.NewDataViewLayoutGridRule()) - lint.AddRule(rules.NewPageNavigationSecurityRule()) - lint.AddRule(rules.NewNoEntityAccessRulesRule()) - lint.AddRule(rules.NewWeakPasswordPolicyRule()) - lint.AddRule(rules.NewDemoUsersActiveRule()) - - // MPR008 - requires BSON inspection - lint.AddRule(rules.NewOverlappingActivitiesRule()) - lint.AddRule(rules.NewLoopChildContainmentRule()) - - // Convention rules (CONV011-CONV014) - lint.AddRule(rules.NewNoCommitInLoopRule()) - lint.AddRule(rules.NewExclusiveSplitCaptionRule()) - lint.AddRule(rules.NewErrorHandlingOnCallsRule()) - lint.AddRule(rules.NewNoContinueErrorHandlingRule()) - - // Load Starlark rules (includes CONV001-010, CONV015-017). - // - // Searched upward from the project, like `mxcli lint` — this command had - // the same .mpr-relative lookup, and it emits a *score*, so a silently - // reduced rule set produced a falsely high one (#904). + // The rule set and the config come from the same two helpers `mxcli + // lint` uses. This command used to build both itself: its inline copy of + // the built-in list had fallen a rule behind (MDL-FLOW01), and it never + // read lint-config.yaml at all — so a rule a team had deliberately + // accepted and disabled still scored against them, and the score could + // not be moved by any configuration (ako/mxcli#525). A score computed + // from a different rule set than the listing that explains it is worse + // than no score. projectDir := filepath.Dir(projectPath) - lintRulesDir := linter.FindLintRulesDir(projectDir) - starlarkRules, loadFailures, err := linter.LoadStarlarkRulesFromDir(lintRulesDir) - if err != nil { - fmt.Fprintf(os.Stderr, "Warning: could not read %s: %v\n", lintRulesDir, err) - } - for _, f := range loadFailures { - fmt.Fprintf(os.Stderr, "Warning: rule file skipped: %s: %s\n", f.Path, f.Reason) - } - if len(loadFailures) > 0 { - fmt.Fprintf(os.Stderr, "Warning: %d rule file(s) skipped — the score below does not include them.\n", len(loadFailures)) - } - for _, rule := range starlarkRules { + lint := linter.New(ctx) + for _, rule := range projectLintRules(projectDir, os.Stderr) { lint.AddRule(rule) } + if cfg, _ := applyLintConfig(lint, projectDir, os.Stderr); cfg != nil && len(cfg.ExcludeModules) > 0 { + ctx.SetExcludedModules(append(excludeModules, cfg.ExcludeModules...)) + } // Run all rules violations, err := lint.Run(context.Background()) diff --git a/cmd/mxcli/lint_setup.go b/cmd/mxcli/lint_setup.go new file mode 100644 index 0000000000..db1d8bf631 --- /dev/null +++ b/cmd/mxcli/lint_setup.go @@ -0,0 +1,71 @@ +// SPDX-License-Identifier: Apache-2.0 + +package main + +import ( + "fmt" + "io" + + "github.com/mendixlabs/mxcli/mdl/linter" +) + +// projectLintRules builds the rule set for a project: the built-in Go rules plus +// every Starlark rule under the nearest .claude/lint-rules/. +// +// It exists so `mxcli lint` and `mxcli report` cannot disagree about what "the +// rules" are. They did: report carried its own inline copy of the built-in list +// that had fallen one rule behind (MDL-FLOW01), so the two commands scored a +// project against different rule sets — the same class as #904, where a silently +// reduced rule set produced a falsely high score, and just as invisible. +// +// Load failures are warned about rather than swallowed, because a rule that +// fails to load reads exactly like a project that has nothing to report. +func projectLintRules(projectDir string, warn io.Writer) []linter.Rule { + lintRules := builtinLintRules() + + // Searched upward from the project, so one directory at the repo root + // serves an app in a subfolder (#904). + lintRulesDir := linter.FindLintRulesDir(projectDir) + starlarkRules, loadFailures, err := linter.LoadStarlarkRulesFromDir(lintRulesDir) + if err != nil { + // Previously discarded in `lint`, which made an unreadable directory + // look exactly like a project with no custom rules. + fmt.Fprintf(warn, "Warning: could not read %s: %v\n", lintRulesDir, err) + } + for _, f := range loadFailures { + fmt.Fprintf(warn, "Warning: rule file skipped: %s: %s\n", f.Path, f.Reason) + } + if len(loadFailures) > 0 { + fmt.Fprintf(warn, "Warning: %d rule file(s) skipped — those rules did not run.\n", len(loadFailures)) + } + for _, rule := range starlarkRules { + lintRules = append(lintRules, rule) + } + return lintRules +} + +// applyLintConfig loads the project's lint-config.yaml and applies it to lint, +// returning the config and the path it came from (empty when there is none). +// +// `mxcli report` did not call this at all (ako/mxcli#525). A team that had +// accepted a rule and disabled it in the config saw `mxcli lint` agree and the +// report's SCORE stay exactly where it was — so the score could not be +// calibrated even in principle, and the natural conclusion was that the tool +// disagreed with a deliberate decision rather than that it had not read the +// file. +// +// It applies the rule overrides and returns the config, leaving the caller to +// merge cfg.ExcludeModules into its own LintContext. That split is deliberate: +// `lint` additionally warns when --modules names a module the config excludes, +// which needs the config and the flags together, and a helper that silently set +// the excludes would make that warning easy to lose. +func applyLintConfig(lint *linter.Linter, projectDir string, warn io.Writer) (*linter.Config, string) { + configPath := linter.FindConfigFile(projectDir) + cfg, err := linter.LoadConfig(configPath) + if err != nil { + fmt.Fprintf(warn, "Warning: failed to load lint config: %v\n", err) + return nil, configPath + } + cfg.ApplyConfig(lint) + return cfg, configPath +} diff --git a/cmd/mxcli/lint_setup_test.go b/cmd/mxcli/lint_setup_test.go new file mode 100644 index 0000000000..a670ddd689 --- /dev/null +++ b/cmd/mxcli/lint_setup_test.go @@ -0,0 +1,123 @@ +// SPDX-License-Identifier: Apache-2.0 + +// ako/mxcli#525: `mxcli report` never loaded lint-config.yaml, so a rule the +// team had deliberately accepted and disabled still scored against the project. +// On the reporting project 61 of 86 findings were two such rules, and the score +// (66/100 against a 99/100 blank-app baseline) could not be moved by any +// configuration — which reads as the tool disagreeing with a decision rather +// than as the tool not having read the file. +// +// The sibling defect in the same command: report carried its own inline copy of +// the built-in rule list, one rule behind lint's. Two commands scoring one +// project against two rule sets is the #904 class again. +package main + +import ( + "io" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/mendixlabs/mxcli/mdl/linter" +) + +// writeLintConfig drops a lint-config.yaml disabling CONV010 into a temp +// project directory, in the first location FindConfigFile searches. +func writeLintConfig(t *testing.T) string { + t.Helper() + dir := t.TempDir() + claude := filepath.Join(dir, ".claude") + if err := os.MkdirAll(claude, 0o755); err != nil { + t.Fatalf("mkdir: %v", err) + } + const cfg = `excludeModules: + - System +rules: + CONV010: + enabled: false +` + if err := os.WriteFile(filepath.Join(claude, "lint-config.yaml"), []byte(cfg), 0o644); err != nil { + t.Fatalf("write config: %v", err) + } + return dir +} + +// TestApplyLintConfig_DisablesARule is the mechanism both commands depend on. +func TestApplyLintConfig_DisablesARule(t *testing.T) { + dir := writeLintConfig(t) + + lint := linter.New(nil) + cfg, path := applyLintConfig(lint, dir, io.Discard) + if cfg == nil { + t.Fatalf("no config loaded from %s", dir) + } + if path == "" { + t.Error("config path is empty, so nothing would be named in a warning") + } + if lint.RuleEnabled("CONV010") { + t.Error("CONV010 is still enabled after a config disabling it") + } + // Control: a rule the config says nothing about must stay enabled. + // Without it, a broken ApplyConfig that disables everything passes. + if !lint.RuleEnabled("CONV007") { + t.Error("CONV007 was disabled by a config that never mentions it") + } + if len(cfg.ExcludeModules) != 1 || cfg.ExcludeModules[0] != "System" { + t.Errorf("excludeModules = %v, want [System]", cfg.ExcludeModules) + } +} + +// TestLintAndReportShareOneRuleSet guards the inline copy, structurally. +// +// A value test cannot catch this one: both commands build their rules inside a +// cobra RunE, so nothing a unit test can call would notice a second list being +// re-added beside the shared helper — which is exactly how report's copy drifted +// a rule behind in the first place. So this asserts the property that matters: +// neither command constructs rules itself. The precedent is +// scripts/check-tunnel-deps.sh, which guards an import graph the same way. +func TestLintAndReportShareOneRuleSet(t *testing.T) { + // Positive control first, so the test cannot pass by finding nothing: + // builtinLintRules is the ONE place allowed to construct rules, and it must + // still be doing so. + setup, err := os.ReadFile("cmd_lint.go") + if err != nil { + t.Fatalf("read cmd_lint.go: %v", err) + } + if !strings.Contains(string(setup), "rules.New") { + t.Fatal("builtinLintRules no longer constructs rules — this guard would pass vacuously") + } + + for _, name := range []string{"cmd_report.go"} { + src, err := os.ReadFile(name) + if err != nil { + t.Fatalf("read %s: %v", name, err) + } + for i, line := range strings.Split(string(src), "\n") { + if strings.Contains(line, "lint.AddRule(rules.New") { + t.Errorf("%s:%d constructs a lint rule inline:\n\t%s\n"+ + "Rules come from projectLintRules, or the two commands score "+ + "a project against different rule sets (ako/mxcli#525).", + name, i+1, strings.TrimSpace(line)) + } + } + } + + // And the shared set really is the built-in list plus nothing, given a + // project directory with no .claude/lint-rules/ in it. MDL-FLOW01 is the + // rule report's inline copy had fallen behind on. + shared := map[string]bool{} + for _, r := range projectLintRules(t.TempDir(), io.Discard) { + shared[r.ID()] = true + } + if len(shared) == 0 { + t.Fatal("no built-in rules at all") + } + if !shared["MDL-FLOW01"] { + t.Error("MDL-FLOW01 missing — the shared rule set is not the built-in list") + } + if len(builtinLintRules()) != len(shared) { + t.Errorf("projectLintRules registered %d rules where the built-ins are %d, "+ + "with no rules dir present", len(shared), len(builtinLintRules())) + } +} diff --git a/cmd/mxcli/lsp_completions_gen.go b/cmd/mxcli/lsp_completions_gen.go index fcf9f4f368..effe71babd 100644 --- a/cmd/mxcli/lsp_completions_gen.go +++ b/cmd/mxcli/lsp_completions_gen.go @@ -441,6 +441,11 @@ var mdlGeneratedKeywords = []protocol.CompletionItem{ {Label: "PUT", Kind: protocol.CompletionItemKindKeyword, Detail: "REST keyword"}, {Label: "PATCH", Kind: protocol.CompletionItemKindKeyword, Detail: "REST keyword"}, {Label: "API", Kind: protocol.CompletionItemKindKeyword, Detail: "REST keyword"}, + {Label: "HIDDEN", Kind: protocol.CompletionItemKindKeyword, Detail: "REST keyword"}, + {Label: "ALLOW", Kind: protocol.CompletionItemKindKeyword, Detail: "REST keyword"}, + {Label: "DISALLOW", Kind: protocol.CompletionItemKindKeyword, Detail: "REST keyword"}, + {Label: "CONCURRENT", Kind: protocol.CompletionItemKindKeyword, Detail: "REST keyword"}, + {Label: "EXECUTION", Kind: protocol.CompletionItemKindKeyword, Detail: "REST keyword"}, {Label: "CLIENT", Kind: protocol.CompletionItemKindKeyword, Detail: "REST keyword"}, {Label: "CLIENTS", Kind: protocol.CompletionItemKindKeyword, Detail: "REST keyword"}, {Label: "PUBLISH", Kind: protocol.CompletionItemKindKeyword, Detail: "REST keyword"}, @@ -585,6 +590,8 @@ var mdlGeneratedKeywords = []protocol.CompletionItem{ {Label: "APPLY", Kind: protocol.CompletionItemKindKeyword, Detail: "Keyword"}, {Label: "ACCESS", Kind: protocol.CompletionItemKindKeyword, Detail: "Keyword"}, {Label: "LEVEL", Kind: protocol.CompletionItemKindKeyword, Detail: "Keyword"}, + {Label: "STRICT", Kind: protocol.CompletionItemKindKeyword, Detail: "Keyword"}, + {Label: "MODE", Kind: protocol.CompletionItemKindKeyword, Detail: "Keyword"}, {Label: "USER", Kind: protocol.CompletionItemKindKeyword, Detail: "Keyword"}, {Label: "TASK", Kind: protocol.CompletionItemKindKeyword, Detail: "Keyword"}, {Label: "DECISION", Kind: protocol.CompletionItemKindKeyword, Detail: "Keyword"}, diff --git a/cmd/mxcli/syntax/features_microflow.go b/cmd/mxcli/syntax/features_microflow.go index dc375d7ee9..dfc1b61e03 100644 --- a/cmd/mxcli/syntax/features_microflow.go +++ b/cmd/mxcli/syntax/features_microflow.go @@ -36,7 +36,25 @@ func init() { "exposed as", "expose as microflow action", "expose as workflow action", "toolbox", "icon", "image", }, - Syntax: "CREATE MICROFLOW Module.Name ($P1: String, $P2: Integer)\n RETURNS Type AS $Result\n [FOLDER 'FolderPath']\n [EXPOSED AS MICROFLOW ACTION 'Caption' IN 'Category'\n [ICON 'icon.png'] [ICON DARK 'icon-dark.png']\n [IMAGE 'image.png'] [IMAGE DARK 'image-dark.png']]\n [EXPOSED AS WORKFLOW ACTION 'Caption' IN 'Category']\n [NOT EXPOSED AS MICROFLOW|WORKFLOW ACTION]\nBEGIN\n \nEND;\n\nEXPOSED AS puts the microflow in Studio Pro's toolbox, so whoever drags it in\ndoes not need to know it is a microflow. There are two toolboxes — the\nmicroflow editor's and the workflow editor's — so the clause names which.\nThe icon is a 64x64 PNG and the image a 256x192 PNG, read from disk relative\nto the .mdl file's own directory. An OMITTED clause preserves what is stored, so\nremoving an entry is NOT EXPOSED and clearing one bitmap is DROP ICON/IMAGE.", + Syntax: "CREATE MICROFLOW Module.Name ($P1: String, $P2: Integer)\n RETURNS Type AS $Result\n [FOLDER 'FolderPath']\n [EXPOSED AS MICROFLOW ACTION 'Caption' IN 'Category'\n [ICON 'icon.png'] [ICON DARK 'icon-dark.png']\n [IMAGE 'image.png'] [IMAGE DARK 'image-dark.png']]\n [EXPOSED AS WORKFLOW ACTION 'Caption' IN 'Category']\n [NOT EXPOSED AS MICROFLOW|WORKFLOW ACTION]\nBEGIN\n \nEND;\n\nEXPOSED AS puts the microflow in Studio Pro's toolbox, so whoever drags it in\ndoes not need to know it is a microflow. There are two toolboxes — the\nmicroflow editor's and the workflow editor's — so the clause names which.\nThe icon is a 64x64 PNG and the image a 256x192 PNG, read from disk relative\nto the .mdl file's own directory. An OMITTED clause preserves what is stored, so\nremoving an entry is NOT EXPOSED and clearing one bitmap is DROP ICON/IMAGE.\n\n" + + "Three document properties have their own header clauses:\n\n" + + " URL 'item/{Key}' the deep link (Mendix 10.6+)\n" + + " URL SEARCH PARAMETERS ($Filter) parameters passed as query arguments\n" + + " DROP URL remove the deep link and its search params\n" + + " EXPORT LEVEL API | HIDDEN the module's public surface on export\n" + + " DISALLOW CONCURRENT EXECUTION ERROR MESSAGE 'text'\n" + + " DISALLOW CONCURRENT EXECUTION ERROR MICROFLOW Module.Name\n" + + " ALLOW CONCURRENT EXECUTION\n\n" + + "An OMITTED clause PRESERVES what is stored — the same rule as EXPOSED AS and\n" + + "@applyentityaccess — so a rewrite that only changes the body leaves all of\n" + + "them alone. DROP URL / EXPORT LEVEL HIDDEN / ALLOW are the explicit forms.\n\n" + + "Three platform rules, each checked before the write rather than at build:\n" + + " MDL-MF01 every {Name} must name a parameter of this microflow\n" + + " MDL-MF02 a PATH parameter may not also be a SEARCH parameter (CE5612)\n" + + " MDL-MF03 DISALLOW needs an error message or microflow (CE4899)\n" + + "and with a project, a URL another microflow already owns (CE0570).\n\n" + + "`Mark as used` still has no clause and is carried, as all of these were\n" + + "before they were authorable (mendixlabs/mxcli#1120).", Example: "CREATE MICROFLOW MyModule.ACT_CreateOrder (\n $CustomerCode: String,\n $Quantity: Integer\n)\nRETURNS MyModule.Order AS $NewOrder\nFOLDER 'Orders'\nEXPOSED AS MICROFLOW ACTION 'Create order' IN 'Orders'\n ICON 'assets/order-64.png'\nBEGIN\n $NewOrder = CREATE MyModule.Order (\n OrderNumber = 'ORD-001',\n Quantity = $Quantity\n );\n COMMIT $NewOrder;\n RETURN $NewOrder;\nEND;", SeeAlso: []string{"microflow.nanoflow", "microflow.variables"}, }) diff --git a/cmd/mxcli/syntax/features_misc.go b/cmd/mxcli/syntax/features_misc.go index 9dc32acdf4..6ee7ed8e72 100644 --- a/cmd/mxcli/syntax/features_misc.go +++ b/cmd/mxcli/syntax/features_misc.go @@ -24,7 +24,20 @@ func init() { "-- OR REPLACE discard the existing document and write a fresh one\n" + "-- OR MODIFY update the existing document in place\n" + "-- Both reuse the existing element's ID, so references from other\n" + - "-- documents survive.", + "-- documents survive.\n" + + "--\n" + + "-- A rewrite rebuilds the document from the statement, so properties MDL\n" + + "-- cannot express are CARRIED OVER rather than reset — a microflow's URL\n" + + "-- and export level, a queued call's binding, translated captions, an\n" + + "-- entity's identity. Nothing would report the loss if they were not: the\n" + + "-- result is a valid document either way, so mxcli check, mx check and\n" + + "-- mxbuild all pass and only Studio Pro shows what went missing.\n" + + "--\n" + + "-- DROP followed by CREATE is a NEW document and keeps none of it, and so\n" + + "-- is a DESCRIBE -> rename -> exec copy. Where a property HAS a spelling,\n" + + "-- DESCRIBE emits it and the copy is faithful (see microflow.create);\n" + + "-- where it does not, DESCRIBE flags the gap as a comment rather than\n" + + "-- producing output that looks complete.", Example: "CREATE OR REPLACE MICROFLOW MyModule.ACT_Recalculate ()\nBEGIN\n RETURN;\nEND;\n\nCREATE OR MODIFY PERSISTENT ENTITY MyModule.Customer (\n Name: String(200)\n);", SeeAlso: []string{"microflow", "domain-model.entity", "page", "document-folder"}, }) diff --git a/cmd/mxcli/syntax/features_page.go b/cmd/mxcli/syntax/features_page.go index 5a9b1bb860..c4609994a0 100644 --- a/cmd/mxcli/syntax/features_page.go +++ b/cmd/mxcli/syntax/features_page.go @@ -155,6 +155,13 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "-- `check --references` rather than failing the build with CE1613.\n" + "-- The alternatives are the URL form above, or `ImageType: icon`.\n\n" + "-- Any pluggable widget by its id (id FIRST, then the name)\nPLUGGABLEWIDGET 'com.mendix.widget.web.badge.Badge' name (value: 'x')\nCUSTOMWIDGET 'com.mendix.widget.custom.x.X' name (prop: 'x') -- legacy spelling\n\n" + + "-- DYNAMICIMAGE shows the image held by an OBJECT, so it needs the entity that\n" + + "-- object belongs to — reachable from the widget's context. Without it mxbuild\n" + + "-- reports CE0489, so the widget cannot build at all:\n" + + "DYNAMICIMAGE imgPhoto (DataSource: database from MyModule.Photo,\n" + + " DefaultImage: 'MyModule.Images.placeholder')\n" + + "-- DefaultImage is the fallback when the object carries none. DisplayAs:\n" + + "-- thumbnail and OnClickType: enlarge are written too.\n\n" + "-- STATICIMAGE takes the same three-part image-collection reference as IMAGE,\n" + "-- so a stored one round-trips through DESCRIBE (mendixlabs/mxcli#1057). Without\n" + "-- it the widget is written with no image and mxbuild reports CE0436:\n" + @@ -243,7 +250,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "icon", "linkbutton", "link button", "nothing", "no action", "inert", "dead button", }, - Syntax: "Action: NOTHING -- deliberately no action (Forms$NoAction)\nAction: SAVE_CHANGES\nAction: SAVE_CHANGES CLOSE_PAGE -- save, then close the pop-up\nAction: CANCEL_CHANGES\nAction: CANCEL_CHANGES CLOSE_PAGE\nAction: CLOSE_PAGE\nAction: DELETE\nAction: DELETE CLOSE_PAGE\nAction: DELETE_OBJECT\nAction: NANOFLOW Module.NF\nAction: NANOFLOW Module.NF(Param: $val)\nAction: OPEN_LINK 'https://example.com'\nAction: SIGN_OUT\nAction: COMPLETE_TASK 'OutcomeName'\nAction: SHOW_PAGE Module.Page\nAction: SHOW_PAGE Module.Page(Param: $currentObject)\nAction: MICROFLOW Module.MF\nAction: MICROFLOW Module.MF(Param: $val)\nAction: CREATE_OBJECT Module.Entity THEN SHOW_PAGE Module.Page\n\nThe list above is exhaustive. Anything else in an action slot is an\nERROR (MDL-WIDGET28), including a real keyword short its argument --\n`Action: OPEN_LINK` without a URL, `Action: SHOW_PAGE` without a page.\nSuch a widget used to be written with NO action at all and rendered as a\ndead control, with check, exec and mxbuild all clean, because a\nno-action widget is legal Mendix (mendixlabs/mxcli#1062). Write NOTHING\nwhen a control really is meant to be inert.\n\nThe same forms serve `OnClick:` (an alias of `Action:`) and `OnChange:`.\n\nA microflow or nanoflow action is a CALL: it needs an argument for every\nparameter the flow declares, or Mendix rejects the page with CE1571. The\nargument list is the same on every widget that takes an action -- a\nCONTAINER (which is clickable) as much as an ACTIONBUTTON. An enclosing\ndata container of the right type supplies it without an argument; a data\ngrid's CONTROL BAR does not, because it is not row-scoped -- pass the\ngrid's selection there (`$dgOrders`).\n\nA SHOW_PAGE argument must be the enclosing widget's context object --\neither $currentObject or the name of the variable the enclosing data\nwidget is bound to. Mendix infers it from that widget, so naming any\nother variable is refused (MDL-PAGEARG01); call a microflow instead.\n\nOPEN_LINK takes a static web address and stores it as a\nForms$StaticOrDynamicString. Mendix also supports a DYNAMIC address, read\nfrom an attribute at runtime; MDL cannot author that one, and DESCRIBE\nflags such a button rather than printing its address as a literal.\n\nButton styles: Default, Primary, Success, Info, Warning, Danger\n\nMendix has THREE icon elements and the keyword picks which one:\n\nIcon: 'Atlas_Core.Atlas_Filled.pencil' -- an icon collection\nIcon: image MyModule.Images.logo -- an IMAGE collection\nIcon: glyph 57377 -- a font code point\n\nThe bare form is the icon-collection icon and any collection in the\nproject works, third-party ones included. The image form points into a\ndifferent document, and is spelled the same way apart from the keyword\n-- write it without `image` and mxcli stores a custom-icon reference,\nwhich fails the build with CE1613 (mendixlabs/mxcli#1059).\n`mxcli check -p … --references` resolves each kind against its own\ncollection and names the remedy when the kind is wrong.\n\nA glyph carries a code and no name. Codes are sparse, and an undefined\none fails only at `mxbuild --target=deploy`, naming the PAGE rather\nthan the icon -- so MDL078 checks it against the font's own table.\nList them with `show glyphs`.\n\nA name may be quoted or bare; a hyphenated segment is double-quoted on\nits own: Atlas_Core.Atlas.\"align-center\".\n\nUse `linkbutton` instead of `actionbutton` for link render mode (same properties).", + Syntax: "Action: NOTHING -- deliberately no action (Forms$NoAction)\nAction: SAVE_CHANGES\nAction: SAVE_CHANGES CLOSE_PAGE -- save, then close the pop-up\nAction: CANCEL_CHANGES\nAction: CANCEL_CHANGES CLOSE_PAGE\nAction: CLOSE_PAGE\nAction: DELETE\nAction: DELETE CLOSE_PAGE\nAction: DELETE_OBJECT\nAction: NANOFLOW Module.NF\nAction: NANOFLOW Module.NF(Param: $val)\nAction: OPEN_LINK 'https://example.com'\nAction: SIGN_OUT\nAction: COMPLETE_TASK 'OutcomeName'\nAction: SHOW_PAGE Module.Page\nAction: SHOW_PAGE Module.Page(Param: $currentObject)\nAction: MICROFLOW Module.MF\nAction: MICROFLOW Module.MF(Param: $val)\nAction: CREATE_OBJECT Module.Entity THEN SHOW_PAGE Module.Page\n\nThe list above is exhaustive. Anything else in an action slot is an\nERROR (MDL-WIDGET28), including a real keyword short its argument --\n`Action: OPEN_LINK` without a URL, `Action: SHOW_PAGE` without a page.\nSuch a widget used to be written with NO action at all and rendered as a\ndead control, with check, exec and mxbuild all clean, because a\nno-action widget is legal Mendix (mendixlabs/mxcli#1062). Write NOTHING\nwhen a control really is meant to be inert.\n\nThe same forms serve `OnClick:` (an alias of `Action:`) and `OnChange:`.\n\nA microflow or nanoflow action is a CALL: it needs an argument for every\nparameter the flow declares, or Mendix rejects the page with CE1571. The\nargument list is the same on every widget that takes an action -- a\nCONTAINER (which is clickable) as much as an ACTIONBUTTON. An enclosing\ndata container of the right type supplies it without an argument; a data\ngrid's CONTROL BAR does not, because it is not row-scoped -- pass the\ngrid's selection there (`$dgOrders`).\n\nA SHOW_PAGE argument must be the enclosing widget's context object --\neither $currentObject or the name of the variable the enclosing data\nwidget is bound to. Mendix infers it from that widget, so naming any\nother variable is refused (MDL-PAGEARG01); call a microflow instead.\nOutside any data widget there is no context object to infer, so such a\nbutton takes NO argument at all -- not a page parameter, not\n$currentObject, not a literal. mxcli used to drop it in silence and\nmxbuild then reported CE1571 per parameter of the target page\n(mendixlabs/mxcli#1029). Route that navigation through a microflow.\n\nOPEN_LINK takes a static web address and stores it as a\nForms$StaticOrDynamicString. Mendix also supports a DYNAMIC address, read\nfrom an attribute at runtime; MDL cannot author that one, and DESCRIBE\nflags such a button rather than printing its address as a literal.\n\nButton styles: Default, Primary, Success, Info, Warning, Danger\n\nMendix has THREE icon elements and the keyword picks which one:\n\nIcon: 'Atlas_Core.Atlas_Filled.pencil' -- an icon collection\nIcon: image MyModule.Images.logo -- an IMAGE collection\nIcon: glyph 57377 -- a font code point\n\nThe bare form is the icon-collection icon and any collection in the\nproject works, third-party ones included. The image form points into a\ndifferent document, and is spelled the same way apart from the keyword\n-- write it without `image` and mxcli stores a custom-icon reference,\nwhich fails the build with CE1613 (mendixlabs/mxcli#1059).\n`mxcli check -p … --references` resolves each kind against its own\ncollection and names the remedy when the kind is wrong.\n\nA glyph carries a code and no name. Codes are sparse, and an undefined\none fails only at `mxbuild --target=deploy`, naming the PAGE rather\nthan the icon -- so MDL078 checks it against the font's own table.\nList them with `show glyphs`.\n\nA name may be quoted or bare; a hyphenated segment is double-quoted on\nits own: Atlas_Core.Atlas.\"align-center\".\n\nUse `linkbutton` instead of `actionbutton` for link render mode (same properties).", Example: "ACTIONBUTTON btnSave (Caption: 'Save', Action: SAVE_CHANGES, ButtonStyle: Primary)\nACTIONBUTTON btnEdit (Caption: 'Edit',\n Action: SHOW_PAGE Module.EditPage(Item: $currentObject))\nLINKBUTTON btnDelete (Caption: 'Delete', Action: DELETE,\n Icon: 'Atlas_Core.Atlas_Filled.pencil')\n\n-- A clickable CONTAINER in a data grid's control bar, calling a nanoflow\n-- with the grid's selection as its argument.\nDATAGRID dgOrders (DataSource: DATABASE FROM Sales.Order, Selection: Single) {\n COLUMN colNr (Attribute: Number, Caption: 'Order #')\n CONTROLBAR cb {\n CONTAINER cShip (Class: 'command',\n Action: NANOFLOW Sales.ACT_Ship($Order = $dgOrders)) {\n ACTIONBUTTON btnShip (Caption: 'Ship')\n }\n }\n}", SeeAlso: []string{"page.widgets"}, }) @@ -268,7 +275,7 @@ CREATE PAGE Sales.Detail (Title: 'Detail', Layout: Atlas_Core.Atlas_Default) { "popup width", "popup height", "popup resizable", "drop template", "insert template", "list view template", }, - Syntax: "ALTER PAGE Module.Name {\n SET property = value ON widgetName; -- widget property names: any casing\n SET Action = MICROFLOW Module.MF ON btnSave; -- any CREATE PAGE action form\n SET DataSource = $Param ON dvOrder;\n SET (prop1 = val1, prop2 = val2) ON widgetName;\n SET Title = 'New Title'; -- page-level (case-sensitive)\n SET Class = 'css-class'; -- page-level CSS class / style\n SET Style = 'css: rule';\n SET PopupWidth = 800; -- page-level pop-up dimensions\n SET PopupHeight = 480;\n SET PopupResizable = true;\n INSERT AFTER widgetName { };\n INSERT BEFORE widgetName { };\n INSERT INTO containerName { };\n DROP WIDGET name1, name2;\n DROP TEMPLATE FOR Module.Specialization IN listViewName;\n REPLACE widgetName WITH { };\n};", + Syntax: "ALTER PAGE Module.Name {\n SET property = value ON widgetName; -- widget property names: any casing\n SET Action = MICROFLOW Module.MF ON btnSave; -- any CREATE PAGE action form\n SET DataSource = $Param ON dvOrder;\n SET (prop1 = val1, prop2 = val2) ON widgetName;\n SET Title = 'New Title'; -- page-level (case-sensitive)\n SET Documentation = 'What this page is for.';\n SET Class = 'css-class'; -- page-level CSS class / style\n SET Style = 'css: rule';\n SET PopupWidth = 800; -- page-level pop-up dimensions\n SET PopupHeight = 480;\n SET PopupResizable = true;\n INSERT AFTER widgetName { };\n INSERT BEFORE widgetName { };\n INSERT INTO containerName { };\n DROP WIDGET name1, name2;\n DROP TEMPLATE FOR Module.Specialization IN listViewName;\n REPLACE widgetName WITH { };\n};", Example: "ALTER PAGE Module.EditPage {\n SET (Caption = 'Save & Close', ButtonStyle = Success) ON btnSave;\n INSERT AFTER txtName {\n TEXTBOX txtMiddleName (Label: 'Middle Name', Attribute: MiddleName)\n };\n DROP WIDGET txtUnused;\n};", SeeAlso: []string{"page.create", "page.show", "snippet.alter"}, }) diff --git a/cmd/mxcli/syntax/features_security.go b/cmd/mxcli/syntax/features_security.go index 270c885789..c0d1395a8e 100644 --- a/cmd/mxcli/syntax/features_security.go +++ b/cmd/mxcli/syntax/features_security.go @@ -131,13 +131,17 @@ func init() { Register(SyntaxFeature{ Path: "security.project-security", - Summary: "Set project security level, demo user and guest access toggles", + Summary: "Set project security level, strict mode, demo user and guest access toggles", Keywords: []string{ "project security", "security level", "prototype", - "production", "off", + "production", "off", "strict mode", "SEC005", }, - Syntax: "ALTER PROJECT SECURITY LEVEL OFF|PROTOTYPE|PRODUCTION;\nALTER PROJECT SECURITY DEMO USERS ON|OFF;", - Example: "ALTER PROJECT SECURITY LEVEL PRODUCTION;\nALTER PROJECT SECURITY DEMO USERS OFF;", + Syntax: "ALTER PROJECT SECURITY LEVEL OFF|PROTOTYPE|PRODUCTION;\n" + + "ALTER PROJECT SECURITY DEMO USERS ON|OFF;\n" + + "ALTER PROJECT SECURITY STRICT MODE ON|OFF; -- clears lint rule SEC005", + Example: "ALTER PROJECT SECURITY LEVEL PRODUCTION;\n" + + "ALTER PROJECT SECURITY DEMO USERS OFF;\n" + + "ALTER PROJECT SECURITY STRICT MODE ON;", SeeAlso: []string{"security.demo-user", "security.guest-access"}, }) diff --git a/docs-site/src/appendixes/common-mistakes.md b/docs-site/src/appendixes/common-mistakes.md index 5a46299e9b..945330d480 100644 --- a/docs-site/src/appendixes/common-mistakes.md +++ b/docs-site/src/appendixes/common-mistakes.md @@ -113,7 +113,7 @@ Mendix uses different "storage names" in BSON `$Type` fields than the "qualified When adding new types, always verify the storage name by: 1. Examining existing MPR files with the `mx` tool or SQLite browser 2. Checking the reflection data in the `reference/mendixmodellib/reflection-data/` directory -3. Looking at the parser cases in `sdk/mpr/parser_microflow.go` +3. Looking at the read cases in `mdl/backend/modelsdk/microflow_read_actions.go` ## Mendix Expression String Escaping diff --git a/docs-site/src/internals/bson-structure.md b/docs-site/src/internals/bson-structure.md index 3d819d3b3c..6389df5c39 100644 --- a/docs-site/src/internals/bson-structure.md +++ b/docs-site/src/internals/bson-structure.md @@ -282,10 +282,12 @@ There is no deduplication within a page. A page with 4 ComboBox widgets requires | File | Purpose | |------|---------| -| `sdk/mpr/writer_widgets.go` | Widget serialization to BSON | -| `sdk/mpr/writer_pages.go` | Page serialization | -| `sdk/mpr/reader_widgets.go` | Widget template extraction and cloning | -| `sdk/mpr/parser_page.go` | Page deserialization | +| `mdl/backend/modelsdk/widget_write.go` | Widget serialization to BSON | +| `mdl/backend/modelsdk/page_write.go` | Page serialization | +| `mdl/backend/modelsdk/widget_pluggable_write.go` | Pluggable widget templates | +| `mdl/backend/modelsdk/page.go` | Page deserialization | +| `modelsdk/codec/encoder.go` | Document → BSON | +| `modelsdk/codec/decoder.go` | BSON → document (polymorphic types) | | `sdk/widgets/loader.go` | Embedded template loading | | `sdk/widgets/templates/mendix-11.6/*.json` | Embedded widget templates | | `reference/mendixmodellib/reflection-data/*.json` | Type definitions | diff --git a/docs-site/src/internals/layers.md b/docs-site/src/internals/layers.md index d0fc3f01d6..b10530187a 100644 --- a/docs-site/src/internals/layers.md +++ b/docs-site/src/internals/layers.md @@ -151,20 +151,24 @@ classDiagram | Package | Purpose | |---------|---------| -| `sdk/mpr/` | MPR file format handling (~18k lines across reader, writer, parser files split by domain) | +| `modelsdk/` | The MPR engine: file format, BSON codec, canonical form | +| `mdl/backend/modelsdk/` | Backend implementation — reads and writes documents through the codec | | `sdk/domainmodel` | Entity, Attribute, Association types | | `sdk/microflows` | Microflow, Activity types (60+ types) | | `sdk/pages` | Page, Widget types (50+ types) | | `sdk/widgets` | Embedded widget templates for pluggable widgets | -The `sdk/mpr/` package is split by domain for maintainability: - -| File Pattern | Purpose | -|--------------|---------| -| `reader.go`, `reader_*.go` | Read-only MPR access, split by element type | -| `writer.go`, `writer_*.go` | Read-write MPR modification (domainmodel, microflow, security, widgets, etc.) | -| `parser.go`, `parser_*.go` | BSON parsing and deserialization (domainmodel, microflow, etc.) | -| `utils.go` | UUID generation utilities | +The engine is split across `modelsdk/`, with the per-document-type mapping in +`mdl/backend/modelsdk/`: + +| Package / pattern | Purpose | +|-------------------|---------| +| `modelsdk/mpr/` | MPR file access: reader, writer, raw units, the write choke point | +| `modelsdk/codec/` | Document ↔ BSON (`encoder.go`, `decoder.go`, type defaults, list markers) | +| `modelsdk/canon/` | Canonical form, identity transplant, write elision ([ADR-0008](../../../docs/13-decisions/0008-identity-and-idempotence.md)) | +| `modelsdk/gen/` | Vendored metamodel types | +| `mdl/backend/modelsdk/*_write.go` | Semantic model → gen → BSON, per document type | +| `mdl/backend/modelsdk/*_read.go` | BSON → gen → semantic model | ## 5. Model Layer (`model/`) diff --git a/docs-site/src/internals/packages.md b/docs-site/src/internals/packages.md index fb770404f1..78a33cdb71 100644 --- a/docs-site/src/internals/packages.md +++ b/docs-site/src/internals/packages.md @@ -169,7 +169,11 @@ github.com/mendixlabs/mxcli/ | `sdk/microflows/` | Yes | Microflow, Nanoflow, 60+ activity types | | `sdk/pages/` | Yes | Page, Layout, 50+ widget types | | `sdk/widgets/` | Yes | Widget template loading and cloning | -| `sdk/mpr/` | Yes | MPR reader, writer, BSON parser | +| `modelsdk/mpr/` | Yes | MPR file format: reader, writer, raw unit access | +| `modelsdk/codec/` | Yes | Document ↔ BSON encode/decode | +| `modelsdk/canon/` | Yes | Canonical form, identity transplant, write elision | +| `modelsdk/gen/` | Yes | Vendored metamodel types | +| `mdl/backend/modelsdk/` | No | Backend implementation: semantic model ↔ gen/BSON | | `mdl/grammar/` | Yes | ANTLR4 generated lexer/parser | | `mdl/ast/` | Yes | AST node types for all MDL statements | | `mdl/visitor/` | Yes | Parse tree to AST conversion | diff --git a/docs-site/src/internals/storage-names.md b/docs-site/src/internals/storage-names.md index f2a986a5ac..abc4722c95 100644 --- a/docs-site/src/internals/storage-names.md +++ b/docs-site/src/internals/storage-names.md @@ -55,7 +55,7 @@ When adding new types, always verify the storage name by: 1. **Examining existing MPR files** with the `mx` tool or an SQLite browser 2. **Checking the reflection data** in `reference/mendixmodellib/reflection-data/` -3. **Looking at the parser cases** in `sdk/mpr/parser_microflow.go` +3. **Looking at the read cases** in `mdl/backend/modelsdk/microflow_read_actions.go` ### Querying Reflection Data diff --git a/docs-site/src/language/alter-page.md b/docs-site/src/language/alter-page.md index 677b342610..819f3cb13a 100644 --- a/docs-site/src/language/alter-page.md +++ b/docs-site/src/language/alter-page.md @@ -51,7 +51,13 @@ ALTER PAGE Module.EditPage { ### SET -- Page-Level Properties Omit the `ON` clause to set page-level properties. These names are case-sensitive: -`Title`, `Class`, `Style`, `PopupWidth`, `PopupHeight`, `PopupResizable`. +`Title`, `Documentation`, `Class`, `Style`, `PopupWidth`, `PopupHeight`, +`PopupResizable`. + +`Documentation` is the same property the `/** … */` doc comment on `CREATE PAGE` +writes. Before it was settable here, documenting an existing page meant re-running +its create — which for a real page means re-emitting its whole widget tree. Setting +it to `''` clears it. ```sql ALTER PAGE Module.EditPage { diff --git a/docs-site/src/language/microflows.md b/docs-site/src/language/microflows.md index 5c06da27bb..8e516f9b3b 100644 --- a/docs-site/src/language/microflows.md +++ b/docs-site/src/language/microflows.md @@ -80,6 +80,64 @@ BEGIN END; ``` +### Document properties + +Four microflow properties live in the header, between the signature and `begin`: + +```sql +CREATE OR MODIFY MICROFLOW Shop.ACT_ShowOrder ($Order: Shop.Order, $Tab: String) +URL 'order/{Order}' +URL SEARCH PARAMETERS ($Tab) +EXPORT LEVEL API +DISALLOW CONCURRENT EXECUTION ERROR MESSAGE 'This order is already being processed' +BEGIN + -- ... +END; +``` + +| Clause | Sets | Clear with | +|---|---|---| +| `URL 'order/{Order}'` | the deep link (Mendix 10.6+) | `DROP URL` | +| `URL SEARCH PARAMETERS ($Tab)` | parameters passed as query arguments | `DROP URL` | +| `EXPORT LEVEL API` | the module's public surface on export | `EXPORT LEVEL HIDDEN` | +| `DISALLOW CONCURRENT EXECUTION ERROR MESSAGE '…'` | what a second caller gets | `ALLOW CONCURRENT EXECUTION` | + +`ERROR MICROFLOW Module.Name` is the other form of the last one. + +**An omitted clause preserves what is stored.** A rewrite that only changes the +body leaves every one of them alone — the same rule as `@excluded` and +`@applyentityaccess`. Clearing is always explicit. + +Studio Pro's **Mark as used** has no clause; it is carried across a rewrite and +cannot be set from MDL. + +#### Rules checked before the write + +These are Mendix constraints that otherwise surface only when you build: + +- Every `{Name}` must name a parameter of this microflow (**MDL-MF01**). A + segment may hold an attribute path — `{Customer/Name}` binds the **Customer** + parameter — so the leading identifier is what matches. +- A parameter used in the **path** may not also be a **search parameter** + (**MDL-MF02**, mxbuild's CE5612). The two sets are disjoint. +- `DISALLOW CONCURRENT EXECUTION` needs a message or a microflow + (**MDL-MF03**, CE4899). +- Two microflows may not share a URL (CE0570). Needs `-p`, since a script alone + cannot see the other microflows. + +#### Copying a microflow + +`DESCRIBE` emits all four clauses, so **describe → rename → exec copies them +faithfully** — that is what the clauses are for. Give the copy a different `URL`, +or the build fails with CE0570. + +`DROP` followed by `CREATE` is a new document and keeps nothing unless the script +restates it. + +An error message is translatable and MDL states one language. Rewriting the same +microflow keeps the others; a **copy** gets only the one shown, and `DESCRIBE` +says so on the line. + ## Folder Organization Place microflows and nanoflows into folders for project organization: diff --git a/docs-site/src/library/builders.md b/docs-site/src/library/builders.md index f172732207..ae083a442f 100644 --- a/docs-site/src/library/builders.md +++ b/docs-site/src/library/builders.md @@ -13,7 +13,7 @@ entity, err := modelAPI.DomainModels.CreateEntity("Customer"). WithStringAttribute("Email", 254). WithIntegerAttribute("Age"). WithBooleanAttribute("IsActive"). - WithDateTimeAttribute("CreatedDate", true). + WithDateTimeAttribute("CreatedDate"). WithDecimalAttribute("Revenue"). WithEnumerationAttribute("Status", "MyModule.CustomerStatus"). Build() @@ -30,7 +30,7 @@ entity, err := modelAPI.DomainModels.CreateEntity("Customer"). | `WithLongAttribute(name)` | Add a long attribute | | `WithDecimalAttribute(name)` | Add a decimal attribute | | `WithBooleanAttribute(name)` | Add a boolean attribute | -| `WithDateTimeAttribute(name, localize)` | Add a datetime attribute | +| `WithDateTimeAttribute(name)` | Add a datetime attribute | | `WithAutoNumberAttribute(name)` | Add an auto-number attribute | | `WithEnumerationAttribute(name, enumRef)` | Add an enumeration attribute | | `Build()` | Create the entity and write it to the project | diff --git a/docs-site/src/library/fluent-api.md b/docs-site/src/library/fluent-api.md index 32e9e87028..01256a5f28 100644 --- a/docs-site/src/library/fluent-api.md +++ b/docs-site/src/library/fluent-api.md @@ -9,21 +9,17 @@ package main import ( "github.com/mendixlabs/mxcli/api" - "github.com/mendixlabs/mxcli/sdk/mpr" ) func main() { - writer, err := mpr.OpenForWriting("/path/to/MyApp.mpr") + modelAPI, err := api.Open("/path/to/MyApp.mpr") if err != nil { panic(err) } - defer writer.Close() - - // Create the high-level API - modelAPI := api.New(writer) + defer modelAPI.Close() // Set the current module context - module, _ := modelAPI.Modules.GetModule("MyModule") + module, _ := modelAPI.Modules.Get("MyModule") modelAPI.SetModule(module) } ``` @@ -84,7 +80,7 @@ customer, err := modelAPI.DomainModels.CreateEntity("Customer"). WithStringAttribute("Email", 254). WithIntegerAttribute("Age"). WithBooleanAttribute("IsActive"). - WithDateTimeAttribute("CreatedDate", true). + WithDateTimeAttribute("CreatedDate"). Build() ``` @@ -126,19 +122,17 @@ package main import ( "github.com/mendixlabs/mxcli/api" - "github.com/mendixlabs/mxcli/sdk/mpr" ) func main() { - writer, err := mpr.OpenForWriting("/path/to/MyApp.mpr") + modelAPI, err := api.Open("/path/to/MyApp.mpr") if err != nil { panic(err) } - defer writer.Close() + defer modelAPI.Close() - modelAPI := api.New(writer) - module, _ := modelAPI.Modules.GetModule("MyModule") + module, _ := modelAPI.Modules.Get("MyModule") modelAPI.SetModule(module) // Create entity with fluent builder @@ -148,14 +142,14 @@ func main() { WithStringAttribute("Email", 254). WithIntegerAttribute("Age"). WithBooleanAttribute("IsActive"). - WithDateTimeAttribute("CreatedDate", true). + WithDateTimeAttribute("CreatedDate"). Build() // Create another entity order, _ := modelAPI.DomainModels.CreateEntity("Order"). Persistent(). WithDecimalAttribute("TotalAmount"). - WithDateTimeAttribute("OrderDate", true). + WithDateTimeAttribute("OrderDate"). Build() // Create association between entities diff --git a/docs-site/src/library/fluent-examples.md b/docs-site/src/library/fluent-examples.md index b0784980c6..016f1e2900 100644 --- a/docs-site/src/library/fluent-examples.md +++ b/docs-site/src/library/fluent-examples.md @@ -12,18 +12,16 @@ package main import ( "log" "github.com/mendixlabs/mxcli/api" - "github.com/mendixlabs/mxcli/sdk/mpr" ) func main() { - writer, err := mpr.OpenForWriting("/path/to/MyApp.mpr") + modelAPI, err := api.Open("/path/to/MyApp.mpr") if err != nil { log.Fatal(err) } - defer writer.Close() + defer modelAPI.Close() - modelAPI := api.New(writer) - module, _ := modelAPI.Modules.GetModule("Sales") + module, _ := modelAPI.Modules.Get("Sales") modelAPI.SetModule(module) // ... examples below @@ -40,14 +38,14 @@ customer, _ := modelAPI.DomainModels.CreateEntity("Customer"). WithStringAttribute("Email", 254). WithIntegerAttribute("Age"). WithBooleanAttribute("IsActive"). - WithDateTimeAttribute("CreatedDate", true). + WithDateTimeAttribute("CreatedDate"). Build() // Create another entity order, _ := modelAPI.DomainModels.CreateEntity("Order"). Persistent(). WithDecimalAttribute("TotalAmount"). - WithDateTimeAttribute("OrderDate", true). + WithDateTimeAttribute("OrderDate"). Build() // Create an enumeration @@ -94,18 +92,16 @@ package main import ( "log" "github.com/mendixlabs/mxcli/api" - "github.com/mendixlabs/mxcli/sdk/mpr" ) func main() { - writer, err := mpr.OpenForWriting("/path/to/MyApp.mpr") + modelAPI, err := api.Open("/path/to/MyApp.mpr") if err != nil { log.Fatal(err) } - defer writer.Close() + defer modelAPI.Close() - modelAPI := api.New(writer) - module, _ := modelAPI.Modules.GetModule("ProductCatalog") + module, _ := modelAPI.Modules.Get("ProductCatalog") modelAPI.SetModule(module) // Enumeration diff --git a/docs-site/src/library/model-api.md b/docs-site/src/library/model-api.md index 158718ff75..48f1465caf 100644 --- a/docs-site/src/library/model-api.md +++ b/docs-site/src/library/model-api.md @@ -7,13 +7,13 @@ The `api` package provides a high-level fluent API inspired by the Mendix Web Ex ```go import ( "github.com/mendixlabs/mxcli/api" - "github.com/mendixlabs/mxcli/sdk/mpr" ) -writer, _ := mpr.OpenForWriting("/path/to/MyApp.mpr") -defer writer.Close() - -modelAPI := api.New(writer) +modelAPI, err := api.Open("/path/to/MyApp.mpr") +if err != nil { + panic(err) +} +defer modelAPI.Close() ``` ## Setting the Module Context @@ -21,7 +21,7 @@ modelAPI := api.New(writer) Most operations require a module context. Set it before calling builders: ```go -module, _ := modelAPI.Modules.GetModule("MyModule") +module, _ := modelAPI.Modules.Get("MyModule") modelAPI.SetModule(module) ``` @@ -91,7 +91,7 @@ page, _ := modelAPI.Pages.CreatePage("CustomerOverview"). modules, _ := modelAPI.Modules.ListModules() // Get a specific module -module, _ := modelAPI.Modules.GetModule("MyModule") +module, _ := modelAPI.Modules.Get("MyModule") ``` ## MDL to Fluent API Mapping diff --git a/docs-site/src/reference/security/README.md b/docs-site/src/reference/security/README.md index 45a42e33e2..f684d9f375 100644 --- a/docs-site/src/reference/security/README.md +++ b/docs-site/src/reference/security/README.md @@ -27,6 +27,7 @@ Mendix security operates at two levels. **Module roles** define permissions with | Show security matrix | `SHOW SECURITY MATRIX [IN module]` | | Alter project security level | `ALTER PROJECT SECURITY LEVEL OFF\|PROTOTYPE\|PRODUCTION` | | Toggle demo users | `ALTER PROJECT SECURITY DEMO USERS ON\|OFF` | +| Toggle strict mode | `ALTER PROJECT SECURITY STRICT MODE ON\|OFF` | | Toggle guest access | `ALTER PROJECT SECURITY GUEST ACCESS ON [ROLE UserRole]\|OFF` | | Drop module role | `DROP MODULE ROLE module.Role` | | Drop user role | `DROP USER ROLE [IF EXISTS] Name` | diff --git a/docs-wiki/architecture/mcp-backend.md b/docs-wiki/architecture/mcp-backend.md index 7acc927f9e..a817e51746 100644 --- a/docs-wiki/architecture/mcp-backend.md +++ b/docs-wiki/architecture/mcp-backend.md @@ -28,7 +28,7 @@ server, **Concord**, fills a few gaps PED lacks (notably document deletion). The same MDL pipeline (grammar → AST → visitor → executor) runs unchanged; only the backend differs, which is exactly what the backend abstraction is for. Choosing the MCP backend means the executor's writes become `ped_*` tool calls against Studio -Pro's in-memory model rather than `sdk/mpr` writer calls — so the edits appear live +Pro's in-memory model rather than MPR writer calls — so the edits appear live in the open project instead of being serialised to the file. The hybrid split creates a **consistency problem the backend has to close itself**. diff --git a/docs-wiki/architecture/mdl-execution.md b/docs-wiki/architecture/mdl-execution.md index de80dadd44..72af27a6ec 100644 --- a/docs-wiki/architecture/mdl-execution.md +++ b/docs-wiki/architecture/mdl-execution.md @@ -22,7 +22,7 @@ The path a single MDL statement travels from text on disk to a write against a ` MDL is parsed by an [ANTLR4 grammar](../../mdl/grammar/MDLParser.g4) split into a top-level dispatch file plus per-domain grammar imports (domain model, microflow, page, security, and so on). ANTLR produces a parse tree; a [listener-based builder](../../mdl/visitor/visitor.go) walks that tree and constructs strongly-typed AST nodes. The visitor is also where raw syntax errors are caught and rewritten into human-actionable hints — reserved-keyword collisions, unescaped apostrophes, quoted GRANT attributes — so the failure a user sees explains the fix rather than ANTLR's internal token names. The full layer-by-layer design lives in [MDL_PARSER_ARCHITECTURE.md](../../docs/03-development/MDL_PARSER_ARCHITECTURE.md). -The [executor](../../mdl/executor/executor.go) dispatches each AST statement to a handler, enforcing per-statement output and wall-clock guards and tracking session state (created/dropped units, modified domain models for security reconciliation). Crucially, handlers never reach into `sdk/mpr` directly: they call through the [backend interface](../../mdl/backend/backend.go), a composition of domain-specific sub-interfaces ([DomainModelBackend](../../mdl/backend/domainmodel.go), `MicroflowBackend`, `PageBackend`, and others). Shared value types live in `mdl/types` so the [backend package](../../mdl/backend/doc.go) stays free of BSON dependencies — see [[rationale/backend-abstraction]]. This is what lets a mock backend exercise handler logic without an MPR file, and what isolates all BSON/SQLite concerns behind one boundary. +The [executor](../../mdl/executor/executor.go) dispatches each AST statement to a handler, enforcing per-statement output and wall-clock guards and tracking session state (created/dropped units, modified domain models for security reconciliation). Crucially, handlers never reach into the storage engine directly: they call through the [backend interface](../../mdl/backend/backend.go), a composition of domain-specific sub-interfaces ([DomainModelBackend](../../mdl/backend/domainmodel.go), `MicroflowBackend`, `PageBackend`, and others). Shared value types live in `mdl/types` so the [backend package](../../mdl/backend/doc.go) stays free of BSON dependencies — see [[rationale/backend-abstraction]]. This is what lets a mock backend exercise handler logic without an MPR file, and what isolates all BSON/SQLite concerns behind one boundary. Common failure modes track the seams: parse errors at the grammar/visitor boundary, nil parse-tree nodes when the visitor reads partial trees (see [[bug-patterns/visitor-wiring-gaps]]), and storage-name or serialization faults below the backend in [[architecture/mpr-read-write]]. @@ -30,7 +30,7 @@ Common failure modes track the seams: parse errors at the grammar/visitor bounda - [docs/03-development/MDL_PARSER_ARCHITECTURE.md](../../docs/03-development/MDL_PARSER_ARCHITECTURE.md) — full layer-by-layer parser design - [mdl/backend/](../../mdl/backend/) — backend interface and per-domain sub-interfaces -- [[rationale/backend-abstraction]] — why the executor never imports `sdk/mpr` +- [[rationale/backend-abstraction]] — why the executor never reaches past `ctx.Backend` - [[rationale/mdl-as-sql]] — why MDL is SQL-shaped in the first place - [[architecture/mpr-read-write]] — what the backend writes into - [[bug-patterns/visitor-wiring-gaps]] — failures at the visitor seam diff --git a/docs-wiki/architecture/mpr-read-write.md b/docs-wiki/architecture/mpr-read-write.md index c76726b8e2..cf92f489ef 100644 --- a/docs-wiki/architecture/mpr-read-write.md +++ b/docs-wiki/architecture/mpr-read-write.md @@ -3,34 +3,34 @@ title: MPR Read/Write category: architecture last-synced: 9ab9afa6 sources: - - sdk/mpr/reader.go - - sdk/mpr/writer_core.go - - sdk/mpr/writer_units.go - - sdk/mpr/parser.go - modelsdk.go + - modelsdk/mpr/reader.go + - modelsdk/codec/decoder.go - modelsdk/mpr/writer_core.go - modelsdk/canon/canon.go - modelsdk/canon/identity.go - docs/13-decisions/0008-identity-and-idempotence.md --- -> **Do not duplicate**: the public API surface (see `README.md` and `modelsdk.go`), specific BSON field tables (see `docs/03-development/PAGE_BSON_SERIALIZATION.md`), the write-safety rules an implementer must follow (CLAUDE.md is canonical), the decision behind conditional writes (see [ADR-0008](../../docs/13-decisions/0008-identity-and-idempotence.md)), or fix recipes (see `.claude/skills/fix-issue.md`). +> **Do not duplicate**: the public API surface (see `README.md` and `modelsdk.go`), specific BSON field tables (see `docs/03-development/PAGE_BSON_SERIALIZATION.md`), the write-safety rules an implementer must follow (CLAUDE.md is canonical), the decision behind conditional writes (see [ADR-0008](../../docs/13-decisions/0008-identity-and-idempotence.md)), or fix recipes (see `.claude/skills/fix-issue.md` and `/mxcli-dev:fix-issue`). ## What this is -The layer that turns a `.mpr` file on disk into typed Go model elements and back. An `.mpr` is a SQLite database whose document rows hold BSON-encoded Mendix model elements, so reading and writing is two problems stacked: SQLite access, and BSON (de)serialization of polymorphic Mendix types. This layer owns both — twice, in fact, since two storage engines implement it side by side. +The layer that turns a `.mpr` file on disk into typed Go model elements and back. An `.mpr` is a SQLite database whose document rows hold BSON-encoded Mendix model elements, so reading and writing is two problems stacked: SQLite access, and BSON (de)serialization of polymorphic Mendix types. This layer owns both. ## How it fits -[`modelsdk.Open`](../../modelsdk.go) returns a read-only [`Reader`](../../sdk/mpr/reader.go); `OpenForWriting` wraps it in a [`Writer`](../../sdk/mpr/writer_core.go). That nesting is deliberate — a writer *is* a reader plus mutation methods, because every safe write first reads the current state. The reader opens SQLite via the pure-Go `modernc.org/sqlite` driver (no CGO), pins a single connection to dodge lock contention, and detects the storage format. +[`modelsdk.Open`](../../modelsdk.go) returns a read-only [`Reader`](../../modelsdk/mpr/reader.go); `OpenForWriting` wraps it in a [`Writer`](../../modelsdk/mpr/writer_core.go). That nesting is deliberate — a writer *is* a reader plus mutation methods, because every safe write first reads the current state. The reader opens SQLite via the pure-Go `modernc.org/sqlite` driver (no CGO), pins a single connection to dodge lock contention, and detects the storage format. Format detection is automatic and defensive. v1 is a single-file database; v2 (Mendix 10.18+) splits metadata from per-document `.mxunit` files under `mprcontents/`. The reader first checks for the folder, then reconciles against the actual DB schema — a `.mpr` copied without its `mprcontents/` folder would otherwise take the v1 path and fail on a `Contents` column that v2 schemas do not have. -The reason this layer is BSON-aware rather than a generic SQLite patcher is that Mendix's BSON is irregular: IDs appear as binary blobs, base64 maps, or `$ID` fields; arrays carry a leading marker (`1`, `2` or `3`) that distinguishes by-name collections from contained ones; and `$Type` discriminators select polymorphic structs. The [parser](../../sdk/mpr/parser.go) encodes these conventions exactly, because Studio Pro rejects any deviation — a wrong storage name, a malformed empty-array marker, or a numeric width mismatch all surface as load-time exceptions. See [[models/storage-vs-qualified-names]] and [[bug-patterns/bson-numeric-width]]. +The reason this layer is BSON-aware rather than a generic SQLite patcher is that Mendix's BSON is irregular: IDs appear as binary blobs, base64 maps, or `$ID` fields; arrays carry a leading marker (`1`, `2` or `3`) that distinguishes by-name collections from contained ones; and `$Type` discriminators select polymorphic structs. The [codec](../../modelsdk/codec/decoder.go) encodes these conventions exactly, because Studio Pro rejects any deviation — a wrong storage name, a malformed empty-array marker, or a numeric width mismatch all surface as load-time exceptions. See [[models/storage-vs-qualified-names]] and [[bug-patterns/bson-numeric-width]]. -**There are two engines, and each funnels every write through one function.** The legacy `sdk/mpr` writer and the default `modelsdk/mpr` writer both expose `UpdateRawUnit` over a private `updateUnit`, and that single choke point is what makes a cross-cutting write policy possible at all: a rule added there applies to every document type without touching a single serializer. The engines are not otherwise symmetric — `modelsdk/mpr` stages v2 file writes through a temp file and rename (so a hard-linked fixture is not clobbered through its shared inode) and bumps the `_Transaction` row Studio Pro watches for external changes, while the legacy writer overwrites in place. Its `WriteTransaction` type, which does stage temp files, currently has no callers; the modelsdk equivalent is the one `codec.Store` uses to flush units in a batch. +**Every write funnels through one function.** `modelsdk/mpr` exposes `UpdateRawUnit` over a private `updateUnit`, and that single choke point is what makes a cross-cutting write policy possible at all: a rule added there applies to every document type without touching a single serializer. `WriteTransaction.WriteUnit` is the second door onto the same policy — it is how `codec.Store` flushes units in a batch. The writer stages v2 file writes through a temp file and rename, so a hard-linked fixture is not clobbered through its shared inode, and bumps the `_Transaction` row Studio Pro watches for external changes. -**A write is conditional.** Since [ADR-0008](../../docs/13-decisions/0008-identity-and-idempotence.md), neither engine writes a unit whose new content is *semantically* equal to what is stored. The comparison cannot be on bytes: a rebuild mints a fresh random `$ID` for every sub-element, so the bytes always differ and byte comparison would skip nothing. [`modelsdk/canon`](../../modelsdk/canon/canon.go) instead compares a canonical form in which each element `$ID` is replaced by its position in a containment walk — and because the set of element IDs comes from that same walk, any occurrence of one of them anywhere in the document is a reference by definition, so the comparison needs no knowledge of *which* properties hold references. That is what makes it implementable today, and why a new document type is covered without registering anything. +There used to be two engines here. The hand-written `sdk/mpr` serializer was deleted once its importer count reached zero ([ADR-0004](../../docs/13-decisions/0004-full-codec-engine.md)), so reaching past the backend abstraction is now a compile error rather than a rule to remember. + +**A write is conditional.** Since [ADR-0008](../../docs/13-decisions/0008-identity-and-idempotence.md), storage does not write a unit whose new content is *semantically* equal to what is stored. The comparison cannot be on bytes: a rebuild mints a fresh random `$ID` for every sub-element, so the bytes always differ and byte comparison would skip nothing. [`modelsdk/canon`](../../modelsdk/canon/canon.go) instead compares a canonical form in which each element `$ID` is replaced by its position in a containment walk — and because the set of element IDs comes from that same walk, any occurrence of one of them anywhere in the document is a reference by definition, so the comparison needs no knowledge of *which* properties hold references. That is what makes it implementable today, and why a new document type is covered without registering anything. The consequence worth internalising is that **not writing is the safe outcome, not merely the cheap one**. Canonical equality means the two documents disagree only about which IDs they picked, and the stored ones are the IDs every pointer inside that unit already agrees with. Rewriting them is how a reverted attempt made projects unopenable. See [[models/element-identity]] for why a unit's IDs are private to it, and what has to be carried across a rebuild rather than re-minted. diff --git a/docs-wiki/architecture/widget-engine.md b/docs-wiki/architecture/widget-engine.md index 43a7360821..648dc75a41 100644 --- a/docs-wiki/architecture/widget-engine.md +++ b/docs-wiki/architecture/widget-engine.md @@ -6,7 +6,7 @@ sources: - sdk/widgets/definitions/loader.go - sdk/widgets/definitions/combobox.def.json - sdk/widgets/templates/README.md - - sdk/mpr/writer_widgets.go + - mdl/backend/modelsdk/widget_write.go - mdl/executor/cmd_pages_builder_v3_widgets.go - docs/03-development/PAGE_BSON_SERIALIZATION.md - docs/03-development/WIDGET_BSON_VERSION_COMPATIBILITY.md @@ -16,7 +16,7 @@ sources: ## What this is -The machinery that turns a one-line MDL widget statement (e.g. `COMBOBOX myCombo (...)`) into the BSON a Mendix pluggable widget requires. Built-in page widgets are serialized directly by [`writer_widgets.go`](../../sdk/mpr/writer_widgets.go); pluggable widgets (ComboBox, DataGrid2, Gallery, filters) are far harder, because their BSON is a self-referential `type`/`object` pair that Studio Pro validates strictly. The widget engine exists to assemble that pair declaratively instead of by hand. +The machinery that turns a one-line MDL widget statement (e.g. `COMBOBOX myCombo (...)`) into the BSON a Mendix pluggable widget requires. Built-in page widgets are serialized directly by [`widget_write.go`](../../mdl/backend/modelsdk/widget_write.go); pluggable widgets (ComboBox, DataGrid2, Gallery, filters) are far harder, because their BSON is a self-referential `type`/`object` pair that Studio Pro validates strictly. The widget engine exists to assemble that pair declaratively instead of by hand. ## How it fits diff --git a/docs-wiki/bug-patterns/bson-numeric-width.md b/docs-wiki/bug-patterns/bson-numeric-width.md index 20289310ea..fcea1ea1e6 100644 --- a/docs-wiki/bug-patterns/bson-numeric-width.md +++ b/docs-wiki/bug-patterns/bson-numeric-width.md @@ -4,10 +4,10 @@ category: bug-pattern last-synced: 4e185f73 sources: - .claude/skills/fix-issue/findings/ - - sdk/mpr/parser.go + - modelsdk/mpr/parser.go --- -> **Do not duplicate**: the per-field fix recipes live in the `fix-issue/findings/*.jsonl` records (issues #583, #585) and the `extractInt` helper signature lives in `sdk/mpr/parser.go`. This page describes the pattern only. +> **Do not duplicate**: the per-field fix recipes live in the `fix-issue/findings/*.jsonl` records (issues #583, #585) and the `extractInt` helper signature lives in `modelsdk/mpr/parser.go`. This page describes the pattern only. ## What this is @@ -19,7 +19,7 @@ Studio Pro writes integer properties at whatever width it chooses — often `int The tell-tale: a numeric field reads `0`/`unlimited` while Studio Pro shows a non-zero value, and the field's read path contains a narrow `.(int32)` (or `.(int64)`) assertion. The class recurs because every new numeric field invites a fresh hand-written assertion, and the failure is silent rather than loud. -The canonical fix is the width-agnostic `extractInt` helper in [`sdk/mpr/parser.go`](../../sdk/mpr/parser.go), which accepts `int32`/`int64`/`int`/`float64`. The per-field recipe — including how to sweep for stray assertions and preserve non-zero defaults for absent fields — is in the symptom table. +The canonical fix is the width-agnostic `extractInt` helper in [`modelsdk/mpr/parser.go`](../../modelsdk/mpr/parser.go), which accepts `int32`/`int64`/`int`/`float64`. The per-field recipe — including how to sweep for stray assertions and preserve non-zero defaults for absent fields — is in the symptom table. ## See also diff --git a/docs-wiki/bug-patterns/widget-type-object-drift.md b/docs-wiki/bug-patterns/widget-type-object-drift.md index 94b54b2c5c..518e3fb543 100644 --- a/docs-wiki/bug-patterns/widget-type-object-drift.md +++ b/docs-wiki/bug-patterns/widget-type-object-drift.md @@ -10,7 +10,7 @@ sources: - .claude/skills/diagnose-ce0463.md - .claude/skills/debug-bson.md - sdk/widgets/templates/README.md - - sdk/mpr/writer_widgets.go + - mdl/backend/modelsdk/widget_write.go --- > **Do not duplicate**: the elimination order, the two controls and the diff --git a/docs-wiki/glossary.md b/docs-wiki/glossary.md index a8541ca29e..61c0dbc496 100644 --- a/docs-wiki/glossary.md +++ b/docs-wiki/glossary.md @@ -4,7 +4,7 @@ category: glossary last-synced: 4e185f73 sources: - CLAUDE.md - - sdk/mpr/parser_microflow.go + - mdl/backend/modelsdk/microflow_read_actions.go - README.md --- diff --git a/docs-wiki/models/association-pointers.md b/docs-wiki/models/association-pointers.md index ed1a34ebb4..14a60fcbb1 100644 --- a/docs-wiki/models/association-pointers.md +++ b/docs-wiki/models/association-pointers.md @@ -4,7 +4,7 @@ category: mental-model last-synced: 4e185f73 sources: - CLAUDE.md - - sdk/mpr/writer_domainmodel.go + - mdl/backend/modelsdk/domainmodel_write.go - sdk/domainmodel/domainmodel.go --- @@ -25,6 +25,6 @@ Getting the inversion wrong silently produces structurally valid BSON that fails ## See also - [../../CLAUDE.md](../../CLAUDE.md) — canonical pointer/keyword mapping table ("Association Parent/Child Pointer Semantics") -- [../../sdk/mpr/writer_domainmodel.go](../../sdk/mpr/writer_domainmodel.go) — `serializeAssociation` writes `ParentPointer`/`ChildPointer` +- [../../mdl/backend/modelsdk/domainmodel_write.go](../../mdl/backend/modelsdk/domainmodel_write.go) — writes `ParentPointer`/`ChildPointer` - [../../sdk/domainmodel/domainmodel.go](../../sdk/domainmodel/domainmodel.go) — `Association.ParentID`/`ChildID` and `MemberAccess` - [[models/storage-vs-qualified-names]] — the other place BSON naming surprises you diff --git a/docs-wiki/models/storage-vs-qualified-names.md b/docs-wiki/models/storage-vs-qualified-names.md index c96a4be6aa..a9c3a08a67 100644 --- a/docs-wiki/models/storage-vs-qualified-names.md +++ b/docs-wiki/models/storage-vs-qualified-names.md @@ -4,7 +4,7 @@ category: mental-model last-synced: 4e185f73 sources: - CLAUDE.md - - sdk/mpr/parser_microflow.go + - mdl/backend/modelsdk/microflow_read_actions.go --- > **Do not duplicate**: the full storage-name mapping table (CLAUDE.md is canonical), the TypeCacheUnknownTypeException fix recipe (symptom table), or reflection-data structure (read the JSON). @@ -26,6 +26,6 @@ When adding a new type, never assume the SDK name is the storage name. Verify ag ## See also - [../../CLAUDE.md](../../CLAUDE.md) — canonical qualified-name → storage-name table ("BSON Storage Names vs Qualified Names") -- [../../sdk/mpr/parser_microflow.go](../../sdk/mpr/parser_microflow.go) — `microflowActionParsers` registers both names per handler +- [../../mdl/backend/modelsdk/microflow_read_actions.go](../../mdl/backend/modelsdk/microflow_read_actions.go) — `actionFromGen` dispatches on both names per handler - [[models/association-pointers]] — another counter-intuitive BSON naming invariant - [[bug-patterns/widget-type-object-drift]] — a related "looks valid, fails on open" failure mode diff --git a/docs-wiki/rationale/backend-abstraction.md b/docs-wiki/rationale/backend-abstraction.md index 0faf073ce6..da60294598 100644 --- a/docs-wiki/rationale/backend-abstraction.md +++ b/docs-wiki/rationale/backend-abstraction.md @@ -14,7 +14,7 @@ sources: ## What this is -The MDL executor never imports `sdk/mpr` for write paths. All storage operations go through domain-grouped interfaces in `mdl/backend/` (`ctx.Backend.*`), with concrete implementations in sibling packages — `mdl/backend/mpr/` for production and `mdl/backend/mock/` for tests. Shared value types live in `mdl/types/` so the interface package depends on no concrete storage at all. +The MDL executor never reaches past `ctx.Backend` for write paths. All storage operations go through domain-grouped interfaces in `mdl/backend/` (`ctx.Backend.*`), with concrete implementations in sibling packages — `mdl/backend/mpr/` for production and `mdl/backend/mock/` for tests. Shared value types live in `mdl/types/` so the interface package depends on no concrete storage at all. ## How it fits @@ -22,7 +22,7 @@ The forcing problem was that the executor was the wrong layer to know about BSON The chosen approach is a thin seam. The executor's job is "given an MDL statement, perform the operation"; BSON is one possible serialization, not the operation itself. Each domain (DomainModel, Microflow, Page, Workflow, ...) gets its own small interface, and `FullBackend` composes them only as a construction-time constraint — handlers receive just the sub-interface they need. Mock stubs return a loud `"MockBackend.X not configured"` error by default rather than `nil, nil`, because a silent test pass is a worse failure than a noisy one. -The key trade-off is **per-feature overhead**: every new operation needs four touches (interface method, MPR implementation, mock stub, compile-time check) and adds indirection. That is accepted to quarantine BSON drift bugs to the packages whose maintainers understand BSON. The boundary is enforced by convention and PR review, not Go visibility — `sdk/mpr` stays importable, so the wrong instinct is the easy one. See [ADR-0002](../../docs/13-decisions/0002-backend-abstraction.md) for the full alternatives and consequences. +The key trade-off is **per-feature overhead**: every new operation needs four touches (interface method, MPR implementation, mock stub, compile-time check) and adds indirection. That is accepted to quarantine BSON drift bugs to the packages whose maintainers understand BSON. The boundary was once enforced by convention and PR review alone. It is now structural: `sdk/mpr` was deleted, so reaching past the abstraction is a compile error rather than a habit to resist. See [ADR-0002](../../docs/13-decisions/0002-backend-abstraction.md) for the full alternatives and consequences. ## See also diff --git a/docs/01-project/ARCHITECTURE.md b/docs/01-project/ARCHITECTURE.md index 9f8105c8ce..3c035687ba 100644 --- a/docs/01-project/ARCHITECTURE.md +++ b/docs/01-project/ARCHITECTURE.md @@ -266,11 +266,11 @@ sequenceDiagram | `mdl/grammar` | ANTLR4 lexer/parser (generated from MDLLexer.g4 + MDLParser.g4) | | `mdl/ast` | AST node types for MDL statements | | `mdl/visitor` | ANTLR listener that builds AST from parse tree | -| `mdl/executor` | Thin orchestrator: parses AST, calls `ctx.Backend.*`, formats output. Handles microflows, nanoflows, pages, workflows, domain models, security, and all other MDL document types. **No `sdk/mpr` imports.** | +| `mdl/executor` | Thin orchestrator: parses AST, calls `ctx.Backend.*`, formats output. Handles microflows, nanoflows, pages, workflows, domain models, security, and all other MDL document types. **No storage-engine imports.** | | `mdl/backend` | Domain-specific backend interfaces (`FullBackend`, `PageMutator`, `WorkflowMutator`, `BackendFactory`) | | `mdl/backend/mpr` | MPR-backed implementation of all backend interfaces; owns all BSON mutation logic | | `mdl/backend/mock` | `MockBackend` with Func-field injection for unit testing without a `.mpr` file | -| `mdl/types` | Shared domain types (`NavigationDocument`, `JavaAction`, `JsonStructure`, EDMX/AsyncAPI parsers, ID utilities) — no `sdk/mpr` dependency | +| `mdl/types` | Shared domain types (`NavigationDocument`, `JavaAction`, `JsonStructure`, EDMX/AsyncAPI parsers, ID utilities) — no storage-engine dependency | | `mdl/bsonutil` | CGO-free BSON ID utilities (`IDToBsonBinary`, `BsonBinaryToID`, `NewIDBsonBinary`) | | `mdl/catalog` | SQLite-based catalog for querying project metadata (entities, microflows, references, permissions, source code) | | `mdl/linter` | Extensible linting framework with built-in rules and Starlark scripting support; includes report generation | @@ -369,20 +369,24 @@ classDiagram | Package | Purpose | |---------|---------| -| `sdk/mpr/` | MPR file format handling (~18k lines across reader, writer, parser files split by domain) | +| `modelsdk/` | The MPR engine: file format, BSON codec, canonical form | +| `mdl/backend/modelsdk/` | Backend implementation — semantic model ↔ gen/BSON, per document type | | `sdk/domainmodel` | Entity, Attribute, Association types | | `sdk/microflows` | Microflow, Activity types (60+ types) | | `sdk/pages` | Page, Widget types (50+ types) | | `sdk/widgets` | Embedded widget templates for pluggable widgets (ComboBox, DataGrid2, Gallery, etc.) | -The `sdk/mpr/` package is split by domain for maintainability: +The engine is split across `modelsdk/`, with the per-document-type mapping in +`mdl/backend/modelsdk/`: -| File Pattern | Purpose | -|--------------|---------| -| `reader.go`, `reader_*.go` | Read-only MPR access, split by element type (documents, widgets, etc.) | -| `writer.go`, `writer_*.go` | Read-write MPR modification (domainmodel, microflow, security, widgets, etc.) | -| `parser.go`, `parser_*.go` | BSON parsing and deserialization (domainmodel, microflow, etc.) | -| `utils.go` | UUID generation utilities | +| Package / pattern | Purpose | +|-------------------|---------| +| `modelsdk/mpr/` | MPR file access: reader, writer, raw units, the single write choke point | +| `modelsdk/codec/` | Document ↔ BSON: `encoder.go`, `decoder.go`, type defaults, list markers | +| `modelsdk/canon/` | Canonical form, identity transplant, write elision (ADR-0008) | +| `modelsdk/gen/` | Vendored metamodel types | +| `mdl/backend/modelsdk/*_write.go` | Semantic model → gen → BSON, per document type | +| `mdl/backend/modelsdk/*_read.go` | BSON → gen → semantic model | ### 5. Model Layer (`model/`) @@ -893,7 +897,7 @@ Key files: `sdk/widgets/augment.go` (augmentation logic), `sdk/widgets/mpk/mpk.g ### 11. Backend Abstraction + Dependency Inversion -The executor **never imports `sdk/mpr`**. All project access goes through `ctx.Backend`, which implements `backend.FullBackend`. This enables: +The executor **never reaches past `ctx.Backend`**. All project access goes through `ctx.Backend`, which implements `backend.FullBackend`. This enables: - Unit tests without a `.mpr` file (inject `MockBackend`) - Alternative storage backends (cloud, in-memory, etc.) - Isolated BSON mutation logic in `mdl/backend/mpr/` @@ -930,7 +934,7 @@ flowchart LR 2. Implement it in `mdl/backend/mpr/` operating on BSON/reader/writer 3. Add a `Func`-field stub in `mdl/backend/mock/` 4. Call `ctx.Backend.YourMethod()` from the executor handler -5. Never call `sdk/mpr` types directly from the executor +5. Never call storage-engine types (`modelsdk/mpr`, `modelsdk/codec`, `modelsdk/gen`) directly from the executor **Mutation pattern (ALTER PAGE / ALTER WORKFLOW):** @@ -943,7 +947,7 @@ The mutator owns the document's lifecycle; the executor only describes *what* to **Shared types (`mdl/types/`):** -Types used by both `mdl/` and `sdk/mpr` live in `mdl/types/`. The `sdk/mpr` package re-exports them as type aliases (`type JavaAction = types.JavaAction`) for backward compatibility. New shared types go in `mdl/types/`, not in `sdk/mpr/reader_types.go`. +Types used by more than one layer live in `mdl/types/`, and the others alias them (`type JavaAction = types.JavaAction`). New shared types go in `mdl/types/` — never as a duplicate definition in a storage package. `modelsdk/mpr/version.ProjectVersion` is the cautionary case: it *duplicates* `types.ProjectVersion` instead of aliasing it, so the two are unrelated Go types printing under the same name. ## Future Architecture Considerations diff --git a/docs/01-project/MDL_FEATURE_MATRIX.md b/docs/01-project/MDL_FEATURE_MATRIX.md index ecb0b0d6b1..e1e3bc5f3e 100644 --- a/docs/01-project/MDL_FEATURE_MATRIX.md +++ b/docs/01-project/MDL_FEATURE_MATRIX.md @@ -97,7 +97,7 @@ linked from the notes — this matrix never restates it: - **MCP** per-feature shapes, gaps, and Studio-Pro-version surface: [`../03-development/PED_MCP_CAPABILITIES.md`](../03-development/PED_MCP_CAPABILITIES.md). - **MPR** is the reference backend the executor was built on; it realizes everything MDL - expresses (read `sdk/mpr/` and `mdl/backend/mpr/` for specifics). + expresses (read `modelsdk/` and `mdl/backend/modelsdk/` for specifics). `Mendix` and `MDL` are `Y` for every row below *by construction* (a row exists only because MDL expresses a Mendix feature); they are kept as columns to make the stack explicit. The diff --git a/docs/01-project/MDL_QUICK_REFERENCE.md b/docs/01-project/MDL_QUICK_REFERENCE.md index 547f77bad3..95eed95b80 100644 --- a/docs/01-project/MDL_QUICK_REFERENCE.md +++ b/docs/01-project/MDL_QUICK_REFERENCE.md @@ -549,6 +549,9 @@ it is for pages. | Log | `log info\|warning\|error [node 'name'] 'message';` | | | Apply entity access | `@applyentityaccess` / `@applyentityaccess(false)` before `create microflow` or `create rule` | Runs the flow under the **current user's** entity access rules instead of with full access. A **security** setting and only ever narrowing, so an ABSENT annotation **preserves** what is stored rather than clearing it — the same rule as `@excluded`. Not available on a nanoflow: it runs in the client and Mendix stores no such property | | Position | `@position(x, y)` | Canvas position (before activity) | +| Deep-link URL | `url 'item/{Key}'` / `url search parameters ($Filter)` / `drop url` | Header clauses on `create microflow`, Mendix **10.6+**. Every `{Name}` must name a parameter (**MDL-MF01**), and a parameter used in the PATH may **not** also be a search parameter (**MDL-MF02** / CE5612) — the two sets are disjoint. With a project, a URL another microflow already owns is refused (CE0570). An OMITTED clause **preserves** what is stored | +| Export level | `export level api` / `export level hidden` | Header clause. Whether the microflow is part of the module's public surface when the module is exported. Keywords, not a quoted string: both `ExportLevel` enums have exactly two members and `'Public'` is neither. Omitted **preserves** | +| Concurrent execution | `disallow concurrent execution error message 'text'` / `… error microflow Mod.Name` / `allow concurrent execution` | Header clause. Mendix **requires** a handler when disallowing (**MDL-MF03** / CE4899). `allow` sets the flag and **leaves** a stored message — Studio Pro greys those fields rather than erasing them, and `canon.CarryTranslations` would restore it anyway. Omitted **preserves** | | Unknown annotation | — | **MDL059**. An annotation that parses and does nothing loses whatever it was meant to express, so a name the target does not read is refused — on a statement *and* before a `create`. Covers a typo (`@applyentityacces`), an annotation on a document kind that reads none (`@excluded` on a queue), and an activity annotation written at document level. The message names what that document does accept | | Parameter position | `@position(x, y)` before a parameter, **inside** the `( … )` list | The only annotation a parameter takes. Omit it and parameters form a row at 200;53, 300;53, …; a parameter off that row is treated as hand-placed, survives a rewrite, and is emitted by DESCRIBE (#993) | | Start event | `@start(x, y)` | Canvas position of the start, on the **first** statement. Omit it and the start is placed one spacing unit left of the first activity and MOVES with it on a rewrite; a start that is not at that derived spot is treated as hand-placed, survives a rewrite, and is emitted by DESCRIBE (#951) | @@ -1384,6 +1387,7 @@ MDL uses explicit property declarations for pages: | Inspect a widget | `describe widget ;` | `describe widget combobox;` — properties, enum values, defaults and the editor rules that HIDE properties under some configurations. **Body containers** names what the widget's body takes, and for an object list the widgets-typed slots *inside one item* plus the widget types that route into each — that is where `column … { textfilter }` is spelled out. Works with no project open; with one, reads the installed `.mpk` (version-accurate, and the only place a Marketplace widget appears). Same output as `mxcli widget describe` | | Widget name | Required after type | `textbox txtName (...)` | | Attribute binding | `attribute: AttrName` | `textbox txt (label: 'Name', attribute: Name)` | +| Attribute over an association | `attribute: Assoc/Attr` (bare association name, multi-hop OK) | `textbox txt (label: 'Rule', attribute: RuleAction_BusinessRule/Name)` — works on textbox, textarea, datepicker, dropdown, checkbox and radiobuttons, the same as on a data grid column | | Variable binding | `datasource: $Var` | `dataview dv (datasource: $Product) { ... }` | | Action binding | `action: type` | `actionbutton btn (caption: 'Save', action: save_changes)` — the forms are a closed set (`mxcli syntax page.action`); anything else is **MDL-WIDGET28** | | No action | `action: nothing` | `actionbutton btn (caption: 'Decorative', action: nothing)` — an explicitly inert control. Write it deliberately: an action keyword **short its argument** (`action: open_link` with no URL) is now an error rather than a widget silently written with no action at all | @@ -1498,6 +1502,8 @@ create page MyModule.Customer_Edit | Visible | `textbox txt (visible: [IsActive])` | Conditional visibility (XPath expression) | | Editable | `textbox txt (editable: [status != 'Closed'])` | Conditional editability (XPath expression) | | Image | `staticimage img (Image: 'Mod.Images.logo')` | Image-collection entry, `Module.Collection.Image`. Omitted → CE0436 "No image selected." | +| DataSource (dynamicimage) | `dynamicimage img (DataSource: database from Mod.Photo)` | The entity holding the image. Omitted → CE0489 "Select an entity for the data source of this dynamic image." | +| DefaultImage | `dynamicimage img (DefaultImage: 'Mod.Images.placeholder')` | Fallback when the object has no image | **Supported Widgets:** - Layout: `layoutgrid`, `row`, `column`, `container`, `customcontainer` @@ -1582,6 +1588,7 @@ Modify an existing page or snippet's widget tree in-place without full `create o | Set property | `set caption = 'New' on widgetName` | Single property on a widget | | Set multiple | `set (caption = 'Save', buttonstyle = success) on btn` | Multiple properties at once | | Page-level set | `set Title = 'New title'` | No ON clause; page-level names are case-sensitive | +| Documentation | `set Documentation = 'What this page is for.'` | Page-level. Same property the `/** … */` doc comment on `CREATE PAGE` writes, so an existing page can be documented without restating it. `''` clears it | | Pop-up dimensions | `set PopupWidth = 800` / `set PopupHeight = 480` / `set PopupResizable = true` | Page-level; apply when the page opens in a pop-up | | Page CSS class / style | `set Class = 'css-class'` / `set Style = 'css: rule'` | Page-level (no ON clause); sets the page's Appearance | | Widget dynamic classes | `set DynamicClasses = 'expr' on widgetName` | Runtime-computed classes on a widget — the surgical alternative to a bulk `update widgets` | @@ -1599,7 +1606,7 @@ Modify an existing page or snippet's widget tree in-place without full `create o | Set layout | `set layout = Module.LayoutName` | Change page layout, auto-maps placeholders | | Set layout + map | `set layout = Module.Layout map (Old as New)` | Explicit placeholder mapping | -**Supported SET properties:** Caption, Label, ButtonStyle, Class, Style, DynamicClasses, Editable, Visible, Name, Title (page-level), Layout (page-level), PopupWidth / PopupHeight / PopupResizable (page-level), and quoted pluggable widget properties. +**Supported SET properties:** Caption, Label, ButtonStyle, Class, Style, DynamicClasses, Editable, Visible, Name, Title (page-level), Documentation (page-level), Layout (page-level), PopupWidth / PopupHeight / PopupResizable (page-level), and quoted pluggable widget properties. **Example:** ```sql diff --git a/docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md b/docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md index a0d5f23a90..de0f7debac 100644 --- a/docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md +++ b/docs/03-development/LEGACY_ENGINE_KNOWN_ISSUES.md @@ -1,6 +1,14 @@ # Legacy Engine — Known Issues -**Status:** tracking list (living document) +> **Superseded — historical record.** The `sdk/mpr` engine this tracks was deleted +> ([ADR-0004](../13-decisions/0004-full-codec-engine.md), +> `docs/plans/2026-09-14-retire-legacy-engine.md`); `--engine` / `MXCLI_ENGINE` +> survive only as a warning-only no-op. Every `sdk/mpr/...` path below names code +> that is gone. Kept because it records what that engine got wrong, which is still +> useful when a construct looks suspicious — but nothing here is actionable, and +> nothing should be added to it. + +**Status:** superseded (kept as a record) **Related:** [ADR-0004: Route all document types through the codec engine](../13-decisions/0004-full-codec-engine.md), [ADR-0002: Backend abstraction](../13-decisions/0002-backend-abstraction.md), [`MODELSDK_ENGINE_ARCHITECTURE.md`](MODELSDK_ENGINE_ARCHITECTURE.md) ## Purpose diff --git a/docs/03-development/MODELSDK_ENGINE_ARCHITECTURE.md b/docs/03-development/MODELSDK_ENGINE_ARCHITECTURE.md index 1868bb21ca..1fd994ce9e 100644 --- a/docs/03-development/MODELSDK_ENGINE_ARCHITECTURE.md +++ b/docs/03-development/MODELSDK_ENGINE_ARCHITECTURE.md @@ -79,19 +79,33 @@ and break the MCP backend + the future format (ADR-0005). See `docs/11-proposals ## Recipe: add a document type or activity group -1. **Find the legacy serializer** for the type (`sdk/mpr/writer_*.go`) — the field set + ordering spec. -2. **Capture real BSON when unsure** — legacy can be wrong (e.g. the index `SortOrder` bug). Dump an - on-disk `.mxunit` or use the MCP/PED probe (`cmd/mcpprobe`) to get authoritative keys/markers. +1. **Get a Studio Pro-authored reference document** of the type — from a Marketplace module that uses + it, or by asking for one to be created in Studio Pro. This is the field set + ordering spec. There is + no second engine to copy from any more, and there is nothing else that will tell you the truth. +2. **Read its BSON**: `mxcli bson dump -p app.mpr --type --object "Mod.Name"`, or the MCP/PED + probe (`cmd/mcpprobe`) against a live Studio Pro. 3. **Write `xToGen`** (+ `xFromGen` if reads/ALTER need it), registering any TypeDefaults / list markers. 4. **`assignXIDs`** walks new sub-elements. -5. **Add a parity test** in `mdl/enginecompare/` (`copyProject` → `Run(Legacy,…)` + `Run(ModelSDK,…)` → - `XCanonBSON` → diff). Add an `XCanonBSON` dumper to `bsoncompare.go` for new top-level types. -6. **Iterate on the diff** until byte-identical. (Per-group this is fast: 1–2 iterations.) +5. **Pin the document against the reference** — re-serialize the reference element by element and assert + the keys, markers and value types match. `mdl/scheduledevents` and `mdl/regularexpressions` are the + worked examples; both found gen wrong about a property that way. +6. **Iterate until the diff is empty**, then build it (`mxcli docker check`) and, where the construct + renders or runs, verify it there too — `mx check` tolerates unknown properties, so a clean build is + not evidence the document is right. 7. **gofmt any hand-edited gen file** or `TestGeneratedCodeIsFormatted` fails. ## Verification truth -`legacy` is the parity baseline, but it is **not infallible** — it has had stale serializers (index -`SortOrder`). When a gen-vs-legacy disagreement appears, the tiebreaker is **real Studio-Pro BSON** -(on-disk dump or MCP capture), not whichever engine you trust. The gen has been wrong (EventHandler keys); -legacy has been wrong (indexes). Capture, don't guess. +**A Studio Pro-authored document is the arbiter.** There used to be a second engine to diff against; +`sdk/mpr` was deleted ([ADR-0004](../13-decisions/0004-full-codec-engine.md)) and it had been wrong often +enough that it was never the real baseline anyway (stale index `SortOrder` serializer). + +Where `modelsdk/gen` and `generated/metamodel` disagree about a property key, **`generated/metamodel` is +the arbiter** — it is built from reflection data carrying storage names, while gen's generator reads the +TypeScript SDK, which has none, and patches them back by hand. The caveat is that it is a snapshot of +11.6.0, so it says nothing about properties introduced later; for those, get a real document. See +CLAUDE.md, "`modelsdk/gen` Binds Some Properties Under the Wrong BSON Key". + +Capture, don't guess — and note what a green build does *not* tell you: mxbuild accepts properties the +project's metamodel does not declare, while Studio Pro throws `InvalidOperationException` at +`MprProperty.cs`. Measured on 10.24.25 with two 11.5-only keys present: 0 errors. diff --git a/docs/03-development/PAGE_BSON_SERIALIZATION.md b/docs/03-development/PAGE_BSON_SERIALIZATION.md index b66ecae631..8e8dafc6e1 100644 --- a/docs/03-development/PAGE_BSON_SERIALIZATION.md +++ b/docs/03-development/PAGE_BSON_SERIALIZATION.md @@ -519,10 +519,12 @@ for key, val in data.items(): | File | Purpose | |------|---------| -| `sdk/mpr/writer_widgets.go` | Widget serialization to BSON | -| `sdk/mpr/writer_pages.go` | Page serialization | -| `sdk/mpr/reader_widgets.go` | Widget template extraction and cloning | -| `sdk/mpr/parser_page.go` | Page deserialization | +| `mdl/backend/modelsdk/widget_write.go` | Widget serialization to BSON | +| `mdl/backend/modelsdk/page_write.go` | Page serialization | +| `mdl/backend/modelsdk/widget_pluggable_write.go` | Pluggable widget templates | +| `mdl/backend/modelsdk/page.go` | Page deserialization | +| `modelsdk/codec/encoder.go` | Document → BSON | +| `modelsdk/codec/decoder.go` | BSON → document | | `sdk/widgets/loader.go` | Embedded template loading | | `sdk/widgets/templates/mendix-11.6/*.json` | Embedded widget templates | | `sdk/pages/pages_widgets_advanced.go` | CustomWidget Go types | diff --git a/docs/03-development/WIDGET_BSON_VERSION_COMPATIBILITY.md b/docs/03-development/WIDGET_BSON_VERSION_COMPATIBILITY.md index 2908f21e69..bf7dca2f24 100644 --- a/docs/03-development/WIDGET_BSON_VERSION_COMPATIBILITY.md +++ b/docs/03-development/WIDGET_BSON_VERSION_COMPATIBILITY.md @@ -351,7 +351,7 @@ The CE0463 fix methodology used for 11.9 generalizes. Steps: `defaultEmptyAppearance` helper) — for the CustomWidget envelope mxcli constructs around filter widgets inside DataGrid columns. -- **WidgetValueType serializer**: `sdk/mpr/writer_widgets_custom.go` +- **WidgetValueType serializer**: `mdl/backend/modelsdk/widget_pluggable_write.go` (`serializeWidgetValueType`) — for the structured-data path (not the RawType clone path) when building widget BSON from typed inputs. diff --git a/docs/05-mdl-specification/10-bson-mapping.md b/docs/05-mdl-specification/10-bson-mapping.md index c9416589d8..c8b2c4c1f2 100644 --- a/docs/05-mdl-specification/10-bson-mapping.md +++ b/docs/05-mdl-specification/10-bson-mapping.md @@ -1259,7 +1259,8 @@ When Studio Pro doesn't display data correctly (e.g., missing attributes, incorr Use this pattern to compare your generated BSON with Mendix-generated BSON: ```go -// in sdk/mpr/reader_units.go there's GetRawMicroflowByName for debugging +// modelsdk/mpr exposes GetRawUnit / GetRawUnitByName for debugging; +// `mxcli bson dump` is the same thing from the command line raw1, _ := reader.GetRawMicroflowByName("Module.BrokenMicroflow") raw2, _ := reader.GetRawMicroflowByName("Module.WorkingMicroflow") diff --git a/docs/05-mdl-specification/11-model-sdk-mapping.md b/docs/05-mdl-specification/11-model-sdk-mapping.md index 68de2801b3..e8d536fc5c 100644 --- a/docs/05-mdl-specification/11-model-sdk-mapping.md +++ b/docs/05-mdl-specification/11-model-sdk-mapping.md @@ -28,7 +28,8 @@ The modelsdk-go library is organized into packages: | `sdk/microflows` | Microflow types (60+ activity types) | | `sdk/pages` | Page and widget types (50+ widgets) | | `sdk/widgets` | Embedded widget templates for pluggable widgets | -| `sdk/mpr` | MPR file reading/writing, BSON parsing | +| `modelsdk/mpr` | MPR file reading/writing | +| `modelsdk/codec` | BSON encode/decode | | `sql` | External database connectivity (PostgreSQL, Oracle, SQL Server) | | `mdl/executor` | MDL statement execution engine | | `mdl/catalog` | SQLite-based catalog for cross-reference queries | @@ -697,7 +698,7 @@ The `api/` package provides a simplified builder API as an alternative to direct import "github.com/mendixlabs/mxcli/api" modelAPI := api.New(writer) -module, _ := modelAPI.Modules.GetModule("Sales") +module, _ := modelAPI.Modules.Get("Sales") modelAPI.SetModule(module) entity, _ := modelAPI.DomainModels.CreateEntity("Customer"). diff --git a/docs/CODE_REVIEW.md b/docs/CODE_REVIEW.md index 321e2ca058..fbdee7d62f 100644 --- a/docs/CODE_REVIEW.md +++ b/docs/CODE_REVIEW.md @@ -1,5 +1,12 @@ # Code Review +> **Point-in-time review, not a current description.** This was written against the +> project when it was called `ModelSDKGo` and when the `sdk/mpr` engine still +> existed; that package has since been deleted +> ([ADR-0004](13-decisions/0004-full-codec-engine.md)). The observations are kept +> as a record — for how the code is laid out today, read +> [`docs/01-project/ARCHITECTURE.md`](01-project/ARCHITECTURE.md). + This document provides a review of the `ModelSDKGo` project, a command-line application for working with Mendix projects. ## 1. Overview diff --git a/docs/GO_LIBRARY.md b/docs/GO_LIBRARY.md index ac306ea50c..065f938947 100644 --- a/docs/GO_LIBRARY.md +++ b/docs/GO_LIBRARY.md @@ -103,21 +103,19 @@ package main import ( "github.com/mendixlabs/mxcli/api" - "github.com/mendixlabs/mxcli/sdk/mpr" ) func main() { - writer, err := mpr.OpenForWriting("/path/to/MyApp.mpr") + // api.Open opens the project for reading and writing and owns the + // connection; api.New(b) wraps a backend the caller already owns. + modelAPI, err := api.Open("/path/to/MyApp.mpr") if err != nil { panic(err) } - defer writer.Close() - - // create the high-level api - modelAPI := api.New(writer) + defer modelAPI.Close() // set the current module context - module, _ := modelAPI.Modules.GetModule("MyModule") + module, _ := modelAPI.Modules.Get("MyModule") modelAPI.SetModule(module) // create entity with fluent builder @@ -127,14 +125,14 @@ func main() { WithStringAttribute("Email", 254). WithIntegerAttribute("Age"). WithBooleanAttribute("IsActive"). - WithDateTimeAttribute("CreatedDate", true). + WithDateTimeAttribute("CreatedDate"). build() // create another entity order, _ := modelAPI.DomainModels.CreateEntity("Order"). persistent(). WithDecimalAttribute("TotalAmount"). - WithDateTimeAttribute("OrderDate", true). + WithDateTimeAttribute("OrderDate"). build() // create association between entities diff --git a/mdl-examples/bug-tests/1029-showpage-arg-with-context.mdl b/mdl-examples/bug-tests/1029-showpage-arg-with-context.mdl new file mode 100644 index 0000000000..dd856a2ee6 --- /dev/null +++ b/mdl-examples/bug-tests/1029-showpage-arg-with-context.mdl @@ -0,0 +1,70 @@ +-- mendixlabs/mxcli#1029, the other half: what the refusal must NOT touch. +-- +-- MDL-PAGEARG01 refuses a `show_page` widget argument only where it can prove +-- the argument is discarded. Every form below has a context object for Mendix to +-- infer, or no argument to lose, and all of them build at 0 errors on mxbuild +-- 11.13.0. This script must PASS check. +-- +-- (`ALTER PAGE … SET Action = …` is the third case the guard stands down on: it +-- builds an action against a stored page the pass never traverses, so it cannot +-- say what encloses the widget. Covered by the unit tests, not here, since it +-- needs a project to alter.) +create module Probe1029Ok; + +create persistent entity Probe1029Ok.Item ( + Name: String(200) +); + +create page Probe1029Ok.Detail ( + params: { $Item: Probe1029Ok.Item }, + title: 'Detail', + layout: Atlas_Core.Atlas_Default +) { + dataview dv (datasource: $Item) { + textbox txt (label: 'Name', attribute: 'Name') + } +}; + +-- No argument at all: nothing to drop, so nothing to refuse. +create page Probe1029Ok.Plain ( + title: 'Plain', + layout: Atlas_Core.Atlas_Default +) { + actionbutton btnNoArgs ( + caption: 'Open', + action: show_page Probe1029Ok.Plain + ) +}; + +-- Inside a data view: the argument names the context object, under either +-- spelling. This is the documented form in create-page.md. +create page Probe1029Ok.Ok ( + params: { $SomeRef: Probe1029Ok.Item }, + title: 'Ok', + layout: Atlas_Core.Atlas_Default +) { + dataview dv (datasource: $SomeRef) { + actionbutton btnCurrent ( + caption: 'Open (currentObject)', + action: show_page Probe1029Ok.Detail(Item: $currentObject) + ) + actionbutton btnByName ( + caption: 'Open (by name)', + action: show_page Probe1029Ok.Detail(Item: $SomeRef) + ) + } +}; + +-- A row-scoped button in a list view: the row object is the context object, and +-- it has no name of its own, so only $currentObject spells it. +create page Probe1029Ok.Listing ( + title: 'Listing', + layout: Atlas_Core.Atlas_Default +) { + listview lv (datasource: database Probe1029Ok.Item) { + actionbutton btnRow ( + caption: 'Open row', + action: show_page Probe1029Ok.Detail(Item: $currentObject) + ) + } +}; diff --git a/mdl-examples/bug-tests/1029-showpage-arg-without-context.fail.mdl b/mdl-examples/bug-tests/1029-showpage-arg-without-context.fail.mdl new file mode 100644 index 0000000000..cf0b997ad2 --- /dev/null +++ b/mdl-examples/bug-tests/1029-showpage-arg-without-context.fail.mdl @@ -0,0 +1,45 @@ +-- mendixlabs/mxcli#1029 — a widget `show_page` argument written where there is +-- no context object. +-- +-- A page-level `actionbutton` (outside any dataview) with +-- `Action: show_page Page(Param: $Var)` silently dropped the argument: `mxcli +-- check --references` said "All references valid", `exec` reported success, and +-- `DESCRIBE PAGE` then printed `(Item: $currentObject)` on a page where +-- $currentObject is unbound. mxbuild 11.13.0 rejected it with one +-- +-- [CE1571] "No argument has been selected for parameter 'Item' and no default +-- is available." +-- +-- per parameter of the target page. mxcli stores this action with an EMPTY +-- ParameterMappings array and lets Mendix infer the argument from the enclosing +-- widget's context object (#296: an explicit mapping is CE0115), so with no +-- enclosing data widget there is nothing to infer and every argument is lost — +-- a variable, a literal and an association path alike. +-- +-- MDL-PAGEARG01 now refuses it at check time. This script must FAIL check. +create module Probe1029; + +create persistent entity Probe1029.Item ( + Name: String(200) +); + +create page Probe1029.Detail ( + params: { $Item: Probe1029.Item }, + title: 'Detail', + layout: Atlas_Core.Atlas_Default +) { + dataview dv (datasource: $Item) { + textbox txt (label: 'Name', attribute: 'Name') + } +}; + +create page Probe1029.List ( + params: { $SomeRef: Probe1029.Item }, + title: 'List', + layout: Atlas_Core.Atlas_Default +) { + actionbutton btnOpen ( + caption: 'Open', + action: show_page Probe1029.Detail(Item: $SomeRef) + ) +}; diff --git a/mdl-examples/bug-tests/1140-flow-arg-page-parameter.mdl b/mdl-examples/bug-tests/1140-flow-arg-page-parameter.mdl new file mode 100644 index 0000000000..8513739e9a --- /dev/null +++ b/mdl-examples/bug-tests/1140-flow-arg-page-parameter.mdl @@ -0,0 +1,138 @@ +-- mendixlabs/mxcli#1140 — a page parameter passed as a nanoflow argument was not +-- wired, so Studio Pro reported CE1571 on opening the page. +-- +-- REPORTED SHAPE. A PANEL_ page with two parameters; the Save button sits inside +-- a dataview bound to the first: +-- +-- $Dto = $Dto -> "correctly wired" +-- $BufferDefinition = ... -> NOT wired. Studio Pro: CE1571 "No argument has +-- been selected for parameter 'BufferDefinition' +-- and no default is available." +-- +-- WHAT WAS ACTUALLY BROKEN — not what the asymmetry suggests. Both arguments were +-- written identically, as a text Expression ("$Dto" / "$BufferDefinition"), and +-- NEITHER was bound. Studio Pro supplies a default for the one that happens to be +-- the dataview's object and reports the other, which is what "and no default is +-- available" says. Chasing why $Dto worked is the wrong thread. +-- +-- Mendix stores a flow argument two ways: a reference to a page parameter, +-- snippet parameter or page variable goes in the mapping's Variable as a +-- Forms$PageVariable; a literal or expression goes in Expression. mxcli only ever +-- wrote the second. Measured on Workflow Commons 4.11.0 (Studio Pro-authored, +-- 42 pages + 84 snippets): of 101 flow parameter mappings, 95 bind through +-- Variable and 6 through Expression — and all 6 of those are Boolean literals. +-- A $-prefixed Expression occurs zero times. +-- +-- WHY NOTHING CAUGHT IT. `mx check` on this exact project reports **0 errors** +-- both before and after the fix — mxbuild does not validate this binding, so the +-- build is not a safety net and the reporter is right that the error appears only +-- in Studio Pro. `mxcli check --references` and `mxcli lint` are silent for the +-- same reason: the nanoflow and both parameters resolve. +-- +-- REPRODUCING (the write side is BSON, so read it rather than building): +-- +-- mxcli exec 1140-flow-arg-page-parameter.mdl -p app.mpr +-- mxcli bson dump -p app.mpr --type page \ +-- --object CustomModule.PANEL_BufferDefinition_Edit | grep -A6 ParameterMapping +-- +-- Before: {"$Type": "Forms$NanoflowParameterMapping", "Parameter": "….Dto", +-- "Expression": "$Dto"} <- unbound +-- After: {"$Type": "Forms$NanoflowParameterMapping", "Parameter": "….Dto", +-- "Expression": "", "Variable": {"$Type": "Forms$PageVariable", +-- "PageParameter": "Dto"}} <- bound +-- +-- The read half was wrong too and hid the write half: the describe readers looked +-- for a `Name` key on that sub-document, which Forms$PageVariable has not, so +-- every argument in Studio Pro-authored content described as absent. Regression +-- coverage: TestNanoflowActionBindsPageParameterThroughVariable +-- (mdl/backend/modelsdk), TestClassifyFlowArgValue and TestPageVariableArgValue +-- (mdl/executor). + +create module CustomModule; + +create persistent entity CustomModule.BufferDefinition ( + Name: String(100) +); + +create non-persistent entity CustomModule.UpdateBufferDefinition ( + Name: String(100) +); + +create or replace layout CustomModule.App_Default ( + layouttype: 'Responsive' +) { + scrollcontainer layoutContainer { + region center { + placeholder Main + } + } +} + +create or replace nanoflow CustomModule.ACT_BufferDefinition_SaveEdit_NF + ($Dto: CustomModule.UpdateBufferDefinition, + $BufferDefinition: CustomModule.BufferDefinition) +begin + change $BufferDefinition (Name = $Dto/Name); +end; + +-- The reported page. Both arguments name a page parameter, and the one that is +-- NOT the dataview's object is the one the report is about. +create or replace page CustomModule.PANEL_BufferDefinition_Edit ( + Title: 'Edit', + Layout: CustomModule.App_Default, + Params: { $Dto: CustomModule.UpdateBufferDefinition, + $BufferDefinition: CustomModule.BufferDefinition } +) { + placeholder Main { + dataview dvEdit (DataSource: $Dto, FormOrientation: Vertical) { + footer footer1 { + actionbutton btnSave ( + Caption: 'Save', + Action: nanoflow CustomModule.ACT_BufferDefinition_SaveEdit_NF( + $Dto = $Dto, + $BufferDefinition = $BufferDefinition + ) + ) + } + } + } +} + +-- CONTROL 1 — a literal argument must stay an Expression. A fix that routed +-- everything through Variable would clear the report and break this, which is the +-- shape of all six Expression-bound mappings in the reference. +create or replace nanoflow CustomModule.ACT_Flag_NF ($Keep: Boolean) +begin + log info 'flag'; +end; + +-- CONTROL 2 — $currentObject is deliberately left as an expression: no Studio Pro +-- reference for the bare form was measured, and mxcli's show_page handling already +-- relies on the context object being inferred rather than named. PANEL_Controls +-- declares a $Row page parameter it never uses, so the control also shows that a +-- name in scope does not drag $currentObject into the Variable form. +create or replace nanoflow CustomModule.ACT_Row_NF ($Row: CustomModule.BufferDefinition) +begin + log info 'row'; +end; + +create or replace page CustomModule.PANEL_Controls ( + Title: 'Controls', + Layout: CustomModule.App_Default, + Params: { $Row: CustomModule.BufferDefinition } +) { + placeholder Main { + listview lvRows (DataSource: DATABASE CustomModule.BufferDefinition) { + container rowBox { + actionbutton btnFlag ( + Caption: 'Flag', + Action: nanoflow CustomModule.ACT_Flag_NF($Keep = true) + ) + actionbutton btnRow ( + Caption: 'Row', + Action: nanoflow CustomModule.ACT_Row_NF($Row = $currentObject) + ) + } + } + } +} diff --git a/mdl-examples/bug-tests/alterpage-527-set-documentation.mdl b/mdl-examples/bug-tests/alterpage-527-set-documentation.mdl new file mode 100644 index 0000000000..d34247b19a --- /dev/null +++ b/mdl-examples/bug-tests/alterpage-527-set-documentation.mdl @@ -0,0 +1,58 @@ +-- @version: 11.0+ +-- ============================================================================ +-- ako/mxcli#527 — ALTER PAGE could not set Documentation: +-- "unsupported page-level property: Documentation +-- (supported: Title, Url, PopupWidth, ... Class, Style)" +-- +-- So documenting an existing page meant re-running its CREATE — the `/** */` +-- doc comment on the create statement was the only source. For a real page that +-- means re-emitting its whole widget tree through a describe → exec round trip +-- that is only as complete as what MDL can spell, so the workaround could +-- silently lose widgets. +-- +-- The field was otherwise fully understood: pageToGen has always written it on +-- CREATE, gen binds it as a plain top-level string, the grammar already parsed +-- the statement and the executor has no allowlist. One missing mutator case. +-- +-- EXPECTED: `mx check` reports 0 errors, and the documentation set by ALTER +-- below replaces what the CREATE doc comment wrote. +-- ============================================================================ + +create module Issue527; + +@position(100, 100) +create persistent entity Issue527.ServiceRequest ( + Description: string(500) +); + +/** + * Written by the doc comment on CREATE — the only route before this fix. + */ +create or replace page Issue527.ServiceRequest_Triage ( + title: 'Triage', + params: { $Request: Issue527.ServiceRequest } +) +{ + dataview dvMain (datasource: $Request) { + text txtDescription (content: 'Triage this request.') + } +} + +-- The statement that was refused. It replaces the doc comment's text in place, +-- with no need to restate the page. +alter page Issue527.ServiceRequest_Triage { + set Documentation = 'Coordinator triage step: set priority, then accept or reject.'; +} + +-- An empty string clears it — removing a doc comment from a script has to be +-- expressible, and the property is a bare string with no unset value. +alter page Issue527.ServiceRequest_Triage { + set Documentation = ''; +} + +-- Back to a real value, so the file leaves the page documented. +alter page Issue527.ServiceRequest_Triage { + set Documentation = 'Coordinator triage step: set priority, then accept or reject.'; +} + +describe page Issue527.ServiceRequest_Triage; diff --git a/mdl-examples/bug-tests/contracts/key-updatable-metadata.xml b/mdl-examples/bug-tests/contracts/key-updatable-metadata.xml new file mode 100644 index 0000000000..a01a24c1e8 --- /dev/null +++ b/mdl-examples/bug-tests/contracts/key-updatable-metadata.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/mdl-examples/bug-tests/microflow-1120-carried-properties.mdl b/mdl-examples/bug-tests/microflow-1120-carried-properties.mdl new file mode 100644 index 0000000000..824c4c91b4 --- /dev/null +++ b/mdl-examples/bug-tests/microflow-1120-carried-properties.mdl @@ -0,0 +1,77 @@ +-- ============================================================================ +-- mendixlabs/mxcli#1120 — a rewrite keeps the microflow properties MDL cannot +-- author: the deep-link URL, the export level, and the concurrency settings +-- ============================================================================ +-- +-- POSITIVE TEST: `mxcli check` MUST accept this file. +-- +-- Studio Pro stores several microflow properties that MDL has no syntax for: +-- +-- Url / UrlSearchParameters the deep link (Mendix 10.6+), e.g. item/{Key} +-- ExportLevel Hidden or API — the module's public surface +-- AllowConcurrentExecution "disallow concurrent execution" +-- ConcurrencyErrorMessage what the second caller gets, translations and all +-- ConcurrencyErrorMicroflow or the microflow that handles it +-- MarkAsUsed suppresses the "unused document" warning +-- +-- `CREATE OR MODIFY MICROFLOW` rebuilds the document from the statement, so all +-- of these used to be replaced by the writer's own literals — even when the +-- script only changed the body. +-- +-- What made this reach a user rather than CI is that NOTHING reports it. +-- Measured end to end on mxbuild 11.6.6 against a real MPR v2 project, with the +-- properties seeded into the stored unit the way Studio Pro writes them: +-- +-- seeded control Url="item/{Key}" ExportLevel="API" mx check 0 errors +-- before the fix Url="" ExportLevel="Hidden" mx check 0 errors +-- after the fix Url="item/{Key}" ExportLevel="API" mx check 0 errors +-- +-- The middle row is the bug: a valid project, a clean build, and the deep link +-- simply gone. The concurrency case is worse in kind — the rebuild wrote +-- "allow", so a microflow that DISALLOWED concurrent execution came back +-- allowing it, and CE4899 fires only on disallow-without-a-message, so the one +-- error covering that area is blind to the direction that removes the +-- protection. +-- +-- One more rule, learned from the seeded fixture rather than from the docs: a +-- parameter used in the URL PATH may not also be a URL SEARCH parameter. The +-- two sets are disjoint and mxbuild rejects the overlap with CE5612. +-- +-- THREE OF THE SIX ARE NOW AUTHORABLE. The URL, the export level and the +-- concurrency settings have header clauses of their own — see +-- doctype-tests/microflow-document-properties.mdl — and DESCRIBE emits them, so +-- describe -> rename -> exec copies them faithfully. That is what preservation +-- alone could never fix: a copy is a NEW document with nothing to preserve from. +-- +-- The preservation rule is unchanged and is what the clauses are layered on: an +-- OMITTED clause still means "the script does not say", so the rewrite below +-- keeps everything. Clearing is explicit (DROP URL / EXPORT LEVEL HIDDEN / +-- ALLOW CONCURRENT EXECUTION), and `Mark as used` still has no clause at all. +-- +-- WHAT THIS FIXTURE PINS is the syntax half — that a body-only rewrite is valid +-- MDL and stays accepted. It cannot pin the preservation itself: `check` runs +-- without a project, so there is no stored document to preserve from. That half +-- is guarded in Go (mdl/backend/modelsdk/microflow_roundtrip_flags_test.go, +-- mdl/executor/microflow_carried_properties_test.go and +-- microflow_authored_properties_test.go, each with the fix reverted as a +-- control) and end to end by the procedure recorded in the findings. + +CREATE OR MODIFY MICROFLOW Bug1120.ACT_Item ($Key: String, $Filter: String) +RETURNS String AS $Result +BEGIN + -- A microflow that, in a real project, would carry Url = 'item/{Key}' with + -- $Filter (NOT $Key — CE5612) as its search parameter, ExportLevel = 'API', + -- and concurrent execution disallowed with an error message. + $Result = $Key + '/' + $Filter; + RETURN $Result; +END; + +-- Re-running the same statement with a different body is the exact shape of the +-- report: nothing here mentions the URL, the export level or concurrency, and +-- none of them may be disturbed. +CREATE OR MODIFY MICROFLOW Bug1120.ACT_Item ($Key: String, $Filter: String) +RETURNS String AS $Result +BEGIN + $Result = $Filter + '/' + $Key; + RETURN $Result; +END; diff --git a/mdl-examples/bug-tests/microflow-1120-url-path-and-search.fail.mdl b/mdl-examples/bug-tests/microflow-1120-url-path-and-search.fail.mdl new file mode 100644 index 0000000000..f76137e109 --- /dev/null +++ b/mdl-examples/bug-tests/microflow-1120-url-path-and-search.fail.mdl @@ -0,0 +1,21 @@ +-- ============================================================================ +-- NEGATIVE TEST: `mxcli check` MUST reject this file. +-- ============================================================================ +-- +-- A parameter used in the URL PATH may not ALSO be a URL SEARCH parameter. +-- mxbuild reports the overlap as CE5612 ("cannot be used as a URL parameter if +-- it is already a URL search parameter"); mxcli reports it as MDL-MF02 before +-- anything is written. +-- +-- This rule is here because it was learned the expensive way. The first version +-- of #1120's own test fixture reused one parameter for both, and every unit test +-- passed — nothing in a unit test validates the model. It took seeding a real +-- project and running `mx check` to find that the fixture described a document +-- Mendix refuses to build. + +CREATE OR MODIFY MICROFLOW Deeplink.ACT_Bad ($Key: String) +URL 'item/{Key}' +URL SEARCH PARAMETERS ($Key) +BEGIN + LOG INFO $Key; +END; diff --git a/mdl-examples/bug-tests/odata-key-attribute-updatable.mdl b/mdl-examples/bug-tests/odata-key-attribute-updatable.mdl new file mode 100644 index 0000000000..213324b63f --- /dev/null +++ b/mdl-examples/bug-tests/odata-key-attribute-updatable.mdl @@ -0,0 +1,102 @@ +-- CREATE OR MODIFY EXTERNAL ENTITIES FROM marked an external entity's KEY +-- attribute Updatable=true whenever the entity set's UpdateRestrictions said +-- the set was updatable. +-- +-- Symptom, verbatim: +-- +-- [error] [CE6630] "'DefinitionId' is marked Updatable=False in the OData +-- service, but True in the app." +-- at Attribute 'MyFirstModule.Definition.DefinitionId' +-- +-- Mendix computes a key property as non-updatable whatever the contract says — +-- a key cannot be changed after the object exists — so following the entity set +-- is exactly one CE6630 per key part on every writable service. +-- +-- Cause: mdl/executor/cmd_contract.go, createExternalEntities' attribute loop. +-- `updatable` started from the entity set's UpdateRestrictions/Updatable and was +-- cleared only by NonUpdatableProperties, Core.Computed, Core.Immutable or a +-- flattened complex-type leaf. Key membership was already in hand as +-- keyPropSet[p.Name] — it was passed to edmToDomainModelAttrType and never +-- consulted for updatability. +-- +-- Fix: the key of a TOP-LEVEL entity is never updatable. Creatable is +-- deliberately NOT cleared with it: the key is written once, at creation, and +-- the report's build flagged it Updatable=False ONLY, with no Creatable error +-- beside it. +-- +-- `isTopLevel` is load-bearing and was NOT in the first version of this fix. +-- A blanket "a key is never updatable" turned the one reported CE6630 into +-- SEVEN of its inverse on the live TripPin contract (mxbuild 11.12.2): +-- +-- [error] [CE6630] "'TripId' is marked Updatable=True in the OData service, +-- but False in the app." +-- at Attribute 'TripPinClient.Trip.TripId' +-- +-- over Trip, PlanItem, Event, Flight, PublicTransportation, Employee and +-- Manager — every one a derived or contained type with NO entity set, mutated +-- through its parent's write flow — while the entity sets in the same contract +-- (Person, Airline, Airport) stayed silent at Updatable=false. `UserName` is +-- the two-sided control inside one document: False on Person, True on Employee +-- and Manager, so the split is the entity set, not inheritance. +-- +-- Measured on mxbuild 11.12.1, two copies of one project, same script: +-- +-- pre-fix (everything follows the set) -> CE6630 on DefinitionId AND Label +-- key-only (key cleared) -> CE6630 on Label +-- full fix (top-level attrs cleared) -> The app contains: 0 errors. +-- +-- and across a seven-variant probe contract of TOP-LEVEL sets (inline record, +-- typed record, UpdateMethod=PATCH, +NonUpdatableProperties/+DeleteRestrictions, +-- unannotated, external , Core.Permissions/ReadWrite) +-- mxbuild was SILENT on every key once it was non-updatable. +-- +-- THE NON-KEY HALF, fixed in a follow-up: `Label` was flagged too, and the +-- rule turned out to be wider than the key. NO attribute of a TOP-LEVEL +-- external entity is updatable, whatever UpdateRestrictions says; EVERY +-- attribute of a non-top-level one is, because it is written through its +-- parent's flow. Measured across TEN top-level contract shapes, each one +-- mxbuild reads as updatable and each answering False: +-- +-- inline typed +-- UpdateMethod=PATCH +NonUpdatableProperties +DeleteRestrictions +-- unannotated external +-- Core.Permissions/ReadWrite Core.OptimisticConcurrency (ETag) +-- DeepUpdateSupport Supported=true NonUpdatableProperties naming ONLY the key +-- +-- The last closes it: the service lists `Id` as the sole non-updatable +-- property -- asserting that `Label` IS updatable -- and mxbuild still says +-- False. Two things the rule is not, each with its own control: not "the +-- entity is read-only" (Creatable follows Insertable on the same attributes), +-- and not the model's "allow creating and changing objects locally" (setting +-- AllowCreateChangeLocally=Yes left the expectation at False). An external +-- object can be changed in memory and handed to an external action; that is +-- what the local-change flag governs, while this flag mirrors what the +-- endpoint itself accepts. +-- +-- So this script now reaches 0 errors, where it was 2 before any fix and 1 +-- after the key-only one. +-- +-- Run it: +-- cp mdl-examples/bug-tests/contracts/key-updatable-metadata.xml \ +-- /key-updatable-metadata.xml +-- mxcli exec odata-key-attribute-updatable.mdl -p /app.mpr +-- mxcli docker check -p /app.mpr +-- +-- Regression coverage: mdl/executor/cmd_contract_key_updatable_test.go + +create or modify module OdataKeyUpd; + +create or modify constant OdataKeyUpd.SvcUrl + type string + default 'https://example.com/odata/v4/App/'; + +create or modify odata client OdataKeyUpd.App ( + ODataVersion: 'OData4', + MetadataUrl: './key-updatable-metadata.xml', + ServiceUrl: '@OdataKeyUpd.SvcUrl' +); + +create or modify external entities from OdataKeyUpd.App into OdataKeyUpd entities (Definition); + +-- DefinitionId (the key, renamed off the reserved word `Id`) and Label. +describe external entity OdataKeyUpd.Definition; diff --git a/mdl-examples/bug-tests/pages-529-input-attribute-over-association.mdl b/mdl-examples/bug-tests/pages-529-input-attribute-over-association.mdl new file mode 100644 index 0000000000..490c595c22 --- /dev/null +++ b/mdl-examples/bug-tests/pages-529-input-attribute-over-association.mdl @@ -0,0 +1,80 @@ +-- @version: 11.0+ +-- ============================================================================ +-- ako/mxcli#529 — an input widget could not bind an attribute over an +-- association. `attribute: Assoc/Attr` on a text box produced a FLAT path and +-- the build failed: +-- +-- [CE1613] "The selected attribute +-- 'Rules.RuleAction.RuleAction_BusinessRule/Name' no longer exists." +-- at Text box [RuleAction_BusinessRule/Name] +-- +-- measured on ako/TestApp at Mendix 11.14.0. Note the shape of the bad value: +-- the association segment was pasted onto the context entity instead of being +-- navigated, because every input builder resolved its attribute with +-- resolveAttributePath, which knows nothing about associations. +-- +-- DataGrid2 columns and DynamicText parameters already resolved the same +-- syntax, so one page could bind an associated attribute in a grid column and +-- fail on the text box beside it. +-- +-- Studio Pro DOES store this on a plain text box. The reference is +-- ako/TestApp's Rules.RuleAction_NewEdit, textBox4: +-- +-- AttributeRef.Attribute = "Rules.BusinessRule.Name" +-- AttributeRef.EntityRef = IndirectEntityRef{ Steps: [ EntityRefStep{ +-- Association: "Rules.RuleAction_BusinessRule", +-- DestinationEntity: "Rules.BusinessRule" } ] } +-- +-- The read half was broken too: DESCRIBE emitted `Attribute: Name`, and +-- RuleAction has no Name — so describe → exec over a Studio Pro page REBOUND +-- the widget to nothing, with `check` clean. +-- +-- EXPECTED: `mx check` reports 0 errors, and `describe page` round-trips the +-- association path rather than flattening it. +-- ============================================================================ + +create module Issue529; + +@position(100, 100) +create persistent entity Issue529.BusinessRule ( + Name: string(200), + Active: boolean default true +); + +-- Deliberately has NO attribute called Name: that is what turns the old +-- behaviour into a binding that cannot resolve, rather than one that merely +-- points somewhere else. +@position(400, 100) +create persistent entity Issue529.RuleAction ( + ActionType: string(200) +); + +create association Issue529.RuleAction_BusinessRule + from Issue529.RuleAction to Issue529.BusinessRule; + +create or replace page Issue529.RuleAction_NewEdit ( + title: 'Edit Rule Action', + layout: Atlas_Core.PopupLayout, + params: { $RuleAction: Issue529.RuleAction } +) +{ + -- Wrapped in a layout grid, as the Studio Pro reference page is (MPR010). + layoutgrid layoutGrid1 { + row row1 { + column col1 (DesktopWidth: AutoFill) { + dataview dvMain (datasource: $RuleAction) { + -- The control: an own attribute, which must stay a bare binding. + textbox tbActionType (label: 'Action type', attribute: ActionType) + + -- The reported case, on each input widget that takes a single attribute. + textbox tbRuleName (label: 'Rule name', attribute: RuleAction_BusinessRule/Name) + textarea taRuleName (label: 'Rule name (area)', attribute: RuleAction_BusinessRule/Name) + checkbox cbActive (label: 'Active', attribute: RuleAction_BusinessRule/Active) + } + } + } + } +} + +-- Round-trip: this must emit `RuleAction_BusinessRule/Name`, not a bare `Name`. +describe page Issue529.RuleAction_NewEdit; diff --git a/mdl-examples/bug-tests/security-524-autonumber-write-rights.mdl b/mdl-examples/bug-tests/security-524-autonumber-write-rights.mdl new file mode 100644 index 0000000000..9c95cbe6ec --- /dev/null +++ b/mdl-examples/bug-tests/security-524-autonumber-write-rights.mdl @@ -0,0 +1,48 @@ +-- @version: 11.0+ +-- ============================================================================ +-- ako/mxcli#524 — `grant write *` on an entity carrying an AUTONUMBER wrote +-- ReadWrite on it, and the build failed CE6592 ("write access is not allowed on +-- this attribute"). Reported from the ChipCoV3 test project, where the +-- workaround was a hand-written REVOKE narrowing the grant. +-- +-- Cause: the CE6592 downgrade asked only whether an attribute was CALCULATED. +-- An autonumber carries no DomainModels$CalculatedValue — its value comes from +-- the database on insert, not from a microflow on read — so it failed that test +-- and kept its write. Mendix forbids write on both for the same reason, so the +-- predicate covered exactly half the rule. +-- +-- The fix is types.WriteRightsForbidden, applied at the GRANT +-- (cmd_security_write.go) AND in ReconcileMemberAccesses, which runs after +-- every program and would otherwise re-break a grant corrected by hand. +-- +-- EXPECTED after running this: `mx check` reports 0 errors, and +-- `show access on entity Issue524.ServiceRequest` shows RequestNumber as +-- ReadOnly while Description stays ReadWrite. +-- ============================================================================ + +create module Issue524; +create module role Issue524.Coordinator; + +@position(100, 100) +create persistent entity Issue524.ServiceRequest ( + -- The autonumber. A seed is required in its own right (MDL023 / CE7247). + RequestNumber: autonumber default 1001, + Description: string(500), + -- The control: a calculated attribute was already downgraded before the fix, + -- so on its own it proves nothing about this one. + Summary: string(200) calculated by Issue524.CALC_Summary +); + +create microflow Issue524.CALC_Summary ($ServiceRequest: Issue524.ServiceRequest) +returns string +begin + return $ServiceRequest/Description; +end + +-- The reported statement: blanket write, no REVOKE narrowing it. +grant Issue524.Coordinator on Issue524.ServiceRequest (read *, write *); + +-- RequestNumber must come back ReadOnly, Summary ReadOnly, Description ReadWrite. +-- A blanket downgrade would also make Description ReadOnly, which is why the +-- plain attribute is part of the check and not just decoration. +show access on entity Issue524.ServiceRequest; diff --git a/mdl-examples/bug-tests/widgets-1057-dynamicimage-source-and-roundtrip.mdl b/mdl-examples/bug-tests/widgets-1057-dynamicimage-source-and-roundtrip.mdl new file mode 100644 index 0000000000..890e56cea3 --- /dev/null +++ b/mdl-examples/bug-tests/widgets-1057-dynamicimage-source-and-roundtrip.mdl @@ -0,0 +1,97 @@ +-- The `dynamicimage` sibling of the mendixlabs/mxcli#1057 static-image gap, and +-- worse than it: TWO defects, only one of which is a round trip. +-- +-- 1. EVERY dynamic image mxcli wrote failed the build. `dynamicImageToGen` called +-- `imageViewerSourceToGen()` with no arguments, writing a +-- Forms$ImageViewerSource carrying nothing but its own $ID and +-- `EntityRef: null`: +-- +-- [error] [CE0489] "Select an entity for the data source of this dynamic +-- image." at Dynamic image 'imgPhoto' +-- +-- 2. DESCRIBE had no case for Forms$ImageViewer, so a stored one came back as +-- +-- -- Forms$ImageViewer (imgPhoto) -- NOT re-executable: mxcli cannot +-- author this widget, so re-running this script would drop it +-- +-- and the replay DELETED the widget. Measured, and the shape of the evidence +-- is the point: after the pre-fix round trip the build got QUIETER — 4 errors +-- to 2 — because dropping the widget also dropped the CE0582 it carried. A +-- round trip that "fixes" a build error by deleting the user's work. +-- +-- Measured on a blank Mendix 11.12.1 project, mxbuild 11.12.1: +-- +-- pre-fix exec -> CE0489, 4 errors +-- pre-fix describe -> exec -> widget gone, 2 errors +-- fixed exec -> 3 errors (CE0582 only), no CE0489 +-- fixed describe -> exec -> Unchanged page +-- writer reverted to a bare source -> CE0489 again, 4 errors +-- +-- There is NO Studio Pro-authored dynamic image in a blank 11.12.1 app (measured: +-- 1 Forms$ImageViewer, and it is mxcli's own), so the widget's shape here is +-- metamodel-derived. What IS pinned to Studio Pro is the element CE0489 asks +-- for: DomainModels$DirectEntityRef{Entity: 'Module.Entity'}, 20 of 20 instances +-- in the same app. +-- +-- CE0582 ("not supported in React client") is Mendix's own deprecation of both +-- legacy image widgets; prefer the pluggable `image` widget on a new page. + +create persistent entity MyFirstModule.Photo extends System.Image ( + Caption: string(200) +); + +create or replace page MyFirstModule.DynImageDemo ( + Title: 'Dynamic image demo', Layout: Atlas_Core.Atlas_Default +) { + placeholder Main { + listview lvPhoto (DataSource: database from MyFirstModule.Photo) { + dynamicimage imgPhoto ( + DataSource: database from MyFirstModule.Photo, + DefaultImage: 'MyFirstModule.Images.gallery', + Width: 200, Height: 200 + ) + } + } +}; + +-- The properties the writer hardcoded beside the source: both size units were +-- always "Auto" and both display flags always false, so none was reachable from +-- MDL. Harmless while nothing described the widget; a silent normalisation on +-- every replay once something did. + +create or replace page MyFirstModule.DynImageThumbnail ( + Title: 'Thumbnail', Layout: Atlas_Core.Atlas_Default +) { + placeholder Main { + listview lvThumbs (DataSource: database from MyFirstModule.Photo) { + dynamicimage imgThumb ( + DataSource: database from MyFirstModule.Photo, + Width: 300, WidthUnit: pixels, + Height: 50, HeightUnit: percentage, + Responsive: false, + DisplayAs: thumbnail, + OnClickType: enlarge + ) + } + } +}; + +-- The CONTROL: a dynamic image with no source of its own must still describe as +-- a `dynamicimage`, with NO DataSource clause invented for it. Emitting one +-- would guess an entity, and a wrong guess is CE0489's sibling rather than a fix +-- for it. Auto units, a responsive full-size image and no enlarge are Mendix's +-- defaults that the writer re-derives, so describing them would put clauses in +-- the author's script they never wrote. +-- +-- This one still builds as CE0489 — deliberately. mxcli writes what it is told; +-- refusing an unbound source is a separate decision from round-tripping one. + +create or replace page MyFirstModule.DynImageUnbound ( + Title: 'Unbound', Layout: Atlas_Core.Atlas_Default +) { + placeholder Main { + listview lvUnbound (DataSource: database from MyFirstModule.Photo) { + dynamicimage imgUnbound (Width: 64, Height: 64) + } + } +}; diff --git a/mdl-examples/doctype-tests/08-security-examples.mdl b/mdl-examples/doctype-tests/08-security-examples.mdl index 58ca53eb4c..0fdfe2cf53 100644 --- a/mdl-examples/doctype-tests/08-security-examples.mdl +++ b/mdl-examples/doctype-tests/08-security-examples.mdl @@ -542,6 +542,24 @@ alter project security demo users on; show demo users; / +-- ============================================================================ +-- Level 7.3: Strict Mode +-- ============================================================================ + +/** + * Strict mode tightens XPath constraint enforcement at runtime (lint rule + * SEC005, which reports it as relevant to CVE-2023-23835). It is only + * MEANINGFUL at PRODUCTION security level — the rule deliberately stays quiet + * below that — but the setting is stored independently of the level, so it can + * be turned on before the level is raised. + */ +alter project security strict mode on; +/ + +-- And back off, so this script leaves the fixture as it found it. +alter project security strict mode off; +/ + -- ############################################################################ -- PART 8: DEMO USERS (CREATE/DROP) diff --git a/mdl-examples/doctype-tests/microflow-document-properties.mdl b/mdl-examples/doctype-tests/microflow-document-properties.mdl new file mode 100644 index 0000000000..d5a79a4602 --- /dev/null +++ b/mdl-examples/doctype-tests/microflow-document-properties.mdl @@ -0,0 +1,68 @@ +-- ============================================================================ +-- Microflow document properties — URL, export level, concurrent execution +-- ============================================================================ +-- +-- Four properties that live in the microflow HEADER rather than its body. All +-- were unauthorable until mendixlabs/mxcli#1120 made them survive a rewrite; +-- the clauses are what let a script set them deliberately, and what make +-- describe -> rename -> exec a faithful copy instead of an approximate one. +-- +-- An OMITTED clause PRESERVES what is stored, so clearing is always explicit: +-- DROP URL, EXPORT LEVEL HIDDEN, ALLOW CONCURRENT EXECUTION. + +CREATE OR MODIFY ENTITY Deeplink.Order ( + OrderNumber: String(20) +); + +-- Everything at once. $Order is the PATH parameter, $Tab the query argument — +-- they must be different parameters (CE5612 / MDL-MF02). +CREATE OR MODIFY MICROFLOW Deeplink.ACT_ShowOrder ($Order: Deeplink.Order, $Tab: String) +URL 'order/{Order}' +URL SEARCH PARAMETERS ($Tab) +EXPORT LEVEL API +DISALLOW CONCURRENT EXECUTION ERROR MESSAGE 'This order is already being processed' +BEGIN + LOG INFO $Tab; +END; + +-- The error handler can be a microflow instead of a message. +CREATE OR MODIFY MICROFLOW Deeplink.ACT_OnBusy () +BEGIN + LOG WARNING 'busy'; +END; + +CREATE OR MODIFY MICROFLOW Deeplink.ACT_Import () +DISALLOW CONCURRENT EXECUTION ERROR MICROFLOW Deeplink.ACT_OnBusy +BEGIN + LOG INFO 'importing'; +END; + +-- A segment may carry an attribute path: {Order/OrderNumber} binds the ORDER +-- parameter by one of its attributes, so the leading identifier is what has to +-- name a parameter. +CREATE OR MODIFY MICROFLOW Deeplink.ACT_ShowByNumber ($Order: Deeplink.Order) +URL 'order/by-number/{Order/OrderNumber}' +BEGIN + LOG INFO 'by number'; +END; + +-- Re-running a statement that mentions NONE of the clauses must leave all four +-- alone. This is the #1120 rule, and the reason every clause is optional. +CREATE OR MODIFY MICROFLOW Deeplink.ACT_ShowOrder ($Order: Deeplink.Order, $Tab: String) +BEGIN + LOG INFO 'body changed, header untouched'; +END; + +-- Clearing is explicit and each clause has its own form. +CREATE OR MODIFY MICROFLOW Deeplink.ACT_Import () +EXPORT LEVEL HIDDEN +ALLOW CONCURRENT EXECUTION +BEGIN + LOG INFO 'importing'; +END; + +CREATE OR MODIFY MICROFLOW Deeplink.ACT_ShowByNumber ($Order: Deeplink.Order) +DROP URL +BEGIN + LOG INFO 'no longer deep-linked'; +END; diff --git a/mdl/ast/ast_microflow.go b/mdl/ast/ast_microflow.go index 84b423b918..35bd0f0ad2 100644 --- a/mdl/ast/ast_microflow.go +++ b/mdl/ast/ast_microflow.go @@ -78,6 +78,39 @@ type CreateMicroflowStmt struct { // entries — one for the microflow editor, one for the workflow editor — so // there can be one of each. Expose []ExposeActionClause + + // URL is the deep link (Mendix 10.6+), e.g. `item/{Key}`. A POINTER for the + // same reason as ApplyEntityAccess: absent means "the script does not say", + // which preserves what is stored, while DROP URL sets an explicit empty. + URL *string + // URLSearchParameters are the parameter names named by `URL SEARCH + // PARAMETERS (...)`, without the `$`. Nil means the clause was absent; + // non-nil and empty means it was stated with an empty list, which clears. + URLSearchParameters *[]string + // ExportLevel is "API" or "Hidden"; nil preserves. + ExportLevel *string + // Concurrency is the DISALLOW/ALLOW CONCURRENT EXECUTION clause; nil + // preserves what is stored. + Concurrency *ConcurrencyClause +} + +// ConcurrencyClause is one DISALLOW/ALLOW CONCURRENT EXECUTION clause. +// +// Mendix requires an error message or an error microflow when execution is +// disallowed (CE4899). The grammar accepts the bare DISALLOW so that the +// omission is reported by name rather than as a parse error; the check is +// types.CheckMicroflowConcurrency. +type ConcurrencyClause struct { + // Allow is true for ALLOW CONCURRENT EXECUTION. + Allow bool + // ErrorMessage is the text shown to the second caller. Only one of + // ErrorMessage / ErrorMicroflow is set. + ErrorMessage string + // ErrorMessageSet distinguishes an omitted message from an empty one, so a + // stored message with translations is not silently replaced by "". + ErrorMessageSet bool + // ErrorMicroflow is the qualified name of the microflow that handles it. + ErrorMicroflow string } // ExposeActionClause is one EXPOSED AS ACTION clause, or its NOT form. diff --git a/mdl/ast/ast_security.go b/mdl/ast/ast_security.go index ab0bad5628..3aa17531ae 100644 --- a/mdl/ast/ast_security.go +++ b/mdl/ast/ast_security.go @@ -201,6 +201,10 @@ type AlterProjectSecurityStmt struct { // ROLE clause on GUEST ACCESS ON. Empty means "keep whatever is stored" — // never "clear it"; the executor refuses ON when nothing is stored either. GuestUserRole string + // StrictModeEnabled is set for ALTER PROJECT SECURITY STRICT MODE ON/OFF. + // A pointer, so "the statement said nothing about it" is distinguishable + // from "the statement asked for off". + StrictModeEnabled *bool } func (s *AlterProjectSecurityStmt) isStatement() {} diff --git a/mdl/backend/mcp/unsupported_gen.go b/mdl/backend/mcp/unsupported_gen.go index 929833b4e2..24c4dd5893 100644 --- a/mdl/backend/mcp/unsupported_gen.go +++ b/mdl/backend/mcp/unsupported_gen.go @@ -1132,6 +1132,11 @@ func (unsupportedBackend) SetProjectSecurityLevel(_ model.ID, _ string) (err0 er return } +func (unsupportedBackend) SetProjectStrictMode(_ model.ID, _ bool) (err0 error) { + err0 = errUnsupported("SetProjectStrictMode") + return +} + func (unsupportedBackend) UpdateAgentEditorAgent(_ *agenteditor.Agent) (err0 error) { err0 = errUnsupported("UpdateAgentEditorAgent") return diff --git a/mdl/backend/mock/backend.go b/mdl/backend/mock/backend.go index 30a8874d80..9b0798f648 100644 --- a/mdl/backend/mock/backend.go +++ b/mdl/backend/mock/backend.go @@ -149,6 +149,7 @@ type MockBackend struct { GetProjectSecurityFunc func() (*security.ProjectSecurity, error) SetProjectSecurityLevelFunc func(unitID model.ID, level string) error SetProjectDemoUsersEnabledFunc func(unitID model.ID, enabled bool) error + SetProjectStrictModeFunc func(unitID model.ID, enabled bool) error SetProjectGuestAccessFunc func(unitID model.ID, enabled bool, guestUserRole string) error AddUserRoleFunc func(unitID model.ID, name string, moduleRoles []string, manageAllRoles bool) error AlterUserRoleModuleRolesFunc func(unitID model.ID, userRoleName string, add bool, moduleRoles []string) error diff --git a/mdl/backend/mock/mock_security.go b/mdl/backend/mock/mock_security.go index 47691ee600..a7e6674709 100644 --- a/mdl/backend/mock/mock_security.go +++ b/mdl/backend/mock/mock_security.go @@ -32,6 +32,13 @@ func (m *MockBackend) SetProjectDemoUsersEnabled(unitID model.ID, enabled bool) return nil } +func (m *MockBackend) SetProjectStrictMode(unitID model.ID, enabled bool) error { + if m.SetProjectStrictModeFunc != nil { + return m.SetProjectStrictModeFunc(unitID, enabled) + } + return fmt.Errorf("MockBackend.SetProjectStrictMode not configured") +} + func (m *MockBackend) SetProjectGuestAccess(unitID model.ID, enabled bool, guestUserRole string) error { if m.SetProjectGuestAccessFunc != nil { return m.SetProjectGuestAccessFunc(unitID, enabled, guestUserRole) diff --git a/mdl/backend/modelsdk/domainmodel_security_write.go b/mdl/backend/modelsdk/domainmodel_security_write.go index f7ba8fdbeb..194e54badc 100644 --- a/mdl/backend/modelsdk/domainmodel_security_write.go +++ b/mdl/backend/modelsdk/domainmodel_security_write.go @@ -335,9 +335,12 @@ func sameStringSet(a, b []string) bool { // into sync with its entity's current members: it adds a MemberAccess for each // attribute, each FROM-side association (regular + cross), and each implicit // system association (System.owner / System.changedBy); removes stale entries -// for members that no longer exist; and downgrades write rights on calculated -// attributes (CE6592). It mirrors the legacy writer's reconcile and is invoked -// by the executor's finalize step after every program run. +// for members that no longer exist; and downgrades write rights on the members +// that may not carry them — calculated attributes AND autonumbers, both CE6592. +// It mirrors the legacy writer's reconcile and is invoked by the executor's +// finalize step after every program run, which is why the autonumber half +// mattered here as much as at the GRANT: a correct grant was re-broken by the +// next write touching the module (ako/mxcli#524). // // Rules with no MemberAccesses yet (a fresh, empty rule) are left untouched — // matching legacy; those are populated at create time by the inline sync in @@ -404,13 +407,16 @@ func (b *Backend) ReconcileMemberAccesses(unitID model.ID, moduleName string) (i // dropped the attribute — is still preserved rather than removed; that is // the opposite direction from this defect and #1047's own control reports // "+1 added, -0 removed". + // noWrite folds the two CE6592 causes — calculated and autonumber — + // into the one question the downgrades below ask. Keeping them apart + // here is what let the autonumber half go missing (ako/mxcli#524). type attrInfo struct { - qn string - calc bool + qn string + noWrite bool } var attrs []attrInfo attrSet := map[string]bool{} - calcSet := map[string]bool{} + noWriteSet := map[string]bool{} claimed := map[string]bool{} collectAttrs := func(owner *genDm.Entity, ownerName string) { for _, ae := range owner.AttributesItems() { @@ -424,10 +430,12 @@ func (b *Backend) ReconcileMemberAccesses(unitID model.ID, moduleName string) (i claimed[a.Name()] = true qn := moduleName + "." + ownerName + "." + a.Name() _, isCalc := a.Value().(*genDm.CalculatedValue) - attrs = append(attrs, attrInfo{qn, isCalc}) + _, isAuto := a.Type().(*genDm.AutoNumberAttributeType) + noWrite := types.WriteRightsForbidden(isCalc, isAuto) + attrs = append(attrs, attrInfo{qn, noWrite}) attrSet[qn] = true - if isCalc { - calcSet[qn] = true + if noWrite { + noWriteSet[qn] = true } } } @@ -523,7 +531,7 @@ func (b *Backend) ReconcileMemberAccesses(unitID model.ID, moduleName string) (i switch { case attrSet[attrRef]: covAttr[attrRef] = true - if calcSet[attrRef] { + if noWriteSet[attrRef] { if r := ma.AccessRights(); r == "ReadWrite" || r == "WriteOnly" { ma.SetAccessRights("ReadOnly") changed = true @@ -577,7 +585,7 @@ func (b *Backend) ReconcileMemberAccesses(unitID model.ID, moduleName string) (i continue } rights := defRights - if ai.calc && (rights == "ReadWrite" || rights == "WriteOnly") { + if ai.noWrite && (rights == "ReadWrite" || rights == "WriteOnly") { rights = "ReadOnly" } rule.AddMemberAccesses(newMemberAccess(rights, ai.qn, true)) diff --git a/mdl/backend/modelsdk/reconcile_autonumber_test.go b/mdl/backend/modelsdk/reconcile_autonumber_test.go new file mode 100644 index 0000000000..aac07dc260 --- /dev/null +++ b/mdl/backend/modelsdk/reconcile_autonumber_test.go @@ -0,0 +1,127 @@ +// SPDX-License-Identifier: Apache-2.0 + +// ako/mxcli#524: `grant write *` on an entity carrying an autonumber wrote +// ReadWrite on it and the build failed CE6592, so the user had to narrow the +// grant with a hand-written REVOKE. +// +// The GRANT path is only half of it. ReconcileMemberAccesses runs on the +// executor's finalize step after EVERY program, so a grant corrected by hand +// was re-broken by the next write touching the module. Both halves shared one +// defect: the downgrade asked whether an attribute was CALCULATED, which an +// autonumber is not — it carries no DomainModels$CalculatedValue, because its +// value comes from the database rather than from a microflow. +package modelsdkbackend + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/domainmodel" +) + +// memberRights maps each member reference in an entity's first access rule to +// the rights it carries. memberRefs (reconcile_inherited_assoc_test.go) reports +// presence only, and presence is not the question here. +func memberRights(t *testing.T, b *Backend, modID model.ID, entityName string) map[string]string { + t.Helper() + dm, err := b.GetDomainModel(modID) + if err != nil { + t.Fatalf("GetDomainModel: %v", err) + } + for _, e := range dm.Entities { + if e.Name != entityName { + continue + } + if len(e.AccessRules) == 0 { + t.Fatalf("entity %s has no access rule", entityName) + } + out := map[string]string{} + for _, ma := range e.AccessRules[0].MemberAccesses { + if ma.AttributeName != "" { + out[ma.AttributeName] = string(ma.AccessRights) + } + } + return out + } + t.Fatalf("entity %s not found", entityName) + return nil +} + +// autoNumberFixture builds one entity with the three attribute shapes that +// matter to CE6592: a plain string, an autonumber, and a calculated decimal. +func autoNumberFixture(t *testing.T) (*Backend, *model.Module, *domainmodel.DomainModel) { + t.Helper() + b, mod, dm := inheritanceFixture(t) + + ent := &domainmodel.Entity{Name: "ZzAutoNum", Persistable: true, + Attributes: []*domainmodel.Attribute{ + {Name: "Description", Type: &domainmodel.StringAttributeType{Length: 200}}, + {Name: "RequestNumber", Type: &domainmodel.AutoNumberAttributeType{}, + Value: &domainmodel.AttributeValue{Type: "StoredValue", DefaultValue: "1001"}}, + {Name: "TotalCost", Type: &domainmodel.DecimalAttributeType{}, + Value: &domainmodel.AttributeValue{ + Type: "CalculatedValue", MicroflowName: "MyFirstModule.CalcTotal"}}, + }} + if err := b.CreateEntity(dm.ID, ent); err != nil { + t.Fatalf("CreateEntity ZzAutoNum: %v", err) + } + return b, mod, dm +} + +// A rule that already carries ReadWrite on the autonumber — the state the GRANT +// used to produce — must be downgraded, not preserved. +func TestReconcile_DowngradesWriteOnAnExistingAutoNumberEntry(t *testing.T) { + b, mod, dm := autoNumberFixture(t) + + grantAll(t, b, dm.ID, "ZzAutoNum", []types.EntityMemberAccess{ + {AttributeRef: "MyFirstModule.ZzAutoNum.Description", AccessRights: "ReadWrite"}, + {AttributeRef: "MyFirstModule.ZzAutoNum.RequestNumber", AccessRights: "ReadWrite"}, + {AttributeRef: "MyFirstModule.ZzAutoNum.TotalCost", AccessRights: "ReadWrite"}, + }) + + if _, err := b.ReconcileMemberAccesses(dm.ID, mod.Name); err != nil { + t.Fatalf("ReconcileMemberAccesses: %v", err) + } + + rights := memberRights(t, b, mod.ID, "ZzAutoNum") + if got := rights["MyFirstModule.ZzAutoNum.RequestNumber"]; got != "ReadOnly" { + t.Errorf("autonumber rights = %q, want ReadOnly — write on an autonumber is CE6592 (all: %v)", got, rights) + } + // Control: the calculated attribute was already downgraded before the fix, + // and the plain one must keep its write. Without the second, a blanket + // "downgrade everything" passes this test while stripping the model. + if got := rights["MyFirstModule.ZzAutoNum.TotalCost"]; got != "ReadOnly" { + t.Errorf("calculated rights = %q, want ReadOnly", got) + } + if got := rights["MyFirstModule.ZzAutoNum.Description"]; got != "ReadWrite" { + t.Errorf("plain attribute rights = %q, want ReadWrite — reconcile must not strip granted write", got) + } +} + +// The other direction: an autonumber MISSING from the rule is added by +// reconcile, and must arrive as ReadOnly rather than inheriting the rule's +// ReadWrite default. This is the path that re-broke a hand-corrected grant. +func TestReconcile_AddsAMissingAutoNumberAsReadOnly(t *testing.T) { + b, mod, dm := autoNumberFixture(t) + + // The shape a user reached by hand: write granted, then REVOKEd off the + // autonumber, so the rule simply has no entry for it. + grantAll(t, b, dm.ID, "ZzAutoNum", []types.EntityMemberAccess{ + {AttributeRef: "MyFirstModule.ZzAutoNum.Description", AccessRights: "ReadWrite"}, + }) + + if _, err := b.ReconcileMemberAccesses(dm.ID, mod.Name); err != nil { + t.Fatalf("ReconcileMemberAccesses: %v", err) + } + + rights := memberRights(t, b, mod.ID, "ZzAutoNum") + got, ok := rights["MyFirstModule.ZzAutoNum.RequestNumber"] + if !ok { + t.Fatalf("reconcile did not add the autonumber at all (CE0066): %v", rights) + } + if got != "ReadOnly" { + t.Errorf("added autonumber rights = %q, want ReadOnly — the rule default is ReadWrite, "+ + "so an added entry that inherits it is CE6592", got) + } +} diff --git a/mdl/backend/modelsdk/security_write.go b/mdl/backend/modelsdk/security_write.go index d8abd30adf..522520d260 100644 --- a/mdl/backend/modelsdk/security_write.go +++ b/mdl/backend/modelsdk/security_write.go @@ -184,6 +184,21 @@ func (b *Backend) SetProjectDemoUsersEnabled(unitID model.ID, enabled bool) erro return b.persistUnit(unitID, ps) } +// SetProjectStrictMode toggles security strict mode. +// +// StrictMode is a plain bool declared by BOTH generated sources — gen binds it +// as property.NewPrimitive[bool]("StrictMode") and generated/metamodel declares +// it on ProjectSecurity — and mxcli already reads it back from real projects, +// so this writes a property Studio Pro knows rather than one gen merely offers. +func (b *Backend) SetProjectStrictMode(unitID model.ID, enabled bool) error { + ps, err := b.loadProjectSecurityGen(unitID) + if err != nil { + return err + } + ps.SetStrictMode(enabled) + return b.persistUnit(unitID, ps) +} + // SetProjectGuestAccess toggles anonymous (guest) access. An empty // guestUserRole leaves the stored role alone, so turning access off and back on // does not lose it. diff --git a/mdl/backend/modelsdk/unimplemented_gen.go b/mdl/backend/modelsdk/unimplemented_gen.go index 43f6f7907a..4fbbbd04f9 100644 --- a/mdl/backend/modelsdk/unimplemented_gen.go +++ b/mdl/backend/modelsdk/unimplemented_gen.go @@ -1026,6 +1026,10 @@ func (unimplemented) SetProjectSecurityLevel(_ model.ID, _ string) error { return errUnimplemented("SetProjectSecurityLevel") } +func (unimplemented) SetProjectStrictMode(_ model.ID, _ bool) error { + return errUnimplemented("SetProjectStrictMode") +} + func (unimplemented) UpdateAgentEditorAgent(_ *agenteditor.Agent) error { return errUnimplemented("UpdateAgentEditorAgent") } diff --git a/mdl/backend/modelsdk/widget_flow_param_variable_test.go b/mdl/backend/modelsdk/widget_flow_param_variable_test.go new file mode 100644 index 0000000000..371e5e6a9f --- /dev/null +++ b/mdl/backend/modelsdk/widget_flow_param_variable_test.go @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: Apache-2.0 + +package modelsdkbackend + +import ( + "testing" + + bsonv1 "go.mongodb.org/mongo-driver/bson" + + "github.com/mendixlabs/mxcli/sdk/pages" +) + +// mendixlabs/mxcli#1140 — a page parameter passed as an argument to a nanoflow or +// microflow button action was written as a text Expression ("$BufferDefinition"). +// Studio Pro binds such an argument through Variable → Forms$PageVariable and +// reports CE1571 "No argument has been selected for parameter 'X' and no default +// is available" for the Expression form. mxbuild accepts it at 0 errors, so the +// build is not a safety net here — the error appears only on opening the page. +// +// Measured on Workflow Commons 4.11.0 (Studio Pro-authored, 42 pages + 84 +// snippets): of 101 Forms$MicroflowParameterMapping / Forms$NanoflowParameterMapping +// elements, 95 bind through Variable → Forms$PageVariable and 6 through Expression +// — and all 6 of those are Boolean literals ("true\n", "false\n"). A $-prefixed +// Expression, the only form mxcli emitted, occurs zero times. +// +// The PageVariable slot follows what the name refers to: PageParameter (20), +// SnippetParameter (58) and Widget (17, a grid's selection — no MDL syntax). + +// paramVariable returns the Variable sub-document of a parameter mapping, or nil. +func paramVariable(pm bsonv1.D) bsonv1.D { + v, ok := docGet(pm, "Variable").(bsonv1.D) + if !ok { + return nil + } + return v +} + +func TestNanoflowActionBindsPageParameterThroughVariable(t *testing.T) { + a := &pages.NanoflowClientAction{ + NanoflowName: "CustomModule.ACT_BufferDefinition_SaveEdit_NF", + ParameterMappings: []*pages.NanoflowParameterMapping{ + {ParameterName: "BufferDefinition", Variable: "$BufferDefinition", VariableKind: "parameter"}, + }, + } + pm := firstParamMapping(t, encodeAction(t, a)) + + if got := docGet(pm, "Parameter"); got != "CustomModule.ACT_BufferDefinition_SaveEdit_NF.BufferDefinition" { + t.Errorf("Parameter = %v", got) + } + v := paramVariable(pm) + if v == nil { + t.Fatalf("mapping has no Variable — the argument is unbound and Studio Pro reports CE1571.\n"+ + "Expression = %v", docGet(pm, "Expression")) + } + if got := docGet(v, "$Type"); got != "Forms$PageVariable" { + t.Errorf("Variable $Type = %v, want Forms$PageVariable", got) + } + if got := docGet(v, "PageParameter"); got != "BufferDefinition" { + t.Errorf("Variable.PageParameter = %v, want BufferDefinition", got) + } + if got := docGet(pm, "Expression"); got == "$BufferDefinition" { + t.Errorf("Expression = %q — the $-reference must not be written as an expression", got) + } +} + +func TestMicroflowActionBindsPageParameterThroughVariable(t *testing.T) { + a := &pages.MicroflowClientAction{ + MicroflowName: "CustomModule.ACT_Save", + ParameterMappings: []*pages.MicroflowParameterMapping{ + {ParameterName: "BufferDefinition", Variable: "$BufferDefinition", VariableKind: "parameter"}, + }, + } + settings, ok := docGet(encodeAction(t, a), "MicroflowSettings").(bsonv1.D) + if !ok { + t.Fatalf("MicroflowSettings missing") + } + pm := firstParamMapping(t, settings) + + v := paramVariable(pm) + if v == nil { + t.Fatalf("mapping has no Variable — CE1571. Expression = %v", docGet(pm, "Expression")) + } + if got := docGet(v, "PageParameter"); got != "BufferDefinition" { + t.Errorf("Variable.PageParameter = %v, want BufferDefinition", got) + } +} + +// A snippet parameter fills a different slot of the same Forms$PageVariable — +// 58 of the 95 Studio Pro bindings are this one. +func TestFlowActionBindsSnippetParameterThroughVariable(t *testing.T) { + a := &pages.NanoflowClientAction{ + NanoflowName: "WorkflowCommons.ACT_AuditTrailViewer_Minimal", + ParameterMappings: []*pages.NanoflowParameterMapping{ + {ParameterName: "AuditTrailViewer", Variable: "$AuditTrailViewer", VariableKind: "snippet"}, + }, + } + v := paramVariable(firstParamMapping(t, encodeAction(t, a))) + if v == nil { + t.Fatal("mapping has no Variable") + } + if got := docGet(v, "SnippetParameter"); got != "AuditTrailViewer" { + t.Errorf("Variable.SnippetParameter = %v, want AuditTrailViewer", got) + } + if got := docGet(v, "PageParameter"); got == "AuditTrailViewer" { + t.Errorf("a snippet parameter was written into the PageParameter slot") + } +} + +// CONTROL — an argument that is NOT a page-variable reference must still be +// written as an Expression, with no Variable. All six Expression-bound mappings +// in the reference are literals of this shape, and a fix that routed everything +// through Variable would pass the tests above and break them. +func TestFlowActionKeepsLiteralArgumentAsExpression(t *testing.T) { + a := &pages.MicroflowClientAction{ + MicroflowName: "WorkflowCommons.ACT_TaskAssignmentHelper_Reassign", + ParameterMappings: []*pages.MicroflowParameterMapping{ + {ParameterName: "KeepAsTargetUser", Expression: "true"}, + }, + } + settings, _ := docGet(encodeAction(t, a), "MicroflowSettings").(bsonv1.D) + pm := firstParamMapping(t, settings) + + if got := docGet(pm, "Expression"); got != "true" { + t.Errorf("Expression = %v, want true", got) + } + if v := paramVariable(pm); v != nil { + t.Errorf("a literal argument was given a Variable: %v", v) + } +} + +// CONTROL — $currentObject is deliberately left as an Expression. No Studio Pro +// reference for the bare form was measured, and the enclosing-context binding is +// what mxcli already relies on elsewhere; changing it without evidence would put +// the working case at risk (#1140 is about a NAMED page parameter). +func TestFlowActionLeavesCurrentObjectAsExpression(t *testing.T) { + a := &pages.NanoflowClientAction{ + NanoflowName: "M.NF", + ParameterMappings: []*pages.NanoflowParameterMapping{ + {ParameterName: "Obj", Variable: "$currentObject"}, + }, + } + pm := firstParamMapping(t, encodeAction(t, a)) + if got := docGet(pm, "Expression"); got != "$currentObject" { + t.Errorf("Expression = %v, want $currentObject", got) + } + if v := paramVariable(pm); v != nil { + t.Errorf("$currentObject was bound through Variable: %v", v) + } +} diff --git a/mdl/backend/modelsdk/widget_write.go b/mdl/backend/modelsdk/widget_write.go index b40fa82a20..2906d8cf94 100644 --- a/mdl/backend/modelsdk/widget_write.go +++ b/mdl/backend/modelsdk/widget_write.go @@ -384,7 +384,7 @@ func widgetToGen(w pages.Widget) (element.Element, error) { g.SetAutoFocus(false) g.SetAutocomplete(true) g.SetAutocompletePurpose("On") - if ref := attributeRefToGen(x.AttributePath); ref != nil { + if ref := inputAttributeRefToGen(x.AttributePath, x.AttributeRefSteps); ref != nil { g.SetAttributeRef(ref) } g.SetEditable(pages.WidgetEditability(&x.BaseWidget)) @@ -449,7 +449,7 @@ func widgetToGen(w pages.Widget) (element.Element, error) { case *pages.CheckBox: g := genPg.NewCheckBox() applyWidgetBase(g, &x.BaseWidget) - if ref := attributeRefToGen(x.AttributePath); ref != nil { + if ref := inputAttributeRefToGen(x.AttributePath, x.AttributeRefSteps); ref != nil { g.SetAttributeRef(ref) } g.SetEditable(pages.WidgetEditability(&x.BaseWidget)) @@ -474,7 +474,7 @@ func widgetToGen(w pages.Widget) (element.Element, error) { applyWidgetBase(g, &x.BaseWidget) g.SetAriaRequired(false) g.SetAutoFocus(false) - if ref := attributeRefToGen(x.AttributePath); ref != nil { + if ref := inputAttributeRefToGen(x.AttributePath, x.AttributeRefSteps); ref != nil { g.SetAttributeRef(ref) } g.SetCounterMessage(captionToGen(x.CounterMessage)) @@ -506,7 +506,7 @@ func widgetToGen(w pages.Widget) (element.Element, error) { g := genPg.NewDatePicker() applyWidgetBase(g, &x.BaseWidget) g.SetAriaRequired(false) - if ref := attributeRefToGen(x.AttributePath); ref != nil { + if ref := inputAttributeRefToGen(x.AttributePath, x.AttributeRefSteps); ref != nil { g.SetAttributeRef(ref) } g.SetEditable(pages.WidgetEditability(&x.BaseWidget)) @@ -529,7 +529,7 @@ func widgetToGen(w pages.Widget) (element.Element, error) { g := genPg.NewRadioButtonGroup() applyWidgetBase(g, &x.BaseWidget) g.SetAriaRequired(false) - if ref := attributeRefToGen(x.AttributePath); ref != nil { + if ref := inputAttributeRefToGen(x.AttributePath, x.AttributeRefSteps); ref != nil { g.SetAttributeRef(ref) } g.SetEditable(pages.WidgetEditability(&x.BaseWidget)) @@ -1183,6 +1183,30 @@ func attributeRefToGen(path string) element.Element { return r } +// inputAttributeRefToGen builds the AttributeRef for an input widget, carrying +// association hops when the binding navigates them. +// +// Studio Pro stores an attribute-over-association binding on a plain text box — +// measured on ako/TestApp's Rules.RuleAction_NewEdit, whose textBox4 holds +// Attribute "Rules.BusinessRule.Name" with an IndirectEntityRef over +// Rules.RuleAction_BusinessRule. mxcli could read that page and not write one: +// every input builder resolved the path with resolveAttributePath, which knows +// nothing about associations, so `attribute: Assoc/Attr` produced a flat +// unresolvable path and the build failed CE1613 (ako/mxcli#529). +// +// Steps with no attribute qualified name fall through to nil the same way +// attributeRefToGen does, rather than emitting an EntityRef hanging off +// nothing. +func inputAttributeRefToGen(path string, steps []pages.AttributeRefStep) element.Element { + if len(steps) == 0 { + return attributeRefToGen(path) + } + if strings.Count(path, ".") < 2 { + return nil + } + return attributeRefWithStepsToGen(path, steps) +} + // attributeRefWithStepsToGen builds a DomainModels$AttributeRef for an attribute // navigated over one or more associations: the final attribute qualified name // plus an EntityRef (DomainModels$IndirectEntityRef) of association hops. Reuses @@ -1492,6 +1516,49 @@ func associationSourceToGen(d *pages.AssociationSource) element.Element { return src } +// parameterMappingTarget is the half of Forms$MicroflowParameterMapping and +// Forms$NanoflowParameterMapping that carries an argument's value. The two gen +// types are unrelated Go types with identical shape, so the binding rule is +// written once against what they have in common rather than twice. +type parameterMappingTarget interface { + SetExpression(string) + SetVariable(element.Element) +} + +// bindParameterMappingValue writes an argument into whichever of the mapping's +// two value slots Mendix uses for it. +// +// A reference to a page parameter, snippet parameter or page variable is a +// Forms$PageVariable under Variable; a literal or expression is text under +// Expression. Measured on Workflow Commons 4.11.0 (Studio Pro-authored): 95 of +// 101 flow parameter mappings bind through Variable, the other 6 through +// Expression — and every one of those 6 is a Boolean literal. A $-prefixed +// Expression, which is all mxcli wrote before #1140, occurs zero times; it leaves +// the parameter unbound, so Studio Pro reports CE1571 while mxbuild builds the +// same document at 0 errors. +// +// kind empty means "not a page-variable reference": variable is then written as +// the expression, preserving what every caller before #1140 relied on — +// $currentObject among them, whose stored form has not been measured. +func bindParameterMappingValue(m parameterMappingTarget, variable, kind, expression string) { + if variable != "" && kind != "" { + // sourceVariableToGen spells the page-parameter slot as the empty kind. + svKind := kind + if svKind == "parameter" { + svKind = "" + } + m.SetVariable(sourceVariableToGen(strings.TrimPrefix(variable, "$"), svKind)) + // Studio Pro writes both keys, the unused one empty. + m.SetExpression("") + return + } + if variable != "" { + m.SetExpression(variable) + return + } + m.SetExpression(expression) +} + // microflowSettingsToGen builds the Forms$MicroflowSettings shared by the // microflow DataView source and the call-microflow action. mappings carries the // argument bindings — for an action's call, and (since #835) for a parameterized @@ -1510,12 +1577,7 @@ func microflowSettingsToGen(microflowName string, mappings []*pages.MicroflowPar assignID(gm) // Parameter is a BY_NAME reference: .. gm.SetParameterQualifiedName(microflowName + "." + pm.ParameterName) - // The bound value: a variable ref ($x, $currentObject) or an expression. - if pm.Variable != "" { - gm.SetExpression(pm.Variable) - } else { - gm.SetExpression(pm.Expression) - } + bindParameterMappingValue(gm, pm.Variable, pm.VariableKind, pm.Expression) s.AddParameterMappings(gm) } return s @@ -1674,11 +1736,7 @@ func clientActionToGen(a pages.ClientAction) (element.Element, error) { assignID(m) // Parameter is a BY_NAME reference: Nanoflow.ParamName. m.SetParameterQualifiedName(x.NanoflowName + "." + pm.ParameterName) - expr := pm.Variable - if expr == "" { - expr = pm.Expression - } - m.SetExpression(expr) + bindParameterMappingValue(m, pm.Variable, pm.VariableKind, pm.Expression) g.AddParameterMappings(m) } return g, nil diff --git a/mdl/backend/modelsdk/widget_write_legacy_gaps.go b/mdl/backend/modelsdk/widget_write_legacy_gaps.go index 39c940267e..89a3fa16b7 100644 --- a/mdl/backend/modelsdk/widget_write_legacy_gaps.go +++ b/mdl/backend/modelsdk/widget_write_legacy_gaps.go @@ -3,10 +3,12 @@ package modelsdkbackend import ( + "fmt" "strings" "github.com/mendixlabs/mxcli/modelsdk/codec" "github.com/mendixlabs/mxcli/modelsdk/element" + genDm "github.com/mendixlabs/mxcli/modelsdk/gen/domainmodels" genPg "github.com/mendixlabs/mxcli/modelsdk/gen/pages" "github.com/mendixlabs/mxcli/sdk/pages" ) @@ -71,7 +73,7 @@ func dropDownToGen(dd *pages.DropDown) (element.Element, error) { g := genPg.NewDropDown() applyWidgetBase(g, &dd.BaseWidget) g.SetAriaRequired(false) - if ref := attributeRefToGen(dd.AttributePath); ref != nil { + if ref := inputAttributeRefToGen(dd.AttributePath, dd.AttributeRefSteps); ref != nil { g.SetAttributeRef(ref) } g.SetEditable(pages.WidgetEditability(&dd.BaseWidget)) @@ -151,24 +153,70 @@ func dynamicImageToGen(img *pages.DynamicImage) (element.Element, error) { return nil, err } g.SetClickAction(click) - g.SetDataSource(imageViewerSourceToGen()) - g.SetDefaultImageQualifiedName("") + // The entity holding the image. Bound to nothing, mxbuild refuses the widget + // with CE0489 "Select an entity for the data source of this dynamic image", + // so this is the difference between a widget that builds and one that does + // not — not a fidelity nicety. + source, err := imageViewerSourceToGen(img.DataSource) + if err != nil { + return nil, err + } + g.SetDataSource(source) + // The fallback image, as the qualified name of an image-collection entry. + // Unset is "", not null; see the header. + g.SetDefaultImageQualifiedName(img.DefaultImageName) g.SetHeight(int32(img.Height)) - g.SetHeightUnit("Auto") - g.SetOnClickEnlarge(false) + g.SetHeightUnit(imageSizeUnit(img.HeightUnit)) + g.SetOnClickEnlarge(img.OnClickEnlarge) g.SetResponsive(img.Responsive) - g.SetShowAsThumbnail(false) + g.SetShowAsThumbnail(img.ShowAsThumbnail) g.SetWidth(int32(img.Width)) - g.SetWidthUnit("Auto") + g.SetWidthUnit(imageSizeUnit(img.WidthUnit)) return g, nil } -// imageViewerSourceToGen builds the empty Forms$ImageViewerSource a dynamic -// image carries when no entity path has been set. -func imageViewerSourceToGen() element.Element { +// imageViewerSourceToGen builds the Forms$ImageViewerSource a dynamic image +// binds through. The source names an ENTITY and nothing else: unlike its +// list-widget siblings it declares no XPath constraint and no sort bar, so only +// an entity-backed source has anywhere to go here. +// +// Its EntityRef is a DomainModels$DirectEntityRef{Entity: "Module.Entity"} — +// pinned to Studio Pro at 20 of 20 instances in a blank 11.12.1 app — and it is +// the element mxbuild's CE0489 is asking for. +// +// A NIL source yields the bare element. That is what mxcli wrote for every +// dynamic image until now and what mxbuild flags as CE0489, and it stays the +// behaviour on purpose: describe emits no DataSource clause for a stored widget +// that has none, so refusing here would make describe -> exec fail on a model +// that already exists (guard-don't-drop, ADR-0005). +// +// Any OTHER source is refused rather than ignored. Forms$ImageViewerSource has +// no slot for a microflow, a nanoflow or an association, and the metamodel's +// context-path variants (EntityPath / SourceVariable) have no Studio Pro +// reference here to pin them against. Writing the holder without them would +// produce CE0489 — a message that says the author forgot the source when they +// did not — so the gap is named at the point it is hit instead. +func imageViewerSourceToGen(ds pages.DataSource) (element.Element, error) { src := genPg.NewImageViewerSource() assignID(src) - return src + src.SetForceFullObjects(false) + switch d := ds.(type) { + case nil: + return src, nil + case *pages.DatabaseSource: + if d.EntityName != "" { + ref := genDm.NewDirectEntityRef() + assignID(ref) + ref.SetEntityQualifiedName(d.EntityName) + src.SetEntityRef(ref) + } + return src, nil + default: + return nil, fmt.Errorf("dynamicimage: a %T data source cannot be stored on a "+ + "Forms$ImageViewerSource, which holds an entity and nothing else — use "+ + "`DataSource: database from Module.Entity` naming the entity that holds "+ + "the image", ds) + } } // nanoflowSourceToGen builds a Forms$NanoflowSource — a list widget's "nanoflow" diff --git a/mdl/backend/modelsdk/widget_write_legacy_gaps_test.go b/mdl/backend/modelsdk/widget_write_legacy_gaps_test.go index 51d221bfbd..8f16efd977 100644 --- a/mdl/backend/modelsdk/widget_write_legacy_gaps_test.go +++ b/mdl/backend/modelsdk/widget_write_legacy_gaps_test.go @@ -178,6 +178,131 @@ func TestDynamicImageMatchesMetamodelShape(t *testing.T) { } } +// The dynamic image's DataSource was built by imageViewerSourceToGen(), which +// took no arguments and wrote a source bound to nothing. mxbuild 11.12.1 refuses +// that outright: +// +// [error] [CE0489] "Select an entity for the data source of this dynamic +// image." at Dynamic image 'imgPhoto' +// +// so this is not a round-trip gap like the static image's — every dynamic image +// mxcli ever wrote failed the build. DomainModels$DirectEntityRef{Entity: "…"} +// is the element it wants, pinned to Studio Pro at 20 of 20 instances in a blank +// 11.12.1 app (there is no Studio Pro dynamic image in that app to pin the +// widget itself against, so the rest of this shape is metamodel-derived). +func TestDynamicImageWritesTheDataSourceEntity(t *testing.T) { + const entity = "MyFirstModule.Photo" + img := &pages.DynamicImage{ + Responsive: true, + DataSource: &pages.DatabaseSource{EntityName: entity}, + } + img.Name = "imgPhoto" + + doc := encodeWidget(t, img) + assertKeys(t, doc, dynamicImageKeys) + + src, ok := docGet(doc, "DataSource").(bsonv1.D) + if !ok { + t.Fatalf("DataSource = %T, want a Forms$ImageViewerSource", docGet(doc, "DataSource")) + } + if got := docGet(src, "$Type"); got != "Forms$ImageViewerSource" { + t.Fatalf("DataSource.$Type = %v", got) + } + ref, ok := docGet(src, "EntityRef").(bsonv1.D) + if !ok { + t.Fatalf("EntityRef = %#v, want a DomainModels$DirectEntityRef — CE0489 without it", + docGet(src, "EntityRef")) + } + if got := docGet(ref, "$Type"); got != "DomainModels$DirectEntityRef" { + t.Errorf("EntityRef.$Type = %v, want DomainModels$DirectEntityRef", got) + } + if got := docGet(ref, "Entity"); got != entity { + t.Errorf("EntityRef.Entity = %#v, want %q", got, entity) + } +} + +// The remaining properties the writer hardcoded: the fallback image (always ""), +// both size units (always "Auto"), and the two display flags (always false). None +// was reachable from MDL, and each is now a silent normalisation on replay rather +// than a visible note, since DESCRIBE emits this widget as re-executable MDL. +func TestDynamicImageWritesFallbackAndDisplayFlags(t *testing.T) { + const fallback = "MyFirstModule.Images.placeholder" + img := &pages.DynamicImage{ + DataSource: &pages.DatabaseSource{EntityName: "MyFirstModule.Photo"}, + DefaultImageName: fallback, + Width: 300, + WidthUnit: "pixels", + Height: 50, + HeightUnit: "percentage", + ShowAsThumbnail: true, + OnClickEnlarge: true, + } + img.Name = "imgPhoto" + + doc := encodeWidget(t, img) + for _, c := range []struct { + key string + want any + }{ + {"DefaultImage", fallback}, + {"WidthUnit", "Pixels"}, + {"HeightUnit", "Percentage"}, + {"ShowAsThumbnail", true}, + {"OnClickEnlarge", true}, + } { + if got := docGet(doc, c.key); got != c.want { + t.Errorf("%s = %#v, want %#v", c.key, got, c.want) + } + } +} + +// A source Forms$ImageViewerSource cannot hold is REFUSED, not ignored. Writing +// the holder without it produces CE0489 — "Select an entity for the data source" +// — which tells the author they forgot something they did not forget. There is no +// check-time rule for this (measured: nothing in validate_widget*.go constrains a +// dynamicimage's source), so the writer is the only place that can say it. +func TestDynamicImageRefusesASourceItCannotStore(t *testing.T) { + img := &pages.DynamicImage{ + DataSource: &pages.MicroflowSource{Microflow: "MyFirstModule.DS_Photo"}, + } + img.Name = "imgPhoto" + + _, err := dynamicImageToGen(img) + if err == nil { + t.Fatal("a microflow source was accepted and silently dropped — the author gets CE0489 instead") + } + for _, want := range []string{"database from", "entity"} { + if !strings.Contains(err.Error(), want) { + t.Errorf("the refusal does not name %q, so it cannot be acted on: %v", want, err) + } + } +} + +// The CONTROL: an unset fallback is the empty string, never null (measured +// across ako/TestApp: 0 nulls against 4,400+ empty strings for by-name +// references), the units default to Studio Pro's "Auto", and the flags to false. +// Without this the tests above could be satisfied by writing whatever came in. +func TestDynamicImageUnsetValuesKeepMendixDefaults(t *testing.T) { + img := &pages.DynamicImage{Responsive: true} + img.Name = "imgBare" + + doc := encodeWidget(t, img) + for _, c := range []struct { + key string + want any + }{ + {"DefaultImage", ""}, + {"WidthUnit", "Auto"}, + {"HeightUnit", "Auto"}, + {"ShowAsThumbnail", false}, + {"OnClickEnlarge", false}, + } { + if got := docGet(doc, c.key); got != c.want { + t.Errorf("%s = %#v for an unset field, want %#v", c.key, got, c.want) + } + } +} + func TestDropDownMatchesMetamodelShape(t *testing.T) { dd := &pages.DropDown{} dd.Name = "d1" diff --git a/mdl/backend/pagemutator/documentation_test.go b/mdl/backend/pagemutator/documentation_test.go new file mode 100644 index 0000000000..52a029dbce --- /dev/null +++ b/mdl/backend/pagemutator/documentation_test.go @@ -0,0 +1,122 @@ +// SPDX-License-Identifier: Apache-2.0 + +// ako/mxcli#527: ALTER PAGE could not set Documentation, so documenting an +// existing page meant re-running its CREATE — which for a real page means +// re-emitting its whole widget tree, and a describe → exec round trip is only as +// complete as what MDL can spell. +package pagemutator + +import ( + "strings" + "testing" + + "go.mongodb.org/mongo-driver/bson" + + "github.com/mendixlabs/mxcli/mdl/backend/bsonnav" +) + +// Documentation is a plain top-level string on the document, the same shape as +// Url — gen binds it as property.NewPrimitive[string]("Documentation") on Page, +// Layout and Snippet alike, and pageToGen has always written it on CREATE. +func TestSetPageLevel_Documentation(t *testing.T) { + m := &Mutator{rawData: makeRawPage(), widgetFinder: findBsonWidget} + + const doc = "Triage step. Coordinator sets priority and accepts the request." + if err := m.SetWidgetProperty("", "Documentation", doc); err != nil { + t.Fatalf("SET Documentation failed: %v", err) + } + if got := bsonnav.DGet(m.rawData, "Documentation"); got != doc { + t.Errorf("Documentation = %v, want %q", got, doc) + } +} + +// An existing value is replaced rather than appended beside itself — a second +// Documentation key is a document Studio Pro resolves against the type's +// property list and cannot open. +func TestSetPageLevel_Documentation_ReplacesExisting(t *testing.T) { + raw := makeRawPage() + raw = append(raw, bson.E{Key: "Documentation", Value: "old"}) + m := &Mutator{rawData: raw, widgetFinder: findBsonWidget} + + if err := m.SetWidgetProperty("", "Documentation", "new"); err != nil { + t.Fatalf("SET Documentation failed: %v", err) + } + if got := bsonnav.DGet(m.rawData, "Documentation"); got != "new" { + t.Errorf("Documentation = %v, want \"new\"", got) + } + n := 0 + for _, e := range m.rawData { + if e.Key == "Documentation" { + n++ + } + } + if n != 1 { + t.Errorf("document carries %d Documentation keys, want 1", n) + } +} + +// Clearing is a real operation — a doc comment removed from a script should be +// removable from the document — so an empty string is stored, not rejected. +func TestSetPageLevel_Documentation_EmptyClears(t *testing.T) { + raw := makeRawPage() + raw = append(raw, bson.E{Key: "Documentation", Value: "old"}) + m := &Mutator{rawData: raw, widgetFinder: findBsonWidget} + + if err := m.SetWidgetProperty("", "Documentation", ""); err != nil { + t.Fatalf("SET Documentation = '' failed: %v", err) + } + if got := bsonnav.DGet(m.rawData, "Documentation"); got != "" { + t.Errorf("Documentation = %v, want empty", got) + } +} + +func TestSetPageLevel_Documentation_NonString(t *testing.T) { + m := &Mutator{rawData: makeRawPage(), widgetFinder: findBsonWidget} + if err := m.SetWidgetProperty("", "Documentation", 42); err == nil { + t.Error("non-string Documentation should be rejected") + } +} + +// The unsupported-property message is the only guidance a reader gets, so it has +// to list what it now accepts. A stale list sends someone to the CREATE +// workaround this change exists to remove. +func TestSetPageLevel_UnsupportedMessageNamesDocumentation(t *testing.T) { + m := &Mutator{rawData: makeRawPage(), widgetFinder: findBsonWidget} + err := m.SetWidgetProperty("", "NotARealProperty", 1) + if err == nil { + t.Fatal("expected an error for an unsupported page-level property") + } + if !strings.Contains(err.Error(), "Documentation") { + t.Errorf("the supported-property list does not mention Documentation: %v", err) + } +} + +// ALTER PAGE, ALTER LAYOUT and ALTER SNIPPET share alterPageOperation and all +// three reach applyPageLevelSetMut through the one OpenPageForMutation, so a +// property added for pages is silently offered to the other two. That is the +// shape that ships silent drops, so run them rather than reasoning about them. +// +// Documentation is safe on all three on the evidence that matters — the key is +// one of the ten measured on Atlas_Core.Atlas_Default at 11.13.0 (see +// layoutToGen), and snippetToGen writes it too. It is NOT safe merely because +// gen declares it: gen also offers Layout.MainPlaceholderName and six siblings +// that no Atlas layout carries, and writing one gives a document mxbuild +// accepts at 0 errors and Studio Pro cannot open. +func TestSetPageLevel_Documentation_LayoutAndSnippet(t *testing.T) { + for _, typeName := range []string{"Forms$Layout", "Forms$Snippet"} { + t.Run(typeName, func(t *testing.T) { + raw := append(bson.D{{Key: "$Type", Value: typeName}}, makeRawPage()...) + m := &Mutator{rawData: raw, widgetFinder: findBsonWidget} + + if err := m.SetWidgetProperty("", "Documentation", "shared frame"); err != nil { + t.Fatalf("SET Documentation on %s failed: %v", typeName, err) + } + if got := bsonnav.DGet(m.rawData, "Documentation"); got != "shared frame" { + t.Errorf("%s Documentation = %v, want %q", typeName, got, "shared frame") + } + if got := bsonnav.DGetString(m.rawData, "$Type"); got != typeName { + t.Errorf("$Type changed to %q", got) + } + }) + } +} diff --git a/mdl/backend/pagemutator/mutator.go b/mdl/backend/pagemutator/mutator.go index 2af3a70040..d2cbb298c2 100644 --- a/mdl/backend/pagemutator/mutator.go +++ b/mdl/backend/pagemutator/mutator.go @@ -2321,6 +2321,25 @@ func applyPageLevelSetMut(rawData bson.D, prop string, value any) (bson.D, error case "Url": strVal, _ := value.(string) rawData = dSetOrAppend(rawData, "Url", strVal) + case "Documentation": + // A plain top-level string, the same shape as Url, and declared on + // Page, Layout and Snippet alike — all three reach this function + // through SetWidgetProperty(""), so one case covers them. + // + // Without it, documenting an existing page meant re-running its CREATE + // (the doc comment is the only other source), which for a real page + // means re-emitting its whole widget tree through a describe → exec + // round trip that is only as complete as what MDL can spell + // (ako/mxcli#527). + // + // An empty string is stored rather than rejected: removing a doc + // comment from a script has to be expressible, and the property is a + // bare string with no unset value. + strVal, ok := value.(string) + if !ok { + return rawData, fmt.Errorf("Documentation value must be a string") + } + rawData = dSetOrAppend(rawData, "Documentation", strVal) case "PopupWidth", "PopupHeight": // Pop-up dimensions live at the top level of the Forms$Page document and // are stored as int64 (matching what Studio Pro and the legacy writer @@ -2359,7 +2378,8 @@ func applyPageLevelSetMut(rawData bson.D, prop string, value any) (bson.D, error } default: return rawData, fmt.Errorf("unsupported page-level property: %s "+ - "(supported: Title, Url, PopupWidth, PopupHeight, PopupResizable, PopupCloseAction, Class, Style)", prop) + "(supported: Title, Url, Documentation, PopupWidth, PopupHeight, PopupResizable, "+ + "PopupCloseAction, Class, Style)", prop) } return rawData, nil } diff --git a/mdl/backend/security.go b/mdl/backend/security.go index b6a25b3f37..3de10febf9 100644 --- a/mdl/backend/security.go +++ b/mdl/backend/security.go @@ -26,6 +26,10 @@ type ProjectSecurityBackend interface { GetProjectSecurity() (*security.ProjectSecurity, error) SetProjectSecurityLevel(unitID model.ID, level string) error SetProjectDemoUsersEnabled(unitID model.ID, enabled bool) error + // SetProjectStrictMode toggles security strict mode, which mxcli has always + // READ (it is what lint rule SEC005 reports on) and could not write, so the + // rule had no remedy short of Studio Pro (ako/mxcli#526). + SetProjectStrictMode(unitID model.ID, enabled bool) error // SetProjectGuestAccess toggles anonymous access. An empty guestUserRole // leaves the stored role untouched — the caller is responsible for having // established that a role exists, because Mendix raises CE0133 on guest diff --git a/mdl/executor/cmd_contract.go b/mdl/executor/cmd_contract.go index 5347f45c67..40ec17fddd 100644 --- a/mdl/executor/cmd_contract.go +++ b/mdl/executor/cmd_contract.go @@ -626,17 +626,47 @@ func createExternalEntities(ctx *ExecContext, s *ast.CreateExternalEntitiesStmt) // write flow. (The earlier permissive default regressed this; the // service that motivated #729 was a narrower ETag/Concurrency case.) defaultCreatable := false - defaultUpdatable := false if !isTopLevel { defaultCreatable = true - defaultUpdatable = true } if entitySet != nil && entitySet.Insertable != nil { defaultCreatable = *entitySet.Insertable } - if entitySet != nil && entitySet.Updatable != nil { - defaultUpdatable = *entitySet.Updatable - } + + // Updatable does NOT follow UpdateRestrictions, and that asymmetry + // with Creatable right above it is the whole of this rule: NO + // attribute of a top-level entity is updatable, and EVERY attribute + // of a non-top-level one is, because the latter is written through + // its parent's flow. + // + // Measured on mxbuild 11.12.1 across ten contract shapes, each a + // top-level set mxbuild reads as updatable and each answering + // False — inline , typed , + // UpdateMethod=PATCH, +NonUpdatableProperties +DeleteRestrictions, + // unannotated, external , + // Core.Permissions/ReadWrite, Core.OptimisticConcurrency (ETag), + // DeepUpdateSupport/Supported=true, and NonUpdatableProperties + // naming ONLY the key. That last one is what closes it: the service + // lists `Id` as the sole non-updatable property, i.e. asserts that + // the others ARE updatable, and mxbuild still says False. Following + // the annotation is one CE6630 per attribute. + // + // It is not "the entity is read-only" — Creatable follows + // Insertable on the very same attributes. It is not the model's + // "allow creating and changing objects locally" either: setting + // AllowCreateChangeLocally=Yes left the expectation at False. An + // external object can be changed in memory and passed to an + // external action, which is what that flag governs; this one + // mirrors what the endpoint itself accepts. + // + // entitySet.Updatable and NonUpdatableProperties are therefore read + // but never consulted here. They are left in place deliberately: if + // a contract is ever found that mxbuild does treat as updatable, + // this is where the per-property list becomes load-bearing again — + // and the key would then need its own guard, since mxbuild computes + // a top-level key as non-updatable independently (one CE6630 per + // key part, measured across the same ten shapes). + defaultUpdatable := !isTopLevel nonInsertable := make(map[string]bool) nonUpdatable := make(map[string]bool) // Filter/Sort restrictions name the properties the service refuses to @@ -687,6 +717,8 @@ func createExternalEntities(ctx *ExecContext, s *ast.CreateExternalEntitiesStmt) // attribute name. remoteName := p.Path() + isKey := keyPropSet[p.Name] + creatable := defaultCreatable updatable := defaultUpdatable if nonInsertable[remoteName] || p.Computed { @@ -695,6 +727,18 @@ func createExternalEntities(ctx *ExecContext, s *ast.CreateExternalEntitiesStmt) if nonUpdatable[remoteName] || p.Computed || p.Immutable { updatable = false } + // The key of a top-level entity is non-updatable for a reason + // of its own — a key cannot be changed after the object exists, + // which is the symptom that was reported ("'DefinitionId' is + // marked Updatable=False in the OData service, but True in the + // app") — but it needs no guard here, because defaultUpdatable + // already answers false for every top-level attribute. On a + // NON-top-level entity the key goes the other way and must stay + // updatable: clearing it there is CE6630 inverted, measured on + // the live TripPin contract over Trip, PlanItem, Event, Flight, + // PublicTransportation, Employee and Manager. `UserName` is the + // two-sided control inside that one document — False on Person + // (an entity set), True on Employee and Manager (derived). // A property reached through a complex type carries NONE of the // four capabilities, whatever the entity set says. // @@ -746,7 +790,7 @@ func createExternalEntities(ctx *ExecContext, s *ast.CreateExternalEntitiesStmt) } attr := &domainmodel.Attribute{ Name: attrName, - Type: edmToDomainModelAttrType(p, keyPropSet[p.Name]), + Type: edmToDomainModelAttrType(p, isKey), RemoteName: remoteName, RemoteType: p.Type, Filterable: filterable, diff --git a/mdl/executor/cmd_contract_complextype_test.go b/mdl/executor/cmd_contract_complextype_test.go index 4a07819dea..912afb487d 100644 --- a/mdl/executor/cmd_contract_complextype_test.go +++ b/mdl/executor/cmd_contract_complextype_test.go @@ -272,15 +272,27 @@ func TestCreateExternalEntities_FlattenedAttributesAreReadOnly(t *testing.T) { } // The control: an ordinary property of the SAME writable entity set must - // still follow the contract. Without it this test passes against an import - // that marks everything read-only. + // still follow the contract on CREATABLE. Without it this test passes + // against an import that marks everything read-only. + // + // Updatable is deliberately NOT asserted true here, and this control used + // to claim it was. That half was assumed rather than measured: mxbuild + // computes every attribute of a TOP-LEVEL entity as Updatable=False, plain + // and flattened alike, across ten contract shapes — including one whose + // NonUpdatableProperties names only the key, i.e. asserts that `Label` is + // updatable. See TestCreateExternalEntities_TopLevelAttributesAreNeverUpdatable. + // So what is special about a flattened attribute is CREATABLE, not both. plain := byName["Label"] if plain == nil { t.Fatal("Label missing") } - if !plain.Creatable || !plain.Updatable { - t.Errorf("Label Creatable=%v Updatable=%v, want both true — the contract says the set is writable", - plain.Creatable, plain.Updatable) + if !plain.Creatable { + t.Error("Label lost Creatable — the contract says the set is insertable, and without " + + "this the test passes against an import that marks everything read-only") + } + if plain.Updatable { + t.Error("Label is Updatable=true on a top-level entity — CE6630 " + + `"'Label' is marked Updatable=False in the OData service, but True in the app."`) } } diff --git a/mdl/executor/cmd_contract_key_updatable_test.go b/mdl/executor/cmd_contract_key_updatable_test.go new file mode 100644 index 0000000000..81bf050c96 --- /dev/null +++ b/mdl/executor/cmd_contract_key_updatable_test.go @@ -0,0 +1,332 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "testing" + + "github.com/mendixlabs/mxcli/sdk/domainmodel" +) + +// A writable OData entity set: the container declares Insertable=true AND +// Updatable=true, and the entity has one key property plus one ordinary one. +// Deliberately free of complex types — the key-updatability defect is +// independent of the ComplexType flattening fixed in mendixlabs/mxcli#1118 and +// reproduces on a contract with no complex type at all. +const writableSetMetadata = ` + + + + + + + + + + + + + + + + + + + + +` + +// The same contract with the set insertable but NOT updatable — the second +// direction the report asks about. +const insertOnlySetMetadata = ` + + + + + + + + + + + + + + + + + + + + +` + +// The reported symptom, on mxbuild 11.12.1: +// +// [error] [CE6630] "'DefinitionId' is marked Updatable=False in the OData +// service, but True in the app." +// at Attribute 'MyFirstModule.Definition.DefinitionId' +// +// Mendix computes a key property as non-updatable whatever the entity set's +// UpdateRestrictions say — a key cannot be changed after creation — so an +// import that lets the key follow the set is exactly one CE6630 per key part. +// +// The key is a special case of the wider rule proved in +// TestCreateExternalEntities_TopLevelAttributesAreNeverUpdatable below — no +// attribute of a top-level entity is updatable — and it is kept as its own +// test because it is the symptom that was reported, and because `Creatable` +// going the other way on the very same attribute is the sharpest statement +// that this is not a read-only stamp. +// +// The other side is TestCreateExternalEntities_DerivedTypeKeyStaysUpdatable, +// which the live TripPin contract supplied after a blanket version of this +// rule turned one CE6630 into seven of its inverse. +func TestCreateExternalEntities_KeyAttributeIsNeverUpdatable(t *testing.T) { + ent, _ := importOne(t, writableSetMetadata, "Definition") + byName := attrByName(ent) + + // `Id` is a Mendix reserved word, so the import renames it — the CE6630 + // in the report names `DefinitionId` for that reason. + key := byName["DefinitionId"] + if key == nil { + t.Fatalf("key attribute DefinitionId missing; got %v", attrNames(ent)) + } + if key.Updatable { + t.Error("key attribute is Updatable=true against a service that computes it False — CE6630 " + + `"'DefinitionId' is marked Updatable=False in the OData service, but True in the app."`) + } + + if !key.Creatable { + t.Error("control failed: the key must stay Creatable — it is written once, at creation, " + + "and clearing both capabilities is CE6630 inverted") + } +} + +// A key is NOT read-only: it is written once, at creation. The report's build +// flagged the key Updatable=False *only* — no Creatable error accompanied it — +// so the rule is "a key cannot be changed after creation", not "the entity is +// read-only". Clearing Creatable too would be the obvious over-correction and +// its own CE6630. +func TestCreateExternalEntities_KeyAttributeStaysCreatable(t *testing.T) { + ent, _ := importOne(t, writableSetMetadata, "Definition") + byName := attrByName(ent) + + key := byName["DefinitionId"] + if key == nil { + t.Fatalf("key attribute DefinitionId missing; got %v", attrNames(ent)) + } + if !key.Creatable { + t.Error("key attribute lost Creatable against an Insertable=true set — " + + "the key is set at creation, so this is CE6630 the other way") + } +} + +// The second direction: a set that is insertable but not updatable. The key +// must be non-updatable here too (it already was, via the set), and Creatable +// must still follow the contract — so the key fix must not be written as +// "clear both capabilities on a key". +func TestCreateExternalEntities_InsertableButNotUpdatableSet(t *testing.T) { + ent, _ := importOne(t, insertOnlySetMetadata, "Definition") + byName := attrByName(ent) + + key := byName["DefinitionId"] + if key == nil { + t.Fatalf("key attribute DefinitionId missing; got %v", attrNames(ent)) + } + if key.Updatable { + t.Error("key attribute Updatable=true against a non-updatable set — CE6630") + } + if !key.Creatable { + t.Error("key attribute Creatable=false against an Insertable=true set — CE6630") + } + + label := byName["Label"] + if label == nil { + t.Fatalf("attribute Label missing; got %v", attrNames(ent)) + } + if label.Updatable { + t.Error("non-key attribute Updatable=true against a non-updatable set — CE6630") + } + if !label.Creatable { + t.Error("control failed: a non-key attribute of an insertable set still follows the set's Insertable=true") + } +} + +// A base type with an entity set, plus a type derived from it that has NONE — +// TripPin's Person/Employee shape, reduced. The derived entity is reached +// through its parent's write flow, which is why its capabilities go the other +// way from a top-level entity's. +const derivedTypeMetadata = ` + + + + + + + + + + + + + + + + + + + + + + + +` + +// The control the live TripPin contract supplied, after a blanket "a key is +// never updatable" turned the reported CE6630 into seven of its inverse: +// +// [error] [CE6630] "'TripId' is marked Updatable=True in the OData service, +// but False in the app." +// at Attribute 'TripPinClient.Trip.TripId' +// +// measured on mxbuild 11.12.2 over Trip, PlanItem, Event, Flight, +// PublicTransportation, Employee and Manager — every one of them a derived or +// contained type with no entity set of its own, mutated through its parent's +// write flow. The entity sets in the same contract (Person, Airline, Airport) +// stayed silent at Updatable=false. +// +// `UserName` is the sharpest form of it: the SAME property is expected False on +// Person and True on Employee and Manager, so the split is the entity set and +// cannot be inheritance or the property itself. That is why the key rule is +// gated on isTopLevel, and why this test exists beside the top-level one — the +// pair is the two-sided control, and either alone passes against a fix that is +// wrong in the other direction. +func TestCreateExternalEntities_DerivedTypeKeyStaysUpdatable(t *testing.T) { + all, _ := importComplexTypeContract(t, derivedTypeMetadata) + + top := all["People"] + if top == nil { + t.Fatalf("top-level entity People not created; got %v", entityNamesOf(all)) + } + if k := attrByName(top)["UserName"]; k == nil { + t.Fatal("People.UserName missing") + } else if k.Updatable { + t.Error("top-level key is Updatable=true — CE6630 " + + `"'UserName' is marked Updatable=False in the OData service, but True in the app."`) + } + + derived := all["Employee"] + if derived == nil { + t.Fatalf("derived entity Employee not created; got %v", entityNamesOf(all)) + } + if k := attrByName(derived)["UserName"]; k == nil { + t.Fatal("Employee.UserName missing") + } else if !k.Updatable { + t.Error("derived-type key lost Updatable — CE6630 inverted, the TripPin failure: " + + `"'UserName' is marked Updatable=True in the OData service, but False in the app."`) + } +} + +func entityNamesOf(m map[string]*domainmodel.Entity) []string { + out := make([]string, 0, len(m)) + for k := range m { + out = append(out, k) + } + return out +} + +// A top-level entity set that names the KEY as the only non-updatable property: +// the service is saying, by name, that `Label` IS updatable. Nothing states the +// case more plainly, and mxbuild still computes False for it. +const explicitNonUpdatableMetadata = ` + + + + + + + + + + + + + + + + + + Id + + + + + + + +` + +// NO attribute of a TOP-LEVEL external entity is updatable, whatever the entity +// set's UpdateRestrictions say. Following the annotation is one CE6630 per +// attribute — "'Label' is marked Updatable=False in the OData service, but True +// in the app." +// +// Measured on mxbuild 11.12.1 across TEN contract shapes, every one of them a +// top-level set mxbuild reads as updatable, every one answering False: +// +// inline typed +// UpdateMethod=PATCH +NonUpdatableProperties +DeleteRestrictions +// unannotated external +// Core.Permissions/ReadWrite Core.OptimisticConcurrency (ETag) +// DeepUpdateSupport/Supported=true NonUpdatableProperties naming ONLY the key +// +// The last is the one that closes it: the service lists `Id` as the sole +// non-updatable property, so it is asserting that `Label` is updatable, and +// mxbuild still says False. The aliased-vocabulary run is the other half — with +// mxbuild reading an Updatable=true set and the app at false on every +// attribute, the build reported Creatable errors and NO Updatable error, so +// False is what it wants rather than merely what it tolerates. +// +// Two things this rule is NOT, each ruled out by its own control: +// +// - It is not "the entity is read-only". Creatable follows Insertable and +// stays true on the same attributes — asserted below, and the reason a +// blanket read-only stamp cannot pass this test. +// - It is not the model's "allow creating and changing objects locally". +// Setting AllowCreateChangeLocally=Yes on a top-level entity left the +// expectation at False (measured). An external entity's object can be +// changed in memory and handed to an external action; that is governed by +// that flag, while this one mirrors what the endpoint itself accepts. +// +// Non-top-level entities go the other way — see +// TestCreateExternalEntities_DerivedTypeKeyStaysUpdatable. +func TestCreateExternalEntities_TopLevelAttributesAreNeverUpdatable(t *testing.T) { + for _, tc := range []struct { + name string + metadata string + }{ + {"updatable set", writableSetMetadata}, + {"non-updatable properties names only the key", explicitNonUpdatableMetadata}, + } { + t.Run(tc.name, func(t *testing.T) { + ent, _ := importOne(t, tc.metadata, "Definition") + byName := attrByName(ent) + + for _, n := range []string{"DefinitionId", "Label"} { + a := byName[n] + if a == nil { + t.Fatalf("attribute %s missing; got %v", n, attrNames(ent)) + } + if a.Updatable { + t.Errorf("%s is Updatable=true on a top-level entity — CE6630 "+ + `"'%s' is marked Updatable=False in the OData service, but True in the app."`, n, n) + } + // The control: Creatable goes the other way on the very same + // attribute, so this cannot pass against a read-only stamp. + if !a.Creatable { + t.Errorf("%s lost Creatable against an Insertable=true set — CE6630 inverted", n) + } + } + }) + } +} diff --git a/mdl/executor/cmd_microflows_build.go b/mdl/executor/cmd_microflows_build.go index 21b4a5a82f..a77fc68cc1 100644 --- a/mdl/executor/cmd_microflows_build.go +++ b/mdl/executor/cmd_microflows_build.go @@ -246,6 +246,16 @@ func buildMicroflowFromStmt(ctx *ExecContext, s *ast.CreateMicroflowStmt, opts b URL: existingURL, URLSearchParameters: existingURLSearchParams, } + // The header clauses overlay the STORED values seeded above, so a clause the + // statement omits keeps what is there. Checked first: the rules are shared + // with `mxcli check`, and a write refused here would otherwise have already + // passed check, which is the drift these two calls exist to prevent. + if err := checkMicroflowDocumentProperties(s); err != nil { + return nil, err + } + if err := applyMicroflowDocumentProperties(ctx, mf, s); err != nil { + return nil, err + } if preserveDocumentation { mf.Documentation = carriedDocumentation(s.DocumentationSet, s.Documentation, existingDocumentation) } diff --git a/mdl/executor/cmd_microflows_show.go b/mdl/executor/cmd_microflows_show.go index 7a63cbd35a..b82ae6f0b8 100644 --- a/mdl/executor/cmd_microflows_show.go +++ b/mdl/executor/cmd_microflows_show.go @@ -11,6 +11,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" "github.com/mendixlabs/mxcli/mdl/microflowgraph" + "github.com/mendixlabs/mxcli/mdl/types" "github.com/mendixlabs/mxcli/model" "github.com/mendixlabs/mxcli/sdk/javaactions" "github.com/mendixlabs/mxcli/sdk/microflows" @@ -306,6 +307,13 @@ func describeMicroflowMode(ctx *ExecContext, name ast.QualifiedName, normalized } lines = append(lines, exposeClauseLines(targetMf)...) + // Shared with renderMicroflowMDL rather than restated. This function is a + // SECOND copy of the microflow header renderer, and the URL clauses were + // added to the other one first — so `describe microflow` printed none of + // them while `diff-local` printed all three. That is duplicate-resolver + // drift in one file; every header property added from here on has to go + // through a shared helper, or the next one diverges the same way. + lines = append(lines, microflowDocumentPropertyLines(targetMf)...) // BEGIN block lines = append(lines, "begin") @@ -622,24 +630,6 @@ func renderMicroflowMDL( if mf.ApplyEntityAccess && flowType == "microflow" { lines = append(lines, "@applyentityaccess") } - // Studio Pro's "Export level". Only worth a line when it is NOT the default - // — every document in every module measured stores "Hidden", so emitting it - // unconditionally would add a comment to every describe to say nothing. - if mf.ExportLevel != "" && mf.ExportLevel != "Hidden" && flowType == "microflow" { - lines = append(lines, fmt.Sprintf( - "-- Export level: %s (MDL cannot author one. Kept when this microflow "+ - "is rewritten, NOT copied to a new one — set it in Studio Pro.)", mf.ExportLevel)) - } - // The deep link (Mendix 10.6+) has no MDL spelling at all, so it cannot be - // emitted as re-executable text. A rewrite preserves it (#1120), but a - // describe -> rename -> exec COPY has nothing to preserve from — same gap - // the annotation above notes, one step further along. Say so rather than - // producing output that silently omits it. - if mf.URL != "" && flowType == "microflow" { - lines = append(lines, fmt.Sprintf( - "-- URL: %s (deep link; MDL cannot author one. Kept when this "+ - "microflow is rewritten, NOT copied to a new one — set it in Studio Pro.)", mf.URL)) - } qualifiedName := name.Module + "." + name.Name if len(mf.Parameters) > 0 { @@ -665,6 +655,9 @@ func renderMicroflowMDL( } lines = append(lines, exposeClauseLines(mf)...) + if flowType == "microflow" { + lines = append(lines, microflowDocumentPropertyLines(mf)...) + } lines = append(lines, "begin") headerLineCount := len(lines) @@ -1700,3 +1693,71 @@ func microflowBodyWarnings( out = append(out, droppedMergeWarnings(ctx, mf.ObjectCollection, labels)...) return out } + +// microflowDocumentPropertyLines emits the URL / EXPORT LEVEL / concurrency +// header clauses. +// +// These were `-- URL:` and `-- Export level:` comments while MDL could not +// author them: a rewrite preserved the properties, but a describe -> rename -> +// exec COPY had nothing to preserve from, so the comment was there to stop the +// output looking complete when it was not. They are real clauses now, which is +// what makes describe a faithful copy operation rather than an approximate one. +// +// Emitted only when NOT the default, for the reason the export-level comment +// gave: every document in every marketplace module measured stores "Hidden" and +// allows concurrent execution, so emitting them unconditionally would add three +// lines to every describe in order to say nothing. +func microflowDocumentPropertyLines(mf *microflows.Microflow) []string { + var lines []string + if mf.URL != "" { + lines = append(lines, fmt.Sprintf("url '%s'", escapeMDLString(mf.URL))) + } + if len(mf.URLSearchParameters) > 0 { + names := make([]string, 0, len(mf.URLSearchParameters)) + for _, qn := range mf.URLSearchParameters { + // Stored as Module.Microflow.Parameter; the clause names the + // parameter, because that is what the reader has in front of them. + parts := strings.Split(qn, ".") + names = append(names, "$"+parts[len(parts)-1]) + } + lines = append(lines, fmt.Sprintf("url search parameters (%s)", strings.Join(names, ", "))) + } + if mf.ExportLevel == types.ExportLevelAPI { + lines = append(lines, "export level api") + } + if !mf.AllowConcurrentExecution { + line := "disallow concurrent execution" + switch { + case mf.ConcurrencyErrorMicroflow != "": + line += " error microflow " + mf.ConcurrencyErrorMicroflow + case mf.ConcurrencyErrorMessage != nil && len(mf.ConcurrencyErrorMessage.Translations) > 0: + // The message is a Texts$Text and MDL states one language, so a + // multi-language message cannot round-trip. Emit the clause anyway — + // dropping it would describe a microflow that fails CE4899 — and + // flag the languages a replay would not carry, the same honesty rule + // DESCRIBE applies to a range bounded by another attribute. + text, langs := describableMessage(mf.ConcurrencyErrorMessage) + line += fmt.Sprintf(" error message '%s'", escapeMDLString(text)) + if len(langs) > 0 { + line += fmt.Sprintf(" -- also translated into %s; replaying this line keeps only the one shown", + strings.Join(langs, ", ")) + } + } + lines = append(lines, line) + } + return lines +} + +// describableMessage returns the text DESCRIBE prints and the other languages +// the stored message carries, sorted. +func describableMessage(t *model.Text) (string, []string) { + var langs []string + for lang := range t.Translations { + langs = append(langs, lang) + } + sort.Strings(langs) + if len(langs) == 0 { + return "", nil + } + return t.Translations[langs[0]], langs[1:] +} diff --git a/mdl/executor/cmd_pages_builder.go b/mdl/executor/cmd_pages_builder.go index 66443fffc1..7d5b13d726 100644 --- a/mdl/executor/cmd_pages_builder.go +++ b/mdl/executor/cmd_pages_builder.go @@ -51,18 +51,19 @@ type pageBuilder struct { // Entity context for resolving short attribute names inside DataViews entityContext string // Qualified entity name (e.g., "Module.Entity") - // Name of the variable the enclosing data widget is bound to, without the "$" - // (e.g. "Car" for `dataview dv (DataSource: $Car)`). Empty when the context - // object has no name of its own — a database/association/microflow source - // supplies a row object addressable only as $currentObject. Used to tell a - // SHOW_PAGE argument that names the context object from one that names - // something else, which mxcli cannot store; see cmd_pages_showpage_args.go. - contextVarName string - - // True once the walk has entered a data-bound widget, so contextVarName is - // meaningful. False means the context object is unknown (ALTER PAGE builds an - // action without traversing the stored page), not that it has no name. - contextKnown bool + // What this point of the walk knows about the object a SHOW_PAGE widget + // action would bind its argument to: whether it is knowable at all, whether + // there is one, and what it is called. Used to tell an argument that names + // the context object from one that names something else — and from one + // written where no context object exists at all, which mxcli cannot store + // either. See cmd_pages_showpage_args.go. + argCtx pageArgContext + + // Name of the widget currently being built, so a refusal names the control + // the author has to go and fix. The check-time mirror gets it from the AST + // node it is looking at; the builder's action path is several calls deep and + // would otherwise say only "this widget". + currentWidget string // Local page/snippet variables (Variables: { $name: Type = 'default' }). // Used to distinguish a $localVar reference from a page parameter when diff --git a/mdl/executor/cmd_pages_builder_input_assoc_test.go b/mdl/executor/cmd_pages_builder_input_assoc_test.go new file mode 100644 index 0000000000..6d6b8bdf40 --- /dev/null +++ b/mdl/executor/cmd_pages_builder_input_assoc_test.go @@ -0,0 +1,158 @@ +// SPDX-License-Identifier: Apache-2.0 + +// ako/mxcli#529 (write half) — an input widget could not bind an attribute over +// an association. Every input builder resolved its `attribute:` with +// resolveAttributePath, which knows nothing about associations, so the slashes +// survived into a flat path that resolved to nothing and the build failed +// CE1613. +// +// Studio Pro DOES store this shape on a plain text box. Measured on +// ako/TestApp, page Rules.RuleAction_NewEdit, textBox4: +// +// Attribute: "Rules.BusinessRule.Name" +// EntityRef: IndirectEntityRef{ Steps: [ EntityRefStep{ +// Association: "Rules.RuleAction_BusinessRule", +// DestinationEntity: "Rules.BusinessRule" } ] } +// +// which is the same structure DataGrid2 columns and DynamicText parameters +// already produced — so one page could bind an associated attribute in a grid +// column and fail on the text box beside it. +package executor + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/domainmodel" + "github.com/mendixlabs/mxcli/sdk/pages" +) + +// ruleActionPB mirrors TestApp's Rules module: RuleAction has no Name of its +// own, and reaches BusinessRule.Name over RuleAction_BusinessRule. The missing +// Name is the point — it is why the old behaviour produced a binding to +// something that does not exist rather than merely a differently-spelled one. +func ruleActionPB(t *testing.T) *pageBuilder { + t.Helper() + const modID = model.ID("mod") + const actionID = model.ID("e-action") + const ruleID = model.ID("e-rule") + return &pageBuilder{ + entityContext: "Rules.RuleAction", + widgetScope: map[string]model.ID{}, + execCache: &executorCache{ + hierarchy: &ContainerHierarchy{moduleNames: map[model.ID]string{modID: "Rules"}}, + domainModels: []*domainmodel.DomainModel{{ + ContainerID: modID, + Entities: []*domainmodel.Entity{ + {BaseElement: model.BaseElement{ID: actionID}, Name: "RuleAction", + Attributes: []*domainmodel.Attribute{{Name: "ActionType"}}}, + {BaseElement: model.BaseElement{ID: ruleID}, Name: "BusinessRule", + Attributes: []*domainmodel.Attribute{{Name: "Name"}}}, + }, + Associations: []*domainmodel.Association{ + {Name: "RuleAction_BusinessRule", ParentID: actionID, ChildID: ruleID, + Type: domainmodel.AssociationTypeReference}, + }, + }}, + }, + } +} + +func TestResolveInputAttribute(t *testing.T) { + pb := ruleActionPB(t) + + t.Run("association path resolves to the final attribute plus one hop", func(t *testing.T) { + qn, steps := pb.resolveInputAttribute("RuleAction_BusinessRule/Name") + if qn != "Rules.BusinessRule.Name" { + t.Errorf("attribute = %q, want Rules.BusinessRule.Name", qn) + } + if len(steps) != 1 { + t.Fatalf("got %d steps, want 1: %+v", len(steps), steps) + } + if steps[0].Association != "Rules.RuleAction_BusinessRule" { + t.Errorf("association = %q, want Rules.RuleAction_BusinessRule", steps[0].Association) + } + if steps[0].DestinationEntity != "Rules.BusinessRule" { + t.Errorf("destination = %q, want Rules.BusinessRule", steps[0].DestinationEntity) + } + }) + + // The control: an own attribute must still resolve against the context and + // carry NO steps. Without it a resolver that returned a step for everything + // would pass the case above. + t.Run("own attribute carries no steps", func(t *testing.T) { + qn, steps := pb.resolveInputAttribute("ActionType") + if qn != "Rules.RuleAction.ActionType" { + t.Errorf("attribute = %q, want Rules.RuleAction.ActionType", qn) + } + if len(steps) != 0 { + t.Errorf("own attribute gained %d association steps: %+v", len(steps), steps) + } + }) + + // A path naming an association the model does not have falls back rather + // than erroring — the reference checker reports the unknown member, and + // failing here would reject paths whose context this pass cannot see. + t.Run("unknown association falls back to the flat path", func(t *testing.T) { + qn, steps := pb.resolveInputAttribute("NotAnAssociation/Name") + if len(steps) != 0 { + t.Errorf("unresolvable path produced steps: %+v", steps) + } + if qn == "" { + t.Error("unresolvable path produced an empty attribute") + } + }) +} + +// Every input widget that takes a single `attribute:` must go through the +// resolver — a builder left on resolveAttributePath is exactly the defect, and +// there were six of them. This drives the real builders rather than the helper, +// because the helper being right says nothing about who calls it. +func TestInputWidgetsResolveAssociationAttributes(t *testing.T) { + for _, widgetType := range []string{"textbox", "textarea", "datepicker", "dropdown", "checkbox", "radiobuttons"} { + t.Run(widgetType, func(t *testing.T) { + pb := ruleActionPB(t) + built, err := pb.buildWidgetV3(&ast.WidgetV3{ + Type: widgetType, + Name: "w1", + Properties: map[string]any{ + "Attribute": "RuleAction_BusinessRule/Name", + }, + }) + if err != nil { + t.Fatalf("build %s: %v", widgetType, err) + } + path, steps := inputAttributeBinding(t, built) + if path != "Rules.BusinessRule.Name" { + t.Errorf("%s attribute = %q, want Rules.BusinessRule.Name", widgetType, path) + } + if len(steps) != 1 || steps[0].Association != "Rules.RuleAction_BusinessRule" { + t.Errorf("%s lost the association hop: %+v", widgetType, steps) + } + }) + } +} + +// inputAttributeBinding reads back the two fields under test from whichever +// input widget the builder produced. +func inputAttributeBinding(t *testing.T, w pages.Widget) (string, []pages.AttributeRefStep) { + t.Helper() + switch x := w.(type) { + case *pages.TextBox: + return x.AttributePath, x.AttributeRefSteps + case *pages.TextArea: + return x.AttributePath, x.AttributeRefSteps + case *pages.DatePicker: + return x.AttributePath, x.AttributeRefSteps + case *pages.DropDown: + return x.AttributePath, x.AttributeRefSteps + case *pages.CheckBox: + return x.AttributePath, x.AttributeRefSteps + case *pages.RadioButtons: + return x.AttributePath, x.AttributeRefSteps + default: + t.Fatalf("unexpected widget type %T — add it to this switch, and to the builder list above", w) + return "", nil + } +} diff --git a/mdl/executor/cmd_pages_builder_v3.go b/mdl/executor/cmd_pages_builder_v3.go index 2d40f84b3c..21bdc1e358 100644 --- a/mdl/executor/cmd_pages_builder_v3.go +++ b/mdl/executor/cmd_pages_builder_v3.go @@ -317,6 +317,19 @@ func (pb *pageBuilder) buildSnippetV3(s *ast.CreateSnippetStmtV3) (*pages.Snippe // as item slots. The dispatch table is consumed by inspection commands and // DESCRIBE-side keyword resolution rather than overriding write-side routing here. func (pb *pageBuilder) buildWidgetV3(w *ast.WidgetV3) (pages.Widget, error) { + // What a SHOW_PAGE argument inside this widget may bind to. The data widgets + // below overwrite it with the context they actually create; this only stops + // "no context object at all" surviving past a widget whose data source this + // pass cannot read. See argContextForSubtreeOf. + if next := argContextForSubtreeOf(w, pb.argCtx); next != pb.argCtx { + old := pb.argCtx + pb.argCtx = next + defer func() { pb.argCtx = old }() + } + oldWidget := pb.currentWidget + pb.currentWidget = w.Name + defer func() { pb.currentWidget = oldWidget }() + var widget pages.Widget var err error @@ -875,7 +888,7 @@ func (pb *pageBuilder) buildDataSourceV3(ds *ast.DataSourceV3) (pages.DataSource }, MicroflowID: mfID, Microflow: ds.Reference, - ParameterMappings: flowArgsToParameterMappings(ds.Args), + ParameterMappings: pb.flowArgsToParameterMappings(ds.Args), }, entityName, nil case "nanoflow": @@ -895,7 +908,7 @@ func (pb *pageBuilder) buildDataSourceV3(ds *ast.DataSourceV3) (pages.DataSource }, NanoflowID: nfID, Nanoflow: ds.Reference, - ParameterMappings: flowArgsToParameterMappings(ds.Args), + ParameterMappings: pb.flowArgsToParameterMappings(ds.Args), }, entityName, nil case "association": @@ -1476,12 +1489,13 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc // (#296). An argument naming anything else therefore cannot be honoured, // and was previously dropped in silence: the button opened the page with // the context object, `mx check` reported 0 errors, and DESCRIBE printed - // the inferred mapping. Refuse instead of re-pointing the argument. - if strVal, ok := arg.Value.(string); ok && !pageArgumentBindsContextObject(strVal, pb.contextVarName, pb.contextKnown) { - return nil, mdlerrors.NewValidationf( - "show_page %s: argument %s: %s cannot be stored — a widget's page argument is always the enclosing context object, which mxcli records by leaving the mapping empty (an explicit one is rejected as CE0115). Writing %s here would silently open the page with %s instead. Use $currentObject%s, or call a microflow that shows the page with the object you want [MDL-PAGEARG01]", - action.Target, arg.Name, strVal, strVal, pb.describeContextObject(), - pb.contextVarAlternative()) + // the inferred mapping. Outside any data widget there is no context + // object to infer at all, so every argument is dropped and the build + // fails CE1571 (#1029). Refuse instead of re-pointing the argument. + if strVal, ok := arg.Value.(string); ok && !pb.argCtx.binds(strVal) { + return nil, mdlerrors.NewValidation( + refuseShowPageArgument(pb.currentWidget, action.Target, arg.Name, strVal, pb.argCtx) + + " [MDL-PAGEARG01]") } mapping := &pages.PageClientParameterMapping{ @@ -1532,10 +1546,14 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc ParameterName: arg.Name, } - // Determine if value is a variable reference or expression + // A page/snippet parameter or page variable binds through + // Variable (a Forms$PageVariable); anything else is an + // Expression. See classifyFlowArgValue — writing a $-reference + // as an Expression leaves the parameter unbound (CE1571, #1140). if strVal, ok := arg.Value.(string); ok { - if strings.HasPrefix(strVal, "$") { - // Variable reference (including $currentObject) + if v, kind := pb.classifyFlowArgValue(strVal); kind != "" { + mapping.Variable, mapping.VariableKind = v, kind + } else if strings.HasPrefix(strVal, "$") { mapping.Variable = strVal } else { mapping.Expression = strVal @@ -1572,10 +1590,14 @@ func (pb *pageBuilder) buildClientActionV3(action *ast.ActionV3) (pages.ClientAc ParameterName: arg.Name, } - // Determine if value is a variable reference or expression + // A page/snippet parameter or page variable binds through + // Variable (a Forms$PageVariable); anything else is an + // Expression. See classifyFlowArgValue — writing a $-reference + // as an Expression leaves the parameter unbound (CE1571, #1140). if strVal, ok := arg.Value.(string); ok { - if strings.HasPrefix(strVal, "$") { - // Variable reference (including $currentObject) + if v, kind := pb.classifyFlowArgValue(strVal); kind != "" { + mapping.Variable, mapping.VariableKind = v, kind + } else if strings.HasPrefix(strVal, "$") { mapping.Variable = strVal } else { mapping.Expression = strVal @@ -1954,6 +1976,33 @@ func (pb *pageBuilder) resolveTemplateAssociationPath(attrRef string, param *pag return true } +// resolveInputAttribute resolves the `attribute:` of an input widget (text box, +// text area, date picker, drop-down, check box, radio buttons) into the +// qualified final attribute plus the association hops to reach it. +// +// A bare name resolves against the enclosing entity context as before and +// carries no steps. `Assoc/Attr` navigates: Studio Pro stores exactly this on a +// plain text box, as ako/TestApp's Rules.RuleAction_NewEdit does for +// Rules.BusinessRule.Name over Rules.RuleAction_BusinessRule. +// +// Before this, every input builder called resolveAttributePath, which knows +// nothing about associations — the slashes survived into a flat path that +// resolved to nothing and the build failed CE1613 (ako/mxcli#529). DataGrid2 +// columns and DynamicText parameters already resolved it, so one page could +// bind an associated attribute in a grid column and fail on the text box beside +// it. +// +// An unresolvable path falls back to resolveAttributePath rather than erroring, +// matching what the column builder does: the reference checker +// (--references) is where an unknown member is reported, and failing here would +// reject paths whose entity context this pass cannot see. +func (pb *pageBuilder) resolveInputAttribute(attr string) (string, []pages.AttributeRefStep) { + if finalQN, steps, ok := pb.resolveAssociationAttributePath(attr); ok { + return finalQN, steps + } + return pb.resolveAttributePath(attr), nil +} + // resolveAssociationAttributePath resolves a context-relative attribute path that // navigates one or more associations (e.g. "Order_Customer/Name" or // "$currentObject/Sales.Order_Customer/Name") into the fully-qualified FINAL @@ -2595,7 +2644,7 @@ func prefixWidgetNames(widgets []*ast.WidgetV3, prefix string) { // needs an argument for every parameter exactly as a call action does — Mendix // reports CE1571 "No argument has been selected for parameter 'X'" otherwise // (#835). The datasource path previously parsed the arguments and dropped them. -func flowArgsToParameterMappings(args []ast.FlowArgV3) []*pages.MicroflowParameterMapping { +func (pb *pageBuilder) flowArgsToParameterMappings(args []ast.FlowArgV3) []*pages.MicroflowParameterMapping { var out []*pages.MicroflowParameterMapping for _, arg := range args { mapping := &pages.MicroflowParameterMapping{ @@ -2605,10 +2654,13 @@ func flowArgsToParameterMappings(args []ast.FlowArgV3) []*pages.MicroflowParamet }, ParameterName: arg.Name, } - // A leading $ marks a variable reference ($currentObject, a page - // parameter); anything else is an expression. + // A page/snippet parameter or page variable binds through Variable (a + // Forms$PageVariable); $currentObject and anything else stays an + // expression. See classifyFlowArgValue (#1140). if strVal, ok := arg.Value.(string); ok { - if strings.HasPrefix(strVal, "$") { + if v, kind := pb.classifyFlowArgValue(strVal); kind != "" { + mapping.Variable, mapping.VariableKind = v, kind + } else if strings.HasPrefix(strVal, "$") { mapping.Variable = strVal } else { mapping.Expression = strVal diff --git a/mdl/executor/cmd_pages_builder_v3_widgets.go b/mdl/executor/cmd_pages_builder_v3_widgets.go index fbc978ce3d..580ed88572 100644 --- a/mdl/executor/cmd_pages_builder_v3_widgets.go +++ b/mdl/executor/cmd_pages_builder_v3_widgets.go @@ -83,15 +83,12 @@ func (pb *pageBuilder) buildDataViewV3(w *ast.WidgetV3) (*pages.DataView, error) // Save and restore entity context so nested DataViews work correctly oldContext := pb.entityContext - oldContextVar := pb.contextVarName - oldContextKnown := pb.contextKnown + oldArgCtx := pb.argCtx pb.entityContext = entityName - pb.contextVarName = contextVarFor(ds) - pb.contextKnown = true + pb.argCtx = enteringDataWidget(ds, entityName) defer func() { pb.entityContext = oldContext - pb.contextVarName = oldContextVar - pb.contextKnown = oldContextKnown + pb.argCtx = oldArgCtx }() // Register the widget name with its entity so template params like $dvOrder.Attr @@ -319,15 +316,12 @@ func (pb *pageBuilder) buildListViewV3(w *ast.WidgetV3) (*pages.ListView, error) // Save and restore entity context so nested containers work correctly oldContext := pb.entityContext - oldContextVar := pb.contextVarName - oldContextKnown := pb.contextKnown + oldArgCtx := pb.argCtx pb.entityContext = entityName - pb.contextVarName = contextVarFor(ds) - pb.contextKnown = true + pb.argCtx = enteringDataWidget(ds, entityName) defer func() { pb.entityContext = oldContext - pb.contextVarName = oldContextVar - pb.contextKnown = oldContextKnown + pb.argCtx = oldArgCtx }() // Register widget name with entity for SELECTION datasource lookup @@ -450,7 +444,7 @@ func (pb *pageBuilder) buildTextBoxV3(w *ast.WidgetV3) (*pages.TextBox, error) { // Handle Attribute (attribute path) if attr := w.GetAttribute(); attr != "" { - tb.AttributePath = pb.resolveAttributePath(attr) + tb.AttributePath, tb.AttributeRefSteps = pb.resolveInputAttribute(attr) } // Handle Label @@ -494,7 +488,7 @@ func (pb *pageBuilder) buildTextAreaV3(w *ast.WidgetV3) (*pages.TextArea, error) // Handle Attribute if attr := w.GetAttribute(); attr != "" { - ta.AttributePath = pb.resolveAttributePath(attr) + ta.AttributePath, ta.AttributeRefSteps = pb.resolveInputAttribute(attr) } // Handle Label @@ -527,7 +521,7 @@ func (pb *pageBuilder) buildDatePickerV3(w *ast.WidgetV3) (*pages.DatePicker, er // Handle Attribute if attr := w.GetAttribute(); attr != "" { - dp.AttributePath = pb.resolveAttributePath(attr) + dp.AttributePath, dp.AttributeRefSteps = pb.resolveInputAttribute(attr) } // Handle Label @@ -560,7 +554,7 @@ func (pb *pageBuilder) buildDropdownV3(w *ast.WidgetV3) (*pages.DropDown, error) // Handle Attribute if attr := w.GetAttribute(); attr != "" { - dd.AttributePath = pb.resolveAttributePath(attr) + dd.AttributePath, dd.AttributeRefSteps = pb.resolveInputAttribute(attr) } // Handle Label @@ -593,7 +587,7 @@ func (pb *pageBuilder) buildCheckBoxV3(w *ast.WidgetV3) (*pages.CheckBox, error) // Handle Attribute if attr := w.GetAttribute(); attr != "" { - cb.AttributePath = pb.resolveAttributePath(attr) + cb.AttributePath, cb.AttributeRefSteps = pb.resolveInputAttribute(attr) } // Handle Label @@ -658,7 +652,7 @@ func (pb *pageBuilder) buildRadioButtonsV3(w *ast.WidgetV3) (*pages.RadioButtons // Get attribute path from Attribute property if attr := w.GetAttribute(); attr != "" { - rb.AttributePath = pb.resolveAttributePath(attr) + rb.AttributePath, rb.AttributeRefSteps = pb.resolveInputAttribute(attr) } // Handle OnChange (the "On change" client action) @@ -1309,12 +1303,46 @@ func (pb *pageBuilder) buildDynamicImageV3(w *ast.WidgetV3) (*pages.DynamicImage Responsive: true, } + // The entity holding the image. Without it mxbuild refuses the widget — + // CE0489 "Select an entity for the data source of this dynamic image" — so + // every dynamic image mxcli wrote before this was broken at build time, not + // merely lossy. The entity must be reachable from the widget's context; a + // wrong one is mxbuild's to reject, not this builder's to guess at. + if ds := w.GetDataSource(); ds != nil { + dataSource, _, err := pb.buildDataSourceV3(ds) + if err != nil { + return nil, mdlerrors.NewBackend("build datasource", err) + } + img.DataSource = dataSource + } + + // The fallback shown when the bound object has no image, as the qualified + // name of an image-collection entry (Module.Collection.Image). + img.DefaultImageName = w.GetStringProp("DefaultImage") + if width := w.GetIntProp("Width"); width > 0 { img.Width = width } if height := w.GetIntProp("Height"); height > 0 { img.Height = height } + img.WidthUnit = pages.WidthUnit(w.GetStringProp("WidthUnit")) + img.HeightUnit = pages.WidthUnit(w.GetStringProp("HeightUnit")) + // Same vocabulary as the pluggable image widget: DisplayAs: thumbnail and + // OnClickType: enlarge. Both were hardcoded false in the writer, so neither + // was reachable from MDL at all. + img.ShowAsThumbnail = strings.EqualFold(w.GetStringProp("DisplayAs"), "thumbnail") + img.OnClickEnlarge = strings.EqualFold(w.GetStringProp("OnClickType"), "enlarge") + // Responsive defaults to TRUE (Mendix's default, set above), so only an + // explicit `Responsive: false` turns it off — an ABSENT property must not + // read as false, which is what GetBoolProp would do. + if raw, ok := lookupPropCI(w, "Responsive"); ok { + v, err := propBool(raw) + if err != nil { + return nil, mdlerrors.NewBackend("dynamicimage Responsive", err) + } + img.Responsive = v + } // Pages$StaticImageViewer.ClickAction / Pages$DynamicImageViewer.ClickAction. // Same story as the list view: the writer already serialises OnClickAction, diff --git a/mdl/executor/cmd_pages_create_v3.go b/mdl/executor/cmd_pages_create_v3.go index 154adc2f30..1226a3d185 100644 --- a/mdl/executor/cmd_pages_create_v3.go +++ b/mdl/executor/cmd_pages_create_v3.go @@ -110,6 +110,9 @@ func execCreatePageV3(ctx *ExecContext, s *ast.CreatePageStmtV3) error { fragments: ctx.Fragments, themeRegistry: ctx.GetThemeRegistry(), widgetBackend: ctx.Backend, + // The root of a document that this pass walks in full: there is no + // enclosing data widget, so there is no context object. #1029. + argCtx: atDocumentRoot(), } page, err := pb.buildPageV3(s) @@ -246,6 +249,9 @@ func execCreateSnippetV3(ctx *ExecContext, s *ast.CreateSnippetStmtV3) error { fragments: ctx.Fragments, themeRegistry: ctx.GetThemeRegistry(), widgetBackend: ctx.Backend, + // The root of a document that this pass walks in full: there is no + // enclosing data widget, so there is no context object. #1029. + argCtx: atDocumentRoot(), } snippet, err := pb.buildSnippetV3(s) diff --git a/mdl/executor/cmd_pages_datasource_args_test.go b/mdl/executor/cmd_pages_datasource_args_test.go index 3e8df8c41c..c9d8fae5fc 100644 --- a/mdl/executor/cmd_pages_datasource_args_test.go +++ b/mdl/executor/cmd_pages_datasource_args_test.go @@ -20,7 +20,10 @@ import ( // // with mxcli check and exec both reporting success. func TestFlowArgsToParameterMappings(t *testing.T) { - got := flowArgsToParameterMappings([]ast.FlowArgV3{ + // An empty builder knows no parameters, so every $-name is unclassifiable + // and the pre-#1140 behaviour applies unchanged — which is the point. + pb := &pageBuilder{} + got := pb.flowArgsToParameterMappings([]ast.FlowArgV3{ {Name: "Name", Value: "$Filter"}, {Name: "Limit", Value: "10"}, {Name: "Ctx", Value: "$currentObject"}, @@ -50,7 +53,8 @@ func TestFlowArgsToParameterMappings(t *testing.T) { // No arguments must stay nil rather than an empty slice, so a datasource without // parameters serializes exactly as it did before this change. func TestFlowArgsToParameterMappings_Empty(t *testing.T) { - if got := flowArgsToParameterMappings(nil); got != nil { + pb := &pageBuilder{} + if got := pb.flowArgsToParameterMappings(nil); got != nil { t.Errorf("no args should yield nil, got %+v", got) } } diff --git a/mdl/executor/cmd_pages_describe.go b/mdl/executor/cmd_pages_describe.go index 5a2cb9e1a6..8049e35a3f 100644 --- a/mdl/executor/cmd_pages_describe.go +++ b/mdl/executor/cmd_pages_describe.go @@ -724,7 +724,12 @@ type rawWidget struct { // is not an image collection, or when none is selected. Without it a // describe -> exec copy loses the image (mxcli-formula1 FINDINGS §142). ImageObject string - OnClickType string // "action", "enlarge" + // DefaultImage is a DYNAMIC image's fallback, shown when the bound object + // carries none — Forms$ImageViewer.DefaultImage, a by-name reference to + // Images$Image, so the same three-part Module.Collection.Image name as + // ImageObject but a different property, which is why it is not that field. + DefaultImage string + OnClickType string // "action", "enlarge" } // rawExplicitProp represents a non-default property extracted from a CustomWidget. diff --git a/mdl/executor/cmd_pages_describe_datasource.go b/mdl/executor/cmd_pages_describe_datasource.go index 0bffbc5db8..c4c3d10880 100644 --- a/mdl/executor/cmd_pages_describe_datasource.go +++ b/mdl/executor/cmd_pages_describe_datasource.go @@ -427,8 +427,10 @@ func xpathConstraintClause(constraint string) string { // document that stores the name bare is left alone instead of losing its first // segment. // -// The bound value lives in Expression for both a variable reference ($Term) and -// a literal (10); Variable is the older spelling and is honoured when present. +// The bound value lives in Expression for a literal or an expression, and in +// Variable — a Forms$PageVariable naming a page parameter, snippet parameter or +// page variable — for a $-reference. Reading only Expression, or reading Variable +// as a flat string, drops every argument Studio Pro wrote (#1140). // A parameterless flow yields nil, which the renderer emits without parentheses // — the grammar makes the list optional, and adding empty parens would churn // every existing description. @@ -449,6 +451,11 @@ func flowSourceArgs(ds map[string]any, settingsKey, flowName string) []rawDataSo } value := extractString(mapping["Expression"]) if value == "" { + value = pageVariableArgValue(mapping["Variable"]) + } + if value == "" { + // A pre-#1140 document, or another writer, may have put the bare + // reference text in Variable. value = extractString(mapping["Variable"]) } if value == "" { diff --git a/mdl/executor/cmd_pages_describe_dynamicimage_test.go b/mdl/executor/cmd_pages_describe_dynamicimage_test.go new file mode 100644 index 0000000000..3a3a9ce5f0 --- /dev/null +++ b/mdl/executor/cmd_pages_describe_dynamicimage_test.go @@ -0,0 +1,211 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "strings" + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/visitor" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/domainmodel" + "github.com/mendixlabs/mxcli/sdk/pages" +) + +// `dynamicimage` is the sibling gap left open by mendixlabs/mxcli#1057, and it +// is worse than the static image's was — TWO defects, only one of which is a +// round trip. +// +// Measured on a blank Mendix 11.12.1 project, mxbuild 11.12.1, before this +// change. mxcli authors the widget, and the build refuses it: +// +// [error] [CE0489] "Select an entity for the data source of this dynamic +// image." at Dynamic image 'imgPhoto' +// +// because dynamicImageToGen called imageViewerSourceToGen() with no arguments +// and wrote a Forms$ImageViewerSource carrying nothing but its own `$ID` and +// `EntityRef: null`. So every dynamic image mxcli has ever written is broken at +// build time, not merely lossy. +// +// And, exactly as for the static image, DESCRIBE had no case for the type: +// +// -- Forms$ImageViewer (imgPhoto) -- NOT re-executable: mxcli cannot author +// this widget, so re-running this script would drop it +// +// There is NO Studio Pro-authored dynamic image in the fixture (measured: 1 +// Forms$ImageViewer in a blank 11.12.1 app, and it is mxcli's own), so the +// document shape here is metamodel-derived. What IS pinned to Studio Pro is the +// part that matters: `DomainModels$DirectEntityRef{Entity: "Module.Entity"}`, +// 20 of 20 instances in the same app, which is the element CE0489 is asking for. + +// storedDynamicImage is one Forms$ImageViewer bound to an entity. +func storedDynamicImage(name, entity string) map[string]any { + src := map[string]any{"$Type": "Forms$ImageViewerSource"} + if entity != "" { + src["EntityRef"] = map[string]any{ + "$Type": "DomainModels$DirectEntityRef", + "Entity": entity, + } + } + return map[string]any{ + "$Type": "Forms$ImageViewer", + "Name": name, + "DataSource": src, + "Responsive": true, + "Width": int32(200), + "Height": int32(200), + } +} + +// rebuildDynamicImage replays one emitted widget through the real parser and the +// real page builder, returning what exec would store. +func rebuildDynamicImage(t *testing.T, widgetMDL string) *pages.DynamicImage { + t.Helper() + src := "create page Mod.P (Title: 'T') {\n" + widgetMDL + "\n}" + prog, errs := visitor.Build(src) + if len(errs) > 0 { + t.Fatalf("DESCRIBE emitted MDL that does not parse (%q): %v", widgetMDL, errs) + } + page := prog.Statements[0].(*ast.CreatePageStmtV3) + // A real domain model, because buildDataSourceV3 RESOLVES the entity it is + // given — the binding is checked, not copied through, which is the whole + // point of writing an EntityRef rather than a name. + pb := &pageBuilder{ + widgetScope: map[string]model.ID{}, + execCache: &executorCache{ + hierarchy: &ContainerHierarchy{moduleNames: map[model.ID]string{ + model.ID("mod-my"): "MyFirstModule", + }}, + domainModels: []*domainmodel.DomainModel{{ + ContainerID: model.ID("mod-my"), + Entities: []*domainmodel.Entity{ + {BaseElement: model.BaseElement{ID: model.ID("e-photo")}, Name: "Photo"}, + }, + }}, + }, + } + widget, err := pb.buildWidgetV3(page.Widgets[0]) + if err != nil { + t.Fatalf("building %q: %v", widgetMDL, err) + } + img, ok := widget.(*pages.DynamicImage) + if !ok { + t.Fatalf("replay built %T, want *pages.DynamicImage", widget) + } + return img +} + +// TestDescribeDynamicImage_RoundTripsTheDataSource is the CE0489 half and the +// describe half at once: the entity the widget is bound to has to survive +// describe -> exec, because it is the one property without which the build +// fails outright. +func TestDescribeDynamicImage_RoundTripsTheDataSource(t *testing.T) { + const entity = "MyFirstModule.Photo" + + got := describeStoredWidget(t, storedDynamicImage("imgPhoto", entity)) + if strings.Contains(got, "NOT re-executable") { + t.Fatalf("an authorable widget is still flagged as unauthorable:\n%s", got) + } + if !strings.Contains(got, "dynamicimage imgPhoto") { + t.Fatalf("describe did not emit the dynamicimage keyword:\n%s", got) + } + if !strings.Contains(got, "database from "+entity) { + t.Fatalf("describe did not emit the data source, so replay rebuilds a widget CE0489 refuses:\n%s", got) + } + + replayed := rebuildDynamicImage(t, strings.TrimRight(got, "\n")) + db, ok := replayed.DataSource.(*pages.DatabaseSource) + if !ok { + t.Fatalf("replay rebuilt DataSource %T, want *pages.DatabaseSource", replayed.DataSource) + } + if db.EntityName != entity { + t.Errorf("replay rebuilt entity %q, want %q — the round trip loses the binding", db.EntityName, entity) + } +} + +// TestDescribeDynamicImage_RoundTripsDefaultImageAndDisplay covers the rest of +// what the widget stores and the writer hardcoded: the fallback image, the size +// units, Responsive, and the two display flags. +func TestDescribeDynamicImage_RoundTripsDefaultImageAndDisplay(t *testing.T) { + const fallback = "MyFirstModule.Images.placeholder" + stored := storedDynamicImage("imgPhoto", "MyFirstModule.Photo") + stored["DefaultImage"] = fallback + stored["WidthUnit"] = "Pixels" + stored["HeightUnit"] = "Percentage" + stored["Responsive"] = false + stored["ShowAsThumbnail"] = true + stored["OnClickEnlarge"] = true + + got := describeStoredWidget(t, stored) + for _, want := range []string{ + "DefaultImage: '" + fallback + "'", + "WidthUnit: pixels", + "HeightUnit: percentage", + "Responsive: false", + "DisplayAs: thumbnail", + "OnClickType: enlarge", + } { + if !strings.Contains(got, want) { + t.Fatalf("describe did not emit %q, so replay normalises it away:\n%s", want, got) + } + } + + replayed := rebuildDynamicImage(t, strings.TrimRight(got, "\n")) + if replayed.DefaultImageName != fallback { + t.Errorf("replay rebuilt DefaultImageName %q, want %q", replayed.DefaultImageName, fallback) + } + if replayed.WidthUnit != "pixels" || replayed.HeightUnit != "percentage" { + t.Errorf("replay rebuilt units %q/%q, want pixels/percentage", replayed.WidthUnit, replayed.HeightUnit) + } + if replayed.Responsive { + t.Error("replay rebuilt Responsive true — a non-responsive image came back responsive") + } + if !replayed.ShowAsThumbnail { + t.Error("replay rebuilt ShowAsThumbnail false — a thumbnail came back full size") + } + if !replayed.OnClickEnlarge { + t.Error("replay rebuilt OnClickEnlarge false — the enlarge-on-click behaviour is gone") + } +} + +// TestDescribeDynamicImage_DefaultsStaySilent is the CONTROL for the test above. +// Auto units, a responsive image, a full-size image and no enlarge are Mendix's +// defaults that the writer re-derives, so emitting them would put clauses in the +// author's script they never wrote — the "invents" half of the describe failure +// class, where each round trip accumulates another default. +func TestDescribeDynamicImage_DefaultsStaySilent(t *testing.T) { + stored := storedDynamicImage("imgAuto", "MyFirstModule.Photo") + stored["WidthUnit"] = "Auto" + stored["HeightUnit"] = "Auto" + stored["ShowAsThumbnail"] = false + stored["OnClickEnlarge"] = false + + got := describeStoredWidget(t, stored) + for _, unwanted := range []string{"WidthUnit", "HeightUnit", "Responsive", "DisplayAs", "OnClickType", "DefaultImage"} { + if strings.Contains(got, unwanted) { + t.Errorf("describe emitted a default %q the writer re-derives:\n%s", unwanted, got) + } + } + if replayed := rebuildDynamicImage(t, strings.TrimRight(got, "\n")); !replayed.Responsive { + t.Error("an image described with no Responsive clause replayed as non-responsive") + } +} + +// TestDescribeDynamicImage_NoSourceIsStillDescribable is the second CONTROL: a +// widget whose source has no entity — which is exactly what mxcli used to write +// — must still describe as a `dynamicimage`, with no DataSource clause invented +// for it. Emitting one would guess an entity, and a wrong guess is CE0489's +// sibling rather than a fix for it. +func TestDescribeDynamicImage_NoSourceIsStillDescribable(t *testing.T) { + got := describeStoredWidget(t, storedDynamicImage("imgUnbound", "")) + if !strings.Contains(got, "dynamicimage imgUnbound") { + t.Fatalf("describe did not emit the dynamicimage keyword:\n%s", got) + } + if strings.Contains(got, "DataSource") || strings.Contains(got, "database from") { + t.Errorf("an unbound source was emitted as a binding:\n%s", got) + } + if strings.Contains(got, "NOT re-executable") { + t.Errorf("an authorable widget is flagged as unauthorable:\n%s", got) + } +} diff --git a/mdl/executor/cmd_pages_describe_flow_args_test.go b/mdl/executor/cmd_pages_describe_flow_args_test.go new file mode 100644 index 0000000000..ec18170e04 --- /dev/null +++ b/mdl/executor/cmd_pages_describe_flow_args_test.go @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import "testing" + +// The read half of mendixlabs/mxcli#1140. +// +// Studio Pro binds an object-typed flow argument through the mapping's Variable — +// a Forms$PageVariable naming a page parameter, snippet parameter or page +// variable — not through Expression. The three action readers and the data-source +// reader all looked for a `Name` key on that sub-document, which +// Forms$PageVariable does not have, so every argument in Studio Pro-authored +// content was described as absent. +// +// Measured before the fix, on Workflow Commons 4.11.0 (Studio Pro-authored): +// +// Action: microflow WorkflowCommons.ACT_ConflictedWorkflowHelper_ApplyJumpTo +// +// for a button whose stored mappings bind $ConflictedWorkflowHelper and +// $ConflictedWorkflowDefinitionView. Replaying that description drops both +// arguments — the #835 loss in a different storage form. +func TestPageVariableArgValue(t *testing.T) { + for _, tc := range []struct { + name string + raw any + want string + }{ + { + "page parameter", + map[string]any{"$Type": "Forms$PageVariable", "PageParameter": "ConflictedWorkflowHelper"}, + "$ConflictedWorkflowHelper", + }, + { + "snippet parameter", + map[string]any{"$Type": "Forms$PageVariable", "SnippetParameter": "AuditTrailViewer"}, + "$AuditTrailViewer", + }, + { + "page variable", + map[string]any{"$Type": "Forms$PageVariable", "LocalVariable": "showStock"}, + "$showStock", + }, + { + // Studio Pro writes every slot, the unused ones empty — so an empty + // string must not be mistaken for a binding. + "all slots present, one filled", + map[string]any{ + "$Type": "Forms$PageVariable", "LocalVariable": "", "PageParameter": "Workflow", + "SnippetParameter": "", "SubKey": "", "UseAllPages": false, "Widget": "", + }, + "$Workflow", + }, + { + // A grid's selection. MDL has no syntax for it, and rendering it as + // $dataGrid23 would re-execute into a different binding. + "widget selection is not rendered", + map[string]any{"$Type": "Forms$PageVariable", "Widget": "dataGrid23"}, + "", + }, + {"no variable", nil, ""}, + {"empty variable", map[string]any{"$Type": "Forms$PageVariable"}, ""}, + {"not a document", "$Workflow", ""}, + } { + t.Run(tc.name, func(t *testing.T) { + if got := pageVariableArgValue(tc.raw); got != tc.want { + t.Errorf("pageVariableArgValue = %q, want %q", got, tc.want) + } + }) + } +} + +// A data source stores its arguments the same way, so the same loss applied +// there. The Expression form must keep working — a pre-#1140 document, and every +// literal argument, is written that way. +func TestDataSourceArgsReadPageVariableBinding(t *testing.T) { + for _, tc := range []struct { + name string + arg map[string]any + want string + }{ + { + "variable binding", + map[string]any{ + "Parameter": "Mod.DS.Order", + "Variable": map[string]any{"$Type": "Forms$PageVariable", "PageParameter": "Order"}, + }, + "microflow Mod.DS(Order: $Order)", + }, + { + "expression binding still read", + map[string]any{"Parameter": "Mod.DS.Order", "Expression": "$Order"}, + "microflow Mod.DS(Order: $Order)", + }, + { + "bare string in Variable still read", + map[string]any{"Parameter": "Mod.DS.Order", "Variable": "$Order"}, + "microflow Mod.DS(Order: $Order)", + }, + } { + t.Run(tc.name, func(t *testing.T) { + ds := map[string]any{ + "$Type": "Forms$MicroflowSource", + "MicroflowSettings": map[string]any{ + "Microflow": "Mod.DS", + "ParameterMappings": []any{int32(3), tc.arg}, + }, + } + got := parseDataSource(ds) + if got == nil { + t.Fatal("datasource not read") + } + if expr := dataSourceExpr(got); expr != tc.want { + t.Errorf("rendered %q, want %q", expr, tc.want) + } + }) + } +} diff --git a/mdl/executor/cmd_pages_describe_input_assoc_test.go b/mdl/executor/cmd_pages_describe_input_assoc_test.go new file mode 100644 index 0000000000..2771dd1b00 --- /dev/null +++ b/mdl/executor/cmd_pages_describe_input_assoc_test.go @@ -0,0 +1,85 @@ +// SPDX-License-Identifier: Apache-2.0 + +// ako/mxcli#529 (read half) — DESCRIBE PAGE silently DROPS the association hops +// of an input widget bound to an attribute over an association. +// +// Measured on ako/TestApp, page Rules.RuleAction_NewEdit, whose textBox4 stores: +// +// DomainModels$AttributeRef +// Attribute: "Rules.BusinessRule.Name" +// EntityRef: DomainModels$IndirectEntityRef +// Steps: [ EntityRefStep{ Association: "Rules.RuleAction_BusinessRule", +// DestinationEntity: "Rules.BusinessRule" } ] +// +// DESCRIBE emitted `Attribute: Name` — and Rules.RuleAction has NO attribute +// called Name, so re-running that output rebinds the widget to something that +// does not exist. A describe → exec round trip over a Studio Pro page therefore +// BREAKS it, with `check` clean, and the damage only shows up at build time as +// CE1613 or in a browser as a blank field. +// +// DataGrid2 columns already got this right (columnAttributeFromRef, bug 7), so +// the same page could round-trip a grid column and destroy a text box. +package executor + +import "testing" + +func TestExtractAttributeRef_KeepsAssociationHops(t *testing.T) { + step := func(assoc, dest string) map[string]any { + return map[string]any{"$Type": "DomainModels$EntityRefStep", "Association": assoc, "DestinationEntity": dest} + } + widget := func(attr string, steps ...map[string]any) map[string]any { + ref := map[string]any{"$Type": "DomainModels$AttributeRef", "Attribute": attr} + if len(steps) > 0 { + items := make([]any, len(steps)) + for i, s := range steps { + items[i] = s + } + ref["EntityRef"] = map[string]any{"$Type": "DomainModels$IndirectEntityRef", "Steps": items} + } + return map[string]any{"AttributeRef": ref} + } + + tests := []struct { + name string + w map[string]any + want string + }{ + { + // The control: a plain binding must stay bare. The enclosing + // dataview establishes the entity, so a qualified name here would + // not re-parse. + name: "own attribute stays bare", + w: widget("Rules.RuleAction.ActionType"), + want: "ActionType", + }, + { + // The reported case, verbatim from TestApp. + name: "single hop keeps the association", + w: widget("Rules.BusinessRule.Name", step("Rules.RuleAction_BusinessRule", "Rules.BusinessRule")), + want: "RuleAction_BusinessRule/Name", + }, + { + name: "two hops", + w: widget("A.Country.Code", + step("A.Order_Customer", "A.Customer"), + step("A.Customer_Country", "A.Country")), + want: "Order_Customer/Customer_Country/Code", + }, + { + // A malformed EntityRef must fall back to the bare attribute rather + // than emitting a half-built path — broken MDL is worse than a + // binding the reader can see is incomplete. + name: "step with no association falls back", + w: widget("A.Country.Code", + map[string]any{"$Type": "DomainModels$EntityRefStep", "DestinationEntity": "A.Country"}), + want: "Code", + }, + } + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + if got := extractAttributeRef(nil, tc.w); got != tc.want { + t.Errorf("extractAttributeRef = %q, want %q", got, tc.want) + } + }) + } +} diff --git a/mdl/executor/cmd_pages_describe_output.go b/mdl/executor/cmd_pages_describe_output.go index 9aacfae3ac..58894ac165 100644 --- a/mdl/executor/cmd_pages_describe_output.go +++ b/mdl/executor/cmd_pages_describe_output.go @@ -865,6 +865,47 @@ func outputWidgetMDLV3(ctx *ExecContext, w rawWidget, indent int) { props = appendAppearanceProps(props, w) formatWidgetProps(ctx.Output, prefix, header, props, "\n") + case "Forms$ImageViewer", "Pages$ImageViewer": + // The DYNAMIC image's sibling case. Property names are shared with the + // static and the pluggable image on purpose — Width/Height, the units, + // Responsive, DisplayAs, OnClickType — so one spelling means one thing + // across all three. `DefaultImage:` is its own, because the fallback is + // a different property from the image a static viewer shows. + header := fmt.Sprintf("dynamicimage %s", mdlIdent(w.Name)) + props := []string{} + props = appendWidgetDataSources(props, w) + if w.DefaultImage != "" { + props = append(props, fmt.Sprintf("DefaultImage: %s", mdlQuote(w.DefaultImage))) + } + if w.ImageWidth != "" { + props = append(props, fmt.Sprintf("Width: %s", w.ImageWidth)) + } + if w.WidthUnit != "" && w.WidthUnit != "auto" { + props = append(props, fmt.Sprintf("WidthUnit: %s", w.WidthUnit)) + } + if w.ImageHeight != "" { + props = append(props, fmt.Sprintf("Height: %s", w.ImageHeight)) + } + if w.HeightUnit != "" && w.HeightUnit != "auto" { + props = append(props, fmt.Sprintf("HeightUnit: %s", w.HeightUnit)) + } + if w.Responsive == "false" { + props = append(props, "Responsive: false") + } + // Only the non-defaults: full size and no enlarge are Mendix's own, and + // the writer re-derives them. + if w.DisplayAs == "thumbnail" { + props = append(props, "DisplayAs: thumbnail") + } + if w.OnClickType == "enlarge" { + props = append(props, "OnClickType: enlarge") + } + if w.Action != "" { + props = append(props, fmt.Sprintf("Action: %s", w.Action)) + } + props = appendAppearanceProps(props, w) + formatWidgetProps(ctx.Output, prefix, header, props, "\n") + case "Forms$SnippetCallWidget", "Pages$SnippetCallWidget": header := fmt.Sprintf("snippetcall %s", mdlIdent(w.Name)) props := []string{} @@ -1472,13 +1513,9 @@ func extractPageParameters(ctx *ExecContext, settings map[string]any) string { } } - // Check for Variable reference (older format - Variable as a map with Name) + // Check for a Forms$PageVariable binding. if value == "" { - if varRef, ok := mappingMap["Variable"].(map[string]any); ok && varRef != nil { - if varName := extractString(varRef["Name"]); varName != "" { - value = "$" + varName - } - } + value = pageVariableArgValue(mappingMap["Variable"]) } if value != "" { @@ -1533,13 +1570,9 @@ func extractMicroflowParameters(ctx *ExecContext, settings map[string]any) strin } } - // Check for Variable reference (older format - Variable as a map with Name) + // Check for a Forms$PageVariable binding. if value == "" { - if varRef, ok := mappingMap["Variable"].(map[string]any); ok && varRef != nil { - if varName := extractString(varRef["Name"]); varName != "" { - value = "$" + varName - } - } + value = pageVariableArgValue(mappingMap["Variable"]) } if value != "" { @@ -1596,13 +1629,9 @@ func extractNanoflowParameters(ctx *ExecContext, action map[string]any) string { } } - // Check for Variable reference (older format - Variable as a map with Name) + // Check for a Forms$PageVariable binding. if value == "" { - if varRef, ok := mappingMap["Variable"].(map[string]any); ok && varRef != nil { - if varName := extractString(varRef["Name"]); varName != "" { - value = "$" + varName - } - } + value = pageVariableArgValue(mappingMap["Variable"]) } if value != "" { diff --git a/mdl/executor/cmd_pages_describe_parse.go b/mdl/executor/cmd_pages_describe_parse.go index 31584fabb8..727e0d613b 100644 --- a/mdl/executor/cmd_pages_describe_parse.go +++ b/mdl/executor/cmd_pages_describe_parse.go @@ -562,6 +562,56 @@ func parseRawWidget(ctx *ExecContext, w map[string]any, parentEntityContext ...s } return []rawWidget{widget} + case "Forms$ImageViewer", "Pages$ImageViewer": + // The DYNAMIC image. Its binding is a Forms$ImageViewerSource, which the + // shared datasource reader already knows (entityBackedSourceTypes), so + // the entity CE0489 asks for costs nothing to read back. + // + // Nothing read this widget at all before, and the writer bound it to no + // entity, so every dynamic image mxcli authored failed the build and + // every stored one was dropped by describe -> exec. + if ds, ok := w["DataSource"].(map[string]any); ok { + widget.DataSource = parseDataSource(ds) + if widget.DataSource != nil && widget.DataSource.Reference != "" { + widget.EntityContext = dataSourceEntityContext(ctx, widget.DataSource) + } + } + if widget.EntityContext == "" { + widget.EntityContext = inheritedCtx + } + // The fallback image, a by-name reference like the static image's. + if fallback, ok := w["DefaultImage"].(string); ok && fallback != "" { + widget.DefaultImage = fallback + } + if width := extractInt(w["Width"]); width > 0 { + widget.ImageWidth = strconv.Itoa(width) + } + if height := extractInt(w["Height"]); height > 0 { + widget.ImageHeight = strconv.Itoa(height) + } + if u, ok := w["WidthUnit"].(string); ok { + widget.WidthUnit = strings.ToLower(u) + } + if u, ok := w["HeightUnit"].(string); ok { + widget.HeightUnit = strings.ToLower(u) + } + if responsive, ok := w["Responsive"].(bool); ok && !responsive { + widget.Responsive = "false" + } + // ShowAsThumbnail and OnClickEnlarge reuse the vocabulary the PLUGGABLE + // image widget already describes with (DisplayAs, OnClickType), so one + // property name means one thing across all three image widgets. + if thumb, ok := w["ShowAsThumbnail"].(bool); ok && thumb { + widget.DisplayAs = "thumbnail" + } + if enlarge, ok := w["OnClickEnlarge"].(bool); ok && enlarge { + widget.OnClickType = "enlarge" + } + if onClick := asActionMap(w["ClickAction"]); onClick != nil { + widget.Action = extractButtonAction(ctx, map[string]any{"Action": onClick}) + } + return []rawWidget{widget} + case "Forms$Label", "Pages$Label": widget.Content = extractTextCaption(ctx, w) return []rawWidget{widget} @@ -896,18 +946,32 @@ func shortAttributeName(attr string) string { return attr } -// extractAttributeRef extracts the attribute reference from an input widget. -// Returns just the attribute name (last segment). +// extractAttributeRef extracts the attribute reference from an input widget as +// the short form MDL accepts: a bare name for an own attribute, or +// `Assoc/.../Attr` when the binding navigates associations. +// +// This used to return the last segment of AttributeRef.Attribute and ignore +// AttributeRef.EntityRef entirely, which silently dropped every association +// hop. Measured on ako/TestApp's Rules.RuleAction_NewEdit, whose text box binds +// Rules.BusinessRule.Name over Rules.RuleAction_BusinessRule: DESCRIBE emitted +// `Attribute: Name`, and Rules.RuleAction has no Name — so a describe → exec +// round trip rebound the widget to nothing. `check` stayed clean and the damage +// surfaced at build time as CE1613, or in a browser as a blank field +// (ako/mxcli#529). +// +// columnAttributeFromRef already did this correctly for DataGrid2 columns (bug +// 7), so one page could round-trip a grid column and destroy a text box beside +// it. Sharing that function is the point: two readers of one BSON shape is how +// the halves drifted apart to begin with. func extractAttributeRef(ctx *ExecContext, w map[string]any) string { attrRef, ok := w["AttributeRef"].(map[string]any) if !ok { return "" } - attr, ok := attrRef["Attribute"].(string) - if !ok { + if _, ok := attrRef["Attribute"].(string); !ok { return "" } - return shortAttributeName(attr) + return columnAttributeFromRef(attrRef) } // parseGalleryContent extracts the content widget from a Gallery. diff --git a/mdl/executor/cmd_pages_flow_args.go b/mdl/executor/cmd_pages_flow_args.go new file mode 100644 index 0000000000..cfbb63bcfb --- /dev/null +++ b/mdl/executor/cmd_pages_flow_args.go @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import "strings" + +// classifyFlowArgValue decides how an action or data-source argument binds. +// +// Mendix stores a flow argument two ways and does not treat them as +// interchangeable: a reference to a page parameter, snippet parameter or page +// variable is a Forms$PageVariable under the mapping's Variable, while a literal +// or expression is text under Expression. Writing a $-reference as an Expression +// leaves the parameter unbound — Studio Pro reports CE1571 "No argument has been +// selected for parameter 'X' and no default is available" — and mxbuild builds +// that same document at 0 errors, so nothing catches it before the page is opened +// (mendixlabs/mxcli#1140). +// +// The four places that built a mapping each carried their own copy of the old +// `strings.HasPrefix(v, "$")` rule (show_page, microflow, nanoflow, data source). +// One classifier instead: the same argument must bind the same way wherever it is +// written, or the spelling decides the storage. +// +// kind is "" when the value is not a page-variable reference, which leaves the +// caller's existing behaviour untouched. Deliberately in that bucket: +// +// - $currentObject — the enclosing context object. No Studio Pro reference for +// the bare form was measured, and mxcli's show_page handling already depends +// on the context object being inferred rather than named (MDL-PAGEARG01), so +// changing it on a guess would risk the case that works. +// - a path or dotted expression ($obj/Module.Assoc, $p.Attr) — an expression, +// which is where Mendix stores it. +// - a name that is not a declared object-typed parameter or variable of the +// document being built — including a primitive parameter, which Mendix binds +// as an expression (all six Expression-bound mappings in the Workflow Commons +// reference are Boolean literals). +func (pb *pageBuilder) classifyFlowArgValue(value string) (variable, kind string) { + name, ok := strings.CutPrefix(value, "$") + if !ok || name == "" { + return "", "" + } + if strings.ContainsAny(name, "/.") { + return "", "" + } + if strings.EqualFold(name, "currentObject") { + return "", "" + } + if pb.localVariables[name] { + return value, "local" + } + // paramScope holds only the entity-typed parameters, which is the set Mendix + // binds through a PageVariable. + if _, isParam := pb.paramScope[name]; isParam { + if pb.isSnippet { + return value, "snippet" + } + return value, "parameter" + } + return "", "" +} + +// pageVariableArgValue renders a mapping's Variable — a Forms$PageVariable — as +// the MDL `$name` that produced it, or "" when there is no variable binding. +// +// This is the read half of #1140, and it was wrong in a way that hid the write +// half. The three describe readers looked for a `Name` key on the sub-document; +// Forms$PageVariable has no such property. Studio Pro names the reference in +// whichever of PageParameter / SnippetParameter / LocalVariable applies, so every +// argument in Studio Pro-authored content described as absent: measured on +// Workflow Commons 4.11.0, `DESCRIBE PAGE` printed +// +// Action: microflow WorkflowCommons.ACT_ConflictedWorkflowHelper_ApplyJumpTo +// +// for a button whose stored mapping binds $ConflictedWorkflowHelper — the same +// describe → exec loss as #835, one storage form over. +// +// Widget is deliberately not read. It names the grid whose SELECTION supplies a +// list argument, which MDL has no syntax for; rendering it as `$widgetName` would +// emit a statement that re-executes into a different binding. +func pageVariableArgValue(raw any) string { + v, ok := raw.(map[string]any) + if !ok || v == nil { + return "" + } + for _, key := range []string{"PageParameter", "SnippetParameter", "LocalVariable"} { + if name := extractString(v[key]); name != "" { + return "$" + name + } + } + return "" +} diff --git a/mdl/executor/cmd_pages_flow_args_test.go b/mdl/executor/cmd_pages_flow_args_test.go new file mode 100644 index 0000000000..a08814495f --- /dev/null +++ b/mdl/executor/cmd_pages_flow_args_test.go @@ -0,0 +1,109 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/model" +) + +// mendixlabs/mxcli#1140. The reported page has two parameters and a Save button +// inside a data view bound to the first: +// +// Action: nanoflow CustomModule.ACT_BufferDefinition_SaveEdit_NF( +// $Dto = $Dto, $BufferDefinition = $BufferDefinition) +// +// "$BufferDefinition = $BufferDefinition → NOT wired — Studio Pro shows CE1571: +// 'No argument has been selected for parameter BufferDefinition and no default is +// available.'" +// +// Both arguments were written the same way — as a text Expression — so the +// asymmetry is not in the writing: Studio Pro supplies a default for the one that +// happens to be the data view's object and reports the other, which is exactly +// what "and no default is available" says. Neither was bound. +func builderWithParams(params ...string) *pageBuilder { + pb := &pageBuilder{paramScope: map[string]model.ID{}, localVariables: map[string]bool{}} + for _, p := range params { + pb.paramScope[p] = model.ID("id-" + p) + } + return pb +} + +func TestClassifyFlowArgValue(t *testing.T) { + pb := builderWithParams("Dto", "BufferDefinition") + pb.localVariables["showStock"] = true + + for _, tc := range []struct { + name, value, wantVar, wantKind string + }{ + {"reported page parameter", "$BufferDefinition", "$BufferDefinition", "parameter"}, + {"the data view's own parameter", "$Dto", "$Dto", "parameter"}, + {"page variable", "$showStock", "$showStock", "local"}, + + // Left as expressions, each for its own reason — see classifyFlowArgValue. + {"current object", "$currentObject", "", ""}, + {"association path", "$Dto/Module.Assoc", "", ""}, + {"attribute path", "$Dto.Name", "", ""}, + {"literal", "true", "", ""}, + {"undeclared name", "$Whatever", "", ""}, + {"bare dollar", "$", "", ""}, + {"empty", "", "", ""}, + } { + t.Run(tc.name, func(t *testing.T) { + gotVar, gotKind := pb.classifyFlowArgValue(tc.value) + if gotVar != tc.wantVar || gotKind != tc.wantKind { + t.Errorf("classifyFlowArgValue(%q) = (%q, %q), want (%q, %q)", + tc.value, gotVar, gotKind, tc.wantVar, tc.wantKind) + } + }) + } +} + +// Inside a snippet the same reference fills the SnippetParameter slot of the +// Forms$PageVariable — 58 of the 95 Studio Pro bindings measured are that one, so +// getting it wrong would be the common case, not the corner. +func TestClassifyFlowArgValueInSnippet(t *testing.T) { + pb := builderWithParams("AuditTrailViewer") + pb.isSnippet = true + + gotVar, gotKind := pb.classifyFlowArgValue("$AuditTrailViewer") + if gotVar != "$AuditTrailViewer" || gotKind != "snippet" { + t.Errorf("in a snippet: got (%q, %q), want ($AuditTrailViewer, snippet)", gotVar, gotKind) + } +} + +// A primitive page parameter is not in paramScope (only entity-typed ones are), +// so it stays an expression — matching the reference, where all six +// Expression-bound mappings are Boolean literals. +func TestClassifyFlowArgValueLeavesPrimitiveParameterAlone(t *testing.T) { + pb := builderWithParams("Order") // "Qty", a primitive, is deliberately absent + if _, kind := pb.classifyFlowArgValue("$Qty"); kind != "" { + t.Errorf("primitive parameter classified as %q, want an expression", kind) + } + if _, kind := pb.classifyFlowArgValue("$Order"); kind != "parameter" { + t.Fatalf("control: the entity-typed parameter is not classified either (%q) — "+ + "the test above proves nothing", kind) + } +} + +// The data-source path shares the classifier, so a parameterized microflow data +// source binds its page-parameter argument the same way a button does. Before +// #1140 these were two copies of one rule in two functions. +func TestDataSourceArgsBindPageParameterThroughVariable(t *testing.T) { + pb := builderWithParams("Order") + got := pb.flowArgsToParameterMappings([]ast.FlowArgV3{ + {Name: "Order", Value: "$Order"}, + {Name: "Limit", Value: "10"}, + }) + if len(got) != 2 { + t.Fatalf("got %d mappings, want 2", len(got)) + } + if got[0].VariableKind != "parameter" || got[0].Variable != "$Order" { + t.Errorf("page-parameter arg = %+v, want Variable=$Order kind=parameter", got[0]) + } + if got[1].VariableKind != "" || got[1].Expression != "10" { + t.Errorf("literal arg = %+v, want Expression=10 and no kind", got[1]) + } +} diff --git a/mdl/executor/cmd_pages_layout_v3.go b/mdl/executor/cmd_pages_layout_v3.go index dccdd29bcc..cee03814c0 100644 --- a/mdl/executor/cmd_pages_layout_v3.go +++ b/mdl/executor/cmd_pages_layout_v3.go @@ -142,6 +142,9 @@ func execCreateLayout(ctx *ExecContext, s *ast.CreateLayoutStmt) error { fragments: ctx.Fragments, themeRegistry: ctx.GetThemeRegistry(), widgetBackend: ctx.Backend, + // The root of a document that this pass walks in full: there is no + // enclosing data widget, so there is no context object. #1029. + argCtx: atDocumentRoot(), } // Built before the old one is deleted: a build failure must leave the diff --git a/mdl/executor/cmd_pages_showpage_args.go b/mdl/executor/cmd_pages_showpage_args.go index 5b09763dfd..49ae5d5714 100644 --- a/mdl/executor/cmd_pages_showpage_args.go +++ b/mdl/executor/cmd_pages_showpage_args.go @@ -10,6 +10,60 @@ import ( "github.com/mendixlabs/mxcli/mdl/linter" ) +// pageArgContext is what a widget-tree walk knows about the object Mendix would +// pass to a page opened by a widget's SHOW_PAGE action. +// +// mxcli stores a widget's show-page action with an EMPTY ParameterMappings array +// and lets Mendix infer the argument from the enclosing widget's context object. +// That is deliberate and twice-confirmed: an explicit Forms$PageParameterMapping +// whose Argument is "$currentObject" makes Studio Pro report CE0115 "parameters do +// not match", because a widget's current-row object is an inferred WidgetValue and +// not an Argument expression (issue #296, re-confirmed against mxbuild 11.12.1 for +// mxcli-formula1 §56). See formSettingsToGen in mdl/backend/modelsdk/widget_write.go. +// +// So the argument is honoured only when it names the context object, either as +// $currentObject or by the name of the variable the enclosing data widget is bound +// to. Anything else is refused by the caller rather than silently re-pointed. +// +// Three states, not two, and the third is what mendixlabs/mxcli#1029 reported: +// +// - known=false — the pass cannot say what encloses the widget. ALTER PAGE's +// SET/INSERT build an action against a stored page they never traverse, so an +// empty varName means nothing about the argument. The guard stands down: it +// only ever refuses what it can prove is discarded. +// - known=true, present=true — a data-bound widget encloses this one, so there +// IS a context object. The argument is honoured when it names that object. +// - known=true, present=false — the walk started at the root of a page, layout +// or snippet and never entered a data widget. There is NO context object: +// $currentObject is unbound and the empty mapping mxcli writes is not an +// inferred one, it is a missing one. EVERY argument is discarded here, +// whatever its form, and mxbuild reports CE1571 per parameter of the target +// page. +type pageArgContext struct { + known bool + present bool + // varName is the name the data source gives the context object, without the + // "$" (e.g. "Car" for `dataview dv (DataSource: $Car)`). Empty when the + // context object has no name of its own — a database, association, microflow + // or selection source supplies a row object addressable only as + // $currentObject. + varName string + // entity is the qualified entity of that context object, for the message. + entity string +} + +// enteringDataWidget is the context below a data-bound widget bound to ds and +// yielding entity. +func enteringDataWidget(ds *ast.DataSourceV3, entity string) pageArgContext { + return pageArgContext{known: true, present: true, varName: contextVarFor(ds), entity: entity} +} + +// atDocumentRoot is the context at the top of a page, snippet or layout the pass +// walks in full: knowable, and empty. +func atDocumentRoot() pageArgContext { + return pageArgContext{known: true} +} + // contextVarFor returns the name a data source gives its context object, without // the "$". Only a parameter/variable source names it; a database, association, // microflow or selection source yields a row object addressable only as @@ -21,108 +75,152 @@ func contextVarFor(ds *ast.DataSourceV3) string { return strings.TrimPrefix(ds.Reference, "$") } -// pageArgumentBindsContextObject reports whether the argument `value`, written on -// a SHOW_PAGE widget action, denotes the object Mendix will actually pass to the -// target page. +// contextFreeContainers are the widget kinds that hold other widgets and bind no +// data of their own, so their children see exactly the context they do. // -// mxcli stores a widget's show-page action with an EMPTY ParameterMappings array -// and lets Mendix infer the argument from the enclosing widget's context object. -// That is deliberate and twice-confirmed: an explicit Forms$PageParameterMapping -// whose Argument is "$currentObject" makes Studio Pro report CE0115 "parameters do -// not match", because a widget's current-row object is an inferred WidgetValue and -// not an Argument expression (issue #296, re-confirmed against mxbuild 11.12.1 for -// mxcli-formula1 §56). See the comment on *pages.PageClientAction in -// sdk/mpr/writer_widgets_action.go. -// -// The half that was missing is what happens when the author names something that -// is NOT the context object. `SHOW_PAGE Detail(Car: $Other)` inside a data view -// bound to $Car stored the same empty array, so the button opened Detail with -// $Car. The argument was not rejected, not warned about, and not visible -// afterwards: DESCRIBE prints the mapping Mendix infers, and `mx check` reports 0 -// errors, because an inferred mapping is perfectly valid. The model is a valid -// model of a different app than the one the author wrote — the exact trap §39's -// reporter spent three cycles in while distrusting a button that was correct. -// -// So the argument is honoured only when it names the context object, either as -// $currentObject or by the name of the variable the enclosing data widget is bound -// to. Anything else is refused by the caller rather than silently re-pointed. +// The list is an allow-list on purpose. Concluding "there is no context object +// here" is only safe when every widget between the page root and this one is +// known to bind nothing, and a widget's data source is not always readable from +// the AST: `datagrid dg (DataSource: Mod.Entity)` — the bare-entity shorthand — +// leaves a plain string rather than a parsed *ast.DataSourceV3, and a pluggable +// widget names its source under its own key. Anything not listed here therefore +// degrades the context to UNKNOWN rather than to ABSENT, so a row-scoped button +// is never refused (mdl-examples/bug-tests/295-showpage-null-variable.mdl is that +// case, and it is exactly what the first cut of #1029 broke). +var contextFreeContainers = map[string]bool{ + "container": true, "customcontainer": true, "slot": true, + "layoutgrid": true, "row": true, "column": true, + "tabcontainer": true, "tabpage": true, + "groupbox": true, "scrollcontainer": true, "region": true, + "header": true, "footer": true, "placeholder": true, +} + +// argContextForChildren is the context the children of w see. +func argContextForChildren(w *ast.WidgetV3, parent pageArgContext) pageArgContext { + if ds := w.GetDataSource(); ds != nil { + // The entity is only used to word the refusal; the executor's builder + // overwrites this with one that carries it. + return enteringDataWidget(ds, "") + } + if parent.known && !parent.present && !contextFreeContainers[strings.ToLower(w.Type)] { + return pageArgContext{} + } + return parent +} + +// argContextForSubtreeOf is argContextForChildren for the executor's builder, +// which builds a widget AND its children in one call. A widget with no children +// carries nothing but its own action, and that action is judged in the context +// its parent supplies — degrading there would stand the guard down on exactly the +// page-level button #1029 is about. +func argContextForSubtreeOf(w *ast.WidgetV3, parent pageArgContext) pageArgContext { + if len(w.Children) == 0 { + return parent + } + return argContextForChildren(w, parent) +} + +// binds reports whether the argument `value`, written on a SHOW_PAGE widget +// action, denotes the object Mendix will actually pass to the target page. // // Arguments that are not a $-reference (a literal or an expression) are left -// alone: they cannot be checked this way, and refusing them would be guesswork. -// validateShowPageArguments is the check-time mirror of the executor guard, so -// `mxcli check` reports the ignored argument without needing a project — the same -// pairing as MDL-WIDGET09. contextVar is the variable the nearest enclosing data -// widget is bound to, "" when the context object has no name of its own. -func validateShowPageArguments(w *ast.WidgetV3, contextVar string, contextKnown bool, locationPrefix string) []linter.Violation { - if w == nil { - return nil +// alone WHERE A CONTEXT OBJECT EXISTS: they cannot be checked against it, and +// refusing them would be guesswork. Where none exists there is nothing to guess +// about — the mapping is empty either way — so they are refused too. +func (c pageArgContext) binds(value string) bool { + if !c.known { + return true } - action := w.GetAction() - if action == nil || action.Type != "showPage" { - return nil + if !c.present { + return false } - var out []linter.Violation - for _, arg := range action.Args { - strVal, ok := arg.Value.(string) - if !ok || pageArgumentBindsContextObject(strVal, contextVar, contextKnown) { - continue - } - bound := "the enclosing widget's context object" - if contextVar != "" { - bound = "$" + contextVar - } - out = append(out, linter.Violation{ - RuleID: "MDL-PAGEARG01", - Severity: linter.SeverityError, - Message: fmt.Sprintf( - "%s: widget `%s`: show_page %s argument `%s: %s` cannot be stored — a widget's page argument is always the enclosing context object, so the page would open with %s instead. Use $currentObject, or call a microflow that shows the page with the object you want", - locationPrefix, w.Name, action.Target, arg.Name, strVal, bound, - ), - }) + if !strings.HasPrefix(value, "$") { + return true } - return out + name := strings.TrimPrefix(value, "$") + // A path expression ($obj/Module.Assoc) is not a plain variable reference. + if strings.ContainsAny(name, "/.") { + return true + } + if strings.EqualFold(name, "currentObject") { + return true + } + return c.varName != "" && strings.EqualFold(name, c.varName) } // describeContextObject names the object Mendix will actually pass, for the // refusal message. -func (pb *pageBuilder) describeContextObject() string { - if pb.contextVarName != "" { - return "$" + pb.contextVarName +func (c pageArgContext) describeContextObject() string { + if c.varName != "" { + return "$" + c.varName } - if pb.entityContext != "" { - return "the row object of the enclosing widget (" + pb.entityContext + ")" + if c.entity != "" { + return "the row object of the enclosing widget (" + c.entity + ")" } return "the enclosing context object" } // contextVarAlternative offers the context variable by name when it has one, so // the message names a spelling that works rather than only one that does not. -func (pb *pageBuilder) contextVarAlternative() string { - if pb.contextVarName == "" { +func (c pageArgContext) contextVarAlternative() string { + if c.varName == "" { return "" } - return " (or $" + pb.contextVarName + ")" + return " (or $" + c.varName + ")" } -// contextKnown is false when the caller did not walk in through a data widget and -// so cannot say what the context object is — ALTER PAGE's SET/INSERT build an -// action against a stored page this pass never traverses. The guard then allows -// the argument: it only ever refuses what it can prove is discarded, and an -// unprovable case must behave exactly as it did before the guard existed. -func pageArgumentBindsContextObject(value, contextVar string, contextKnown bool) bool { - if !contextKnown { - return true +// refuseShowPageArgument is the single wording `mxcli check` and `exec` share. +// Two copies in two currencies is how a resolver drifts, and this one already +// had to be told about a third context state. +// +// The rule ID is NOT in the text: `mxcli check` renders it from the violation, +// so embedding it here prints it twice. The exec path, which has no violation to +// render, appends it itself. +func refuseShowPageArgument(widgetName, target, argName, argValue string, c pageArgContext) string { + where := "widget `" + widgetName + "`" + if widgetName == "" { + where = "this widget" } - if !strings.HasPrefix(value, "$") { - return true + if !c.present { + return fmt.Sprintf( + "show_page %s: argument %s: %s cannot be stored — %s is not inside a data view, list view or grid row, "+ + "so there is no context object at all, and a widget's page argument is always that object. "+ + "The page would be opened with no argument, which mxbuild reports as CE1571 \"No argument has been "+ + "selected for parameter '%s'\". Put the button inside a data widget bound to the object, or call a "+ + "microflow that shows the page with it", + target, argName, argValue, where, argName) } - name := strings.TrimPrefix(value, "$") - // A path expression ($obj/Module.Assoc) is not a plain variable reference. - if strings.ContainsAny(name, "/.") { - return true + return fmt.Sprintf( + "show_page %s: argument %s: %s cannot be stored — a widget's page argument is always the enclosing context "+ + "object, which mxcli records by leaving the mapping empty (an explicit one is rejected as CE0115). "+ + "Writing %s here would silently open the page with %s instead. Use $currentObject%s, or call a microflow "+ + "that shows the page with the object you want", + target, argName, argValue, argValue, c.describeContextObject(), c.contextVarAlternative()) +} + +// validateShowPageArguments is the check-time mirror of the executor guard, so +// `mxcli check` reports the ignored argument without needing a project — the same +// pairing as MDL-WIDGET09. +func validateShowPageArguments(w *ast.WidgetV3, c pageArgContext, locationPrefix string) []linter.Violation { + if w == nil { + return nil } - if strings.EqualFold(name, "currentObject") { - return true + action := w.GetAction() + if action == nil || action.Type != "showPage" { + return nil + } + var out []linter.Violation + for _, arg := range action.Args { + strVal, ok := arg.Value.(string) + if !ok || c.binds(strVal) { + continue + } + out = append(out, linter.Violation{ + RuleID: "MDL-PAGEARG01", + Severity: linter.SeverityError, + Message: fmt.Sprintf("%s: %s", + locationPrefix, refuseShowPageArgument(w.Name, action.Target, arg.Name, strVal, c)), + }) } - return contextVar != "" && strings.EqualFold(name, contextVar) + return out } diff --git a/mdl/executor/cmd_pages_showpage_args_test.go b/mdl/executor/cmd_pages_showpage_args_test.go index f116ca23a8..091d422d17 100644 --- a/mdl/executor/cmd_pages_showpage_args_test.go +++ b/mdl/executor/cmd_pages_showpage_args_test.go @@ -3,6 +3,7 @@ package executor import ( + "strings" "testing" "github.com/mendixlabs/mxcli/mdl/ast" @@ -14,63 +15,76 @@ import ( // printed the inferred mapping — so nothing anywhere said the written argument had // been ignored. func TestPageArgumentBindsContextObject(t *testing.T) { + inDataView := func(varName string) pageArgContext { + return pageArgContext{known: true, present: true, varName: varName} + } cases := []struct { - name string - value string - contextVar string - contextKnown bool - want bool + name string + value string + ctx pageArgContext + want bool }{ // The context object, under either spelling. - {"currentObject in a database-backed list", "$currentObject", "", true, true}, - {"currentObject inside a parameter data view", "$currentObject", "Car", true, true}, - {"the context variable by its own name", "$Car", "Car", true, true}, - {"case-insensitive, as MDL identifiers are", "$car", "Car", true, true}, + {"currentObject in a database-backed list", "$currentObject", inDataView(""), true}, + {"currentObject inside a parameter data view", "$currentObject", inDataView("Car"), true}, + {"the context variable by its own name", "$Car", inDataView("Car"), true}, + {"case-insensitive, as MDL identifiers are", "$car", inDataView("Car"), true}, // The bug: a different variable, silently re-pointed at the context object. - {"another page parameter", "$Other", "Car", true, false}, - {"any variable in a database-backed list", "$Other", "", true, false}, + {"another page parameter", "$Other", inDataView("Car"), false}, + {"any variable in a database-backed list", "$Other", inDataView(""), false}, - // Not a plain variable reference — not checkable this way, so left alone. - {"an association path", "$currentObject/Sales.Order_Customer", "Car", true, true}, - {"a literal", "'Sales'", "Car", true, true}, - {"an expression", "1 + 2", "Car", true, true}, + // Not a plain variable reference — not checkable against a context object + // that exists, so left alone. + {"an association path", "$currentObject/Sales.Order_Customer", inDataView("Car"), true}, + {"a literal", "'Sales'", inDataView("Car"), true}, + {"an expression", "1 + 2", inDataView("Car"), true}, // ALTER PAGE builds an action without traversing the stored page, so the // context object is unknown, not absent. The guard must stay quiet — refusing // here would reject `SET Action = SHOW_PAGE P(Car: $Car) ON btnGo`, which is // correct code. - {"context unknown (ALTER PAGE)", "$Car", "", false, true}, - {"context unknown, any variable", "$Other", "", false, true}, + {"context unknown (ALTER PAGE)", "$Car", pageArgContext{}, true}, + {"context unknown, any variable", "$Other", pageArgContext{}, true}, + + // mendixlabs/mxcli#1029: no enclosing data widget at all. $currentObject is + // unbound here, so the empty ParameterMappings mxcli writes is not an + // inferred mapping but a missing one — CE1571 at build. EVERY form of + // argument is discarded, including the ones that are left alone above, + // because there is no context object for them to be checked against. + {"#1029: a page parameter on a page-level button", "$SomeRef", atDocumentRoot(), false}, + {"#1029: $currentObject outside any data widget", "$currentObject", atDocumentRoot(), false}, + {"#1029: a literal outside any data widget", "'literal'", atDocumentRoot(), false}, + {"#1029: an association path outside any data widget", "$SomeRef/Mod.Assoc", atDocumentRoot(), false}, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - if got := pageArgumentBindsContextObject(tc.value, tc.contextVar, tc.contextKnown); got != tc.want { - t.Errorf("pageArgumentBindsContextObject(%q, %q, %v) = %v, want %v", - tc.value, tc.contextVar, tc.contextKnown, got, tc.want) + if got := tc.ctx.binds(tc.value); got != tc.want { + t.Errorf("pageArgContext%+v.binds(%q) = %v, want %v", tc.ctx, tc.value, got, tc.want) } }) } } +func showPageButton(arg string) *ast.WidgetV3 { + return &ast.WidgetV3{ + Type: "actionbutton", + Name: "btnGo", + Properties: map[string]any{ + "Action": &ast.ActionV3{ + Type: "showPage", + Target: "Mod.Detail", + Args: []ast.FlowArgV3{{Name: "Car", Value: arg}}, + }, + }, + } +} + // The check-time mirror must see the context variable of the nearest enclosing // data widget, not of the widget carrying the action — a button has no data // source of its own. func TestValidateShowPageArguments_ThroughTheWidgetTree(t *testing.T) { - button := func(arg string) *ast.WidgetV3 { - return &ast.WidgetV3{ - Type: "actionbutton", - Name: "btnGo", - Properties: map[string]any{ - "Action": &ast.ActionV3{ - Type: "showPage", - Target: "Mod.Detail", - Args: []ast.FlowArgV3{{Name: "Car", Value: arg}}, - }, - }, - } - } dataviewOn := func(ref string, child *ast.WidgetV3) *ast.WidgetV3 { return &ast.WidgetV3{ Type: "dataview", @@ -87,9 +101,9 @@ func TestValidateShowPageArguments_ThroughTheWidgetTree(t *testing.T) { tree []*ast.WidgetV3 want int }{ - {"argument is the context variable", []*ast.WidgetV3{dataviewOn("$Car", button("$Car"))}, 0}, - {"argument is $currentObject", []*ast.WidgetV3{dataviewOn("$Car", button("$currentObject"))}, 0}, - {"argument is another variable", []*ast.WidgetV3{dataviewOn("$Car", button("$Other"))}, 1}, + {"argument is the context variable", []*ast.WidgetV3{dataviewOn("$Car", showPageButton("$Car"))}, 0}, + {"argument is $currentObject", []*ast.WidgetV3{dataviewOn("$Car", showPageButton("$currentObject"))}, 0}, + {"argument is another variable", []*ast.WidgetV3{dataviewOn("$Car", showPageButton("$Other"))}, 1}, } // The tree walk resolves every widget against the registry, so a real one is @@ -115,3 +129,93 @@ func TestValidateShowPageArguments_ThroughTheWidgetTree(t *testing.T) { }) } } + +// mendixlabs/mxcli#1029: "A page-level `actionbutton` (outside any dataview) with +// `Action: show_page Page(Param: $Var)` silently drops the argument and rebinds +// every target-page parameter to `$currentObject` — CE1571 at build." +// +// `mxcli check` reported "All references valid", `exec` reported success, and +// `DESCRIBE PAGE` then printed `(Item: $currentObject)` on a page where +// $currentObject is unbound. The check-time half of the guard is what a reporter +// meets first, so it is asserted on the same widget tree the report used: a +// button at the ROOT of the page, with nothing data-bound above it. +func TestValidateShowPageArguments_NoEnclosingDataWidget(t *testing.T) { + registry := LoadWidgetRegistry("") + if registry == nil { + t.Fatal("LoadWidgetRegistry returned nil") + } + + // Every variant the report says it bisected, each one still discarded. + for _, arg := range []string{"$SomeRef", "$currentObject", "'literal'", "$SomeRef/Mod.Assoc"} { + t.Run(arg, func(t *testing.T) { + tree := []*ast.WidgetV3{showPageButton(arg)} + var hits int + var msg string + for _, v := range validateWidgetTree(tree, registry, "page Test.List") { + if v.RuleID == "MDL-PAGEARG01" { + hits++ + msg = v.Message + } + } + if hits != 1 { + t.Fatalf("MDL-PAGEARG01 violations = %d, want 1 — a page-level show_page argument is dropped and builds to CE1571", hits) + } + // The message has to name the failure the reporter will see from + // mxbuild, or it sends them looking for a different bug. + for _, want := range []string{"CE1571", "btnGo", "Mod.Detail"} { + if !strings.Contains(msg, want) { + t.Errorf("refusal message does not mention %q: %s", want, msg) + } + } + }) + } + + // A nested container changes nothing: what matters is that no ancestor is + // data-bound, not how deep the button sits. + nested := []*ast.WidgetV3{{ + Type: "container", + Name: "c1", + Children: []*ast.WidgetV3{{Type: "layoutgrid", Name: "lg", Children: []*ast.WidgetV3{showPageButton("$SomeRef")}}}, + }} + var hits int + for _, v := range validateWidgetTree(nested, registry, "page Test.List") { + if v.RuleID == "MDL-PAGEARG01" { + hits++ + } + } + if hits != 1 { + t.Errorf("MDL-PAGEARG01 violations inside plain containers = %d, want 1", hits) + } + + // The control for the refusal: a zero-argument show_page at page level is + // exactly what the report says still works, and must stay silent. + noArgs := []*ast.WidgetV3{{ + Type: "actionbutton", + Name: "btnGo", + Properties: map[string]any{"Action": &ast.ActionV3{Type: "showPage", Target: "Mod.Detail"}}, + }} + for _, v := range validateWidgetTree(noArgs, registry, "page Test.List") { + if v.RuleID == "MDL-PAGEARG01" { + t.Errorf("a show_page with no arguments must not be refused: %s", v.Message) + } + } +} + +// ALTER PAGE grafts widgets into a page this pass never traverses, so it cannot +// say whether a data widget encloses them. The #1029 refusal must not reach it — +// `ALTER PAGE … INSERT actionbutton b (action: show_page P(Car: $Car)) INTO dv` +// is correct code, and refusing it is the false positive the contextKnown flag +// was introduced for in the first place. +func TestValidateShowPageArguments_AlterPageStandsDown(t *testing.T) { + registry := LoadWidgetRegistry("") + if registry == nil { + t.Fatal("LoadWidgetRegistry returned nil") + } + for _, arg := range []string{"$Car", "$currentObject", "'literal'"} { + for _, v := range validateWidgetSubtree([]*ast.WidgetV3{showPageButton(arg)}, registry, "alter Mod.P") { + if v.RuleID == "MDL-PAGEARG01" { + t.Errorf("ALTER PAGE INSERT of %s was refused: %s", arg, v.Message) + } + } + } +} diff --git a/mdl/executor/cmd_security_grant_autonumber_test.go b/mdl/executor/cmd_security_grant_autonumber_test.go new file mode 100644 index 0000000000..abec483530 --- /dev/null +++ b/mdl/executor/cmd_security_grant_autonumber_test.go @@ -0,0 +1,125 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend" + "github.com/mendixlabs/mxcli/mdl/backend/mock" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/domainmodel" + "github.com/mendixlabs/mxcli/sdk/security" +) + +// grantWriteRightsFixture builds one entity carrying the three attribute shapes +// that matter to CE6592 — a plain one, a calculated one, and an autonumber — and +// captures the access rule a GRANT writes for it. +type grantWriteRightsFixture struct { + ctx *ExecContext + captured *backend.EntityAccessRuleParams +} + +func newGrantWriteRightsFixture(t *testing.T) *grantWriteRightsFixture { + t.Helper() + + mod := mkModule("FieldService") + h := mkHierarchy(mod) + + req := &domainmodel.Entity{ + BaseElement: model.BaseElement{ID: model.ID("e-req")}, + Name: "ServiceRequest", + Attributes: []*domainmodel.Attribute{ + {Name: "Description", Type: &domainmodel.StringAttributeType{Length: 200}}, + { + Name: "TotalCost", + Type: &domainmodel.DecimalAttributeType{}, + Value: &domainmodel.AttributeValue{Type: "CalculatedValue", MicroflowName: "FieldService.CalcTotal"}, + }, + {Name: "RequestNumber", Type: &domainmodel.AutoNumberAttributeType{}}, + }, + } + dm := &domainmodel.DomainModel{ + BaseElement: model.BaseElement{ID: "dm-fs"}, + ContainerID: mod.ID, + Entities: []*domainmodel.Entity{req}, + } + + f := &grantWriteRightsFixture{} + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + ListModulesFunc: func() ([]*model.Module, error) { return []*model.Module{mod}, nil }, + GetModuleByNameFunc: func(string) (*model.Module, error) { return mod, nil }, + ListDomainModelsFunc: func() ([]*domainmodel.DomainModel, error) { + return []*domainmodel.DomainModel{dm}, nil + }, + GetDomainModelFunc: func(model.ID) (*domainmodel.DomainModel, error) { return dm, nil }, + GetModuleSecurityFunc: func(model.ID) (*security.ModuleSecurity, error) { + return &security.ModuleSecurity{ModuleRoles: []*security.ModuleRole{{Name: "Coordinator"}}}, nil + }, + AddEntityAccessRuleFunc: func(p backend.EntityAccessRuleParams) error { + cp := p + f.captured = &cp + return nil + }, + ReconcileMemberAccessesFunc: func(model.ID, string) (int, error) { return 0, nil }, + } + ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h)) + f.ctx = ctx + return f +} + +func (f *grantWriteRightsFixture) attrRights(ref string) (string, bool) { + if f.captured == nil { + return "", false + } + for _, ma := range f.captured.MemberAccesses { + if ma.AttributeRef == ref { + return ma.AccessRights, true + } + } + return "", false +} + +// TestGrantWriteAll_AutoNumberDowngradedToReadOnly pins ako/mxcli#524: `grant +// write *` on an entity with an autonumber wrote ReadWrite on it and the build +// failed CE6592, so the user had to narrow the grant with a hand-written REVOKE. +// +// The calculated attribute in the same fixture is the CONTROL. It was already +// downgraded before this fix, so a test asserting only the autonumber could pass +// against a build where the predicate had simply been widened to "every +// attribute" — which would silently strip write rights from the whole model. +// Asserting the plain attribute keeps ReadWrite is what makes the pair mean +// something. +func TestGrantWriteAll_AutoNumberDowngradedToReadOnly(t *testing.T) { + f := newGrantWriteRightsFixture(t) + + stmt := &ast.GrantEntityAccessStmt{ + Entity: ast.QualifiedName{Module: "FieldService", Name: "ServiceRequest"}, + Roles: []ast.QualifiedName{{Module: "FieldService", Name: "Coordinator"}}, + Rights: []ast.EntityAccessRight{{Type: ast.EntityAccessWriteAll}}, + } + if err := execGrantEntityAccess(f.ctx, stmt); err != nil { + t.Fatalf("grant failed: %v", err) + } + + const autoRef = "FieldService.ServiceRequest.RequestNumber" + got, ok := f.attrRights(autoRef) + if !ok { + t.Fatalf("no MemberAccess for the autonumber at all; got %+v", f.captured.MemberAccesses) + } + if got != "ReadOnly" { + t.Errorf("autonumber rights = %q, want ReadOnly — write on an autonumber is CE6592", got) + } + + // Control 1: the calculated attribute, already covered before the fix. + if got, _ := f.attrRights("FieldService.ServiceRequest.TotalCost"); got != "ReadOnly" { + t.Errorf("calculated rights = %q, want ReadOnly", got) + } + // Control 2: an ordinary attribute must keep the write the statement asked + // for. Without this the test passes against a blanket downgrade. + if got, _ := f.attrRights("FieldService.ServiceRequest.Description"); got != "ReadWrite" { + t.Errorf("plain attribute rights = %q, want ReadWrite — the grant asked for write", got) + } +} diff --git a/mdl/executor/cmd_security_strict_mode_test.go b/mdl/executor/cmd_security_strict_mode_test.go new file mode 100644 index 0000000000..dad33ae7e9 --- /dev/null +++ b/mdl/executor/cmd_security_strict_mode_test.go @@ -0,0 +1,133 @@ +// SPDX-License-Identifier: Apache-2.0 + +// ako/mxcli#526: lint rule SEC005 reports "strict mode disabled" and MDL had no +// statement that would clear it — a rule with no remedy, recorded on the +// reporting project as the one finding that stayed Open with "needs Studio Pro". +// +// StrictMode was read everywhere and written nowhere: security_read.go reads it, +// `show security` prints it, the Starlark rule lints it, and +// ProjectSecurity.SetStrictMode existed in gen and was never called. +package executor + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/backend/mock" + "github.com/mendixlabs/mxcli/mdl/visitor" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/security" +) + +type strictModeCapture struct { + called bool + unitID model.ID + enabled bool +} + +func newStrictModeCtx(t *testing.T) (*ExecContext, *strictModeCapture) { + t.Helper() + cap := &strictModeCapture{} + mb := &mock.MockBackend{ + IsConnectedFunc: func() bool { return true }, + GetProjectSecurityFunc: func() (*security.ProjectSecurity, error) { + return &security.ProjectSecurity{ + BaseElement: model.BaseElement{ID: model.ID("ps-1")}, + UserRoles: []*security.UserRole{{Name: "Administrator"}}, + }, nil + }, + SetProjectStrictModeFunc: func(unitID model.ID, enabled bool) error { + cap.called, cap.unitID, cap.enabled = true, unitID, enabled + return nil + }, + } + ctx, _ := newMockCtx(t, withBackend(mb)) + return ctx, cap +} + +func TestAlterProjectSecurity_StrictMode(t *testing.T) { + for _, tc := range []struct { + name string + want bool + }{{"on", true}, {"off", false}} { + t.Run(tc.name, func(t *testing.T) { + ctx, cap := newStrictModeCtx(t) + enabled := tc.want + err := execAlterProjectSecurity(ctx, &ast.AlterProjectSecurityStmt{ + StrictModeEnabled: &enabled, + }) + if err != nil { + t.Fatalf("alter failed: %v", err) + } + if !cap.called { + t.Fatal("backend was never asked to set strict mode") + } + if cap.enabled != tc.want { + t.Errorf("strict mode set to %v, want %v", cap.enabled, tc.want) + } + if cap.unitID != model.ID("ps-1") { + t.Errorf("unit id = %q, want the project security element", cap.unitID) + } + }) + } +} + +// A statement about something else must not touch strict mode. The field is a +// pointer precisely so "said nothing" is distinguishable from "asked for off" — +// a bool would silently disable strict mode on every DEMO USERS toggle. +func TestAlterProjectSecurity_StrictModeUntouchedByOtherClauses(t *testing.T) { + ctx, cap := newStrictModeCtx(t) + demo := true + if err := execAlterProjectSecurity(ctx, &ast.AlterProjectSecurityStmt{ + DemoUsersEnabled: &demo, + }); err != nil { + t.Fatalf("alter failed: %v", err) + } + if cap.called { + t.Error("a DEMO USERS statement wrote strict mode") + } +} + +// The grammar half: both spellings reach the AST with the right value, and the +// new STRICT/MODE tokens stay usable as ordinary identifiers. +func TestParse_AlterProjectSecurityStrictMode(t *testing.T) { + for _, tc := range []struct { + src string + want bool + }{ + {"ALTER PROJECT SECURITY STRICT MODE ON;", true}, + {"ALTER PROJECT SECURITY STRICT MODE OFF;", false}, + } { + prog, errs := visitor.Build(tc.src) + if len(errs) > 0 { + t.Fatalf("%s: parse failed: %v", tc.src, errs) + } + if len(prog.Statements) != 1 { + t.Fatalf("%s: got %d statements, want 1", tc.src, len(prog.Statements)) + } + stmt, ok := prog.Statements[0].(*ast.AlterProjectSecurityStmt) + if !ok { + t.Fatalf("%s: got %T, want *ast.AlterProjectSecurityStmt", tc.src, prog.Statements[0]) + } + if stmt.StrictModeEnabled == nil { + t.Fatalf("%s: StrictModeEnabled not set", tc.src) + } + if *stmt.StrictModeEnabled != tc.want { + t.Errorf("%s: StrictModeEnabled = %v, want %v", tc.src, *stmt.StrictModeEnabled, tc.want) + } + } +} + +// STRICT and MODE are new lexer tokens, so they must stay usable as names — +// `mode` in particular is an entirely plausible attribute. A new keyword left +// out of the parser's `keyword` rule silently breaks every model already using +// the word, which is the trap this guards. +func TestParse_StrictAndModeRemainUsableAsIdentifiers(t *testing.T) { + const src = `CREATE ENTITY Sales.Shipment ( + Mode: String(20), + Strict: Boolean +);` + if _, errs := visitor.Build(src); len(errs) > 0 { + t.Errorf("an attribute named Mode or Strict no longer parses: %v", errs) + } +} diff --git a/mdl/executor/cmd_security_write.go b/mdl/executor/cmd_security_write.go index b51128104b..619d2d12c9 100644 --- a/mdl/executor/cmd_security_write.go +++ b/mdl/executor/cmd_security_write.go @@ -424,8 +424,12 @@ func execGrantEntityAccess(ctx *ExecContext, s *ast.GrantEntityAccessStmt) error } else if readMemberSet[mem.Name] { rights = "ReadOnly" } - // Calculated attributes cannot have write rights (CE6592) - if mem.IsCalculated && (rights == "ReadWrite" || rights == "WriteOnly") { + // Neither a calculated attribute nor an autonumber may carry write + // rights — both are CE6592. The autonumber half was missing, so + // `grant write *` on an entity with one wrote ReadWrite and failed the + // build (ako/mxcli#524). + if types.WriteRightsForbidden(mem.IsCalculated, mem.IsAutoNumber) && + (rights == "ReadWrite" || rights == "WriteOnly") { rights = "ReadOnly" } grantedMembers[mem.Name] = true @@ -1170,6 +1174,17 @@ func execAlterProjectSecurity(ctx *ExecContext, s *ast.AlterProjectSecurityStmt) } } + if s.StrictModeEnabled != nil { + if err := ctx.Backend.SetProjectStrictMode(ps.ID, *s.StrictModeEnabled); err != nil { + return mdlerrors.NewBackend("set strict mode", err) + } + state := "disabled" + if *s.StrictModeEnabled { + state = "enabled" + } + fmt.Fprintf(ctx.Output, "Strict mode %s\n", state) + } + return nil } diff --git a/mdl/executor/entity_hierarchy.go b/mdl/executor/entity_hierarchy.go index cde39c7ddb..8d7a2d7f49 100644 --- a/mdl/executor/entity_hierarchy.go +++ b/mdl/executor/entity_hierarchy.go @@ -22,9 +22,15 @@ type EntityMember struct { Name string // bare member name, as written in GRANT // Ref is the reference stored in MemberAccess, qualified against the entity // that DECLARES the member — which is an ancestor for an inherited one. - Ref string - Inherited bool + Ref string + Inherited bool + // IsCalculated and IsAutoNumber are the two shapes Mendix refuses write + // access on (CE6592). They are separate flags rather than one because they + // are unrelated in the model — an autonumber carries no CalculatedValue — + // and asking whether write is allowed is types.WriteRightsForbidden's job, + // not a caller's. IsCalculated bool + IsAutoNumber bool } // EntityMembers returns every member of an entity's access surface: its own @@ -98,6 +104,7 @@ func EntityMembersFor(b entityLookupBackend, entityQN string) []EntityMember { Ref: currentQN + "." + attr.Name, Inherited: depth > 0, IsCalculated: attr.Value != nil && attr.Value.Type == "CalculatedValue", + IsAutoNumber: isAutoNumberAttr(attr), }) } currentQN = entity.GeneralizationRef @@ -230,3 +237,16 @@ func ResolveMemberType(b entityLookupBackend, entityQN, memberName string) strin } return "" } + +// isAutoNumberAttr reports whether an attribute's value is generated by the +// database on insert. Mendix refuses write rights on one (CE6592), the same as +// for a calculated attribute and for the same reason, but the two look nothing +// alike in the model: this is a property of the attribute's TYPE, where +// calculated is a property of its VALUE. +func isAutoNumberAttr(attr *domainmodel.Attribute) bool { + if attr == nil { + return false + } + _, ok := attr.Type.(*domainmodel.AutoNumberAttributeType) + return ok +} diff --git a/mdl/executor/exec_context.go b/mdl/executor/exec_context.go index b62b36906e..7b689d06e8 100644 --- a/mdl/executor/exec_context.go +++ b/mdl/executor/exec_context.go @@ -143,8 +143,13 @@ func fileExists(path string) bool { } // Connected returns true if a project is connected via the Backend. +// +// Nil-safe on the receiver, which makes checkFeature's documented contract +// ("safe to call when not connected") true for a nil ctx as well. Every +// version-gated command reaches this through checkFeature, so without the guard +// each one panics rather than skipping when exercised without a context. func (ctx *ExecContext) Connected() bool { - return ctx.Backend != nil && ctx.Backend.IsConnected() + return ctx != nil && ctx.Backend != nil && ctx.Backend.IsConnected() } // ConnectedForWrite returns true if a project is connected and the backend diff --git a/mdl/executor/microflow_authored_properties_test.go b/mdl/executor/microflow_authored_properties_test.go new file mode 100644 index 0000000000..663f2e7bf1 --- /dev/null +++ b/mdl/executor/microflow_authored_properties_test.go @@ -0,0 +1,138 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "testing" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/microflows" +) + +func strp(s string) *string { return &s } +func slicep(s []string) *[]string { return &s } + +// TestAuthoredProperties_AbsentPreserves is the rule the whole clause design +// rests on, and the reason every field is a pointer. +// +// These properties were carried unconditionally before they were authorable +// (mendixlabs/mxcli#1120). Making them authorable must not turn "the script did +// not say" into "set it to the zero value" — that is the bug the carry fixed, +// reintroduced through the front door. +func TestAuthoredProperties_AbsentPreserves(t *testing.T) { + stored := µflows.Microflow{ + Name: "ACT_Item", + URL: "item/{Key}", + URLSearchParameters: []string{"M.ACT_Item.Filter"}, + ExportLevel: "API", + AllowConcurrentExecution: false, + ConcurrencyErrorMessage: &model.Text{Translations: map[string]string{"en_US": "Busy"}}, + } + // A statement with no clauses at all. + if err := applyMicroflowDocumentProperties(nil, stored, &ast.CreateMicroflowStmt{}); err != nil { + t.Fatalf("apply: %v", err) + } + if stored.URL != "item/{Key}" || len(stored.URLSearchParameters) != 1 { + t.Errorf("URL not preserved: %q %v", stored.URL, stored.URLSearchParameters) + } + if stored.ExportLevel != "API" { + t.Errorf("export level not preserved: %q", stored.ExportLevel) + } + if stored.AllowConcurrentExecution || stored.ConcurrencyErrorMessage == nil { + t.Error("concurrency not preserved") + } +} + +// TestAuthoredProperties_StatedOverrides is the other half: a clause that IS +// written must take effect, or the feature does nothing. +func TestAuthoredProperties_StatedOverrides(t *testing.T) { + mf := µflows.Microflow{Name: "ACT_Item", ExportLevel: "Hidden", AllowConcurrentExecution: true} + stmt := &ast.CreateMicroflowStmt{ + Name: ast.QualifiedName{Module: "M", Name: "ACT_Item"}, + Parameters: []ast.MicroflowParam{{Name: "Key"}, {Name: "Filter"}}, + URL: strp("item/{Key}"), + ExportLevel: strp("API"), + Concurrency: &ast.ConcurrencyClause{ErrorMessage: "Busy", ErrorMessageSet: true}, + } + stmt.URLSearchParameters = slicep([]string{"Filter"}) + if err := applyMicroflowDocumentProperties(nil, mf, stmt); err != nil { + t.Fatalf("apply: %v", err) + } + if mf.URL != "item/{Key}" || mf.ExportLevel != "API" || mf.AllowConcurrentExecution { + t.Errorf("clauses not applied: %#v", mf) + } + // The stored form is the qualified name, built from the parameter's DECLARED + // spelling — a qualified name that disagrees with its parameter does not + // resolve. + if len(mf.URLSearchParameters) != 1 || mf.URLSearchParameters[0] != "M.ACT_Item.Filter" { + t.Errorf("search parameter not qualified: %v", mf.URLSearchParameters) + } + if mf.ConcurrencyErrorMessage == nil || mf.ConcurrencyErrorMessage.Translations["en_US"] != "Busy" { + t.Errorf("error message not applied: %#v", mf.ConcurrencyErrorMessage) + } +} + +// TestAuthoredProperties_AllowKeepsTheMessage pins a deliberate non-clearing. +// +// ALLOW CONCURRENT EXECUTION sets the flag and LEAVES a stored error message. +// Two reasons, and the first is that it could not work anyway: +// canon.CarryTranslations copies a stored text's languages back onto a rebuilt +// document, because a rebuild cannot say them — so an emptied message returns on +// the next write. Measured end to end: after `allow concurrent execution` the +// stored en_US text was still on disk, byte-identical, with its original $ID. +// The second is that Studio Pro greys those fields rather than erasing them, so +// keeping the message is what re-ticking the box expects. +func TestAuthoredProperties_AllowKeepsTheMessage(t *testing.T) { + mf := µflows.Microflow{ + Name: "ACT_Item", + AllowConcurrentExecution: false, + ConcurrencyErrorMessage: &model.Text{Translations: map[string]string{"en_US": "Busy"}}, + } + stmt := &ast.CreateMicroflowStmt{Concurrency: &ast.ConcurrencyClause{Allow: true}} + if err := applyMicroflowDocumentProperties(nil, mf, stmt); err != nil { + t.Fatalf("apply: %v", err) + } + if !mf.AllowConcurrentExecution { + t.Error("ALLOW did not set the flag") + } + if mf.ConcurrencyErrorMessage == nil { + t.Error("ALLOW erased the error message; it is inert, not deleted — and " + + "CarryTranslations would put it back on the next write anyway") + } +} + +// TestAuthoredProperties_DropURLClearsBoth: a search-parameter list without a +// URL is configuration for a deep link that no longer exists. +func TestAuthoredProperties_DropURLClearsBoth(t *testing.T) { + mf := µflows.Microflow{ + Name: "ACT_Item", + URL: "item/{Key}", + URLSearchParameters: []string{"M.ACT_Item.Filter"}, + } + stmt := &ast.CreateMicroflowStmt{URL: strp(""), URLSearchParameters: slicep(nil)} + if err := applyMicroflowDocumentProperties(nil, mf, stmt); err != nil { + t.Fatalf("apply: %v", err) + } + if mf.URL != "" || len(mf.URLSearchParameters) != 0 { + t.Errorf("DROP URL left %q %v", mf.URL, mf.URLSearchParameters) + } +} + +// TestAuthoredProperties_ChecksRefuseBeforeTheWrite pins that exec applies the +// SAME rules `mxcli check` reports, so a script cannot pass check and fail exec. +func TestAuthoredProperties_ChecksRefuseBeforeTheWrite(t *testing.T) { + stmt := &ast.CreateMicroflowStmt{ + Name: ast.QualifiedName{Module: "M", Name: "ACT_Item"}, + Parameters: []ast.MicroflowParam{{Name: "Key"}}, + URL: strp("item/{Key}"), + } + stmt.URLSearchParameters = slicep([]string{"Key"}) // CE5612: path AND search + + if problems := MicroflowDocumentPropertyProblems(stmt); len(problems) == 0 { + t.Fatal("check did not report the CE5612 overlap") + } + if err := checkMicroflowDocumentProperties(stmt); err == nil { + t.Error("exec accepted a statement check refuses — the two have drifted") + } +} diff --git a/mdl/executor/microflow_carried_properties_test.go b/mdl/executor/microflow_carried_properties_test.go index d543202406..7885a49487 100644 --- a/mdl/executor/microflow_carried_properties_test.go +++ b/mdl/executor/microflow_carried_properties_test.go @@ -112,16 +112,19 @@ func TestCreateOrModifyMicroflow_PreservesExportLevel(t *testing.T) { } } -// TestDescribeMicroflow_ReportsUnauthorableProperties covers the read side of -// both carries. Neither the deep link nor a non-default export level has an MDL -// spelling, so DESCRIBE cannot emit them as re-executable text — but a -// describe -> rename -> exec COPY has nothing to preserve from, so staying -// silent would hand the reader output that looks complete and is not. +// TestDescribeMicroflow_EmitsAuthoredProperties is the round-trip half. // -// The export-level line is conditional on purpose: every document in every -// module measured stores "Hidden", so emitting it unconditionally would add a -// comment to every describe in order to say nothing. -func TestDescribeMicroflow_ReportsUnauthorableProperties(t *testing.T) { +// This test used to assert the OPPOSITE: while the deep link and export level +// had no MDL spelling, DESCRIBE emitted them as `-- URL:` / `-- Export level:` +// comments so its output did not look complete when it was not. They are real +// clauses now, which is what makes describe -> rename -> exec a faithful copy +// rather than an approximate one — the hole preservation alone could not close, +// because a copy is a new document with nothing to preserve from. +// +// Still conditional: every document in every marketplace module measured stores +// Hidden and allows concurrent execution, so emitting the defaults would add +// three lines to every describe in order to say nothing. +func TestDescribeMicroflow_EmitsAuthoredProperties(t *testing.T) { ctx, _ := newMockCtx(t) name := ast.QualifiedName{Module: "MyModule", Name: "ACT_Item"} @@ -129,101 +132,63 @@ func TestDescribeMicroflow_ReportsUnauthorableProperties(t *testing.T) { return renderMicroflowMDL(ctx, "microflow", mf, name, nil, nil, nil) } - got := render(µflows.Microflow{Name: "ACT_Item", URL: "item/{Key}", ExportLevel: "API"}) - if !strings.Contains(got, "-- URL: item/{Key}") { - t.Errorf("describe omitted the deep link; the output reads as complete:\n%s", got) - } - if !strings.Contains(got, "-- Export level: API") { - t.Errorf("describe omitted a non-default export level:\n%s", got) - } - - // The control: an ordinary microflow gets neither line. Without this the - // test would pass against a describer that comments on every microflow. - plain := render(µflows.Microflow{Name: "ACT_Item", ExportLevel: "Hidden"}) - if strings.Contains(plain, "-- URL:") || strings.Contains(plain, "-- Export level:") { - t.Errorf("describe commented on defaults:\n%s", plain) + got := render(µflows.Microflow{ + Name: "ACT_Item", + URL: "item/{Key}", + URLSearchParameters: []string{"MyModule.ACT_Item.Filter"}, + ExportLevel: "API", + AllowConcurrentExecution: false, + ConcurrencyErrorMessage: &model.Text{Translations: map[string]string{"en_US": "Busy"}}, + }) + for _, want := range []string{ + "url 'item/{Key}'", + "url search parameters ($Filter)", + "export level api", + "disallow concurrent execution error message 'Busy'", + } { + if !strings.Contains(got, want) { + t.Errorf("describe omitted %q:\n%s", want, got) + } + } + // The clause names the PARAMETER, not the stored qualified name: that is + // what the reader has in front of them, and it is what re-executing needs. + if strings.Contains(got, "MyModule.ACT_Item.Filter") { + t.Errorf("search parameter emitted as a qualified name:\n%s", got) + } + + // The control: an ordinary microflow gets none of these lines. Without it + // the test would pass against a describer that emits them unconditionally. + plain := render(µflows.Microflow{ + Name: "ACT_Item", ExportLevel: "Hidden", AllowConcurrentExecution: true, + }) + for _, unwanted := range []string{"url ", "export level", "concurrent execution"} { + if strings.Contains(plain, unwanted) { + t.Errorf("describe emitted %q for a default microflow:\n%s", unwanted, plain) + } } } -// TestCreateOrModifyMicroflow_PreservesConcurrencySettings is the executor half, -// and the one that was actually reachable by a user: the backend already read -// AllowConcurrentExecution and MarkAsUsed back, but the rebuild in -// buildMicroflowFromStmt overwrote both with its own literals before the backend -// ever saw them. +// TestDescribeMicroflow_FlagsUntranslatableMessage: a concurrency error message +// is a Texts$Text and MDL states ONE language, so a translated message cannot +// round-trip through the clause. DESCRIBE emits it anyway — omitting it would +// describe a microflow that fails CE4899 — and names the languages a replay +// would not carry, the same honesty rule it applies to a range bounded by +// another attribute. // -// The direction is why this went unreported. The rebuild wrote `true`, so a -// microflow that DISALLOWED concurrent execution came back allowing it — the -// running app's concurrency protection removed. CE4899 fires on -// disallow-without-a-message, never on allow, so no checker says anything; the -// error message and its translations go at the same time. -func TestCreateOrModifyMicroflow_PreservesConcurrencySettings(t *testing.T) { - const moduleID = model.ID("module-1") - stored := []*microflows.Microflow{{ - BaseElement: model.BaseElement{ID: "mf-serial"}, - ContainerID: moduleID, - Name: "ACT_Serial", +// Note this is about a COPY. Rewriting the same microflow keeps every language, +// because canon.CarryTranslations pairs the texts and carries them (measured: +// restating the English text left the Dutch one untouched). +func TestDescribeMicroflow_FlagsUntranslatableMessage(t *testing.T) { + ctx, _ := newMockCtx(t) + got := renderMicroflowMDL(ctx, "microflow", µflows.Microflow{ + Name: "ACT_Item", AllowConcurrentExecution: false, - MarkAsUsed: true, ConcurrencyErrorMessage: &model.Text{Translations: map[string]string{ - "en_US": "Already running", - "nl_NL": "Wordt al uitgevoerd", + "en_US": "Busy", "nl_NL": "Bezet", }}, - ConcurrencyErrorMicroflow: "MyModule.ACT_OnBusy", - }} - ctx, written := microflowWriteProbe(t, stored, moduleID) - - stmt := &ast.CreateMicroflowStmt{ - Name: ast.QualifiedName{Module: "MyModule", Name: "ACT_Serial"}, - CreateOrModify: true, - } - if err := execCreateMicroflow(ctx, stmt); err != nil { - t.Fatalf("CREATE OR MODIFY MICROFLOW failed: %v", err) - } - if *written == nil { - t.Fatal("no microflow was written") - } - if (*written).AllowConcurrentExecution { - t.Error("rewrite re-allowed concurrent execution; the app's concurrency " + - "protection is gone and no checker reports it") - } - if !(*written).MarkAsUsed { - t.Error("rewrite cleared MarkAsUsed; the document is reported unused again") - } - if got := (*written).ConcurrencyErrorMicroflow; got != "MyModule.ACT_OnBusy" { - t.Errorf("rewrite dropped the concurrency error microflow: %q", got) - } - msg := (*written).ConcurrencyErrorMessage - if msg == nil || len(msg.Translations) != 2 { - t.Fatalf("rewrite dropped the concurrency error message (or its translations): %#v", msg) - } -} + }, ast.QualifiedName{Module: "MyModule", Name: "ACT_Item"}, nil, nil, nil) -// TestCreateMicroflow_ConcurrencyDefaults is the control: a NEW microflow still -// gets Mendix's defaults. Carrying is only ever from a stored document, so the -// fix must not change what a create produces — allow concurrency, not marked as -// used, no error handling. -func TestCreateMicroflow_ConcurrencyDefaults(t *testing.T) { - const moduleID = model.ID("module-1") - ctx, written := microflowWriteProbe(t, nil, moduleID) - - stmt := &ast.CreateMicroflowStmt{ - Name: ast.QualifiedName{Module: "MyModule", Name: "ACT_Fresh"}, - } - if err := execCreateMicroflow(ctx, stmt); err != nil { - t.Fatalf("CREATE MICROFLOW failed: %v", err) - } - got := *written - if got == nil { - t.Fatal("no microflow was written") - } - if !got.AllowConcurrentExecution { - t.Error("a new microflow must default to allowing concurrent execution") - } - if got.MarkAsUsed { - t.Error("a new microflow must not be marked as used") - } - if got.ConcurrencyErrorMessage != nil || got.ConcurrencyErrorMicroflow != "" { - t.Errorf("a new microflow acquired concurrency error handling: %#v / %q", - got.ConcurrencyErrorMessage, got.ConcurrencyErrorMicroflow) + if !strings.Contains(got, "also translated into nl_NL") { + t.Errorf("describe did not flag the language a replay would drop:\n%s", got) } } diff --git a/mdl/executor/microflow_document_properties.go b/mdl/executor/microflow_document_properties.go new file mode 100644 index 0000000000..dde8569d88 --- /dev/null +++ b/mdl/executor/microflow_document_properties.go @@ -0,0 +1,207 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "fmt" + "strings" + + "github.com/mendixlabs/mxcli/mdl/ast" + mdlerrors "github.com/mendixlabs/mxcli/mdl/errors" + "github.com/mendixlabs/mxcli/mdl/types" + "github.com/mendixlabs/mxcli/model" + "github.com/mendixlabs/mxcli/sdk/microflows" +) + +// applyMicroflowDocumentProperties overlays the URL / EXPORT LEVEL / concurrency +// clauses onto a microflow whose fields already hold the STORED values. +// +// The order matters and is the point: the caller seeds mf from the stored +// document, and this only overwrites what the statement actually said. That is +// what keeps "absent preserves" true for properties a script never mentions, +// which is the rule these clauses exist to let a script opt out of rather than +// to abolish (mendixlabs/mxcli#1120). +func applyMicroflowDocumentProperties(ctx *ExecContext, mf *microflows.Microflow, s *ast.CreateMicroflowStmt) error { + // Deep links are Mendix 10.6+. Gate on the clause being STATED, not on the + // resulting value: a rewrite that carries a stored URL forward on an older + // project is preserving what is already there, and refusing that would make + // the guard destroy the very thing it protects. + if s.URL != nil || s.URLSearchParameters != nil { + if err := checkFeature(ctx, "microflows", "deep_link_url", + "a microflow URL (deep link)", + "upgrade your project to 10.6+, or set the URL in Studio Pro"); err != nil { + return err + } + } + if s.URL != nil { + if err := checkURLNotTaken(ctx, mf, *s.URL); err != nil { + return err + } + mf.URL = *s.URL + } + if s.URLSearchParameters != nil { + mf.URLSearchParameters = qualifiedSearchParams(mf.Name, s) + } + if s.ExportLevel != nil { + mf.ExportLevel = *s.ExportLevel + } + if s.Concurrency == nil { + return nil + } + + // A clause sets what it states and leaves the sibling alone — it does NOT + // clear the other kind of error handling. Two independent reasons: + // + // 1. It would not work. canon.CarryTranslations copies a stored text's + // other languages onto the rebuilt document, because a rebuild drops + // every language MDL could not state. It cannot distinguish "cleared on + // purpose" from "the statement had no way to say it", so an emptied + // message comes straight back. Only a targeted patch may claim + // ContentsOwnTranslations, and a microflow rebuild is not one. + // 2. It should not. Studio Pro greys the error fields out when concurrent + // execution is allowed rather than erasing them, so re-ticking the box + // restores the message. Matching that keeps a message the user may want + // back, and an inert stored message breaks nothing: mxbuild reads it + // only when execution is disallowed. + c := s.Concurrency + mf.AllowConcurrentExecution = c.Allow + switch { + case c.ErrorMicroflow != "": + mf.ConcurrencyErrorMicroflow = c.ErrorMicroflow + case c.ErrorMessageSet: + // One language, and the rest survive: CarryTranslations pairs this text + // with the stored one by containment path and copies the other languages + // back, so restating an English message does not drop its Dutch + // translation. That is what makes ERROR MESSAGE safe to author at all — + // the guard this once had was refusing a round trip the platform already + // handles. + mf.ConcurrencyErrorMessage = &model.Text{ + Translations: map[string]string{messageLanguage(ctx, mf.ConcurrencyErrorMessage): c.ErrorMessage}, + } + } + return nil +} + +// qualifiedSearchParams renders the bare parameter names a script wrote as the +// Module.Microflow.Parameter qualified names Mendix stores. +func qualifiedSearchParams(microflowName string, s *ast.CreateMicroflowStmt) []string { + names := *s.URLSearchParameters + if len(names) == 0 { + return nil + } + out := make([]string, 0, len(names)) + for _, n := range names { + // Use the parameter's DECLARED spelling: the check accepts a + // case-insensitive match, and a qualified name that disagrees with the + // parameter it points at does not resolve. + spelling := n + for _, p := range s.Parameters { + if strings.EqualFold(p.Name, n) { + spelling = p.Name + break + } + } + out = append(out, fmt.Sprintf("%s.%s.%s", s.Name.Module, microflowName, spelling)) + } + return out +} + +// messageLanguage picks the language a single-string MDL message is written in: +// the stored message's own language when there is exactly one, else the +// PROJECT's default. +// +// Not a hardcoded "en_US". Mendix has no language-neutral text, so a message +// written under the wrong code is invisible to a Dutch app and adds a language +// rather than editing the one that is there — #1113, in the catalog. +func messageLanguage(ctx *ExecContext, stored *model.Text) string { + if stored != nil && len(stored.Translations) == 1 { + for lang := range stored.Translations { + return lang + } + } + return describeDefaultLanguage(ctx) +} + +// MicroflowDocumentPropertyProblems returns every rule violation in the URL / +// EXPORT LEVEL / concurrency clauses of one statement. +// +// Exported and statement-shaped because `mxcli check` and the executor MUST +// apply the identical rules: check calls it to report MDL084-MDL086 with source +// positions, exec calls it to refuse the write. The rules themselves live one +// layer down in mdl/types, so neither caller owns them. +func MicroflowDocumentPropertyProblems(s *ast.CreateMicroflowStmt) []string { + if s == nil { + return nil + } + names := make([]string, 0, len(s.Parameters)) + for _, p := range s.Parameters { + names = append(names, p.Name) + } + + var problems []string + url, search := "", []string(nil) + if s.URL != nil { + url = *s.URL + } + if s.URLSearchParameters != nil { + search = *s.URLSearchParameters + } + problems = append(problems, types.CheckMicroflowURL(url, search, names)...) + + if s.ExportLevel != nil { + if p := types.CheckExportLevel(*s.ExportLevel); p != "" { + problems = append(problems, p) + } + } + if c := s.Concurrency; c != nil && !c.Allow { + if p := types.CheckMicroflowConcurrency(true, c.ErrorMessageSet, c.ErrorMicroflow); p != "" { + problems = append(problems, p) + } + } + return problems +} + +// checkMicroflowDocumentProperties refuses a write that breaks any of them. +func checkMicroflowDocumentProperties(s *ast.CreateMicroflowStmt) error { + if problems := MicroflowDocumentPropertyProblems(s); len(problems) > 0 { + return mdlerrors.NewUnsupported(fmt.Sprintf("microflow %s: %s", + s.Name.String(), strings.Join(problems, "; "))) + } + return nil +} + +// checkURLNotTaken refuses a deep link another microflow already owns. +// +// Mendix requires URLs to be unique across the app and reports a collision as +// CE0570 ("The URL 'item/{Key}' of microflow 'A' is conflicting with the URL of +// microflow 'B'"). Like CE5612, it is invisible until a build — and this feature +// makes it reachable in a way it was not before: `describe` now emits the URL, +// so the describe -> rename -> exec COPY that the clauses exist to enable +// produces two microflows with one URL unless the copy is edited. That is the +// first thing anyone will do with this, so it is worth refusing by name. +// +// Needs the project, so it lives here rather than in the statement-local rules: +// a script cannot see the other microflows, and neither can `mxcli check` +// without -p. +func checkURLNotTaken(ctx *ExecContext, mf *microflows.Microflow, url string) error { + if url == "" || ctx == nil || ctx.Backend == nil { + return nil + } + others, err := ctx.Backend.ListMicroflows() + if err != nil { + return nil // not this guard's business; the write path reports its own errors + } + for _, other := range others { + // The microflow being rewritten owns its own URL, and an excluded + // document is not part of the app, so neither can collide. + if other.ID == mf.ID || other.Excluded || !strings.EqualFold(other.URL, url) { + continue + } + return mdlerrors.NewUnsupported(fmt.Sprintf( + "URL %q is already the deep link of microflow %s — Mendix requires them to be "+ + "unique and reports a duplicate as CE0570.\n"+ + " Give this microflow a different URL, or `drop url` on the other one.", + url, other.Name)) + } + return nil +} diff --git a/mdl/executor/validate_microflow_properties.go b/mdl/executor/validate_microflow_properties.go new file mode 100644 index 0000000000..592ccaa97a --- /dev/null +++ b/mdl/executor/validate_microflow_properties.go @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 + +package executor + +import ( + "fmt" + "strings" + + "github.com/mendixlabs/mxcli/mdl/ast" + "github.com/mendixlabs/mxcli/mdl/linter" +) + +// validateMicroflowDocumentProperties (MDL-MF01…MF03) reports the platform +// rules on a microflow's URL, export level and concurrency clauses. +// +// It calls the SAME function the writer calls — MicroflowDocumentPropertyProblems, +// over the rules in mdl/types — because a script that `mxcli check` accepts and +// `exec` then refuses is the drift the layout-placeholder rule was moved into +// mdl/types to prevent (mendixlabs/mxcli#1063). Check reports every problem with +// a rule ID; exec refuses on the first. +// +// The rules themselves, and why each is a rule rather than a preference: +// +// MDL-MF01 a {Name} placeholder must name a parameter of this microflow +// MDL-MF02 a path parameter may not also be a search parameter → CE5612 +// MDL-MF03 DISALLOW CONCURRENT EXECUTION needs a handler → CE4899 +// +// MF02 is the one that is easy to get wrong and impossible to see without a +// build: it was found by seeding a real project and running mx check, after the +// first version of this feature's own test fixture used one parameter for both +// and described a document Mendix refuses to build. +func validateMicroflowDocumentProperties(stmt ast.Statement) []linter.Violation { + mf, ok := stmt.(*ast.CreateMicroflowStmt) + if !ok { + return nil + } + problems := MicroflowDocumentPropertyProblems(mf) + if len(problems) == 0 { + return nil + } + out := make([]linter.Violation, 0, len(problems)) + for _, p := range problems { + out = append(out, linter.Violation{ + RuleID: ruleIDForProblem(p), + Severity: linter.SeverityError, + Message: fmt.Sprintf("microflow %s: %s", mf.Name.String(), p), + }) + } + return out +} + +// ruleIDForProblem maps a problem sentence to its rule ID. The mapping is on the +// sentence rather than a typed error because the rules live in mdl/types, which +// must not depend on the linter's vocabulary — the same separation that lets the +// writer use them without importing the checker. +func ruleIDForProblem(problem string) string { + switch { + case strings.Contains(problem, "CE5612"): + return "MDL-MF02" + case strings.Contains(problem, "CE4899"): + return "MDL-MF03" + case strings.Contains(problem, "MicroflowsExportLevel"): + return "MDL-MF04" + default: + return "MDL-MF01" + } +} diff --git a/mdl/executor/validate_program.go b/mdl/executor/validate_program.go index 397e83fe25..22150fc32a 100644 --- a/mdl/executor/validate_program.go +++ b/mdl/executor/validate_program.go @@ -63,6 +63,9 @@ func ValidateProgram(prog *ast.Program, projectPath string) []linter.Violation { // script passed check AND exec and failed a build later // (mendixlabs/mxcli#1063). violations = append(violations, validateLayoutPlaceholders(stmt)...) + // A microflow's URL / export level / concurrency clauses, against the + // same rules the writer applies (MDL-MF01..MF04). + violations = append(violations, validateMicroflowDocumentProperties(stmt)...) // A page with parameters and a Url must name each parameter in it (CE5601). if pageStmt, ok := stmt.(*ast.CreatePageStmtV3); ok { violations = append(violations, ValidatePageURLParameters(pageStmt)...) diff --git a/mdl/executor/validate_widgets.go b/mdl/executor/validate_widgets.go index f9304e00cb..07ec1a3d84 100644 --- a/mdl/executor/validate_widgets.go +++ b/mdl/executor/validate_widgets.go @@ -111,9 +111,9 @@ func ValidateWidgetPropertiesForStatement(stmt ast.Statement, registry *WidgetRe for _, op := range s.Operations { switch o := op.(type) { case *ast.InsertWidgetOp: - out = append(out, validateWidgetTree(o.Widgets, registry, "alter "+s.PageName.String())...) + out = append(out, validateWidgetSubtree(o.Widgets, registry, "alter "+s.PageName.String())...) case *ast.ReplaceWidgetOp: - out = append(out, validateWidgetTree(o.NewWidgets, registry, "alter "+s.PageName.String())...) + out = append(out, validateWidgetSubtree(o.NewWidgets, registry, "alter "+s.PageName.String())...) } } return out @@ -121,10 +121,23 @@ func ValidateWidgetPropertiesForStatement(stmt ast.Statement, registry *WidgetRe return nil } -// validateWidgetTree recursively walks the AST widget tree and validates -// pluggable widgets it encounters. +// validateWidgetTree recursively walks a WHOLE document's AST widget tree — +// CREATE PAGE/SNIPPET, where `widgets` is the root — and validates the pluggable +// widgets it encounters. +// +// The root of a document has no context object: nothing encloses it, so +// $currentObject is unbound there. That is a fact this pass can state, unlike +// validateWidgetSubtree below, and MDL-PAGEARG01 needs it (#1029). func validateWidgetTree(widgets []*ast.WidgetV3, registry *WidgetRegistry, locationPrefix string) []linter.Violation { - return validateWidgetTreeIn(widgets, registry, locationPrefix, nil, nil, "", false) + return validateWidgetTreeIn(widgets, registry, locationPrefix, nil, nil, atDocumentRoot()) +} + +// validateWidgetSubtree is validateWidgetTree for widgets that will be grafted +// into a page this pass never sees — ALTER PAGE's INSERT and REPLACE. What +// encloses them is unknown, so rules that depend on the enclosing context stand +// down rather than guess. +func validateWidgetSubtree(widgets []*ast.WidgetV3, registry *WidgetRegistry, locationPrefix string) []linter.Violation { + return validateWidgetTreeIn(widgets, registry, locationPrefix, nil, nil, pageArgContext{}) } // validateWidgetTreeIn is validateWidgetTree with the *parent* widget's @@ -135,7 +148,7 @@ func validateWidgetTree(widgets []*ast.WidgetV3, registry *WidgetRegistry, locat // must be exempt from the MDL-WIDGET07 "unrecognized property, silently dropped" // warning. When the parent mapping is known, the child's enumeration // sub-properties are validated against their member keys (MDL-WIDGET08). (9a) -func validateWidgetTreeIn(widgets []*ast.WidgetV3, registry *WidgetRegistry, locationPrefix string, parentObjectLists map[string]*ObjectListMapping, parent *ast.WidgetV3, contextVar string, contextKnown bool) []linter.Violation { +func validateWidgetTreeIn(widgets []*ast.WidgetV3, registry *WidgetRegistry, locationPrefix string, parentObjectLists map[string]*ObjectListMapping, parent *ast.WidgetV3, argCtx pageArgContext) []linter.Violation { var out []linter.Violation for _, w := range widgets { if w == nil { @@ -173,7 +186,7 @@ func validateWidgetTreeIn(widgets []*ast.WidgetV3, registry *WidgetRegistry, loc out = append(out, validateDatasourceXPathAssociationEmpty(w, locationPrefix)...) out = append(out, validateComboBoxAssociation(w, locationPrefix)...) // A show_page argument naming anything but the context object is dropped. - out = append(out, validateShowPageArguments(w, contextVar, contextKnown, locationPrefix)...) + out = append(out, validateShowPageArguments(w, argCtx, locationPrefix)...) // Unknown-property warning applies only to built-in widgets; pluggable // widgets get the stricter def.json check (MDL-WIDGET01) above, and // object-list items are validated by the object-list engine. @@ -212,12 +225,7 @@ func validateWidgetTreeIn(widgets []*ast.WidgetV3, registry *WidgetRegistry, loc // Reported once per grid, not once per column — see the rule's comment. out = append(out, validateDataGrid2ColumnNames(w, locationPrefix)...) if len(w.Children) > 0 { - // A data-bound widget renames the context object for everything below it. - childContextVar, childContextKnown := contextVar, contextKnown - if ds := w.GetDataSource(); ds != nil { - childContextVar, childContextKnown = contextVarFor(ds), true - } - out = append(out, validateWidgetTreeIn(w.Children, registry, locationPrefix, objectListMappingSet(def), w, childContextVar, childContextKnown)...) + out = append(out, validateWidgetTreeIn(w.Children, registry, locationPrefix, objectListMappingSet(def), w, argContextForChildren(w, argCtx))...) } } out = append(out, validateConsecutiveDynamicText(widgets, locationPrefix)...) @@ -665,6 +673,9 @@ var staticWidgetKnownProps = func() map[string]bool { // (mendixlabs/mxcli#1057). Describe emits it, so leaving it out here // makes the describe -> create round trip warn about its own output. "Image", + // dynamicimage's fallback image, and the two display flags it shares + // with the pluggable image widget. Same reason: describe emits them. + "DefaultImage", "OnClickType", // fragment / building-block sentinel-internal keys (USE_FRAGMENT / // USE_BUILDING_BLOCK), consumed by the expander, never serialized "Args", "DataSourceOverride", "ActionOverride", @@ -695,7 +706,7 @@ var staticWidgetKnownPropList = func() []string { "Attributes", "FilterType", "DesignProperties", "Width", "Height", "Visible", "Editable", "Tooltip", "DynamicClasses", "WidthUnit", "HeightUnit", "DesktopColumns", "TabletColumns", "PhoneColumns", "PageSize", "Pagination", - "Image") + "Image", "DefaultImage", "DisplayAs", "OnClickType") return list }() diff --git a/mdl/executor/validate_widgets_test.go b/mdl/executor/validate_widgets_test.go index b1fb3ddcfc..86197aca05 100644 --- a/mdl/executor/validate_widgets_test.go +++ b/mdl/executor/validate_widgets_test.go @@ -109,6 +109,7 @@ func TestStaticWidgetKnownPropsCoverDescribe(t *testing.T) { "Caption", "CaptionAttribute", "CaptionParams", "Class", "Collapsible", "ColumnWidth", "Content", "ContentParams", "DataSource", "DesignProperties", "DesktopColumns", "DisplayAs", "DynamicCellClass", "DynamicClasses", "Editable", "FilterType", "HeaderMode", + "DefaultImage", "OnClickType", "Height", "HeightUnit", "Hidable", "Image", "ImageType", "ImageUrl", "Label", "LabelWidth", "OnClick", "PageSize", "Pagination", "PagingPosition", "PhoneColumns", "PhoneWidth", "ReadOnlyStyle", "RenderMode", "Responsive", "Selection", "ShowContentAs", diff --git a/mdl/executor/widget_engine.go b/mdl/executor/widget_engine.go index 72d16883f6..98c2c205f8 100644 --- a/mdl/executor/widget_engine.go +++ b/mdl/executor/widget_engine.go @@ -299,12 +299,10 @@ func NewPluggableWidgetEngine(b backend.WidgetBuilderBackend, pb *pageBuilder) * func (e *PluggableWidgetEngine) Build(def *WidgetDefinition, w *ast.WidgetV3) (*pages.CustomWidget, error) { // Save and restore entity context (DataSource mappings may change it) oldEntityContext := e.pageBuilder.entityContext - oldContextVar := e.pageBuilder.contextVarName - oldContextKnown := e.pageBuilder.contextKnown + oldArgCtx := e.pageBuilder.argCtx defer func() { e.pageBuilder.entityContext = oldEntityContext - e.pageBuilder.contextVarName = oldContextVar - e.pageBuilder.contextKnown = oldContextKnown + e.pageBuilder.argCtx = oldArgCtx }() // Remember the containing context for properties that name members of it @@ -450,8 +448,7 @@ func (e *PluggableWidgetEngine) Build(def *WidgetDefinition, w *ast.WidgetV3) (* e.recordDataSourceEntity(propKey, entityName) if entityName != "" { e.pageBuilder.entityContext = entityName - e.pageBuilder.contextVarName = contextVarFor(ds) - e.pageBuilder.contextKnown = true + e.pageBuilder.argCtx = enteringDataWidget(ds, entityName) } } } @@ -1291,8 +1288,7 @@ func (e *PluggableWidgetEngine) resolveMapping(mapping PropertyMapping, w *ast.W e.recordDataSourceEntity(mapping.PropertyKey, entityName) if entityName != "" { e.pageBuilder.entityContext = entityName - e.pageBuilder.contextVarName = contextVarFor(ds) - e.pageBuilder.contextKnown = true + e.pageBuilder.argCtx = enteringDataWidget(ds, entityName) if w.Name != "" { e.pageBuilder.paramEntityNames[w.Name] = entityName } diff --git a/mdl/grammar/MDLLexer.g4 b/mdl/grammar/MDLLexer.g4 index fc799b3ffd..fdf73f5a6e 100644 --- a/mdl/grammar/MDLLexer.g4 +++ b/mdl/grammar/MDLLexer.g4 @@ -581,6 +581,15 @@ POST: P O S T; PUT: P U T; PATCH: P A T C H; API: A P I; + +// Microflow document properties (mendixlabs/mxcli#1120 follow-up). All five are +// in identifierOrKeyword so they stay usable as element names — HIDDEN and +// ALLOW in particular are plausible attribute names. +HIDDEN_KW: H I D D E N; // HIDDEN is reserved by ANTLR (the hidden channel) +ALLOW: A L L O W; +DISALLOW: D I S A L L O W; +CONCURRENT: C O N C U R R E N T; +EXECUTION: E X E C U T I O N; CLIENT: C L I E N T; CLIENTS: C L I E N T S; PUBLISH: P U B L I S H; @@ -751,6 +760,8 @@ MATRIX: M A T R I X; APPLY: A P P L Y; ACCESS: A C C E S S; LEVEL: L E V E L; +STRICT: S T R I C T; +MODE: M O D E; USER: U S E R; TASK: T A S K; DECISION: D E C I S I O N; diff --git a/mdl/grammar/domains/MDLMicroflow.g4 b/mdl/grammar/domains/MDLMicroflow.g4 index 03982170b4..f96b8fd3d2 100644 --- a/mdl/grammar/domains/MDLMicroflow.g4 +++ b/mdl/grammar/domains/MDLMicroflow.g4 @@ -155,6 +155,64 @@ microflowOptions microflowOption : FOLDER STRING_LITERAL | microflowExposedClause + | microflowUrlClause + | microflowExportLevelClause + | microflowConcurrencyClause + ; + +// URL 'item/{Key}' — the deep link (Mendix 10.6+) +// URL SEARCH PARAMETERS ($Filter) — parameters supplied as query arguments +// DROP URL — remove the deep link and its search params +// +// The {Name} placeholders are Mendix's own spelling, kept verbatim inside the +// string rather than given a parallel MDL syntax. Each must name a parameter of +// this microflow, and a parameter used in the PATH may not also be a SEARCH +// parameter — mxbuild rejects that overlap with CE5612. Both are checked by +// types.CheckMicroflowURL, which `mxcli check` and the executor share. +// +// As with EXPOSED AS, an ABSENT clause preserves what is stored; DROP URL is how +// a script asks for the deep link to go away — the same spelling as DROP ICON. +microflowUrlClause + : URL STRING_LITERAL + | URL SEARCH PARAMETERS LPAREN microflowUrlSearchParams? RPAREN + | DROP URL + ; + +microflowUrlSearchParams + : VARIABLE (COMMA VARIABLE)* COMMA? + ; + +// EXPORT LEVEL API | HIDDEN — whether the microflow is part of the module's +// public surface when the module is exported as a package. +// +// The members are keywords, not a quoted string, although image collections +// spell their own export level `EXPORT LEVEL 'Public'`: "Public" is not a member +// of either enum (both are API | Hidden), so that quoted form let a value the +// metamodel does not declare into the grammar's own documentation. A keyword +// makes the same mistake a parse error. +microflowExportLevelClause + : EXPORT LEVEL (API | HIDDEN_KW) + ; + +// DISALLOW CONCURRENT EXECUTION ERROR MESSAGE 'Already running' +// DISALLOW CONCURRENT EXECUTION ERROR MICROFLOW Module.Name +// ALLOW CONCURRENT EXECUTION +// +// Mendix REQUIRES an error message or an error microflow when concurrent +// execution is disallowed (CE4899), so the grammar accepts the bare DISALLOW and +// types.CheckMicroflowConcurrency refuses it with that CE number — a check with +// an explanation rather than a parse error reading "expecting ERROR". +microflowConcurrencyClause + : DISALLOW CONCURRENT EXECUTION microflowConcurrencyError? + | ALLOW CONCURRENT EXECUTION + ; + +// ERROR_MESSAGE is one token, not ERROR + MESSAGE — it already exists for an +// association's delete behaviour, and re-splitting it here would make the lexer +// ambiguous. It accepts `error message`, `error_message` and `errormessage`. +microflowConcurrencyError + : ERROR_MESSAGE STRING_LITERAL + | ERROR MICROFLOW qualifiedName ; // EXPOSED AS MICROFLOW ACTION 'Caption' IN 'Category' diff --git a/mdl/grammar/domains/MDLSecurity.g4 b/mdl/grammar/domains/MDLSecurity.g4 index a301f28f1b..840068fa26 100644 --- a/mdl/grammar/domains/MDLSecurity.g4 +++ b/mdl/grammar/domains/MDLSecurity.g4 @@ -109,6 +109,13 @@ alterProjectSecurityStatement // the executor refuses ON when neither source supplies a role. | ALTER PROJECT SECURITY GUEST ACCESS ON (ROLE identifierOrKeyword)? | ALTER PROJECT SECURITY GUEST ACCESS OFF + // Strict mode is a plain bool on Security$ProjectSecurity, declared by BOTH + // generated sources and already read back from real projects — so this + // writes a property Studio Pro knows, not one gen merely offers. + // + // mxcli LINTED for it (SEC005) and offered no way to clear it, which is a + // rule with no remedy (ako/mxcli#526). + | ALTER PROJECT SECURITY STRICT MODE (ON | OFF) ; createDemoUserStatement diff --git a/mdl/grammar/domains/MDLSettings.g4 b/mdl/grammar/domains/MDLSettings.g4 index 8e7c9bc97c..bc84de5981 100644 --- a/mdl/grammar/domains/MDLSettings.g4 +++ b/mdl/grammar/domains/MDLSettings.g4 @@ -673,6 +673,9 @@ keyword | PHONEWIDTH | TABLETWIDTH | READONLY | RENDERMODE | REQUIRED | NULLABLE | SELECTION | STYLE | STYLING | TABINDEX | TITLE | TOOLTIP | URL | POSITION | VISIBLE | WIDTH | HEIGHT | WIDGETTYPE + // Microflow document properties — keywords only inside a microflow header, + // so they must stay usable as element names everywhere else. + | HIDDEN_KW | ALLOW | DISALLOW | CONCURRENT | EXECUTION | VARIABLES_KW // Button actions @@ -688,7 +691,12 @@ keyword | ACCESS | APPLY | AUTH | AUTHENTICATION | BASIC | DEMO | DESCRIPTION | GRANT | GUEST | LEVEL | MANAGE | MATRIX | OFF | OWNER | PASSWORD | PRODUCTION | PROTOTYPE - | REVOKE | ROLE | ROLES | SECURITY | SESSION | USER | USERNAME | USERS + | REVOKE | ROLE | ROLES | SECURITY | SESSION | STRICT | USER | USERNAME | USERS + // MODE is listed here rather than left reserved because `mode` is an + // entirely plausible attribute or widget-property name, and a new keyword + // that is not in this rule silently breaks every model that already uses + // the word. + | MODE // Validation | CONSTRAINT | FEEDBACK | PATTERN | RANGE | REGEX | RULE | VALIDATION | WITHOUT diff --git a/mdl/grammar/domains/MDLWorkflow.g4 b/mdl/grammar/domains/MDLWorkflow.g4 index 7fb14616a6..e911213c59 100644 --- a/mdl/grammar/domains/MDLWorkflow.g4 +++ b/mdl/grammar/domains/MDLWorkflow.g4 @@ -18,7 +18,12 @@ createWorkflowStatement (PARAMETER VARIABLE COLON qualifiedName)? (DISPLAY display=STRING_LITERAL)? (DESCRIPTION description=STRING_LITERAL)? - (EXPORT LEVEL (IDENTIFIER | API))? + // HIDDEN_KW is listed beside IDENTIFIER because `Hidden` used to lex as an + // identifier and stopped when the microflow clauses made it a keyword. + // Anything matching a bare IDENTIFIER here is one token away from the same + // break — the hazard `identifierOrKeyword` exists to absorb, which this + // rule bypasses by taking IDENTIFIER directly. + (EXPORT LEVEL (IDENTIFIER | API | HIDDEN_KW))? (OVERVIEW PAGE qualifiedName)? (DUE DATE_TYPE dueDate=STRING_LITERAL)? workflowEventHandlerClause* @@ -287,7 +292,7 @@ alterWorkflowAction workflowSetProperty : DISPLAY STRING_LITERAL | DESCRIPTION STRING_LITERAL - | EXPORT LEVEL (IDENTIFIER | API) + | EXPORT LEVEL (IDENTIFIER | API | HIDDEN_KW) | DUE DATE_TYPE STRING_LITERAL | OVERVIEW PAGE qualifiedName | PARAMETER VARIABLE COLON qualifiedName diff --git a/mdl/linter/linter.go b/mdl/linter/linter.go index aba20fe3cd..2e1d0b1f2d 100644 --- a/mdl/linter/linter.go +++ b/mdl/linter/linter.go @@ -221,3 +221,13 @@ func Summarize(violations []Violation) Summary { s.Total = len(violations) return s } + +// RuleEnabled reports whether a rule will run. A rule with no configuration runs +// by default, so this answers the same question Run asks rather than reporting +// whether a configuration exists. +func (l *Linter) RuleEnabled(ruleID string) bool { + if config, ok := l.configs[ruleID]; ok { + return config.Enabled + } + return true +} diff --git a/mdl/types/member_write_rights.go b/mdl/types/member_write_rights.go new file mode 100644 index 0000000000..597abd4c52 --- /dev/null +++ b/mdl/types/member_write_rights.go @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 + +package types + +// WriteRightsForbidden reports whether Mendix refuses write access on an entity +// member with these properties, which the build reports as CE6592. +// +// There are two causes and they are easy to mistake for one. A CALCULATED +// attribute has its value produced by a microflow on read; an AUTONUMBER has its +// value produced by the database on insert. Neither is a value a user may set, +// so Mendix rejects an access rule granting write on either — but they are +// unrelated in the model: an autonumber carries no DomainModels$CalculatedValue, +// so a predicate written as "is calculated" covers exactly half the rule. +// +// That half-rule is what mendixlabs/mxcli#524 was: `grant write *` on an entity +// with an autonumber wrote ReadWrite and failed the build, and the user had to +// narrow the grant with a REVOKE by hand. +// +// The rule lives here, in one currency-free place, because the two callers speak +// different ones — the executor sees sdk/domainmodel types and the codec backend +// sees modelsdk/gen types, and neither may import the other. Each side detects +// its own two booleans and asks this function what they mean. Two copies of the +// predicate in two currencies is how a resolver drifts, which is the mistake +// CLAUDE.md records against the layout placeholder check. +func WriteRightsForbidden(isCalculated, isAutoNumber bool) bool { + return isCalculated || isAutoNumber +} diff --git a/mdl/types/microflow_properties.go b/mdl/types/microflow_properties.go new file mode 100644 index 0000000000..7b09e1bf74 --- /dev/null +++ b/mdl/types/microflow_properties.go @@ -0,0 +1,148 @@ +// SPDX-License-Identifier: Apache-2.0 + +package types + +import ( + "fmt" + "regexp" + "sort" + "strings" +) + +// Microflow document properties MDL can author but Mendix constrains. Each rule +// here is enforced by BOTH `mxcli check` and the writer, from this one function +// — the layout-placeholder lesson (two copies in two currencies is how a +// resolver drifts), and the reason these live in mdl/types rather than in either +// caller. + +// ExportLevelAPI and ExportLevelHidden are the two members of +// MicroflowsExportLevel. Validated against generated/metamodel, not passed +// through from user text. +const ( + ExportLevelAPI = "API" + ExportLevelHidden = "Hidden" +) + +// urlPlaceholder matches Mendix's {Name} parameter placeholders in a deep link. +var urlPlaceholder = regexp.MustCompile(`\{([^{}]*)\}`) + +// URLPathParameters returns the parameter names a deep-link URL interpolates, +// in order of appearance, without duplicates. +// +// A segment binds its parameter by the LEADING identifier, because Mendix allows +// an attribute path inside it: `{Customer/Name}` binds the Customer parameter by +// one of its attributes. Matching the whole segment would read that as a +// parameter named "Customer/Name" and flag a correct URL — the same rule +// urlBindsParameter applies on the page side (CE5601). +func URLPathParameters(url string) []string { + var out []string + seen := map[string]bool{} + for _, m := range urlPlaceholder.FindAllStringSubmatch(url, -1) { + name := strings.TrimSpace(m[1]) + if idx := strings.IndexAny(name, "/"); idx >= 0 { + name = strings.TrimSpace(name[:idx]) + } + if name == "" || seen[strings.ToLower(name)] { + continue + } + seen[strings.ToLower(name)] = true + out = append(out, name) + } + return out +} + +// CheckMicroflowURL validates a deep link against the microflow's parameters. +// +// Two rules, both measured against mxbuild 11.6.6 rather than inferred: +// +// - A {Name} placeholder must name a parameter of this microflow. +// - A parameter used in the PATH may not ALSO be a search parameter. That +// overlap is CE5612 ("cannot be used as a URL parameter if it is already a +// URL search parameter"); the two sets are disjoint. This was found by +// seeding a real project and running mx check — the first version of the +// fixture used one parameter for both and described a document Mendix +// refuses to build. +// +// paramNames is every parameter the microflow declares; searchParams is what the +// URL SEARCH PARAMETERS clause named. Both are compared case-insensitively, +// matching how MDL resolves parameter references elsewhere. +func CheckMicroflowURL(url string, searchParams []string, paramNames []string) []string { + if url == "" && len(searchParams) == 0 { + return nil + } + declared := map[string]string{} // lower -> declared spelling + for _, p := range paramNames { + declared[strings.ToLower(p)] = p + } + + var problems []string + pathParams := URLPathParameters(url) + inPath := map[string]bool{} + for _, name := range pathParams { + inPath[strings.ToLower(name)] = true + if _, ok := declared[strings.ToLower(name)]; !ok { + problems = append(problems, fmt.Sprintf( + "URL placeholder {%s} does not name a parameter of this microflow%s", + name, declaredHint(paramNames))) + } + } + + var overlap []string + for _, sp := range searchParams { + if _, ok := declared[strings.ToLower(sp)]; !ok { + problems = append(problems, fmt.Sprintf( + "URL search parameter $%s is not a parameter of this microflow%s", + sp, declaredHint(paramNames))) + continue + } + if inPath[strings.ToLower(sp)] { + overlap = append(overlap, sp) + } + } + if len(overlap) > 0 { + sort.Strings(overlap) + problems = append(problems, fmt.Sprintf( + "parameter(s) %s appear in the URL path AND in URL SEARCH PARAMETERS — "+ + "Mendix rejects that as CE5612. A parameter is either part of the path "+ + "or a query argument, never both: drop it from one of the two", + "$"+strings.Join(overlap, ", $"))) + } + return problems +} + +// CheckMicroflowConcurrency reports the CE4899 omission: Mendix requires an +// error message or an error microflow when concurrent execution is disallowed. +// +// It is a check rather than a grammar rule so the message can say which CE +// number it prevents, instead of a parse error reading "expecting ERROR". +func CheckMicroflowConcurrency(disallow, hasMessage bool, errorMicroflow string) string { + if !disallow || hasMessage || errorMicroflow != "" { + return "" + } + return "DISALLOW CONCURRENT EXECUTION needs an error handler — Mendix reports " + + "CE4899 without one. Add `ERROR MESSAGE 'text'` or `ERROR MICROFLOW Module.Name`" +} + +// CheckExportLevel validates an export level against the enum's two members. +func CheckExportLevel(level string) string { + switch level { + case "", ExportLevelAPI, ExportLevelHidden: + return "" + } + return fmt.Sprintf("export level %q is not a member of MicroflowsExportLevel — use %s or %s", + level, ExportLevelAPI, ExportLevelHidden) +} + +// declaredHint lists the parameters that ARE declared. +// +// It does not special-case a case-only mismatch: resolution is already +// case-insensitive, matching how MDL resolves parameter references everywhere +// else, so `{key}` against a `$Key` parameter resolves and never reaches here. +// A "did you mean" branch for that case was written and deleted — unreachable +// code a reader would have trusted. +func declaredHint(params []string) string { + if len(params) == 0 { + return " (this microflow has no parameters)" + } + return fmt.Sprintf(" (declared: $%s)", strings.Join(params, ", $")) +} diff --git a/mdl/types/microflow_properties_test.go b/mdl/types/microflow_properties_test.go new file mode 100644 index 0000000000..2954c8ad6d --- /dev/null +++ b/mdl/types/microflow_properties_test.go @@ -0,0 +1,96 @@ +// SPDX-License-Identifier: Apache-2.0 + +package types + +import "testing" + +// TestCheckMicroflowURL_PathAndSearchAreDisjoint is CE5612, the rule that is +// invisible without a build and that this feature's own first fixture broke. +func TestCheckMicroflowURL_PathAndSearchAreDisjoint(t *testing.T) { + params := []string{"Key", "Filter"} + + if got := CheckMicroflowURL("item/{Key}", []string{"Filter"}, params); len(got) != 0 { + t.Errorf("a disjoint path/search split must be accepted, got %v", got) + } + + got := CheckMicroflowURL("item/{Key}", []string{"Key"}, params) + if len(got) != 1 { + t.Fatalf("reusing one parameter for both must be refused, got %v", got) + } + if !contains(got[0], "CE5612") { + t.Errorf("the message must name the build error it prevents: %q", got[0]) + } +} + +// TestCheckMicroflowURL_PlaceholderNamesAParameter catches the typo that +// otherwise reaches a build. +func TestCheckMicroflowURL_PlaceholderNamesAParameter(t *testing.T) { + // An unknown name lists what IS declared rather than guessing at a + // correction. A case-only mismatch is NOT one of these: it resolves. + got := CheckMicroflowURL("item/{Nope}", nil, []string{"Key"}) + if len(got) != 1 || !contains(got[0], "declared: $Key") { + t.Errorf("an unrelated name should list the parameters, got %v", got) + } + if got := CheckMicroflowURL("item/{Key}", nil, []string{"Key"}); len(got) != 0 { + t.Errorf("a correct placeholder must be accepted, got %v", got) + } + // Case-insensitive resolution is the rule, not a near-miss report: MDL + // resolves parameter references that way everywhere else. + if got := CheckMicroflowURL("item/{KEY}", []string{"filter"}, []string{"Key", "Filter"}); len(got) != 0 { + t.Errorf("case-insensitive resolution must be accepted, got %v", got) + } +} + +// TestURLPathParameters_AttributePath is the rule the page-side validator +// already knew and this one nearly missed: Mendix allows an attribute path in a +// segment, so `{Customer/Name}` binds the Customer PARAMETER. Matching the whole +// segment would read that as a parameter named "Customer/Name" and flag a URL +// that builds perfectly well. +func TestURLPathParameters_AttributePath(t *testing.T) { + got := URLPathParameters("order/{Customer/Name}/{Id}") + if len(got) != 2 || got[0] != "Customer" || got[1] != "Id" { + t.Fatalf("got %v, want [Customer Id]", got) + } + if problems := CheckMicroflowURL("order/{Customer/Name}", nil, []string{"Customer"}); len(problems) != 0 { + t.Errorf("an attribute path must resolve to its parameter, got %v", problems) + } +} + +// TestCheckMicroflowConcurrency is CE4899: disallowing needs a handler. +func TestCheckMicroflowConcurrency(t *testing.T) { + if got := CheckMicroflowConcurrency(true, false, ""); !contains(got, "CE4899") { + t.Errorf("a bare DISALLOW must be refused by name, got %q", got) + } + if got := CheckMicroflowConcurrency(true, true, ""); got != "" { + t.Errorf("a message satisfies it, got %q", got) + } + if got := CheckMicroflowConcurrency(true, false, "Mod.OnBusy"); got != "" { + t.Errorf("an error microflow satisfies it, got %q", got) + } + if got := CheckMicroflowConcurrency(false, false, ""); got != "" { + t.Errorf("ALLOW needs no handler, got %q", got) + } +} + +// TestCheckExportLevel pins the enum. "Public" is the value the image-collection +// grammar's own example comment uses and neither enum declares — the reason this +// clause takes keywords rather than a quoted string. +func TestCheckExportLevel(t *testing.T) { + for _, ok := range []string{"", ExportLevelAPI, ExportLevelHidden} { + if got := CheckExportLevel(ok); got != "" { + t.Errorf("%q must be accepted, got %q", ok, got) + } + } + if got := CheckExportLevel("Public"); got == "" { + t.Error("Public is not a member of MicroflowsExportLevel and must be refused") + } +} + +func contains(s, sub string) bool { + for i := 0; i+len(sub) <= len(s); i++ { + if s[i:i+len(sub)] == sub { + return true + } + } + return false +} diff --git a/mdl/visitor/visitor_microflow.go b/mdl/visitor/visitor_microflow.go index fa30890386..464870e172 100644 --- a/mdl/visitor/visitor_microflow.go +++ b/mdl/visitor/visitor_microflow.go @@ -8,6 +8,7 @@ import ( "github.com/mendixlabs/mxcli/mdl/ast" "github.com/mendixlabs/mxcli/mdl/grammar/parser" + "github.com/mendixlabs/mxcli/mdl/types" ) func (b *Builder) ExitCreateMicroflowStatement(ctx *parser.CreateMicroflowStatementContext) { @@ -36,6 +37,7 @@ func (b *Builder) ExitCreateMicroflowStatement(ctx *parser.CreateMicroflowStatem if exposed := optCtx.MicroflowExposedClause(); exposed != nil { stmt.Expose = append(stmt.Expose, buildExposeActionClause(exposed)) } + applyMicroflowDocumentProperty(stmt, optCtx) } } @@ -446,3 +448,63 @@ func applyEntityAccessAnnotation(createStmt parser.ICreateStatementContext) *boo } return nil } + +// applyMicroflowDocumentProperty reads the URL / EXPORT LEVEL / concurrency +// header clauses onto the statement. +// +// Every field it sets is a POINTER, and that is the whole design: an absent +// clause must stay distinguishable from a stated one, because absent preserves +// what is stored and stated overrides it (mendixlabs/mxcli#1120). A plain value +// would make every rewrite that did not mention the clause reset the property — +// exactly the bug that made these worth authoring in the first place. +func applyMicroflowDocumentProperty(stmt *ast.CreateMicroflowStmt, optCtx *parser.MicroflowOptionContext) { + if u := optCtx.MicroflowUrlClause(); u != nil { + uc := u.(*parser.MicroflowUrlClauseContext) + switch { + case uc.DROP() != nil: + // DROP URL clears both halves: a search-parameter list without a URL + // is configuration for a deep link that no longer exists. + empty, none := "", []string{} + stmt.URL, stmt.URLSearchParameters = &empty, &none + case uc.SEARCH() != nil: + names := []string{} + if sp := uc.MicroflowUrlSearchParams(); sp != nil { + spCtx := sp.(*parser.MicroflowUrlSearchParamsContext) + for _, v := range spCtx.AllVARIABLE() { + names = append(names, strings.TrimPrefix(v.GetText(), "$")) + } + } + stmt.URLSearchParameters = &names + case uc.STRING_LITERAL() != nil: + url := unquoteString(uc.STRING_LITERAL().GetText()) + stmt.URL = &url + } + } + + if el := optCtx.MicroflowExportLevelClause(); el != nil { + elCtx := el.(*parser.MicroflowExportLevelClauseContext) + // The keyword, mapped to the member's stored spelling — never the user's + // casing, which is not what MicroflowsExportLevel declares. + level := types.ExportLevelHidden + if elCtx.API() != nil { + level = types.ExportLevelAPI + } + stmt.ExportLevel = &level + } + + if c := optCtx.MicroflowConcurrencyClause(); c != nil { + cCtx := c.(*parser.MicroflowConcurrencyClauseContext) + clause := &ast.ConcurrencyClause{Allow: cCtx.ALLOW() != nil} + if e := cCtx.MicroflowConcurrencyError(); e != nil { + eCtx := e.(*parser.MicroflowConcurrencyErrorContext) + if eCtx.ERROR_MESSAGE() != nil && eCtx.STRING_LITERAL() != nil { + clause.ErrorMessage = unquoteString(eCtx.STRING_LITERAL().GetText()) + clause.ErrorMessageSet = true + } + if qn := eCtx.QualifiedName(); qn != nil { + clause.ErrorMicroflow = buildQualifiedName(qn).String() + } + } + stmt.Concurrency = clause + } +} diff --git a/mdl/visitor/visitor_security.go b/mdl/visitor/visitor_security.go index 3057b46705..4a3b156c76 100644 --- a/mdl/visitor/visitor_security.go +++ b/mdl/visitor/visitor_security.go @@ -425,6 +425,9 @@ func (b *Builder) ExitAlterProjectSecurityStatement(ctx *parser.AlterProjectSecu if roleCtx := ctx.IdentifierOrKeyword(); roleCtx != nil { stmt.GuestUserRole = unquoteIdentifier(roleCtx.GetText()) } + } else if ctx.STRICT() != nil { + enabled := ctx.ON() != nil + stmt.StrictModeEnabled = &enabled } b.statements = append(b.statements, stmt) diff --git a/mdl/visitor/visitor_workflow.go b/mdl/visitor/visitor_workflow.go index c474dd5a09..4c568f2ec6 100644 --- a/mdl/visitor/visitor_workflow.go +++ b/mdl/visitor/visitor_workflow.go @@ -47,11 +47,19 @@ func (b *Builder) ExitCreateWorkflowStatement(ctx *parser.CreateWorkflowStatemen stmt.Description = unquoteString(tok.GetText()) } - // EXPORT LEVEL (Identifier | API) + // EXPORT LEVEL (Identifier | API | Hidden) + // + // HIDDEN_KW is read alongside IDENTIFIER because `Hidden` was an ordinary + // identifier here until the microflow header clauses made it a keyword — at + // which point this read silently produced "" and three tests caught it. Any + // rule taking a bare IDENTIFIER for a fixed vocabulary has the same fragility. if ctx.EXPORT() != nil && ctx.LEVEL() != nil { - if ctx.IDENTIFIER() != nil { + switch { + case ctx.IDENTIFIER() != nil: stmt.ExportLevel = ctx.IDENTIFIER().GetText() - } else if ctx.API() != nil { + case ctx.HIDDEN_KW() != nil: + stmt.ExportLevel = ctx.HIDDEN_KW().GetText() + case ctx.API() != nil: stmt.ExportLevel = "API" } } @@ -337,10 +345,14 @@ func buildWorkflowSetPropertyOp(ctx *parser.WorkflowSetPropertyContext) *ast.Set op.Value = unquoteString(ctx.STRING_LITERAL().GetText()) } else if ctx.EXPORT() != nil { op.Property = "export_level" - if ctx.API() != nil { + // HIDDEN_KW alongside IDENTIFIER — see the CREATE side above. + switch { + case ctx.API() != nil: op.Value = "API" - } else if ctx.IDENTIFIER() != nil { + case ctx.IDENTIFIER() != nil: op.Value = ctx.IDENTIFIER().GetText() + case ctx.HIDDEN_KW() != nil: + op.Value = ctx.HIDDEN_KW().GetText() } } else if ctx.DUE() != nil { op.Property = "due_date" diff --git a/sdk/pages/pages_widgets_action.go b/sdk/pages/pages_widgets_action.go index a1c9c88c1c..8a17145316 100644 --- a/sdk/pages/pages_widgets_action.go +++ b/sdk/pages/pages_widgets_action.go @@ -194,11 +194,24 @@ const ( // MicroflowParameterMapping maps a microflow parameter to a value in a MicroflowClientAction. // BSON storage type: Forms$MicroflowParameterMapping (not Pages$ or Microflows$). +// +// An argument binds one of two ways, and Mendix does not treat them as +// interchangeable. A reference to a page parameter, snippet parameter or page +// variable is stored as a Forms$PageVariable under Variable; anything else — a +// literal, an expression — is stored as text under Expression. Writing a +// $-reference as an Expression leaves the parameter unbound: Studio Pro reports +// CE1571 and mxbuild builds it at 0 errors (mendixlabs/mxcli#1140). +// +// VariableKind is what says which of the two applies, and which slot of the +// PageVariable to fill. Empty means "not a page-variable reference" — Variable is +// then still written as the Expression, which is what $currentObject and every +// pre-#1140 caller relies on. type MicroflowParameterMapping struct { model.BaseElement - ParameterName string `json:"parameterName"` // Parameter name (without $) - Variable string `json:"variable,omitempty"` // Variable reference (e.g., "$Customer") - Expression string `json:"expression,omitempty"` // Expression value + ParameterName string `json:"parameterName"` // Parameter name (without $) + Variable string `json:"variable,omitempty"` // Variable reference (e.g., "$Customer") + VariableKind string `json:"variableKind,omitempty"` // "" | "parameter" | "snippet" | "local" + Expression string `json:"expression,omitempty"` // Expression value } // MicroflowClientAction calls a microflow. @@ -213,11 +226,15 @@ func (MicroflowClientAction) isClientAction() {} // NanoflowParameterMapping maps a nanoflow parameter to a value in a NanoflowClientAction. // BSON storage type: Forms$NanoflowParameterMapping (not Pages$). +// +// Same two binding forms as MicroflowParameterMapping — see its comment for why +// VariableKind decides between them. type NanoflowParameterMapping struct { model.BaseElement - ParameterName string `json:"parameterName"` // Parameter name (without $) - Variable string `json:"variable,omitempty"` // Variable reference (e.g., "$Customer") - Expression string `json:"expression,omitempty"` // Expression value + ParameterName string `json:"parameterName"` // Parameter name (without $) + Variable string `json:"variable,omitempty"` // Variable reference (e.g., "$Customer") + VariableKind string `json:"variableKind,omitempty"` // "" | "parameter" | "snippet" | "local" + Expression string `json:"expression,omitempty"` // Expression value } // NanoflowClientAction calls a nanoflow. diff --git a/sdk/pages/pages_widgets_display.go b/sdk/pages/pages_widgets_display.go index 102c94ffdf..1c6bbb52b7 100644 --- a/sdk/pages/pages_widgets_display.go +++ b/sdk/pages/pages_widgets_display.go @@ -83,12 +83,30 @@ type Title struct { // DynamicImage represents a dynamic image widget. type DynamicImage struct { BaseWidget - DefaultImage model.ID `json:"defaultImage,omitempty"` - Width int `json:"width,omitempty"` - WidthUnit WidthUnit `json:"widthUnit,omitempty"` - Height int `json:"height,omitempty"` - Responsive bool `json:"responsive"` - OnClickAction ClientAction `json:"onClickAction,omitempty"` + // DataSource is the entity holding the image, stored as the EntityRef of a + // Forms$ImageViewerSource. Without it mxbuild refuses the widget outright — + // CE0489 "Select an entity for the data source of this dynamic image" — so + // this is the one field the widget cannot be written without. + DataSource DataSource `json:"dataSource,omitempty"` + // DefaultImageName is the fallback image shown when the object has none, as + // the three-part qualified name of an image-collection entry + // (Module.Collection.Image). Forms$ImageViewer.DefaultImage is a by-name + // reference to Images$Image, so a NAME is what Mendix stores — the + // DefaultImage (model.ID) field that used to stand here was never filled by + // anything and named the wrong thing. + DefaultImageName string `json:"defaultImageName,omitempty"` + Width int `json:"width,omitempty"` + WidthUnit WidthUnit `json:"widthUnit,omitempty"` + Height int `json:"height,omitempty"` + // HeightUnit is the sibling of WidthUnit, which had no field while the + // writer hardcoded both to "Auto". Empty means Auto (Mendix's default). + HeightUnit WidthUnit `json:"heightUnit,omitempty"` + // ShowAsThumbnail and OnClickEnlarge were both hardcoded false by the + // writer, so neither was reachable from MDL. + ShowAsThumbnail bool `json:"showAsThumbnail,omitempty"` + OnClickEnlarge bool `json:"onClickEnlarge,omitempty"` + Responsive bool `json:"responsive"` + OnClickAction ClientAction `json:"onClickAction,omitempty"` } // StaticImage represents a static image widget. diff --git a/sdk/pages/pages_widgets_input.go b/sdk/pages/pages_widgets_input.go index e9880e802e..06dc8848b6 100644 --- a/sdk/pages/pages_widgets_input.go +++ b/sdk/pages/pages_widgets_input.go @@ -11,28 +11,30 @@ import ( // TextBox represents a text input widget. type TextBox struct { BaseWidget - Label string `json:"label,omitempty"` - AttributePath string `json:"attributePath,omitempty"` - FormattingInfo *FormattingInfo `json:"formattingInfo,omitempty"` - Placeholder *model.Text `json:"placeholder,omitempty"` - MaxLength int `json:"maxLength,omitempty"` - IsPassword bool `json:"isPassword,omitempty"` - ReadOnly bool `json:"readOnly,omitempty"` - OnChangeAction ClientAction `json:"onChangeAction,omitempty"` - OnEnterAction ClientAction `json:"onEnterAction,omitempty"` + Label string `json:"label,omitempty"` + AttributePath string `json:"attributePath,omitempty"` + AttributeRefSteps []AttributeRefStep `json:"attributeRefSteps,omitempty"` // association hops when the attribute is reached over associations (AttributeRef.EntityRef) + FormattingInfo *FormattingInfo `json:"formattingInfo,omitempty"` + Placeholder *model.Text `json:"placeholder,omitempty"` + MaxLength int `json:"maxLength,omitempty"` + IsPassword bool `json:"isPassword,omitempty"` + ReadOnly bool `json:"readOnly,omitempty"` + OnChangeAction ClientAction `json:"onChangeAction,omitempty"` + OnEnterAction ClientAction `json:"onEnterAction,omitempty"` } // TextArea represents a multi-line text input widget. type TextArea struct { BaseWidget - Label string `json:"label,omitempty"` - AttributePath string `json:"attributePath,omitempty"` - Placeholder *model.Text `json:"placeholder,omitempty"` - MaxLength int `json:"maxLength,omitempty"` - CounterMessage *model.Text `json:"counterMessage,omitempty"` - Rows int `json:"rows,omitempty"` - ReadOnly bool `json:"readOnly,omitempty"` - OnChangeAction ClientAction `json:"onChangeAction,omitempty"` + Label string `json:"label,omitempty"` + AttributePath string `json:"attributePath,omitempty"` + AttributeRefSteps []AttributeRefStep `json:"attributeRefSteps,omitempty"` // association hops when the attribute is reached over associations (AttributeRef.EntityRef) + Placeholder *model.Text `json:"placeholder,omitempty"` + MaxLength int `json:"maxLength,omitempty"` + CounterMessage *model.Text `json:"counterMessage,omitempty"` + Rows int `json:"rows,omitempty"` + ReadOnly bool `json:"readOnly,omitempty"` + OnChangeAction ClientAction `json:"onChangeAction,omitempty"` } // FormattingInfo represents formatting configuration. @@ -49,22 +51,24 @@ type FormattingInfo struct { // DatePicker represents a date picker widget. type DatePicker struct { BaseWidget - Label string `json:"label,omitempty"` - AttributePath string `json:"attributePath,omitempty"` - Placeholder *model.Text `json:"placeholder,omitempty"` - DateFormat string `json:"dateFormat,omitempty"` - ReadOnly bool `json:"readOnly,omitempty"` - OnChangeAction ClientAction `json:"onChangeAction,omitempty"` + Label string `json:"label,omitempty"` + AttributePath string `json:"attributePath,omitempty"` + AttributeRefSteps []AttributeRefStep `json:"attributeRefSteps,omitempty"` // association hops when the attribute is reached over associations (AttributeRef.EntityRef) + Placeholder *model.Text `json:"placeholder,omitempty"` + DateFormat string `json:"dateFormat,omitempty"` + ReadOnly bool `json:"readOnly,omitempty"` + OnChangeAction ClientAction `json:"onChangeAction,omitempty"` } // DropDown represents a drop-down selection widget. type DropDown struct { BaseWidget - Label string `json:"label,omitempty"` - AttributePath string `json:"attributePath,omitempty"` - EmptyOption *model.Text `json:"emptyOption,omitempty"` - ReadOnly bool `json:"readOnly,omitempty"` - OnChangeAction ClientAction `json:"onChangeAction,omitempty"` + Label string `json:"label,omitempty"` + AttributePath string `json:"attributePath,omitempty"` + AttributeRefSteps []AttributeRefStep `json:"attributeRefSteps,omitempty"` // association hops when the attribute is reached over associations (AttributeRef.EntityRef) + EmptyOption *model.Text `json:"emptyOption,omitempty"` + ReadOnly bool `json:"readOnly,omitempty"` + OnChangeAction ClientAction `json:"onChangeAction,omitempty"` } // ReferenceSelector represents a reference selector widget. @@ -112,9 +116,10 @@ type ReferenceSetSelector struct { // CheckBox represents a checkbox widget. type CheckBox struct { BaseWidget - Label string `json:"label,omitempty"` - AttributePath string `json:"attributePath,omitempty"` - ReadOnly bool `json:"readOnly,omitempty"` + Label string `json:"label,omitempty"` + AttributePath string `json:"attributePath,omitempty"` + AttributeRefSteps []AttributeRefStep `json:"attributeRefSteps,omitempty"` // association hops when the attribute is reached over associations (AttributeRef.EntityRef) + ReadOnly bool `json:"readOnly,omitempty"` // ReadOnlyStyle is Mendix's "Read-only style": Inherit, Control or Text. // Empty means unset — the writer keeps the stored default (Inherit), so a // script that never mentions it produces the document it always did. @@ -130,11 +135,12 @@ type CheckBox struct { // RadioButtons represents a radio button group widget. type RadioButtons struct { BaseWidget - Label string `json:"label,omitempty"` - AttributePath string `json:"attributePath,omitempty"` - RenderDirection RenderDirection `json:"renderDirection,omitempty"` - ReadOnly bool `json:"readOnly,omitempty"` - OnChangeAction ClientAction `json:"onChangeAction,omitempty"` + Label string `json:"label,omitempty"` + AttributePath string `json:"attributePath,omitempty"` + AttributeRefSteps []AttributeRefStep `json:"attributeRefSteps,omitempty"` // association hops when the attribute is reached over associations (AttributeRef.EntityRef) + RenderDirection RenderDirection `json:"renderDirection,omitempty"` + ReadOnly bool `json:"readOnly,omitempty"` + OnChangeAction ClientAction `json:"onChangeAction,omitempty"` } // RenderDirection represents the direction for rendering. diff --git a/sdk/versions/mendix-10.yaml b/sdk/versions/mendix-10.yaml index 9700e4ee09..457d222a68 100644 --- a/sdk/versions/mendix-10.yaml +++ b/sdk/versions/mendix-10.yaml @@ -43,6 +43,10 @@ features: basic: min_version: "10.0.0" mdl: "CREATE MICROFLOW Module.Name (...) BEGIN ... END" + deep_link_url: + min_version: "10.6.0" + mdl: "URL 'item/{Key}' / URL SEARCH PARAMETERS ($Filter) / DROP URL" + notes: "Microflow deep links. A path parameter may not also be a search parameter (CE5612)." synchronize: min_version: "10.0.0" mdl: "SYNCHRONIZE ALL | $Var[, $Var...] (nanoflow only)" diff --git a/sdk/versions/mendix-11.yaml b/sdk/versions/mendix-11.yaml index 2c2a4c7b65..0e21cf0975 100644 --- a/sdk/versions/mendix-11.yaml +++ b/sdk/versions/mendix-11.yaml @@ -40,6 +40,10 @@ features: basic: min_version: "10.0.0" mdl: "CREATE MICROFLOW Module.Name (...) BEGIN ... END" + deep_link_url: + min_version: "11.0.0" + mdl: "URL 'item/{Key}' / URL SEARCH PARAMETERS ($Filter) / DROP URL" + notes: "Microflow deep links (10.6+, so available throughout 11.x)." synchronize: min_version: "10.0.0" mdl: "SYNCHRONIZE ALL | $Var[, $Var...] (nanoflow only)" diff --git a/vscode-mdl/bun.lock b/vscode-mdl/bun.lock index 6a2816aeba..2289e6341d 100644 --- a/vscode-mdl/bun.lock +++ b/vscode-mdl/bun.lock @@ -11,42 +11,36 @@ "devDependencies": { "@types/node": "^26.0.0", "@types/vscode": "^1.80.0", - "@vscode/vsce": "^3.7.1", + "@vscode/vsce": "^4.0.0", "esbuild": "^0.28.0", "typescript": "^7.0.2", }, }, }, "packages": { - "@azu/format-text": ["@azu/format-text@1.0.2", "", {}, "sha512-Swi4N7Edy1Eqq82GxgEECXSSLyn6GOb5htRFPzBDdUkECGXtlf12ynO5oJSpWKPwCaUssOu7NfhDcCWpIC6Ywg=="], - - "@azu/style-format": ["@azu/style-format@1.0.1", "", { "dependencies": { "@azu/format-text": "^1.0.1" } }, "sha512-AHcTojlNBdD/3/KxIKlg8sxIWHfOtQszLvOpagLTO+bjC3u7SAszu1lf//u7JJC50aUSH+BVWDD/KvaA6Gfn5g=="], - "@azure/abort-controller": ["@azure/abort-controller@2.1.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-nBrLsEWm4J2u5LpAPjxADTlq3trDgVZZXHNKabeXZtpq3d3AbN/KGO82R87rdDz5/lYB024rtEf10/q0urNgsA=="], "@azure/core-auth": ["@azure/core-auth@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-util": "^1.13.0", "tslib": "^2.6.2" } }, "sha512-ykRMW8PjVAn+RS6ww5cmK9U2CyH9p4Q88YJwvUslfuMmN98w/2rdGRLPqJYObapBCdzBVeDgYWdJnFPFb7qzpg=="], "@azure/core-client": ["@azure/core-client@1.10.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-rest-pipeline": "^1.22.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "tslib": "^2.6.2" } }, "sha512-Nh5PhEOeY6PrnxNPsEHRr9eimxLwgLlpmguQaHKBinFYA/RU9+kOYVOQqOrTsCL+KSxrLLl1gD8Dk5BFW/7l/w=="], + "@azure/core-process": ["@azure/core-process@1.0.0", "", {}, "sha512-/shnJ+ooO8WPxDhPEeI/2oRQuubn16gZ6CvlbpWbEswZfzwI9tI/sMAHmF3x1LuQ9yZYXfLW3TjzGMLEC5blKg=="], + "@azure/core-rest-pipeline": ["@azure/core-rest-pipeline@1.22.2", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@azure/core-auth": "^1.10.0", "@azure/core-tracing": "^1.3.0", "@azure/core-util": "^1.13.0", "@azure/logger": "^1.3.0", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-MzHym+wOi8CLUlKCQu12de0nwcq9k9Kuv43j4Wa++CsCpJwps2eeBQwD2Bu8snkxTtDKDx4GwjuR9E8yC8LNrg=="], "@azure/core-tracing": ["@azure/core-tracing@1.3.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9MWKevR7Hz8kNzzPLfX4EAtGM2b8mr50HPDBvio96bURP/9C+HjdH3sBlLSNNrvRAr5/k/svoH457gB5IKpmwQ=="], "@azure/core-util": ["@azure/core-util@1.13.1", "", { "dependencies": { "@azure/abort-controller": "^2.1.2", "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-XPArKLzsvl0Hf0CaGyKHUyVgF7oDnhKoP85Xv6M4StF/1AhfORhZudHtOyf2s+FcbuQ9dPRAjB8J2KvRRMUK2A=="], - "@azure/identity": ["@azure/identity@4.13.0", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^4.2.0", "@azure/msal-node": "^3.5.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-uWC0fssc+hs1TGGVkkghiaFkkS7NkTxfnCH+Hdg+yTehTpMcehpok4PgUKKdyCH+9ldu6FhiHRv84Ntqj1vVcw=="], + "@azure/identity": ["@azure/identity@4.13.3", "", { "dependencies": { "@azure/abort-controller": "^2.0.0", "@azure/core-auth": "^1.9.0", "@azure/core-client": "^1.9.2", "@azure/core-process": "^1.0.0", "@azure/core-rest-pipeline": "^1.17.0", "@azure/core-tracing": "^1.0.0", "@azure/core-util": "^1.11.0", "@azure/logger": "^1.0.0", "@azure/msal-browser": "^5.5.0", "@azure/msal-node": "^6.0.0", "open": "^10.1.0", "tslib": "^2.2.0" } }, "sha512-zGQPtqvXPgSA8yfV2CkIQ1qirqk0p9AIVpC5uEkdXQYcKl07QHvyaGYRnZOk0AsQUmxNb4wfkcwY5di8Z5xa9A=="], "@azure/logger": ["@azure/logger@1.3.0", "", { "dependencies": { "@typespec/ts-http-runtime": "^0.3.0", "tslib": "^2.6.2" } }, "sha512-fCqPIfOcLE+CGqGPd66c8bZpwAji98tZ4JI9i/mlTNTlsIWslCfpg48s/ypyLxZTump5sypjrKn2/kY7q8oAbA=="], - "@azure/msal-browser": ["@azure/msal-browser@4.28.1", "", { "dependencies": { "@azure/msal-common": "15.14.1" } }, "sha512-al2u2fTchbClq3L4C1NlqLm+vwKfhYCPtZN2LR/9xJVaQ4Mnrwf5vANvuyPSJHcGvw50UBmhuVmYUAhTEetTpA=="], + "@azure/msal-browser": ["@azure/msal-browser@5.22.0", "", { "dependencies": { "@azure/msal-common": "16.14.1" } }, "sha512-5kgu9xeEKgGc2JeidxAtU15NJTqiH/CMCRRQAJ4Rac56kB7KVg91vbNmn+z3RO1vNomPN69UvjKG9h1Pghx6dQ=="], - "@azure/msal-common": ["@azure/msal-common@15.14.1", "", {}, "sha512-IkzF7Pywt6QKTS0kwdCv/XV8x8JXknZDvSjj/IccooxnP373T5jaadO3FnOrbWo3S0UqkfIDyZNTaQ/oAgRdXw=="], + "@azure/msal-common": ["@azure/msal-common@16.14.1", "", {}, "sha512-Or6xhPNyi4zHW25158yxBoyxuCqNSPa5YBVqfF1J5Ks4MJWBo/USXdp05DQIPu1Zli00YZu6t0+h6KvHJealxQ=="], - "@azure/msal-node": ["@azure/msal-node@3.8.6", "", { "dependencies": { "@azure/msal-common": "15.14.1", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } }, "sha512-XTmhdItcBckcVVTy65Xp+42xG4LX5GK+9AqAsXPXk4IqUNv+LyQo5TMwNjuFYBfAB2GTG9iSQGk+QLc03vhf3w=="], - - "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], - - "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + "@azure/msal-node": ["@azure/msal-node@6.0.1", "", { "dependencies": { "@azure/msal-common": "16.14.1", "jsonwebtoken": "^9.0.0" } }, "sha512-ixSO1Y/kCVRthRs+hSx/5qkwaunX1/RAePhlMN0wIpIQ4WEZ6AREGGnGd1AP0qspHVsAwzWQKGubpjh50JyJIQ=="], "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.0", "", { "os": "aix", "cpu": "ppc64" }, "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA=="], @@ -100,29 +94,35 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.0", "", { "os": "win32", "cpu": "x64" }, "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw=="], - "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], + "@napi-rs/keyring": ["@napi-rs/keyring@1.3.0", "", { "optionalDependencies": { "@napi-rs/keyring-darwin-arm64": "1.3.0", "@napi-rs/keyring-darwin-x64": "1.3.0", "@napi-rs/keyring-freebsd-x64": "1.3.0", "@napi-rs/keyring-linux-arm-gnueabihf": "1.3.0", "@napi-rs/keyring-linux-arm64-gnu": "1.3.0", "@napi-rs/keyring-linux-arm64-musl": "1.3.0", "@napi-rs/keyring-linux-riscv64-gnu": "1.3.0", "@napi-rs/keyring-linux-x64-gnu": "1.3.0", "@napi-rs/keyring-linux-x64-musl": "1.3.0", "@napi-rs/keyring-win32-arm64-msvc": "1.3.0", "@napi-rs/keyring-win32-ia32-msvc": "1.3.0", "@napi-rs/keyring-win32-x64-msvc": "1.3.0" } }, "sha512-WrOw/bcXm0f9qHkumlT1QlArXSTWqaY9sunsDpOk+yCCorCKMxvWT/a3xko4EYHVdeZoh00yI2TydXn6eyICDA=="], - "@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g=="], + "@napi-rs/keyring-darwin-arm64": ["@napi-rs/keyring-darwin-arm64@1.3.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pl76hJvdYUBn6I24bXiOBMA9nbDapo3I5B+f3OorjDU4dUMSypXeKbOVehJe8fhgTiH24flMyTS3aAIy43xegQ=="], - "@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A=="], + "@napi-rs/keyring-darwin-x64": ["@napi-rs/keyring-darwin-x64@1.3.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-YcJtEV5LA3cvA4z3BurgxH5IhTsW1JfIvcAAcqcecwk06Si9F9NqkxbZVIfDwQ8oRHgaBmT3zZJnLAotCrVahw=="], - "@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg=="], + "@napi-rs/keyring-freebsd-x64": ["@napi-rs/keyring-freebsd-x64@1.3.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-vlLf31TGhfRAaxLDBhg8b89ss0HHD/lyNmL5F3UjSaz5CUXElsJmKYq9fqA/B+cZKUEUcLHHGhF0I/CqcFdaVw=="], - "@secretlint/config-creator": ["@secretlint/config-creator@10.2.2", "", { "dependencies": { "@secretlint/types": "^10.2.2" } }, "sha512-BynOBe7Hn3LJjb3CqCHZjeNB09s/vgf0baBaHVw67w7gHF0d25c3ZsZ5+vv8TgwSchRdUCRrbbcq5i2B1fJ2QQ=="], + "@napi-rs/keyring-linux-arm-gnueabihf": ["@napi-rs/keyring-linux-arm-gnueabihf@1.3.0", "", { "os": "linux", "cpu": "arm" }, "sha512-KiWdMMu/Inz/bHHIAGrnF7r54FZDYXuHO6UFF/rhIrshUsxbMG1Rl9lEymNtqqsVo927G0VYcb02FzWQ3iBQRQ=="], - "@secretlint/config-loader": ["@secretlint/config-loader@10.2.2", "", { "dependencies": { "@secretlint/profiler": "^10.2.2", "@secretlint/resolver": "^10.2.2", "@secretlint/types": "^10.2.2", "ajv": "^8.17.1", "debug": "^4.4.1", "rc-config-loader": "^4.1.3" } }, "sha512-ndjjQNgLg4DIcMJp4iaRD6xb9ijWQZVbd9694Ol2IszBIbGPPkwZHzJYKICbTBmh6AH/pLr0CiCaWdGJU7RbpQ=="], + "@napi-rs/keyring-linux-arm64-gnu": ["@napi-rs/keyring-linux-arm64-gnu@1.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-eyKGpY40lm9Jvs1aD294XRH4y7+TlJM0YVAryZeXA6TX0mb4gMkxVXwSQv7MCwgah7raeUd0dKUb4BPAYIgcMg=="], - "@secretlint/core": ["@secretlint/core@10.2.2", "", { "dependencies": { "@secretlint/profiler": "^10.2.2", "@secretlint/types": "^10.2.2", "debug": "^4.4.1", "structured-source": "^4.0.0" } }, "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw=="], + "@napi-rs/keyring-linux-arm64-musl": ["@napi-rs/keyring-linux-arm64-musl@1.3.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-iIK6JWHXAJqDrEyLY3TmswwloVyt2vj+04TZnew+uSJ9gnDO8EwRbp3/iw3LpWaXiDO7VomGO6y8I0Id8uBZSw=="], - "@secretlint/formatter": ["@secretlint/formatter@10.2.2", "", { "dependencies": { "@secretlint/resolver": "^10.2.2", "@secretlint/types": "^10.2.2", "@textlint/linter-formatter": "^15.2.0", "@textlint/module-interop": "^15.2.0", "@textlint/types": "^15.2.0", "chalk": "^5.4.1", "debug": "^4.4.1", "pluralize": "^8.0.0", "strip-ansi": "^7.1.0", "table": "^6.9.0", "terminal-link": "^4.0.0" } }, "sha512-10f/eKV+8YdGKNQmoDUD1QnYL7TzhI2kzyx95vsJKbEa8akzLAR5ZrWIZ3LbcMmBLzxlSQMMccRmi05yDQ5YDA=="], + "@napi-rs/keyring-linux-riscv64-gnu": ["@napi-rs/keyring-linux-riscv64-gnu@1.3.0", "", { "os": "linux", "cpu": "none" }, "sha512-/PGqrwn6EwgtK6vccASSXJRfOSP4vN1F4ASsIQ+7MdrK6hNvAJ1FZPrIuD5gGGdxezo3F++To2Wq7DbuGIeuNQ=="], - "@secretlint/node": ["@secretlint/node@10.2.2", "", { "dependencies": { "@secretlint/config-loader": "^10.2.2", "@secretlint/core": "^10.2.2", "@secretlint/formatter": "^10.2.2", "@secretlint/profiler": "^10.2.2", "@secretlint/source-creator": "^10.2.2", "@secretlint/types": "^10.2.2", "debug": "^4.4.1", "p-map": "^7.0.3" } }, "sha512-eZGJQgcg/3WRBwX1bRnss7RmHHK/YlP/l7zOQsrjexYt6l+JJa5YhUmHbuGXS94yW0++3YkEJp0kQGYhiw1DMQ=="], + "@napi-rs/keyring-linux-x64-gnu": ["@napi-rs/keyring-linux-x64-gnu@1.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-2PDK1WKWTu9lBGq9VvNEkSlQD3O7YwVpmnyN2M3cy4v7NJ/8gDMd9GXv3G+FVXN13uhp4gnnPBS+ScefmEeD2A=="], - "@secretlint/profiler": ["@secretlint/profiler@10.2.2", "", {}, "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig=="], + "@napi-rs/keyring-linux-x64-musl": ["@napi-rs/keyring-linux-x64-musl@1.3.0", "", { "os": "linux", "cpu": "x64" }, "sha512-oJ2HkX8YUo46QBkn0pG+HuIKQNqr523q6vBobCn+P95s4C4K6/kLBqHY/1bg5J4ap31DzsznhnFKcfBNBsjCnw=="], + + "@napi-rs/keyring-win32-arm64-msvc": ["@napi-rs/keyring-win32-arm64-msvc@1.3.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-tOd3c/uAaeoE4ycVlmAdSvygz0Zt3zdca6Y7gokBeIbaRDWpjDIUOpU3MvML59XAaqyuKGsVVu0F/DZb1lHPmw=="], + + "@napi-rs/keyring-win32-ia32-msvc": ["@napi-rs/keyring-win32-ia32-msvc@1.3.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-sPSqeAFZMGqP1R++M2JTza7GQJJ/TpCo6JU6Vcd4jnebvOaEDs9b7eipakU1PJdSvhpC2yXMCNRk9gXfrhuwHQ=="], - "@secretlint/resolver": ["@secretlint/resolver@10.2.2", "", {}, "sha512-3md0cp12e+Ae5V+crPQYGd6aaO7ahw95s28OlULGyclyyUtf861UoRGS2prnUrKh7MZb23kdDOyGCYb9br5e4w=="], + "@napi-rs/keyring-win32-x64-msvc": ["@napi-rs/keyring-win32-x64-msvc@1.3.0", "", { "os": "win32", "cpu": "x64" }, "sha512-4DnCWXwDc0HRKwyRlG5y0VhKZW2tNRQfKKfyj6IX/KWfDNyq9hn4n+GL1auyDcOO/v8PwnhmYo2+rOOqCkvvOg=="], - "@secretlint/secretlint-formatter-sarif": ["@secretlint/secretlint-formatter-sarif@10.2.2", "", { "dependencies": { "node-sarif-builder": "^3.2.0" } }, "sha512-ojiF9TGRKJJw308DnYBucHxkpNovDNu1XvPh7IfUp0A12gzTtxuWDqdpuVezL7/IP8Ua7mp5/VkDMN9OLp1doQ=="], + "@secretlint/core": ["@secretlint/core@10.2.2", "", { "dependencies": { "@secretlint/profiler": "^10.2.2", "@secretlint/types": "^10.2.2", "debug": "^4.4.1", "structured-source": "^4.0.0" } }, "sha512-6rdwBwLP9+TO3rRjMVW1tX+lQeo5gBbxl1I5F8nh8bgGtKwdlCMhMKsBWzWg1ostxx/tIG7OjZI0/BxsP8bUgw=="], + + "@secretlint/profiler": ["@secretlint/profiler@10.2.2", "", {}, "sha512-qm9rWfkh/o8OvzMIfY8a5bCmgIniSpltbVlUVl983zDG1bUuQNd1/5lUEeWx5o/WJ99bXxS7yNI4/KIXfHexig=="], "@secretlint/secretlint-rule-no-dotenv": ["@secretlint/secretlint-rule-no-dotenv@10.2.2", "", { "dependencies": { "@secretlint/types": "^10.2.2" } }, "sha512-KJRbIShA9DVc5Va3yArtJ6QDzGjg3PRa1uYp9As4RsyKtKSSZjI64jVca57FZ8gbuk4em0/0Jq+uy6485wxIdg=="], @@ -132,24 +132,8 @@ "@secretlint/types": ["@secretlint/types@10.2.2", "", {}, "sha512-Nqc90v4lWCXyakD6xNyNACBJNJ0tNCwj2WNk/7ivyacYHxiITVgmLUFXTBOeCdy79iz6HtN9Y31uw/jbLrdOAg=="], - "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@2.3.0", "", {}, "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg=="], - - "@textlint/ast-node-types": ["@textlint/ast-node-types@15.5.2", "", {}, "sha512-fCaOxoup5LIyBEo7R1oYWE7V4bSX0KQeHh66twon9e9usaLE3ijgF8QjYsR6joCssdeCHVd0wHm7ppsEyTr6vg=="], - - "@textlint/linter-formatter": ["@textlint/linter-formatter@15.5.2", "", { "dependencies": { "@azu/format-text": "^1.0.2", "@azu/style-format": "^1.0.1", "@textlint/module-interop": "15.5.2", "@textlint/resolver": "15.5.2", "@textlint/types": "15.5.2", "chalk": "^4.1.2", "debug": "^4.4.3", "js-yaml": "^4.1.1", "lodash": "^4.17.23", "pluralize": "^2.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "table": "^6.9.0", "text-table": "^0.2.0" } }, "sha512-jAw7jWM8+wU9cG6Uu31jGyD1B+PAVePCvnPKC/oov+2iBPKk3ao30zc/Itmi7FvXo4oPaL9PmzPPQhyniPVgVg=="], - - "@textlint/module-interop": ["@textlint/module-interop@15.5.2", "", {}, "sha512-mg6rMQ3+YjwiXCYoQXbyVfDucpTa1q5mhspd/9qHBxUq4uY6W8GU42rmT3GW0V1yOfQ9z/iRrgPtkp71s8JzXg=="], - - "@textlint/resolver": ["@textlint/resolver@15.5.2", "", {}, "sha512-YEITdjRiJaQrGLUWxWXl4TEg+d2C7+TNNjbGPHPH7V7CCnXm+S9GTjGAL7Q2WSGJyFEKt88Jvx6XdJffRv4HEA=="], - - "@textlint/types": ["@textlint/types@15.5.2", "", { "dependencies": { "@textlint/ast-node-types": "15.5.2" } }, "sha512-sJOrlVLLXp4/EZtiWKWq9y2fWyZlI8GP+24rnU5avtPWBIMm/1w97yzKrAqYF8czx2MqR391z5akhnfhj2f/AQ=="], - "@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="], - "@types/normalize-package-data": ["@types/normalize-package-data@2.4.4", "", {}, "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA=="], - - "@types/sarif": ["@types/sarif@2.1.7", "", {}, "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ=="], - "@types/vscode": ["@types/vscode@1.108.1", "", {}, "sha512-DerV0BbSzt87TbrqmZ7lRDIYaMiqvP8tmJTzW2p49ZBVtGUnGAu2RGQd1Wv4XMzEVUpaHbsemVM5nfuQJj7H6w=="], "@typescript/typescript-aix-ppc64": ["@typescript/typescript-aix-ppc64@7.0.2", "", { "os": "aix", "cpu": "ppc64" }, "sha512-MTKKkWB7p/0E9xi1d1tHtZ5PiLkGEMIq88pK2CubZjOsLtYTLqhgIgi6zepFa+9GHZ6h05NMCkQxGKiPXMxXtQ=="], @@ -194,9 +178,9 @@ "@typespec/ts-http-runtime": ["@typespec/ts-http-runtime@0.3.2", "", { "dependencies": { "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.0", "tslib": "^2.6.2" } }, "sha512-IlqQ/Gv22xUC1r/WQm4StLkYQmaaTsXAhUVsNE0+xiyf0yRFiH5++q78U3bw6bLKDCTmh0uqKB9eG9+Bt75Dkg=="], - "@vscode/vsce": ["@vscode/vsce@3.7.1", "", { "dependencies": { "@azure/identity": "^4.1.0", "@secretlint/node": "^10.1.2", "@secretlint/secretlint-formatter-sarif": "^10.1.2", "@secretlint/secretlint-rule-no-dotenv": "^10.1.2", "@secretlint/secretlint-rule-preset-recommend": "^10.1.2", "@vscode/vsce-sign": "^2.0.0", "azure-devops-node-api": "^12.5.0", "chalk": "^4.1.2", "cheerio": "^1.0.0-rc.9", "cockatiel": "^3.1.2", "commander": "^12.1.0", "form-data": "^4.0.0", "glob": "^11.0.0", "hosted-git-info": "^4.0.2", "jsonc-parser": "^3.2.0", "leven": "^3.1.0", "markdown-it": "^14.1.0", "mime": "^1.3.4", "minimatch": "^3.0.3", "parse-semver": "^1.1.1", "read": "^1.0.7", "secretlint": "^10.1.2", "semver": "^7.5.2", "tmp": "^0.2.3", "typed-rest-client": "^1.8.4", "url-join": "^4.0.1", "xml2js": "^0.5.0", "yauzl": "^2.3.1", "yazl": "^2.2.2" }, "optionalDependencies": { "keytar": "^7.7.0" }, "bin": { "vsce": "vsce" } }, "sha512-OTm2XdMt2YkpSn2Nx7z2EJtSuhRHsTPYsSK59hr3v8jRArK+2UEoju4Jumn1CmpgoBLGI6ReHLJ/czYltNUW3g=="], + "@vscode/vsce": ["@vscode/vsce@4.0.0", "", { "dependencies": { "@azure/identity": "^4.13.2", "@napi-rs/keyring": "^1.3.0", "@secretlint/core": "^10.2.2", "@secretlint/secretlint-rule-no-dotenv": "^10.2.2", "@secretlint/secretlint-rule-preset-recommend": "^10.2.2", "@secretlint/source-creator": "^10.2.2", "@secretlint/types": "^10.2.2", "@vscode/vsce-sign": "^2.1.0", "azure-devops-node-api": "^12.5.0", "cockatiel": "^3.2.1", "commander": "^12.1.0", "hosted-git-info": "^4.1.0", "jsonc-parser": "^3.3.1", "marked": "^18.0.11", "mime": "^1.6.0", "minimatch": "^10.2.6", "parse5": "^8.0.1", "proper-lockfile": "^4.1.2", "read": "^1.0.7", "semver": "^7.8.5", "tinyglobby": "^0.2.17", "typed-rest-client": "^1.8.11", "url-join": "^4.0.1", "xml2js": "^0.5.0", "yauzl": "^3.4.0", "yazl": "^2.5.1" }, "bin": { "vsce": "vsce" } }, "sha512-NImwuLaenMmb5D5Jer9/lzi/F9ZQUBOp8Azhj/BVYcTFgixv8KehFXqEUDjQlD2tAiw2E6dDGyjTuAB//di60A=="], - "@vscode/vsce-sign": ["@vscode/vsce-sign@2.0.9", "", { "optionalDependencies": { "@vscode/vsce-sign-alpine-arm64": "2.0.6", "@vscode/vsce-sign-alpine-x64": "2.0.6", "@vscode/vsce-sign-darwin-arm64": "2.0.6", "@vscode/vsce-sign-darwin-x64": "2.0.6", "@vscode/vsce-sign-linux-arm": "2.0.6", "@vscode/vsce-sign-linux-arm64": "2.0.6", "@vscode/vsce-sign-linux-x64": "2.0.6", "@vscode/vsce-sign-win32-arm64": "2.0.6", "@vscode/vsce-sign-win32-x64": "2.0.6" } }, "sha512-8IvaRvtFyzUnGGl3f5+1Cnor3LqaUWvhaUjAYO8Y39OUYlOf3cRd+dowuQYLpZcP3uwSG+mURwjEBOSq4SOJ0g=="], + "@vscode/vsce-sign": ["@vscode/vsce-sign@2.1.0", "", { "optionalDependencies": { "@vscode/vsce-sign-alpine-arm64": "2.0.6", "@vscode/vsce-sign-alpine-x64": "2.0.6", "@vscode/vsce-sign-darwin-arm64": "2.0.6", "@vscode/vsce-sign-darwin-x64": "2.0.6", "@vscode/vsce-sign-linux-arm": "2.0.6", "@vscode/vsce-sign-linux-arm64": "2.0.6", "@vscode/vsce-sign-linux-x64": "2.0.6", "@vscode/vsce-sign-win32-arm64": "2.0.6", "@vscode/vsce-sign-win32-x64": "2.0.6" } }, "sha512-9AQrqazrBgTgRSuwleLVXUrIUphY02/SFCh2TKYoLV/xifJAdblhdmEmw5gUrYSPQ3sRwNs9iyCMD14sATEE6g=="], "@vscode/vsce-sign-alpine-arm64": ["@vscode/vsce-sign-alpine-arm64@2.0.6", "", { "os": "none", "cpu": "arm64" }, "sha512-wKkJBsvKF+f0GfsUuGT0tSW0kZL87QggEiqNqK6/8hvqsXvpx8OsTEc3mnE1kejkh5r+qUyQ7PtF8jZYN0mo8Q=="], @@ -218,39 +202,15 @@ "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], - - "ansi-escapes": ["ansi-escapes@7.3.0", "", { "dependencies": { "environment": "^1.0.0" } }, "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg=="], - - "ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], - - "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "astral-regex": ["astral-regex@2.0.0", "", {}, "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ=="], - - "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], - "azure-devops-node-api": ["azure-devops-node-api@12.5.0", "", { "dependencies": { "tunnel": "0.0.6", "typed-rest-client": "^1.8.4" } }, "sha512-R5eFskGvOm3U/GzeAuxRkUsAl0hrAwGgWn6zAd2KrZmrEhWZVqLew4OOupbQlXUuojUzpGtq62SmdhJ06N88og=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], - - "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "binaryextensions": ["binaryextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-sXnYK/Ij80TO3lcqZVV2YgfKN5QjUWIRk/XSm2J/4bd/lPko3lvk0O4ZppH6m+6hB2/GTu+ptNwVFe1xh+QLQw=="], - "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], - - "boolbase": ["boolbase@1.0.0", "", {}, "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="], - "boundary": ["boundary@2.0.0", "", {}, "sha512-rJKn5ooC9u8q13IMCrW0RSp31pxBCHE3y9V/tp3TdWSLf8Em3p6Di4NBpfzbJge9YjjFEsD0RtFEjtvHL5VyEA=="], - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], - - "braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="], - - "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + "brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], "buffer-crc32": ["buffer-crc32@0.2.13", "", {}, "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ=="], @@ -262,71 +222,25 @@ "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - - "cheerio": ["cheerio@1.2.0", "", { "dependencies": { "cheerio-select": "^2.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "encoding-sniffer": "^0.2.1", "htmlparser2": "^10.1.0", "parse5": "^7.3.0", "parse5-htmlparser2-tree-adapter": "^7.1.0", "parse5-parser-stream": "^7.1.2", "undici": "^7.19.0", "whatwg-mimetype": "^4.0.0" } }, "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg=="], - - "cheerio-select": ["cheerio-select@2.1.0", "", { "dependencies": { "boolbase": "^1.0.0", "css-select": "^5.1.0", "css-what": "^6.1.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.0.1" } }, "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g=="], - - "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], - "cockatiel": ["cockatiel@3.2.1", "", {}, "sha512-gfrHV6ZPkquExvMh9IOkKsBzNDk6sDuZ6DdBGUBkvFnTCqCxzpuq48RySgP0AnaqQkw2zynOFj9yly6T1Q2G5Q=="], - "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], - - "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], - - "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], - "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], - "concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="], - - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], - - "css-select": ["css-select@5.2.2", "", { "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", "domhandler": "^5.0.2", "domutils": "^3.0.1", "nth-check": "^2.0.1" } }, "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw=="], - - "css-what": ["css-what@6.2.2", "", {}, "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA=="], - "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="], - - "deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="], - "default-browser": ["default-browser@5.5.0", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-H9LMLr5zwIbSxrmvikGuI/5KGhZ8E2zH3stkMgM5LpOWDutGM2JZaj460Udnf1a+946zc7YBgrqEWwbk7zHvGw=="], "default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="], "define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="], - "delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="], - - "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - - "dom-serializer": ["dom-serializer@2.0.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", "entities": "^4.2.0" } }, "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg=="], - - "domelementtype": ["domelementtype@2.3.0", "", {}, "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw=="], - - "domhandler": ["domhandler@5.0.3", "", { "dependencies": { "domelementtype": "^2.3.0" } }, "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w=="], - - "domutils": ["domutils@3.2.2", "", { "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", "domhandler": "^5.0.3" } }, "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw=="], - "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], "editions": ["editions@6.22.0", "", { "dependencies": { "version-range": "^4.15.0" } }, "sha512-UgGlf8IW75je7HZjNDpJdCv4cGJWIi6yumFdZ0R7A8/CIhQiWUjyGLCxdHpd8bmyD1gnkfUNK0oeOXqUS2cpfQ=="], - "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - - "encoding-sniffer": ["encoding-sniffer@0.2.1", "", { "dependencies": { "iconv-lite": "^0.6.3", "whatwg-encoding": "^3.1.1" } }, "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw=="], - - "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], - - "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], - - "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], + "entities": ["entities@8.1.0", "", {}, "sha512-kxL7msIffSuh9aaFAMD7rxAIuTRMAHMeBtgHW2yUdWw732ZNh4MehkF2gdjvtdmikkaIP9bFDDJOPlsvm7avrA=="], "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], @@ -334,31 +248,9 @@ "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], - "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], - "esbuild": ["esbuild@0.28.0", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.0", "@esbuild/android-arm": "0.28.0", "@esbuild/android-arm64": "0.28.0", "@esbuild/android-x64": "0.28.0", "@esbuild/darwin-arm64": "0.28.0", "@esbuild/darwin-x64": "0.28.0", "@esbuild/freebsd-arm64": "0.28.0", "@esbuild/freebsd-x64": "0.28.0", "@esbuild/linux-arm": "0.28.0", "@esbuild/linux-arm64": "0.28.0", "@esbuild/linux-ia32": "0.28.0", "@esbuild/linux-loong64": "0.28.0", "@esbuild/linux-mips64el": "0.28.0", "@esbuild/linux-ppc64": "0.28.0", "@esbuild/linux-riscv64": "0.28.0", "@esbuild/linux-s390x": "0.28.0", "@esbuild/linux-x64": "0.28.0", "@esbuild/netbsd-arm64": "0.28.0", "@esbuild/netbsd-x64": "0.28.0", "@esbuild/openbsd-arm64": "0.28.0", "@esbuild/openbsd-x64": "0.28.0", "@esbuild/openharmony-arm64": "0.28.0", "@esbuild/sunos-x64": "0.28.0", "@esbuild/win32-arm64": "0.28.0", "@esbuild/win32-ia32": "0.28.0", "@esbuild/win32-x64": "0.28.0" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw=="], - "expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="], - - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], - - "fast-glob": ["fast-glob@3.3.3", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.8" } }, "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg=="], - - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], - - "fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="], - - "fd-slicer": ["fd-slicer@1.1.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g=="], - - "fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="], - - "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], - - "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], - - "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], - - "fs-extra": ["fs-extra@11.3.4", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], @@ -366,92 +258,36 @@ "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="], - - "glob": ["glob@11.1.0", "", { "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw=="], - - "glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="], - - "globby": ["globby@14.1.0", "", { "dependencies": { "@sindresorhus/merge-streams": "^2.1.0", "fast-glob": "^3.3.3", "ignore": "^7.0.3", "path-type": "^6.0.0", "slash": "^5.1.0", "unicorn-magic": "^0.3.0" } }, "sha512-0Ia46fDOaT7k4og1PDW4YbodWWr3scS2vAr2lTbsplOt2WkKp0vQbkI9wKis/T5LV/dqPjO3bpS/z6GTJB82LA=="], - "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], - "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], - "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], - "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], - "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], "hosted-git-info": ["hosted-git-info@4.1.0", "", { "dependencies": { "lru-cache": "^6.0.0" } }, "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA=="], - "htmlparser2": ["htmlparser2@10.1.0", "", { "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", "domutils": "^3.2.2", "entities": "^7.0.1" } }, "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ=="], - "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - "iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="], - - "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], - - "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - - "index-to-position": ["index-to-position@1.2.0", "", {}, "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw=="], - - "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], - - "ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="], - "is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="], - "is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="], - - "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - - "is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="], - "is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="], - "is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="], - "is-wsl": ["is-wsl@3.1.0", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw=="], - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - "istextorbinary": ["istextorbinary@9.5.0", "", { "dependencies": { "binaryextensions": "^6.11.0", "editions": "^6.21.0", "textextensions": "^6.11.0" } }, "sha512-5mbUj3SiZXCuRf9fT3ibzbSSEWiy63gFfksmGfdOzujPjW3k+z8WvIBxcJHBoQNlaZaiyB25deviif2+osLmLw=="], - "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], - - "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], - - "js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="], - - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], - "jsonc-parser": ["jsonc-parser@3.3.1", "", {}, "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ=="], - "jsonfile": ["jsonfile@6.2.0", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg=="], - "jsonwebtoken": ["jsonwebtoken@9.0.3", "", { "dependencies": { "jws": "^4.0.1", "lodash.includes": "^4.3.0", "lodash.isboolean": "^3.0.3", "lodash.isinteger": "^4.0.4", "lodash.isnumber": "^3.0.3", "lodash.isplainobject": "^4.0.6", "lodash.isstring": "^4.0.1", "lodash.once": "^4.0.0", "ms": "^2.1.1", "semver": "^7.5.4" } }, "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g=="], "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], - "keytar": ["keytar@7.9.0", "", { "dependencies": { "node-addon-api": "^4.3.0", "prebuild-install": "^7.0.1" } }, "sha512-VPD8mtVtm5JNtA2AErl6Chp06JBfy7diFQ7TQQhdpWOl6MrCRB+eRbvAZUsbGQS9kiMq0coJsy0W0vHpDCkWsQ=="], - - "leven": ["leven@3.1.0", "", {}, "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A=="], - - "linkify-it": ["linkify-it@5.0.0", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ=="], - - "lodash": ["lodash@4.18.1", "", {}, "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q=="], - "lodash.includes": ["lodash.includes@4.3.0", "", {}, "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w=="], "lodash.isboolean": ["lodash.isboolean@3.0.3", "", {}, "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg=="], @@ -466,127 +302,45 @@ "lodash.once": ["lodash.once@4.1.1", "", {}, "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg=="], - "lodash.truncate": ["lodash.truncate@4.4.2", "", {}, "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw=="], - "lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], - "markdown-it": ["markdown-it@14.1.1", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.4.0", "linkify-it": "^5.0.0", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA=="], + "marked": ["marked@18.0.13", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-xTxVzZsBFwunP6HDmtBkabUQEYArnP7/rMDGmPj9SlrKlQ4i8MdYVow+nJL0eOqwpUqhzBoTBRADGN6uYwPyOw=="], "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], - "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], - - "merge2": ["merge2@1.4.1", "", {}, "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg=="], - - "micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA=="], - "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], - "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], - - "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], - - "mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="], - - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], - - "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - - "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], - - "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], "mute-stream": ["mute-stream@0.0.8", "", {}, "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA=="], - "napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="], - - "node-abi": ["node-abi@3.87.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ=="], - - "node-addon-api": ["node-addon-api@4.3.0", "", {}, "sha512-73sE9+3UaLYYFmDsFZnqCInzPyh3MqIwZO9cw58yIqAZhONrrabrYyYe3TuIqtIiOuTXVhsGau8hcrhhwSsDIQ=="], - - "node-sarif-builder": ["node-sarif-builder@3.4.0", "", { "dependencies": { "@types/sarif": "^2.1.7", "fs-extra": "^11.1.1" } }, "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg=="], - - "normalize-package-data": ["normalize-package-data@6.0.2", "", { "dependencies": { "hosted-git-info": "^7.0.0", "semver": "^7.3.5", "validate-npm-package-license": "^3.0.4" } }, "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g=="], - - "nth-check": ["nth-check@2.1.1", "", { "dependencies": { "boolbase": "^1.0.0" } }, "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w=="], - "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], - "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], - "open": ["open@10.2.0", "", { "dependencies": { "default-browser": "^5.2.1", "define-lazy-prop": "^3.0.0", "is-inside-container": "^1.0.0", "wsl-utils": "^0.1.0" } }, "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA=="], - "p-map": ["p-map@7.0.4", "", {}, "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ=="], - - "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], - - "parse-json": ["parse-json@8.3.0", "", { "dependencies": { "@babel/code-frame": "^7.26.2", "index-to-position": "^1.1.0", "type-fest": "^4.39.1" } }, "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ=="], - - "parse-semver": ["parse-semver@1.1.1", "", { "dependencies": { "semver": "^5.1.0" } }, "sha512-Eg1OuNntBMH0ojvEKSrvDSnwLmvVuUOSdylH/pSCPNMIspLlweJyIWXCE+k/5hm3cj/EBUYwmWkjhBALNP4LXQ=="], - - "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], - - "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@7.1.0", "", { "dependencies": { "domhandler": "^5.0.3", "parse5": "^7.0.0" } }, "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g=="], - - "parse5-parser-stream": ["parse5-parser-stream@7.1.2", "", { "dependencies": { "parse5": "^7.0.0" } }, "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow=="], - - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], - - "path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="], - - "path-type": ["path-type@6.0.0", "", {}, "sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ=="], + "parse5": ["parse5@8.0.1", "", { "dependencies": { "entities": "^8.0.0" } }, "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw=="], "pend": ["pend@1.2.0", "", {}, "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg=="], - "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], - - "picomatch": ["picomatch@2.3.2", "", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="], - - "pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="], - - "prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="], - - "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + "picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="], - "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], + "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], "qs": ["qs@6.14.1", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ=="], - "queue-microtask": ["queue-microtask@1.2.3", "", {}, "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A=="], - - "rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="], - - "rc-config-loader": ["rc-config-loader@4.1.4", "", { "dependencies": { "debug": "^4.4.3", "js-yaml": "^4.1.1", "json5": "^2.2.3", "require-from-string": "^2.0.2" } }, "sha512-3GiwEzklkbXTDp52UR5nT8iXgYAx1V9ZG/kDZT7p60u2GCv2XTwQq4NzinMoMpNtXhmt3WkhYXcj6HH8HdwCEQ=="], - "read": ["read@1.0.7", "", { "dependencies": { "mute-stream": "~0.0.4" } }, "sha512-rSOKNYUmaxy0om1BNjMN4ezNT6VKK+2xF4GBhc81mkH7L60i6dp8qPYrkndNLT3QPphoII3maL9PVC9XmhHwVQ=="], - "read-pkg": ["read-pkg@9.0.1", "", { "dependencies": { "@types/normalize-package-data": "^2.4.3", "normalize-package-data": "^6.0.0", "parse-json": "^8.0.0", "type-fest": "^4.6.0", "unicorn-magic": "^0.1.0" } }, "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA=="], - - "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], - - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], - - "reusify": ["reusify@1.1.0", "", {}, "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw=="], + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], "run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="], - "run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA=="], - "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], - "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], - "sax": ["sax@1.4.4", "", {}, "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw=="], - "secretlint": ["secretlint@10.2.2", "", { "dependencies": { "@secretlint/config-creator": "^10.2.2", "@secretlint/formatter": "^10.2.2", "@secretlint/node": "^10.2.2", "@secretlint/profiler": "^10.2.2", "debug": "^4.4.1", "globby": "^14.1.0", "read-pkg": "^9.0.1" }, "bin": "./bin/secretlint.js" }, "sha512-xVpkeHV/aoWe4vP4TansF622nBEImzCY73y/0042DuJ29iKIaqgoJ8fGxre3rVSHHbxar4FdJobmTnLp9AU0eg=="], - - "semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], - - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], - - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], @@ -596,86 +350,28 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], - - "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], - - "simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="], - - "slash": ["slash@5.1.0", "", {}, "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg=="], - - "slice-ansi": ["slice-ansi@4.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "astral-regex": "^2.0.0", "is-fullwidth-code-point": "^3.0.0" } }, "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ=="], - - "spdx-correct": ["spdx-correct@3.2.0", "", { "dependencies": { "spdx-expression-parse": "^3.0.0", "spdx-license-ids": "^3.0.0" } }, "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA=="], - - "spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="], - - "spdx-expression-parse": ["spdx-expression-parse@3.0.1", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q=="], - - "spdx-license-ids": ["spdx-license-ids@3.0.23", "", {}, "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw=="], - - "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], - - "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], - - "strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], - - "strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="], + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], "structured-source": ["structured-source@4.0.0", "", { "dependencies": { "boundary": "^2.0.0" } }, "sha512-qGzRFNJDjFieQkl/sVOI2dUjHKRyL9dAJi2gCPGJLbJHBIkyOHxjuocpIEfbLioX+qSJpvbYdT49/YCdMznKxA=="], - "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], - - "supports-hyperlinks": ["supports-hyperlinks@3.2.0", "", { "dependencies": { "has-flag": "^4.0.0", "supports-color": "^7.0.0" } }, "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig=="], - - "table": ["table@6.9.0", "", { "dependencies": { "ajv": "^8.0.1", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", "string-width": "^4.2.3", "strip-ansi": "^6.0.1" } }, "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A=="], - - "tar-fs": ["tar-fs@2.1.4", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ=="], - - "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], - - "terminal-link": ["terminal-link@4.0.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "supports-hyperlinks": "^3.2.0" } }, "sha512-lk+vH+MccxNqgVqSnkMVKx4VLJfnLjDBGzH16JVZjKE2DoxP57s6/vt6JmXV5I3jBcfGrxNrYtC+mPtU7WJztA=="], - - "text-table": ["text-table@0.2.0", "", {}, "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw=="], - "textextensions": ["textextensions@6.11.0", "", { "dependencies": { "editions": "^6.21.0" } }, "sha512-tXJwSr9355kFJI3lbCkPpUH5cP8/M0GGy2xLO34aZCjMXBaK3SoPnZwr/oWmo1FdCnELcs4npdCIOFtq9W3ruQ=="], - "tmp": ["tmp@0.2.5", "", {}, "sha512-voyz6MApa1rQGUxT3E+BK7/ROe8itEx7vD8/HEvt4xwXucvQ5G5oeEiHkmHZJuBO21RpOf+YYm9MOivj709jow=="], - - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], + "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "tunnel": ["tunnel@0.0.6", "", {}, "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg=="], - "tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="], - - "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], - "typed-rest-client": ["typed-rest-client@1.8.11", "", { "dependencies": { "qs": "^6.9.1", "tunnel": "0.0.6", "underscore": "^1.12.1" } }, "sha512-5UvfMpd1oelmUPRbbaVnq+rHP7ng2cE4qoQkQeAqxRL6PklkxsM0g32/HL0yfvruK6ojQ5x8EE+HF4YV6DtuCA=="], "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], - "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], - "underscore": ["underscore@1.13.7", "", {}, "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g=="], - "undici": ["undici@7.20.0", "", {}, "sha512-MJZrkjyd7DeC+uPZh+5/YaMDxFiiEEaDgbUSVMXayofAkDWF1088CDo+2RPg7B1BuS1qf1vgNE7xqwPxE0DuSQ=="], - "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], - "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], - - "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], - "url-join": ["url-join@4.0.1", "", {}, "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA=="], - "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], - - "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], - - "validate-npm-package-license": ["validate-npm-package-license@3.0.4", "", { "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew=="], - "version-range": ["version-range@4.15.0", "", {}, "sha512-Ck0EJbAGxHwprkzFO966t4/5QkRuzh+/I1RxhLgUKKwEn+Cd8NwM60mE3AqBZg5gYODoXW0EFsQvbZjRlvdqbg=="], "vscode-jsonrpc": ["vscode-jsonrpc@8.2.0", "", {}, "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA=="], @@ -688,14 +384,6 @@ "vscode-languageserver-types": ["vscode-languageserver-types@3.17.5", "", {}, "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg=="], - "whatwg-encoding": ["whatwg-encoding@3.1.1", "", { "dependencies": { "iconv-lite": "0.6.3" } }, "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ=="], - - "whatwg-mimetype": ["whatwg-mimetype@4.0.0", "", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="], - - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], - - "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], - "wsl-utils": ["wsl-utils@0.1.0", "", { "dependencies": { "is-wsl": "^3.1.0" } }, "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw=="], "xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], @@ -704,58 +392,20 @@ "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], - "yauzl": ["yauzl@2.10.0", "", { "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g=="], + "yauzl": ["yauzl@3.4.0", "", { "dependencies": { "pend": "~1.2.0" } }, "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw=="], "yazl": ["yazl@2.5.1", "", { "dependencies": { "buffer-crc32": "~0.2.3" } }, "sha512-phENi2PLiHnHb6QBVot+dJnaAZ0xosj7p3fWl+znIjBDlnMI2PsZCJZ306BPTFOaHf5qdDEI8x5qFrSOBN5vrw=="], - "@secretlint/formatter/chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], - - "@textlint/linter-formatter/pluralize": ["pluralize@2.0.0", "", {}, "sha512-TqNZzQCD4S42De9IfnnBvILN7HAW7riLqsCyp8lgjXeysyPlX5HhqKAcJHHHb9XskE4/a+7VGC9zzx8Ls0jOAw=="], - - "@textlint/linter-formatter/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "glob/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - - "htmlparser2/entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], - - "normalize-package-data/hosted-git-info": ["hosted-git-info@7.0.2", "", { "dependencies": { "lru-cache": "^10.0.1" } }, "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w=="], - - "parse-semver/semver": ["semver@5.7.2", "", { "bin": { "semver": "bin/semver" } }, "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g=="], - - "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], - - "path-scurry/lru-cache": ["lru-cache@11.3.0", "", {}, "sha512-sr8xPKE25m6vJVcrdn6NxtC0fVfuPowbscLypegRgOm0yXSqr5JNHCAY3hnusdJ7HRBW04j6Ip4khvHU778DuQ=="], - - "read-pkg/unicorn-magic": ["unicorn-magic@0.1.0", "", {}, "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ=="], - - "string-width/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], - - "table/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "jsonwebtoken/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], "vscode-languageclient/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], - "vscode-languageclient/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="], - "vscode-languageclient/vscode-languageserver-protocol": ["vscode-languageserver-protocol@3.18.0", "", { "dependencies": { "vscode-jsonrpc": "9.0.0", "vscode-languageserver-types": "3.18.0" } }, "sha512-Zdz+kJ12Iz6tc11xfZyEo501bBATHXrCjmMfnaR3pMnf1CoqZBKIynba3P+/bi9VEdrMbNtAVKYpKhbODvqy+Q=="], - "@textlint/linter-formatter/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "glob/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], - - "normalize-package-data/hosted-git-info/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - - "string-width/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - - "table/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "vscode-languageclient/minimatch/brace-expansion": ["brace-expansion@5.0.5", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="], "vscode-languageclient/vscode-languageserver-protocol/vscode-jsonrpc": ["vscode-jsonrpc@9.0.0", "", {}, "sha512-+VvMmQPJhtvJ+8O+zu2JKIRiLxXF8NW7krWgyMGeOHrp4Cn23T5hc0v2LknNeopDOB70wghHAds7mKtcZ0I4Sg=="], "vscode-languageclient/vscode-languageserver-protocol/vscode-languageserver-types": ["vscode-languageserver-types@3.18.0", "", {}, "sha512-8TsGPNMIMiiBdkORgRSvLjuiEIiAFtO+KssmYWxQ+uSVvlf7RjK8YKCOjPzZ+YA04jXEV7+7LvkSmHkhpNS99g=="], - - "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - - "vscode-languageclient/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], } }