Skip to content

Commit f43a3f9

Browse files
committed
Added rapidminer graph skill
1 parent 0583803 commit f43a3f9

File tree

1 file changed

+308
-0
lines changed

1 file changed

+308
-0
lines changed
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
# Connecting Mendix to RapidMiner / AnzoGraph via SPARQL
2+
3+
Use this skill when you need to fetch data from a RapidMiner graph mart (or any SPARQL 1.1 HTTP endpoint like AnzoGraph) and surface it in a Mendix app.
4+
5+
## When to Use
6+
7+
- An external graph database exposes a SPARQL HTTP endpoint with Basic Auth
8+
- You want the graph results to become Mendix entities (for display, search, further processing)
9+
- You have read-only needs — the pattern fits SELECT queries that return tabular results
10+
11+
## Endpoint shape
12+
13+
A RapidMiner / AnzoGraph graphmart endpoint looks like:
14+
15+
```
16+
https://<host>/sparql/graphmart/<url-encoded-graphmart-uri>
17+
```
18+
19+
Example:
20+
```
21+
https://graphstudio.mendixdemo.com/sparql/graphmart/http%3A%2F%2Fcambridgesemantics.com%2FGraphmart%2F3617250aca6a40d88972c1c0de38f86a
22+
```
23+
24+
Two things to note:
25+
1. The graphmart URI is **URL-encoded and embedded in the path** (colons and slashes become `%3A` / `%2F`).
26+
2. SPARQL queries are sent as the **POST body** with `Content-Type: application/sparql-query`, and the response is JSON when `Accept: application/sparql-results+json`.
27+
28+
Verify with curl first:
29+
30+
```bash
31+
curl -u 'user@example.com:password' \
32+
-H 'Accept: application/sparql-results+json' \
33+
-H 'Content-Type: application/sparql-query' \
34+
--data-binary 'SELECT ?s WHERE { ?s a <http://example.com/Foo> } LIMIT 10' \
35+
'https://host/sparql/graphmart/<encoded-uri>'
36+
```
37+
38+
If curl returns `200` and a JSON `results.bindings` array, you're ready to wire it into Mendix.
39+
40+
## SPARQL JSON result shape
41+
42+
Every SPARQL HTTP result looks like this:
43+
44+
```json
45+
{
46+
"head": { "vars": ["customer", "customerId", "customerName"] },
47+
"results": {
48+
"bindings": [
49+
{
50+
"customer": {"type": "uri", "value": "http://.../Customer/0000000"},
51+
"customerId": {"type": "literal", "value": "CUST001"},
52+
"customerName": {"type": "literal", "value": "Global Tech Solutions Inc."}
53+
}
54+
]
55+
}
56+
}
57+
```
58+
59+
Each row in `bindings` is an object of `{var: {type, value}}`. A JSLT transformer flattens this to something directly mappable into Mendix entities.
60+
61+
## The full pipeline
62+
63+
```
64+
┌────────────────────┐ ┌────────────────────┐ ┌────────────────────┐ ┌────────────────┐
65+
│ Inline REST CALL │─▶│ Data Transformer │─▶│ Import Mapping │─▶│ Mendix Entity │
66+
│ POST + Basic Auth │ │ JSLT: flatten │ │ JSON → entities │ │ (persistent) │
67+
│ SPARQL as body │ │ results.bindings │ │ │ │ │
68+
└────────────────────┘ └────────────────────┘ └────────────────────┘ └────────────────┘
69+
```
70+
71+
**Why inline `REST CALL` rather than `CREATE REST CLIENT` + `SEND REST REQUEST`?**
72+
73+
- At the time of writing, REST Client `AUTHENTICATION: BASIC (Username: '...', Password: '...')` silently fails to attach the `Authorization` header when the password contains special characters (e.g. `!`). Result: `401 Unauthorized`.
74+
- Inline `REST CALL ... AUTH BASIC '<user>' PASSWORD '<pass>'` handles the same credentials correctly.
75+
76+
**Why persistent entities for the final list?**
77+
78+
- Non-persistent `ReferenceSet` children can't be extracted as a `List` in MDL microflows (no documented `RETRIEVE ... BY ASSOCIATION` syntax), and `LOOP $c IN $Parent/Assoc` fails at build time.
79+
- Persistent entities work with `DataSource: DATABASE` on a DataGrid — the standard happy path.
80+
81+
## Step-by-step template
82+
83+
### 1. Persistent target entity
84+
85+
```sql
86+
@Position(100, 100)
87+
CREATE PERSISTENT ENTITY MyModule.Customer (
88+
CustomerUri: String(500),
89+
CustomerId: String(50),
90+
CustomerName: String(200)
91+
);
92+
/
93+
```
94+
95+
### 2. Non-persistent wrapper (for the import mapping only)
96+
97+
Import mappings need a single root entity. A tiny non-persistent wrapper with one dummy attribute is enough:
98+
99+
```sql
100+
@Position(400, 100)
101+
CREATE NON-PERSISTENT ENTITY MyModule.CustomerImport (
102+
DummyAttr: String(10)
103+
);
104+
/
105+
106+
CREATE ASSOCIATION MyModule.CustomerImport_Customer
107+
FROM MyModule.CustomerImport
108+
TO MyModule.Customer
109+
TYPE ReferenceSet;
110+
/
111+
```
112+
113+
### 3. Data Transformer (JSLT) — flatten SPARQL response
114+
115+
Take the nested `results.bindings[].*.value` shape and emit a flat `customers[]` array:
116+
117+
```sql
118+
CREATE DATA TRANSFORMER MyModule.SimplifyCustomers
119+
SOURCE JSON '{"head":{"vars":["customer","customerId","customerName"]},"results":{"bindings":[{"customer":{"type":"uri","value":"http://.../Customer/0"},"customerId":{"type":"literal","value":"CUST001"},"customerName":{"type":"literal","value":"Global Tech Solutions Inc."}}]}}'
120+
{
121+
JSLT $$
122+
{
123+
"customers": [for (.results.bindings)
124+
{
125+
"customerUri": .customer.value,
126+
"customerId": .customerId.value,
127+
"customerName": .customerName.value
128+
}
129+
]
130+
}
131+
$$;
132+
};
133+
```
134+
135+
**JSLT notes for this runtime:**
136+
137+
- `[for (.path.to.array) <expr>]` works for iteration.
138+
- `.field.subfield` path access works.
139+
- `[N]` array indexing works.
140+
- `$var[start : end]` slice works for strings — **do not use `substring(...)`** (it silently drops the field from the output).
141+
- `let` variables and `if/else` expressions work.
142+
- `def fn(arg) ...` helper functions work.
143+
144+
### 4. JSON structure + Import Mapping
145+
146+
The JSON structure represents the **transformed** shape (after JSLT), not the raw SPARQL response:
147+
148+
```sql
149+
CREATE JSON STRUCTURE MyModule.JSON_Customers
150+
SNIPPET '{"customers":[{"customerUri":"http://example.com/Customer/0","customerId":"CUST001","customerName":"Global Tech Solutions Inc."}]}';
151+
152+
CREATE IMPORT MAPPING MyModule.IMM_Customers
153+
WITH JSON STRUCTURE MyModule.JSON_Customers
154+
{
155+
CREATE MyModule.CustomerImport {
156+
CREATE MyModule.CustomerImport_Customer/MyModule.Customer = customers {
157+
CustomerUri = customerUri,
158+
CustomerId = customerId,
159+
CustomerName = customerName
160+
}
161+
}
162+
};
163+
```
164+
165+
### 5. Microflow — the actual API call
166+
167+
```sql
168+
CREATE MICROFLOW MyModule.ACT_RefreshCustomers ()
169+
RETURNS Boolean AS $Success
170+
BEGIN
171+
LOG INFO NODE 'MyModule' '=== Refresh start ===';
172+
173+
-- Clear existing persistent records (full replace)
174+
RETRIEVE $Existing FROM MyModule.Customer;
175+
LOOP $C IN $Existing BEGIN
176+
DELETE $C;
177+
END LOOP;
178+
179+
-- Inline REST CALL — NOT the REST Client (see notes)
180+
$RawJson = REST CALL POST 'https://graphstudio.mendixdemo.com/sparql/graphmart/http%3A%2F%2Fcambridgesemantics.com%2FGraphmart%2F3617250aca6a40d88972c1c0de38f86a'
181+
HEADER 'Accept' = 'application/sparql-results+json'
182+
HEADER 'Content-Type' = 'application/sparql-query'
183+
AUTH BASIC '<username>' PASSWORD '<password>'
184+
BODY 'PREFIX model: <http://cambridgesemantics.com/SourceLayer/c4ce0eca2e7241f2aee13b46fbdca3f8/Model#> SELECT ?customer ?customerId ?customerName FROM <http://cambridgesemantics.com/SourceLayer/c4ce0eca2e7241f2aee13b46fbdca3f8/Model> WHERE {1} ?customer a model:ExamplePlmBom.Customer; model:ExamplePlmBom.Customer.id ?customerId; model:ExamplePlmBom.Customer.name ?customerName; {2}'
185+
WITH ({1} = '{', {2} = '}')
186+
TIMEOUT 60
187+
RETURNS String
188+
ON ERROR CONTINUE;
189+
190+
LOG INFO NODE 'MyModule' '{1}' WITH ({1} = 'HTTP status: ' + toString($latestHttpResponse/StatusCode));
191+
192+
IF $latestHttpResponse/StatusCode = 200 THEN
193+
$SimplifiedJson = TRANSFORM $RawJson WITH MyModule.SimplifyCustomers;
194+
$ImportResult = IMPORT FROM MAPPING MyModule.IMM_Customers($SimplifiedJson);
195+
LOG INFO NODE 'MyModule' '=== Done ===';
196+
END IF;
197+
198+
RETURN true;
199+
END;
200+
/
201+
```
202+
203+
### 6. Page
204+
205+
```sql
206+
CREATE PAGE MyModule.Customer_Overview (
207+
Title: 'Customers (from Graph Mart)',
208+
Layout: Atlas_Core.Atlas_Default
209+
) {
210+
DYNAMICTEXT heading (Content: 'Customers', RenderMode: H2)
211+
ACTIONBUTTON btnRefresh (Caption: 'Refresh', Action: MICROFLOW MyModule.ACT_RefreshCustomers, ButtonStyle: Primary)
212+
DATAGRID gridCustomers (DataSource: DATABASE MyModule.Customer SORT BY CustomerId ASC) {
213+
COLUMN colId (Attribute: CustomerId, Caption: 'ID')
214+
COLUMN colName (Attribute: CustomerName, Caption: 'Name')
215+
COLUMN colUri (Attribute: CustomerUri, Caption: 'URI')
216+
}
217+
}
218+
/
219+
```
220+
221+
## Gotchas (things that burned an hour during development)
222+
223+
### `!` in Basic Auth password → 401
224+
225+
REST Client `AUTHENTICATION: BASIC (...)` with a literal password containing `!` sends no auth header at runtime. Workaround: use inline `REST CALL ... AUTH BASIC '<user>' PASSWORD '<pass>'`. The inline form works with the same literal credentials.
226+
227+
### SPARQL `{` braces in `BODY` templates are consumed as placeholder escapes
228+
229+
In `REST CALL ... BODY '...'`, the body is a template string where `{1}`, `{2}` are placeholders. A literal `{` must be escaped as `{{`, but in this runtime `{{` is sent **literally** rather than being converted to `{` → server returns `400 Bad Request`.
230+
231+
**Solution:** pass literal braces as placeholder values:
232+
233+
```sql
234+
BODY '... WHERE {1} ... {2}'
235+
WITH ({1} = '{', {2} = '}')
236+
```
237+
238+
### JSON structure auto-detects ISO strings as DateTime
239+
240+
If your JSLT emits ISO 8601 timestamps (`"2026-04-13T14:00"`) and the target Mendix attribute is `String`, `CREATE JSON STRUCTURE ... SNIPPET '...'` will infer `DateTime` from the sample and mxbuild fails with `CE5015` ("schema type DateTime doesn't match attribute type String").
241+
242+
**Solutions:**
243+
- Use a non-ISO sample value in the snippet (e.g. `"2026-04-13 14:00 CET"`).
244+
- Or slice/format the timestamp in JSLT so it doesn't look like ISO 8601 (`$rawTime[11 : 16]` for `HH:MM`).
245+
- Or change the target attribute to `DateTime`.
246+
247+
### Non-persistent child lists can't be extracted in microflows
248+
249+
The import mapping happily populates `CustomerImport` with a `ReferenceSet` of `Customer` children, but:
250+
- `RETURN $Root/MyModule.CustomerImport_Customer` → "Error(s) in expression" at build
251+
- `DECLARE $C List of MyModule.Customer = $Root/...` → "Error(s) in expression"
252+
- `LOOP $c IN $Root/MyModule.CustomerImport_Customer` → "The 'Iterate over' property is required"
253+
- DataGrid `DataSource: $currentObject/MyModule.CustomerImport_Customer` → BSON serializer drops the datasource
254+
255+
**Solution:** Make the target entity **persistent**. The import mapping commits them automatically, and the page uses the standard `DataSource: DATABASE MyModule.Customer` for the grid. A full replace on each refresh (delete-all-then-import) keeps data consistent with the graph.
256+
257+
### Rapid drop/create cycles on the same entity can corrupt the MPR
258+
259+
If you `DROP ENTITY X` then `CREATE ENTITY X` repeatedly while associations referencing `X` exist, the associations may hold the **old** entity GUID → mxbuild fails with `KeyNotFoundException`. Fix by dropping/recreating the broken association after the entity change.
260+
261+
## Exploring the graph
262+
263+
Before building the pipeline, explore the graph to understand what's there. Useful SPARQL queries (send via curl):
264+
265+
**List all classes with counts:**
266+
```sparql
267+
SELECT DISTINCT ?class (COUNT(?s) AS ?count)
268+
FROM <http://.../Model>
269+
WHERE { ?s a ?class }
270+
GROUP BY ?class
271+
ORDER BY DESC(?count)
272+
```
273+
274+
**List properties used by a given class:**
275+
```sparql
276+
PREFIX model: <http://.../Model#>
277+
SELECT DISTINCT ?property
278+
FROM <http://.../Model>
279+
WHERE {
280+
?s a model:ExamplePlmBom.Customer ;
281+
?property ?o .
282+
}
283+
ORDER BY ?property
284+
```
285+
286+
**Filter to a single namespace (skip rdf/owl noise):**
287+
```sparql
288+
SELECT DISTINCT ?class ?property
289+
FROM <http://.../Model>
290+
WHERE {
291+
?s a ?class ;
292+
?property ?o .
293+
FILTER(STRSTARTS(STR(?class), "http://.../Model#MyPrefix"))
294+
}
295+
```
296+
297+
## Credential management
298+
299+
For demos, literal credentials inline in the microflow are the simplest and most reliable. For anything else, put them in a project constant and reference it from the microflow via `$ConstantName` (requires a non-trivial amount of setup — see the project settings skill).
300+
301+
**Do not** use `$ConstantName` in `CREATE REST CLIENT ... AUTHENTICATION: BASIC (Username: $C, Password: $C)` — the MDL parser rejects the `$` prefix there, and the skill files' claim of `Rest$ConstantValue` serialization isn't reachable.
302+
303+
## Related skills
304+
305+
- [rest-client.md](./rest-client.md) — REST Client + SEND REST REQUEST pattern (preferred when Basic Auth is not needed or uses simple passwords)
306+
- [json-structures-and-mappings.md](./json-structures-and-mappings.md) — JSON structure / import mapping details
307+
- [rest-call-from-json.md](./rest-call-from-json.md) — inline REST CALL + mapping pipeline
308+
- [write-microflows.md](./write-microflows.md) — microflow syntax reference

0 commit comments

Comments
 (0)