|
| 1 | +# Proposal: Bulk External Action Support from OData Contracts |
| 2 | + |
| 3 | +## Problem |
| 4 | + |
| 5 | +Issue [#143](https://github.com/mendixlabs/mxcli/issues/143) requests importing all entities **and actions** from a consumed OData service. `CREATE EXTERNAL ENTITIES FROM Module.Service` now handles bulk entity creation. This proposal covers the action side: generating the artifacts needed to call external OData actions. |
| 6 | + |
| 7 | +## Current State |
| 8 | + |
| 9 | +| Capability | Status | |
| 10 | +|------------|--------| |
| 11 | +| Browse contract actions | ✅ `SHOW CONTRACT ACTIONS FROM Module.Service` | |
| 12 | +| Describe contract action | ✅ `DESCRIBE CONTRACT ACTION Module.Service.Action` | |
| 13 | +| Call external action in microflow | ✅ `CALL EXTERNAL ACTION Module.Service.Action(...)` | |
| 14 | +| Bulk-create external entities | ✅ `CREATE EXTERNAL ENTITIES FROM Module.Service` | |
| 15 | +| Bulk-create action support artifacts | ❌ Not implemented | |
| 16 | +| Parse complex types from $metadata | ❌ Not implemented | |
| 17 | +| Response tree depth handling | ❌ Not modeled | |
| 18 | + |
| 19 | +## What Calling an External Action Requires |
| 20 | + |
| 21 | +### Artifacts per action |
| 22 | + |
| 23 | +1. **Parameter entities** — for each action parameter with a complex type (non-primitive), a non-persistent entity (NPE) or external entity must exist in the domain model |
| 24 | +2. **Return type entity** — if the action returns a complex type, the corresponding entity must exist |
| 25 | +3. **The microflow** — with `CALL EXTERNAL ACTION` wired up with parameter mappings |
| 26 | + |
| 27 | +### Type classification |
| 28 | + |
| 29 | +OData `$metadata` defines several type kinds that map differently to Mendix: |
| 30 | + |
| 31 | +| OData type | Mendix equivalent | Example | |
| 32 | +|------------|-------------------|---------| |
| 33 | +| `Edm.String`, `Edm.Int32`, etc. | Primitive attribute types | `Edm.String` → `String(200)` | |
| 34 | +| Entity type (has entity set) | External entity | `TripPin.Person` → external entity | |
| 35 | +| Complex type (no entity set) | Non-persistent entity | `TripPin.City` → NPE with `CityName`, `Region` | |
| 36 | +| `Collection(Namespace.Type)` | List of entity | `Collection(TripPin.Trip)` → list parameter | |
| 37 | +| Enum type | Enumeration | `TripPin.PersonGender` → enumeration | |
| 38 | + |
| 39 | +### Response tree depth (key complexity) |
| 40 | + |
| 41 | +When Studio Pro configures an external action call, it allows the user to choose **how deep** the response should be deserialized: |
| 42 | + |
| 43 | +- **Top-level only** — only the returned entity's own attributes |
| 44 | +- **With associations** — also deserialize associated complex objects / navigation properties |
| 45 | + |
| 46 | +This is controlled in the BSON by the `VariableDataType` field on `CallExternalAction`, which specifies both the return type and what parts of the object graph to materialize. The exact mechanism needs investigation by creating reference examples in Studio Pro. |
| 47 | + |
| 48 | +**Open questions:** |
| 49 | +- How is the depth/scope stored in BSON? Is it a separate field or encoded in `VariableDataType`? |
| 50 | +- Can it reference navigation properties selectively (like `$expand` in OData)? |
| 51 | +- Does it affect which NPEs need to exist? (i.e., if you only request top-level, do you still need NPEs for nested complex types?) |
| 52 | + |
| 53 | +## Gaps to Address |
| 54 | + |
| 55 | +### 1. Complex type parsing in EDMX |
| 56 | + |
| 57 | +`sdk/mpr/edmx.go` currently parses entity types, enum types, and actions from `$metadata` XML. It does **not** parse `<ComplexType>` elements. These are required because: |
| 58 | + |
| 59 | +- Action parameters often use complex types as input |
| 60 | +- Action return types may be complex types |
| 61 | +- Navigation properties on entity types may reference complex types |
| 62 | + |
| 63 | +**Work needed:** |
| 64 | +- Add `EdmComplexType` struct (similar to `EdmEntityType` but without key properties or entity set) |
| 65 | +- Parse `<ComplexType>` in `ParseEdmx()` |
| 66 | +- Store on `EdmSchema.ComplexTypes` |
| 67 | + |
| 68 | +### 2. Type resolution for action parameters |
| 69 | + |
| 70 | +`EdmAction.Parameters[].Type` can be: |
| 71 | +- A primitive (`Edm.String`) — no entity needed |
| 72 | +- A qualified entity type (`Namespace.Customer`) — needs external entity |
| 73 | +- A qualified complex type (`Namespace.Address`) — needs NPE |
| 74 | +- A collection (`Collection(Namespace.Item)`) — needs entity + list parameter type |
| 75 | +- An enum (`Namespace.Status`) — needs enumeration |
| 76 | + |
| 77 | +Resolution requires looking up whether a type name refers to an entity type (has entity set), complex type, or enum type in the schema. |
| 78 | + |
| 79 | +### 3. Response tree depth |
| 80 | + |
| 81 | +The core design question: when generating action call scaffolding, how do we handle the response tree? |
| 82 | + |
| 83 | +**Option A: Top-level only (simplest)** |
| 84 | +Generate only the immediate return type entity. Users manually expand if needed. |
| 85 | + |
| 86 | +**Option B: Full tree (Studio Pro default)** |
| 87 | +Walk navigation properties of the return type, recursively create NPEs/external entities for all reachable types. |
| 88 | + |
| 89 | +**Option C: User-controlled depth** |
| 90 | +```sql |
| 91 | +CREATE EXTERNAL ACTIONS FROM Module.Service DEPTH 1; -- top-level only |
| 92 | +CREATE EXTERNAL ACTIONS FROM Module.Service DEPTH 2; -- include direct associations |
| 93 | +``` |
| 94 | + |
| 95 | +**Recommendation:** Start with Option A, document that users can add depth manually. Investigate Studio Pro's BSON to understand the exact storage before implementing deeper options. |
| 96 | + |
| 97 | +### 4. CallExternalAction BSON completeness |
| 98 | + |
| 99 | +Current parser/writer for `CallExternalAction` may be missing fields that Studio Pro writes. Specifically: |
| 100 | +- `VariableDataType` — not currently parsed (controls return type inference) |
| 101 | +- Response depth/scope fields — unknown, needs BSON investigation |
| 102 | + |
| 103 | +## Proposed Syntax |
| 104 | + |
| 105 | +### Phase 1: Entity/type scaffolding only |
| 106 | + |
| 107 | +```sql |
| 108 | +-- Create NPEs and enumerations for all action parameter/return types |
| 109 | +-- that don't already exist as entities in the project |
| 110 | +CREATE EXTERNAL ACTIONS FROM Module.Service; |
| 111 | + |
| 112 | +-- Filter to specific actions |
| 113 | +CREATE EXTERNAL ACTIONS FROM Module.Service ACTIONS (GetTrips, CreateTrip); |
| 114 | + |
| 115 | +-- Into a different module |
| 116 | +CREATE EXTERNAL ACTIONS FROM Module.Service INTO Integration; |
| 117 | + |
| 118 | +-- Idempotent |
| 119 | +CREATE OR MODIFY EXTERNAL ACTIONS FROM Module.Service; |
| 120 | +``` |
| 121 | + |
| 122 | +This would: |
| 123 | +1. Parse all actions from cached `$metadata` |
| 124 | +2. For each action, resolve parameter types and return type |
| 125 | +3. Create NPEs for complex types that don't have entity sets |
| 126 | +4. Create external entities for entity types that aren't already imported |
| 127 | +5. Create enumerations for enum types |
| 128 | +6. Output a summary of what was created |
| 129 | + |
| 130 | +It would **not** generate microflows — that's the user's job (or a Phase 2 feature). |
| 131 | + |
| 132 | +### Phase 2: Microflow generation (future) |
| 133 | + |
| 134 | +```sql |
| 135 | +-- Generate stub microflows that call each action |
| 136 | +CREATE EXTERNAL ACTION MICROFLOWS FROM Module.Service; |
| 137 | +``` |
| 138 | + |
| 139 | +### Phase 3: DESCRIBE FORMAT mdl for actions (future) |
| 140 | + |
| 141 | +```sql |
| 142 | +-- Generate a complete MDL script for calling an action |
| 143 | +DESCRIBE CONTRACT ACTION Module.Service.GetTrips FORMAT mdl; |
| 144 | +``` |
| 145 | + |
| 146 | +Would output something like: |
| 147 | + |
| 148 | +```sql |
| 149 | +-- Required NPE for return type |
| 150 | +CREATE NON-PERSISTENT ENTITY Module.Trip ( |
| 151 | + TripId: Long, |
| 152 | + Name: String(200), |
| 153 | + Description: String(500) |
| 154 | +); |
| 155 | + |
| 156 | +-- Microflow to call the action |
| 157 | +CREATE MICROFLOW Module.ACT_GetTrips($PersonId: String) RETURNS List of Module.Trip |
| 158 | +BEGIN |
| 159 | + $Result = CALL EXTERNAL ACTION Module.Service.GetTrips(personId = $PersonId); |
| 160 | + RETURN $Result; |
| 161 | +END; |
| 162 | +``` |
| 163 | + |
| 164 | +## Investigation Required Before Implementation |
| 165 | + |
| 166 | +Before coding, these questions need answers from Studio Pro reference examples: |
| 167 | + |
| 168 | +1. **Create a reference project** with a consumed OData service (e.g., TripPin) that has actions with complex type parameters and return types |
| 169 | +2. **Inspect the BSON** for `CallExternalAction` to understand: |
| 170 | + - How `VariableDataType` encodes the return type |
| 171 | + - Whether there's a depth/expand field for response tree |
| 172 | + - How bound actions differ in storage |
| 173 | +3. **Inspect the NPEs** that Studio Pro creates for complex types: |
| 174 | + - Are they plain `DomainModels$EntityImpl` with `Persistable: false`? |
| 175 | + - Do they have a special `Source` field (like external entities have `Rest$ODataRemoteEntitySource`)? |
| 176 | + - How are associations between NPEs and external entities stored? |
| 177 | +4. **Inspect enumerations** from OData: |
| 178 | + - Do they use `Rest$ODataRemoteEnumerationSource`? |
| 179 | + - How are they linked back to the consumed service? |
| 180 | + |
| 181 | +## Implementation Order |
| 182 | + |
| 183 | +1. **Parse complex types** from `$metadata` (`sdk/mpr/edmx.go`) |
| 184 | +2. **Type resolver** — given a qualified type name, determine if it's entity/complex/enum/primitive |
| 185 | +3. **Phase 1 executor** — `CREATE EXTERNAL ACTIONS FROM` creates NPEs, enums, external entities |
| 186 | +4. **BSON investigation** — Studio Pro reference project for CallExternalAction fields |
| 187 | +5. **Phase 2** — `DESCRIBE CONTRACT ACTION ... FORMAT mdl` generates complete MDL |
| 188 | +6. **Phase 3** — microflow generation |
| 189 | + |
| 190 | +## Related Files |
| 191 | + |
| 192 | +- `sdk/mpr/edmx.go` — EDMX parsing (needs ComplexType support) |
| 193 | +- `sdk/mpr/parser_microflow_actions.go` — CallExternalAction parser |
| 194 | +- `sdk/mpr/writer_microflow_actions.go` — CallExternalAction writer |
| 195 | +- `sdk/microflows/microflows_actions.go` — CallExternalAction struct |
| 196 | +- `mdl/executor/cmd_contract.go` — contract browsing + CREATE EXTERNAL ENTITIES |
| 197 | +- `mdl/executor/cmd_odata.go` — OData CRUD commands |
| 198 | +- `mdl/ast/ast_odata.go` — OData AST nodes |
| 199 | +- `docs/11-proposals/odata-services-proposal.md` — original OData proposal (Phase 3 section) |
0 commit comments