Skip to content

Commit 5f7f526

Browse files
authored
Merge pull request #952 from ako/main
Sync ako/mxcli: rules as a document type, workflow data-loss fixes, and the test-annotation framework
2 parents b763353 + 9972063 commit 5f7f526

164 files changed

Lines changed: 10107 additions & 419 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.claude/skills/fix-issue.md‎

Lines changed: 15 additions & 0 deletions
Large diffs are not rendered by default.

‎.claude/skills/mendix/README.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ Detailed syntax for each MDL document type:
2020
| [mdl-entities.md](mdl-entities.md) | Entity, attribute, association syntax | Creating domain models |
2121
| [write-microflows.md](write-microflows.md) | Microflow syntax reference | Writing microflow logic |
2222
| [write-nanoflows.md](write-nanoflows.md) | Nanoflow syntax reference | Writing client-side nanoflow logic |
23+
| [write-rules.md](write-rules.md) | Rule syntax reference | Writing reusable decision logic a decision calls |
2324
| [write-oql-queries.md](write-oql-queries.md) | OQL query syntax | Creating VIEW entities |
2425
| [create-page.md](create-page.md) | Page and widget syntax | Creating pages |
2526
| [fragments.md](fragments.md) | Fragment (reusable widget group) syntax | Reusing widget patterns across pages |
@@ -46,6 +47,7 @@ External system integration:
4647
| [demo-data.md](demo-data.md) | Demo data & IMPORT | Seeding data, `import from` bulk import from external DB |
4748
| [rest-client.md](rest-client.md) | REST API consumption | Calling external REST APIs via consumed REST client documents |
4849
| [rest-call-from-json.md](rest-call-from-json.md) | REST CALL end-to-end | JSON Structure → Entities → Import Mapping → REST CALL microflow |
50+
| [mock-rest-apis.md](mock-rest-apis.md) | Mock a REST dependency | Building or debugging a REST integration without the live API; forcing 404/500; running offline or in CI |
4951
| [json-structures-and-mappings.md](json-structures-and-mappings.md) | JSON structures & mappings | CREATE/DESCRIBE JSON structures, import/export mappings, domain model patterns |
5052
| [java-actions.md](java-actions.md) | Custom Java actions | Extending with Java code |
5153
| [download-marketplace-content.md](download-marketplace-content.md) | Marketplace download & install | Adding a marketplace module/widget; downloading a `.mpk`; module-update caveat |

‎.claude/skills/mendix/create-page.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -319,6 +319,13 @@ describe icon collection Atlas_Core.Atlas_Filled -- every icon + its reference
319319
- `action: show_page Module.PageName(Param: $value)` - Navigate with parameters
320320
- `action: show_page Module.PageName($Param = $value)` - Also accepted (microflow-style)
321321
- `action: create_object Module.Entity then show_page Module.PageName` - Create and navigate
322+
- **A `show_page` argument must be the context object.** Mendix takes the page
323+
argument from the enclosing data widget, so the only spellings that mean
324+
anything are `$currentObject` or the name of the variable that widget is bound
325+
to (`datasource: $Customer` → `(Customer: $Customer)` is fine). Naming any other
326+
variable is refused as **MDL-PAGEARG01** — it used to be accepted and silently
327+
opened the page with the context object anyway. To open a page with something
328+
else, call a microflow that shows it.
322329

323330
**Button Styles:** `default`, `primary`, `success`, `info`, `warning`, `danger`, `inverse`
324331
- Case-insensitive (`primary` and `Primary` both work).

