Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
66062ab
feat(init): seed a lint config that excludes the System module
claude Aug 6, 2026
4949de9
fix(oql): take the column set from all rows, not just the first
claude Aug 6, 2026
e28c0b4
feat(check): reject a loop variable used outside its loop (MDL053)
claude Aug 6, 2026
b472da2
docs: correct the attribution of the loop-scope and OQL fixes
claude Aug 6, 2026
c963902
feat(test): run microflow tests without Docker (mxcli test --local)
claude Aug 6, 2026
2deb20d
Merge pull request #105 from ako/claude/mxbuild-diagnostics-spike-emta6h
ako Aug 6, 2026
6c74296
docs(bootstrap-prompt): interview the user before provisioning
claude Aug 7, 2026
d85e8f1
docs(bootstrap-prompt): cover multi-app solutions; default to 11.13.0
claude Aug 7, 2026
0bb93b8
docs(bootstrap-prompt): give each app in a solution its own hostname
claude Aug 7, 2026
27f6426
feat(run): honour the app's configured Application root URL
claude Aug 7, 2026
3db05dd
fix(pages): reference an inherited attribute against its declaring en…
claude Aug 7, 2026
a87de0a
fix(init): make the SessionStart hook survive an idle reap
claude Aug 7, 2026
ae85635
feat(exec,check): accept "-" to read MDL from stdin
claude Aug 7, 2026
8515fa9
docs(syntax): correct two spellings the parser rejects, and pin them
claude Aug 7, 2026
9236202
Merge pull request #106 from ako/claude/mxbuild-diagnostics-spike-emta6h
ako Aug 7, 2026
2952c2a
docs: spike a custom request handler as a re-invokable test entry point
claude Aug 7, 2026
54b2939
fix(microflows): lowercase word operators in preserved expression source
claude Aug 7, 2026
f3ce554
feat(entities): IF NOT EXISTS / IF EXISTS on ADD|DROP EVENT HANDLER
claude Aug 7, 2026
112baae
feat(security): warn when demo users cannot materialise
claude Aug 7, 2026
98c7f2d
feat(entities): refuse a parameterized microflow on BEFORE CREATE
claude Aug 7, 2026
10db720
fix(theme): theme the login page, and give link text its own token
claude Aug 7, 2026
0b9b621
feat(run): say why a sign-in was refused instead of "Sign in failed"
claude Aug 7, 2026
aa0f503
feat(new): run the first build so a fresh clone does not go dirty
claude Aug 7, 2026
f6940c0
feat(check): catch a forward page reference without a project
claude Aug 7, 2026
06c137a
docs(theme): explain the clipped nav label, and why not to "fix" it i…
claude Aug 7, 2026
eb66c54
feat(test): run local tests over a token-guarded endpoint
claude Aug 7, 2026
2828e34
fix(alter-page): write conditional settings Attribute as "" not null
claude Aug 7, 2026
0325361
fix(pages): allow keyword-named functions in widget conditional expre…
claude Aug 7, 2026
37784b5
fix(microflows): write DownloadFileAction on the modelsdk engine
claude Aug 7, 2026
8b81f56
fix(run): resolve a relative project path where MxBuild is called
claude Aug 7, 2026
f3b2530
Merge pull request #107 from ako/claude/mxbuild-diagnostics-spike-emta6h
ako Aug 7, 2026
537ab1b
feat(test): add --watch to keep the runtime warm across runs
claude Aug 7, 2026
64fa663
Merge branch 'main' into claude/mxcli-issues-ovfoxk
ako Aug 7, 2026
b254d1b
Merge pull request #108 from ako/claude/mxcli-issues-ovfoxk
ako Aug 7, 2026
a3e5b24
feat(test): add --attach to run against an already-running app
claude Aug 7, 2026
78de078
Merge branch 'main' into claude/mxcli-unit-test-perf-n7ggx8
claude Aug 7, 2026
1bdd46a
Merge pull request #109 from ako/claude/mxcli-unit-test-perf-n7ggx8
ako Aug 7, 2026
d9cadfa
fix(odata): set the two defaults a published service needs to build
claude Aug 7, 2026
6b5db79
feat(check): report OData property names that are silently discarded
claude Aug 7, 2026
fa0cdb6
feat(odata): let a published entity turn off Countable/Skip/Top
claude Aug 7, 2026
04aadde
fix(odata): make DESCRIBE of a published service re-executable
claude Aug 7, 2026
305a9fa
docs(odata): document ReadMode microflow, ServiceName and the query o…
claude Aug 7, 2026
1fb51d7
fix(check): warn on a database-connection type Studio Pro does not offer
claude Aug 7, 2026
83e85c2
fix(init): stop inventing a project when the directory has none
claude Aug 7, 2026
f0d9e38
feat(settings): read one configuration, and show its root URL
claude Aug 7, 2026
68d235f
build(grammar): pin the ANTLR version where the failure happens
claude Aug 7, 2026
8e7bfcc
docs(new): say that step 6 links the local binary on Linux
claude Aug 7, 2026
4646a40
Merge pull request #110 from ako/claude/mxbuild-diagnostics-spike-emta6h
ako Aug 7, 2026
38484ea
feat(java-deps): resolve declared JAR dependencies instead of silentl…
claude Aug 7, 2026
e0744b9
feat(mdl): make SET optional, so `$Total = 5;` parses
claude Aug 7, 2026
d70c3e5
fix(test): let a test path be relative to the project as well as the CWD
claude Aug 7, 2026
a948566
docs(database): two dynamic-query rules that cost an afternoon each
claude Aug 7, 2026
45ae6a6
Merge pull request #111 from ako/claude/mxbuild-diagnostics-spike-emta6h
ako Aug 7, 2026
8488c1e
fix(microflows): keep aggregates whole now that SET is optional
claude Aug 7, 2026
cbe06d5
Merge pull request #112 from ako/claude/mxbuild-diagnostics-spike-emta6h
ako Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .claude/skills/fix-issue.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions .claude/skills/mendix/cheatsheet-errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,19 @@ set $count = 1;
1. Check microflow exists: `show microflows in ModuleName`
2. Use fully qualified name: `Module.MicroflowName`

### "page not found" for a page the script creates further down (MDL-PAGE01)

**Problem**: A widget action targets a page created by a LATER statement in the
same script. Page references resolve in statement order, and `exec` is not
transactional — the statements before the failure are already written.

**Fix**:
1. Move the `create page` for the target above the page that links to it.
2. If two pages link to each other, no ordering works: create one without the
linking widget, then add it with `alter page ... insert`.
3. Commit before executing a large script — recovery from a partial run is
`git checkout -- App.mpr mprcontents/`.

## Studio Pro Error Code Reference

| Code | Message | Common Cause |
Expand Down
30 changes: 30 additions & 0 deletions .claude/skills/mendix/create-page.md
Original file line number Diff line number Diff line change
Expand Up @@ -1082,8 +1082,38 @@ textbox txtHidden (label: 'Hidden', attribute: Name, visible: false)
-- A quoted-string expression is also accepted (CREATE and ALTER). Unlike the
-- bracket form, it is NOT auto-rooted — write $currentObject/ yourself.
dynamictext ovChip (content: 'chip', visible: '$currentObject/Name != empty')

-- Function calls work in the bracket form, including functions whose name is
-- also an MDL keyword (trim, length, find). Arguments are rooted like any other
-- reference.
dynamictext tTrim (content: 'x', visible: [trim($currentObject/Slug) != ''])
textbox txtSlug (label: 'Slug', attribute: Slug, editable: [length(Slug) > 0])
```

> **`visible:`/`editable:` is a Mendix *expression*, not XPath** — a different
> function set from a datasource `where` clause, even though both use `[ ... ]`:
>
> | | `visible:` / `editable:` (client expression) | `where [ … ]` (XPath) |
> |---|---|---|
> | String tests | `trim()`, `length()`, `toUpperCase()`, `find()`, `contains()` | `contains()`, `starts-with()`, `ends-with()`, `string-length()` |
> | `length()` | character count | number of elements in a list |
> | Emptiness | `$currentObject/X != ''` / `!= empty` | `[X = empty]` or `[X = NULL]` — a **keyword**, never `empty(…)` |
> | Aggregates | not available | `count()`/`avg()`/`min()`/`max()`/`sum()` are Java-API-only |
>
> mxcli's grammar accepts any function name in both and lets MxBuild adjudicate,
> so a wrong-context call surfaces as **CE0117** "Error(s) in expression" at
> build rather than as a parse error. See the Mendix reference guide:
> [XPath constraint functions](https://docs.mendix.com/refguide/xpath-constraint-functions/),
> [XPath keywords](https://docs.mendix.com/refguide/xpath-keywords-and-system-variables/).

> **An unparseable conditional is an error, not a silent drop.** If the
> expression inside `visible: [ ... ]` / `editable: [ ... ]` can't be parsed, the
> property has nowhere to go and would vanish on write — leaving the widget
> unconditionally visible/editable, which looks identical to a specificity bug in
> the running app. `mxcli check` reports this as **MDL-WIDGET19** and fails the
> command instead. Until v0.16.x, `trim(…)` and `length(…)` hit exactly this path
> and disappeared without a word (issue #852).

> **Attribute rooting is automatic** — a bare attribute in a widget
> visibility/editability expression (`[Name != '']`, `[IsActive]`) is rooted in the
> widget data context as `$currentObject/Name != ''` for you, so it no longer
Expand Down
86 changes: 78 additions & 8 deletions .claude/skills/mendix/database-connections.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,60 @@ end;

### Supported Database Types

| Database | TYPE Value |
|----------|------------|
| Oracle | `'Oracle'` |
| PostgreSQL | `'PostgreSQL'` |
| MySQL | `'MySQL'` |
| SQL Server | `'MSSQL'` or `'SQLServer'` |
| Snowflake | `'Snowflake'` |
| Amazon Redshift | `'Redshift'` |
These are the values Studio Pro's own connector editor offers — read out of the
shipped bundle at `modeler/ide-client/database-connector-editor/`, identical on
11.10.0, 11.12.1 and 11.13.0.

| Database | TYPE Value | Studio Pro label |
|----------|------------|------------------|
| SQL Server | `'MSSQL'` | Microsoft SQL |
| MySQL | `'MySQL'` | MySQL |
| Oracle | `'Oracle'` | Oracle |
| PostgreSQL | `'PostgreSQL'` | PostgreSQL |
| Snowflake | `'Snowflake'` | Snowflake |
| *anything else* | `'BYOD'` | Other |

**`'BYOD'` — bring your own driver.** Selecting it forces connection-string
configuration and **skips the driver-presence check**; its only validation is
that the connection string is non-empty. That is the hook for any JDBC driver
Mendix ships no picker entry for (DuckDB, SQLite, ClickHouse, …). Verified end to
end on Mendix 11.13: a booted runtime opened `jdbc:duckdb:` through a `BYOD`
connection and returned real rows — the runtime accepts it, not just the editor.

### Getting the driver onto the classpath

The driver JAR has to be *resolved*, and declaring it is not resolving it:

```sql
ALTER MODULE MyModule ADD JAR DEPENDENCY (
group = 'org.duckdb', artifact = 'duckdb_jdbc', version = '1.5.5.1', included = true
);
```

writes the coordinate to the model — `list jar dependencies` will report it — and
downloads **nothing**. MxBuild does not resolve it either: a full
`mxbuild --target=deploy` emits a `build.gradle` with no dependencies block. The
first symptom is a runtime `SQLException: No JDBC driver found in app for URL`,
from a connection that looks correctly configured.

Studio Pro runs the resolution when you edit Module Settings. Headless, ask for it:

```bash
mxcli sync-java-deps -p app.mpr # download into vendorlib/
mxcli sync-java-deps -p app.mpr --check # report what is missing, exit 1 (build gate)
```

`mxcli run --local` does this automatically for anything not already in
`vendorlib/`, so the warm loop works from a fresh clone. Dropping the jar into
`userlib/` by hand works too — it is the same classpath — but then the model and
the file system disagree about where the dependency comes from.

**`'Redshift'` and `'SQLServer'` are not real values.** Both appeared in an
earlier version of this table and neither is in the picker on any version
checked. mxcli writes the type string through unchanged and **mxbuild does not
validate it** — `type 'Redshift'` builds 0 errors and simply does not connect —
so `mxcli check` warns about an unrecognised type (MDL-DB01) rather than letting
a green build hide it.

## Query Definition Syntax

Expand Down Expand Up @@ -315,6 +361,30 @@ $ResultList = execute database query Module.Connection.QueryName
dynamic 'SELECT id, name FROM employees WHERE active = true LIMIT 10';
```