‎.claude/skills/mendix/manage-security.md‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,43 @@ alter project security demo users on;
262262
alter project security demo users off;
263263
```
264264

265+
### Guest (Anonymous) Access
266+
267+
Anonymous access is what makes part of an app public — a product catalogue anyone
268+
can browse without signing in. It is one flag plus a user role, and the role is
269+
the important half: **whatever that role can read is the app's public surface.**
270+
271+
```sql
272+
-- The role anonymous visitors are given. System.User is what lets an
273+
-- unauthenticated session exist at all.
274+
create user role Anonymous (Shop.Viewer, System.User);
275+
276+
alter project security guest access on role Anonymous;
277+
278+
-- Now grant exactly what should be public — and nothing else.
279+
grant Anonymous on Shop.Product (read *);
280+
281+
-- Re-enabling later does not need the role retyped; the stored one is used.
282+
alter project security guest access off;
283+
alter project security guest access on;
284+
```
285+
286+
Three things worth knowing:
287+
288+
- **The role is mandatory.** Mendix fails the build with **CE0133** ("No user role
289+
for anonymous users selected even though the feature anonymous users is
290+
enabled") when access is on with no role. `guest access on` is refused unless a
291+
role is given or one is already stored.
292+
- **Mendix does not check the role exists**, so mxcli does. A misspelled role
293+
would otherwise build with zero errors and leave anonymous visitors with no
294+
access at all — a broken public site that passes every check.
295+
- **`off` keeps the stored role**, so toggling access while testing does not lose
296+
it. Guest access off with a role set is valid Mendix.
297+
298+
Review anonymous entity access the way lint rule **SEC004** asks you to: any
299+
unconstrained `read *` granted to the anonymous role is readable by the whole
300+
internet (DIVD-2022-00019). Add an XPath constraint or do not grant it.
301+
265302
### Demo Users
266303

267304
```sql
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
# Mock REST APIs Skill
2+
3+
Use this skill when a REST integration needs an endpoint you control instead of a
4+
live third-party API — while building it, while reproducing a bug, or while
5+
verifying the app in a browser or a test run.
6+
7+
Developing against the real API means network, rate limits, credentials, and a
8+
payload that can change under you. None of that is where Mendix integration
9+
defects live: those are in the mapping, the entity types, the error handler, and
10+
the BSON. A mock removes the variables that are not the bug.
11+
12+
## When to Use This Skill
13+
14+
- Building a REST client or `REST CALL` microflow before (or without) real credentials
15+
- Reproducing a payload-shaped bug **deterministically** — a shape small enough to read, that behaves the same on every run
16+
- Exercising error paths: 404, 500, a timeout, a 401 from missing auth
17+
- Verifying the app (`test-app.md`) or running a suite (`test-microflows.md`) offline or in CI
18+
- Redirecting the outbound calls of an app whose model you must not edit
19+
20+
## Two separate problems
21+
22+
Almost every wasted hour here comes from conflating them:
23+
24+
| Problem | Answer |
25+
|---|---|
26+
| **Something must answer the request** | A mock server: Prism (from a contract), WireMock, mitmproxy |
27+
| **The app must send the request there** | A constant, a `BaseUrl`, or a forward proxy — see below |
28+
29+
A mock server is **not** an interceptor. Prism serves one contract at one port
30+
and answers only clients that address it. Asking it to "catch all calls the app
31+
already makes" is a category error — that is the forward-proxy job, further down.
32+
33+
## 1. Point the app at the mock
34+
35+
Three routes, cheapest first. Pick by how the URL is built.
36+
37+
### The URL is built in the microflow — use a constant, change nothing per run
38+
39+
A `REST CALL` URL is an **expression**, so it can be assembled from a constant
40+
(`@Module.Constant` is Mendix's constant reference — `$Name` is a *variable*):
41+
42+
```sql
43+
create constant MyModule.ApiBaseUrl type String default 'https://api.example.com/v1';
44+
45+
create microflow MyModule.CallApi() returns string
46+
begin
47+
$response = rest call get @MyModule.ApiBaseUrl + '/rates'
48+
header Accept = 'application/json'
49+
returns string;
50+
return $response;
51+
end;
52+
```
53+
54+
Then swap the endpoint per run, with no model change and nothing committed:
55+
56+
```bash
57+
# this run only — never written to the project
58+
mxcli run --local -p app.mpr --constant MyModule.ApiBaseUrl=http://127.0.0.1:4020
59+
60+
# a test suite against the mock (--constant needs --local)
61+
mxcli test tests/ -p app.mpr --local --constant MyModule.ApiBaseUrl=http://127.0.0.1:4020
62+
63+
# machine-local default, gitignored: every run picks it up
64+
mxcli constant set MyModule.ApiBaseUrl http://127.0.0.1:4020 -p app.mpr
65+
66+
# flip it on an app that is already running
67+
mxcli constant set MyModule.ApiBaseUrl http://127.0.0.1:4020 -p app.mpr --apply
68+
```
69+
70+
`constant set` refuses a name the project does not define, so a typo cannot
71+
silently apply to nothing. `mxcli constant list -p app.mpr` shows the winning
72+
value for every constant **and which layer set it** — read it first whenever a
73+
run does not use the endpoint you expected.
74+
75+
### The call goes through a REST client document — rewrite `BaseUrl`
76+
77+
A REST client document's `BaseUrl` is a **literal**; it cannot reference a
78+
constant. Point it at the mock by re-running the create, which is a one-line diff:
79+
80+
```sql
81+
create or modify rest client MyModule.RatesAPI (
82+
OpenAPI: 'specs/rates.json',
83+
BaseUrl: 'http://127.0.0.1:4020'
84+
);
85+
```
86+
87+
`BaseUrl` also overrides `servers[0].url` at import time, so one contract can be
88+
imported against the mock and later re-pointed at production.
89+
90+
### You cannot edit the model at all — use a forward proxy
91+
92+
See §3. This is the most work and the last resort.
93+
94+
## 2. Prism: serve an OpenAPI contract as a mock
95+
96+
```bash
97+
npm install -g @stoplight/prism-cli # ~15s
98+
prism mock specs/rates.json --port 4020 # serves the contract's `example` values
99+
```
100+
101+
Everything below cost real time to find out and is not on Prism's front page:
102+
103+
- **Prism mounts paths at the root** and ignores any base path in
104+
`servers[0].url`. Address it as `http://127.0.0.1:4020`, not
105+
`http://127.0.0.1:4020/v1` — otherwise every path 404s while the server looks
106+
perfectly healthy.
107+
- **Make `servers[0].url` absolute in the contract you import.** mxcli's OpenAPI
108+
import only accepts an `http://` or `https://` URL as `BaseUrl`; a relative one
109+
(`/api/v3`) is skipped with the warning *"server URL … is relative and cannot
110+
be used as BaseUrl; set BaseUrl explicitly in CREATE REST CLIENT"*, and a
111+
client with no `BaseUrl` fails at call time, not at import time.
112+
- **`Prefer: code=404`** on the request forces any status the contract documents.
113+
This is the only practical way to drive a Mendix error handler through a real
114+
HTTP response rather than by hand-editing the model.
115+
- **`prism mock -d`** returns schema-generated random data instead of the
116+
`example` values. Run the suite both ways: a mapping that quietly depends on
117+
one fixed payload passes under `example` and fails under `-d`.
118+
- **Prism enforces the contract's `security`**, so a call with no `Authorization`
119+
header gets a real 401. Useful — but know the ceiling before you design around
120+
it: a REST client document's header value may be a literal, a `$Variable`, or a
121+
literal **prefix** plus a variable (`'Bearer ' + $Token`), and nothing else. A
122+
token that must be computed per call belongs in a `REST CALL` expression, not
123+
in the document.
124+
- **Cut a subset; never point Prism at a vendor's full contract.** The official
125+
Microsoft Graph spec is 41 MB of YAML: it downloads in seconds and Prism was
126+
still printing "Starting Prism…" when killed at a 300-second cap. Importing it
127+
would also generate thousands of operations into the module.
128+
129+
A contract small enough to read is the point. Hand-cut one path with one
130+
`example` per response code you care about, and keep it in the project under
131+
`specs/` next to the `.mpr` — the same relative path the `OpenAPI:` clause takes.
132+
133+
## 3. Forward proxy: when the model cannot change
134+
135+
For an app whose model you must not touch, redirect the JVM instead. The Mendix
136+
runtime honours the standard Java proxy properties, and `mxcli run --local`
137+
passes your environment through to the runtime JVM — including `JAVA_TOOL_OPTIONS`,
138+
which mxcli **appends** to rather than replaces, so an exported value survives
139+
even alongside `--trace`. No model change, nothing committed:
140+
141+
```bash
142+
export JAVA_TOOL_OPTIONS="-Dhttp.proxyHost=127.0.0.1 -Dhttp.proxyPort=8080"
143+
mxcli run --local -p app.mpr
144+
```
145+
146+
Two things to know before committing to this route:
147+
148+
- **HTTPS is the real work.** The proxy must present a certificate the JVM
149+
trusts. WireMock 3.13.2 on Java 21 cannot generate a usable one; use mitmproxy
150+
(which ships a CA you install into the JVM truststore) or supply your own
151+
keystore. Plain `http://` targets need none of this — one more reason to have
152+
the mock on loopback HTTP.
153+
- **Loopback is not proxied.** `127.0.0.1` sits in the runtime's
154+
`http.nonProxyHosts`, so app→mock traffic on loopback works *inside* a proxied
155+
container without any exemption of your own. It also means a proxy on
156+
`127.0.0.1` will not intercept loopback calls — that is not a bug to hunt.
157+
158+
For **consumed OData** services specifically there is a fourth route that needs
159+
no JVM flags: `System.ConsumedODataConfiguration` carries `ProxyConfiguration`,
160+
`ProxyHost` and `ProxyPort` as data (see `system-module.md`), so the proxy can be
161+
set per service at runtime.
162+
163+
## Verify the mock before blaming Mendix
164+
165+
Always prove the endpoint from the shell first. A Mendix error message cannot
166+
distinguish "the mock is not running" from "the mapping is wrong".
167+
168+
```bash
169+
curl -sS -i http://127.0.0.1:4020/rates # 200 + the example payload?
170+
curl -sS -i -H 'Prefer: code=404' http://127.0.0.1:4020/rates
171+
```
172+
173+
Then, and only then, run the microflow and check the payload actually reached it
174+
(`mxcli oql`, or the runtime log under `mxcli run --local`).
175+
176+
## Failure modes, symptoms first
177+
178+
| Symptom | Cause | Fix |
179+
|---|---|---|
180+
| Every path 404s, server looks fine | The client address includes the contract's base path | Address Prism at the root: `http://127.0.0.1:4020` |
181+
| Import produced a client with no `BaseUrl` | `servers[0].url` is relative — mxcli warned and skipped it | Make it absolute, or pass `BaseUrl:` explicitly |
182+
| Prism never finishes starting | Vendor contract is tens of MB | Cut the paths you need into a small contract |
183+
| Mock returns 401 | The contract declares `security`; the call sent no credentials | Add the header, or drop `security` from your cut contract |
184+
| Calls still reach the real API | Proxy properties not applied, or the target is loopback (never proxied) | Check the JVM args; prefer the constant route over a proxy |
185+
| Endpoint swapped but the app disagrees | An override on a different layer wins | `mxcli constant list -p app.mpr` — it names the layer |
186+
| Works with `example` values, fails in CI | The mapping depends on one fixed payload | Run `prism mock -d` locally and fix the mapping |
187+
188+
## Related Skills
189+
190+
- `rest-client.md` — the three ways to call a REST API; where the contract goes once you have one
191+
- `rest-call-from-json.md` — JSON structure → entities → import mapping → `REST CALL`
192+
- `test-app.md` — browser verification; a REST app's prerequisite is a reachable endpoint
193+
- `test-microflows.md` — running a suite; `--constant` points it at the mock
194+
- `run-local.md` — `mxcli run --local`, the warm loop the mock plugs into

‎.claude/skills/mendix/overview-pages.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -534,6 +534,13 @@ navigationlist widgetName {
534534
- `action: microflow Module.MicroflowName(Param: $value)` - Call microflow with parameters
535535
- `action: show_page Module.PageName` - Navigate to page
536536
- `action: show_page Module.PageName(Param: $value)` - Navigate with parameters
537+
- **A `show_page` argument must be the context object.** Mendix takes the page
538+
argument from the enclosing data widget, so the only spellings that mean
539+
anything are `$currentObject` or the name of the variable that widget is bound
540+
to (`datasource: $Customer` → `(Customer: $Customer)` is fine). Naming any other
541+
variable is refused as **MDL-PAGEARG01** — it used to be accepted and silently
542+
opened the page with the context object anyway. To open a page with something
543+
else, call a microflow that shows it.
537544

538545
## Handling Circular Dependencies
539546

‎.claude/skills/mendix/rest-client.md‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ Mendix offers three ways to call REST APIs from microflows. Choose based on the
1414

1515
Both REST Client approaches can be combined with **Data Transformers** (Mendix 11.9+) and **Import/Export Mappings** to map between JSON and entities.
1616

17+
No API to call against yet — or one you would rather not depend on while building? [mock-rest-apis.md](mock-rest-apis.md) covers standing up an endpoint you control and pointing the app at it.
18+
1719
---
1820

1921
## Approach 0: OpenAPI Import (Fastest)
@@ -44,7 +46,14 @@ This generates:
4446
- Basic auth if the spec declares it at the top level
4547
- The spec stored inside the document for Studio Pro parity
4648

47-
`BaseUrl` is optional. When omitted, `servers[0].url` from the spec is used. When provided, it overrides that value — useful when the spec points at production but you need to import against staging or a different version.
49+
`BaseUrl` is optional. When omitted, `servers[0].url` from the spec is used — but only if it is
50+
**absolute**. A relative server URL (`/api/v3`) cannot be a `BaseUrl`: the import warns
51+
(*"server URL … is relative and cannot be used as BaseUrl"*) and leaves the client without one,
52+
which fails at call time rather than at import time. Set `BaseUrl` explicitly in that case.
53+
54+
When provided, `BaseUrl` overrides the spec's value — useful when the spec points at production
55+
but you need to import against staging, a different version, or a local mock
56+
(see [mock-rest-apis.md](mock-rest-apis.md)).
4857

4958
**Preview without writing:**
5059
```sql

‎.claude/skills/mendix/test-app.md‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ The devcontainer created by `mxcli init` installs:
2121
- **Chromium (headless shell)** — installed via `@playwright/cli`'s **bundled** `playwright-core`, into a shared `PLAYWRIGHT_BROWSERS_PATH`, and exposed at the stable path `/usr/local/bin/mx-headless-shell`. The generated `.playwright/cli.config.json` pins `executablePath` to that symlink.
2222
- **Docker-in-Docker** — Mendix + PostgreSQL running via `mxcli docker run`
2323

24+
If the app calls an external REST API, that endpoint is a prerequisite too — a verification run that depends on a live third party is not repeatable. See [mock-rest-apis.md](mock-rest-apis.md).
25+
2426
The app must be running before verification:
2527

2628
```bash

0 commit comments

Comments
 (0)