**A dynamic override still requires a value for every declared parameter** —
including the ones the replacement SQL does not use. The parameter list belongs
to the query *definition*, not to the SQL string, so Mendix asks for all of them
whatever you substitute. Pass a placeholder for the unused ones:

```sql
-- The definition declares $driverId; this SQL ignores it, and the call still
-- has to supply it.
$Count = execute database query F1.DuckDB.CountAllDrivers
dynamic 'SELECT count(*) AS n FROM read_csv(''/data/f1db-drivers.csv'')'
( driverId = 'unused' );
```

**A `{param}` placeholder can be concatenated into a path**, which is what keeps
absolute paths out of the model — bind the data directory as one constant and
build the file name around it:

```sql
-- read_csv({dataDir} || '/f1db-drivers.csv')
```

Verified against DuckDB through the connector on Mendix 11.13, and against a
standalone JDBC harness before that.

### Parameterized Queries

Pass values for query parameters defined with `parameter` in the query definition:
Expand Down
66 changes: 64 additions & 2 deletions .claude/skills/mendix/odata-data-sharing.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,8 +223,12 @@ create odata service ProductApi.ProductDataApi (
ODataVersion: OData4,
namespace: 'DefaultNamespace',
ServiceName: 'ProductDataApi',
Summary: 'Product and customer data API',
PublishAssociations: No
Summary: 'Product and customer data API'
-- PublishAssociations is left at its default (Yes = associations as links).
-- Setting it to No means "associations as an associated object id", which
-- Mendix only allows when the system ID is published as the key — publishing
-- an ordinary attribute as the key then fails the build with CE7375, even
-- when no associations are exposed at all.
)
authentication basic
{
Expand Down Expand Up @@ -412,6 +416,64 @@ alter entity ShopClient.Product set allow_create_change_locally = true;
alter entity ShopClient.Product set allow_create_change_locally = false;
```

## Publishing a Non-Persistable Entity (no copy of the data)

A published entity does **not** have to be persistable. Back it with a read
microflow and the rows are produced per request — nothing is stored, and there
is no refresh job to keep a copy in step with the source. This is the shape to
use when the data lives outside Mendix (an external database, a CSV, an API).

```sql
create non-persistent entity Api.Lap (
LapKey: string(60),
Driver: string(120),
LapTime: decimal
);

-- While Countable is Yes (the default), the read microflow MUST take a
-- $Response: System.ODataResponse parameter — Mendix asks it for the count.
CREATE MICROFLOW Api.Read_Laps ($Response: System.ODataResponse)
RETURNS List of Api.Lap AS $Laps
BEGIN
-- retrieve from wherever the data actually lives, e.g. EXECUTE DATABASE QUERY
$Laps = CREATE LIST OF Api.Lap;
RETURN $Laps;
END;

create odata service Api.LapApi (
path: 'odata/laps/',
version: '1.0.0',
ODataVersion: OData4,
namespace: 'Api.Laps'
)
authentication basic
{
publish entity Api.Lap as 'Laps' (
ReadMode: microflow Api.Read_Laps,
InsertMode: not_supported,
UpdateMode: not_supported,
DeleteMode: not_supported
)
expose (
LapKey as 'lapKey' (KEY, Filterable, Sortable),
Driver (Filterable, Sortable),
LapTime (Sortable)
);
};
```

Two things worth knowing before you write this:

- **`ReadMode: microflow Module.MF`** is the whole feature. `InsertMode`,
`UpdateMode` and `DeleteMode` take the same form for a read-write resource.
- **Counting is not free.** If the count means a full scan of the underlying
source, set `Countable: No` on the published entity — the read microflow then
takes no parameters at all. `SkipSupported: No` and `TopSupported: No` turn
off `$skip` and `$top` the same way. All three default to Yes.

`PublishAssociations` must stay at its default (Yes) here: a non-persistable
entity cannot publish its ID, so object-id mode can never build for it.

## Step-by-Step: Read-Write API with Microflow Handlers

For write operations (insert, update, delete), the OData service delegates to microflows that map between the view entity and the underlying persistent entities.
Expand Down
22 changes: 22 additions & 0 deletions .claude/skills/mendix/run-local.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ Launch `run --local` as the **sole** command in its invocation (don't chain a tr
| `--screenshot-path` / `--screenshot-url` | `.mxcli/run-local.png` / app root | Screenshot output / page (URL or `/path`) |
| `--screenshot-user` / `--screenshot-password` | — | Log in once, reuse session (pages behind login) |
| `--runtime-log` | `.mxcli/runtime.log` | Runtime log file: JVM stdout/stderr **and** the application log (microflow `LOG` output + server stack traces, via an attached file log subscriber). `-` disables. |
| `--test-endpoint` | off | Host mxcli's token-guarded test endpoint so `mxcli test … --attach` can run a suite against this app with no boot of its own. Installed **before** the boot (the handler registers from after-startup), your own after-startup microflow is chained not displaced, and both are removed on exit. See `test-microflows.md`. |
| `--debug` | off | Enable the microflow debugger at boot + start a session, so `mxcli debug break/paused/…` works from another terminal (see `debug-microflows.md`). No breakpoints = no behaviour change; disabled on shutdown. |
| `--debug-pass` | `mxdebug` | Debugger password when `--debug` is set |
| `--metrics` | off | Register a Prometheus meter registry at boot; the runtime serves metrics at `http://127.0.0.1:<admin-port>/prometheus` |
Expand Down Expand Up @@ -248,6 +249,27 @@ marker); the subscriber is re-attached on every restart and never rotates the fi
the JVM tee's handle stays valid). Override the path with `--runtime-log <path>`, or
pass `--runtime-log -` to disable the file (and the subscriber) entirely.

## "Sign in failed" that is not about the password

The local runtime is **unlicensed**, and an unlicensed runtime caps concurrent
sessions at a handful. Past the cap it refuses the sign-in, and the login page
reports that as a plain **"Sign in failed"** — exactly what a wrong password
looks like. The real reason is written only to the runtime log:

```
Maximum number of sessions exceeded! (You are currently using a trial license)
```

So: when a login you know is correct starts failing, `grep -c "Maximum number of
sessions" .mxcli/runtime.log` before touching the credentials or the user's
password in the model. `--screenshot-user` does this for you — a rejected sign-in
now reads the log and says so instead of quietly screenshotting the login page.

Sessions are held until they expire; restarting `run --local` clears them all.
A script that drives the app through a browser should **sign out at the end**,
otherwise each run leaks a slot and the fifth or sixth run is the one that fails —
which makes it look like a change you just made broke authentication.

## External browser preview (`--hub`)

`--hub <url>` exposes the running app in a **browser at a public URL** without the app
Expand Down
Loading
Loading