Skip to content

Commit f6adb72

Browse files
akoclaude
andcommitted
feat: CREATE EXTERNAL ENTITIES bulk import from OData contract (#143)
Add CREATE [OR MODIFY] EXTERNAL ENTITIES FROM Module.Service syntax that bulk-creates external entities from a consumed OData service's cached $metadata. Supports INTO module redirect and ENTITIES name filter. Closes #143 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 5742d01 commit f6adb72

File tree

16 files changed

+9007
-8282
lines changed

16 files changed

+9007
-8282
lines changed

.claude/skills/mendix/browse-integrations.md

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,25 @@ WHERE ObjectType IN ('ODATA_CLIENT', 'REST_CLIENT', 'ODATA_SERVICE',
111111
AND ModuleName = 'Integration';
112112
```
113113

114-
## Workflow: Import an Entity from a Contract
114+
## Workflow: Import Entities from a Contract
115+
116+
### Bulk import (all or filtered)
117+
118+
```sql
119+
-- Import all entity types at once
120+
CREATE EXTERNAL ENTITIES FROM MyModule.SalesforceAPI;
121+
122+
-- Import into a different module
123+
CREATE EXTERNAL ENTITIES FROM MyModule.SalesforceAPI INTO Integration;
124+
125+
-- Import only specific entities
126+
CREATE EXTERNAL ENTITIES FROM MyModule.SalesforceAPI ENTITIES (PurchaseOrder, Supplier);
127+
128+
-- Idempotent re-import (updates existing)
129+
CREATE OR MODIFY EXTERNAL ENTITIES FROM MyModule.SalesforceAPI;
130+
```
131+
132+
### Single entity (with customization)
115133

116134
1. Browse available entities:
117135
```sql

.claude/skills/mendix/odata-data-sharing.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -273,6 +273,20 @@ FROM ODATA CLIENT ProductClient.ProductDataApiClient
273273
GRANT ProductClient.User ON ProductClient.CustomerAddressesEE (READ *);
274274
```
275275

276+
**Bulk alternative:** Instead of creating external entities one by one, import all (or a subset) from the contract:
277+
278+
```sql
279+
-- All entities from the service
280+
CREATE EXTERNAL ENTITIES FROM ProductClient.ProductDataApiClient;
281+
282+
-- Or specific ones only
283+
CREATE EXTERNAL ENTITIES FROM ProductClient.ProductDataApiClient
284+
ENTITIES (Product, CustomerAddress);
285+
286+
-- Idempotent re-import
287+
CREATE OR MODIFY EXTERNAL ENTITIES FROM ProductClient.ProductDataApiClient;
288+
```
289+
276290
## Step-by-Step: Read-Write API with Microflow Handlers
277291

278292
For write operations (insert, update, delete), the OData service delegates to microflows that map between the view entity and the underlying persistent entities.

cmd/mxcli/help_topics/odata.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,24 @@ CREATE EXTERNAL ENTITY
108108
Email: String(200)
109109
);
110110

111+
CREATE EXTERNAL ENTITIES (bulk from $metadata)
112+
-----------------------------------------------
113+
114+
-- Create all entity types as external entities
115+
CREATE EXTERNAL ENTITIES FROM MyModule.ExternalAPI;
116+
117+
-- Create into a different module
118+
CREATE EXTERNAL ENTITIES FROM MyModule.ExternalAPI INTO Integration;
119+
120+
-- Create only specific entities
121+
CREATE EXTERNAL ENTITIES FROM MyModule.ExternalAPI ENTITIES (Customer, Order);
122+
123+
-- Idempotent — updates existing entities
124+
CREATE OR MODIFY EXTERNAL ENTITIES FROM MyModule.ExternalAPI;
125+
126+
-- Combine all options
127+
CREATE OR MODIFY EXTERNAL ENTITIES FROM MyModule.ExternalAPI INTO Integration ENTITIES (Customer, Order);
128+
111129
ALTER / DROP
112130
-----------
113131

docs-site/src/appendixes/quick-reference.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ CREATE PERSISTENT ENTITY Module.Photo (
2626
| Create with extends | `CREATE PERSISTENT ENTITY Module.Name EXTENDS Parent.Entity (attrs);` | EXTENDS before `(` |
2727
| Create view entity | `CREATE VIEW ENTITY Module.Name (attrs) AS SELECT ...;` | OQL-backed read-only |
2828
| Create external entity | `CREATE EXTERNAL ENTITY Module.Name FROM ODATA CLIENT Module.Client (...) (attrs);` | From consumed OData |
29+
| Create external entities | `CREATE [OR MODIFY] EXTERNAL ENTITIES FROM Module.Client [INTO Module] [ENTITIES (...)];` | Bulk from $metadata |
2930
| Drop entity | `DROP ENTITY Module.Name;` | |
3031
| Describe entity | `DESCRIBE ENTITY Module.Name;` | Full MDL output |
3132
| List entities | `LIST ENTITIES [IN Module];` | List all or filter by module |
@@ -94,6 +95,7 @@ CREATE CONSTANT MyModule.EnableLogging TYPE Boolean DEFAULT true;
9495
| List external entities | `LIST EXTERNAL ENTITIES [IN Module];` | OData-backed entities |
9596
| List external actions | `LIST EXTERNAL ACTIONS [IN Module];` | Actions used in microflows |
9697
| Create external entity | `CREATE [OR MODIFY] EXTERNAL ENTITY Module.Name FROM ODATA CLIENT Module.Client (...) (attrs);` | |
98+
| Create external entities | `CREATE [OR MODIFY] EXTERNAL ENTITIES FROM Module.Client [INTO Module] [ENTITIES (...)];` | Bulk from $metadata |
9799
| Grant OData access | `GRANT ACCESS ON ODATA SERVICE Module.Name TO Module.Role, ...;` | |
98100
| Revoke OData access | `REVOKE ACCESS ON ODATA SERVICE Module.Name FROM Module.Role, ...;` | |
99101
| List contract entities | `LIST CONTRACT ENTITIES FROM Module.Client;` | Browse cached $metadata |

docs-site/src/reference/odata/README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,25 @@ Mendix supports consuming and publishing OData services. Consumed services (ODat
2525
| Statement | Description |
2626
|-----------|-------------|
2727
| [CREATE EXTERNAL ENTITY](create-external-entity.md) | Import an entity from a consumed OData service |
28+
| [CREATE EXTERNAL ENTITIES](#create-external-entities) | Bulk-create external entities from cached $metadata |
29+
30+
## CREATE EXTERNAL ENTITIES
31+
32+
Bulk-create external entities from a consumed OData service's cached `$metadata`.
33+
34+
```sql
35+
-- Create all entity types from the contract
36+
CREATE EXTERNAL ENTITIES FROM Module.Service;
37+
38+
-- Create into a specific module
39+
CREATE EXTERNAL ENTITIES FROM Module.Service INTO TargetModule;
40+
41+
-- Filter to specific entities
42+
CREATE EXTERNAL ENTITIES FROM Module.Service ENTITIES (Customer, Order);
43+
44+
-- Idempotent — update existing entities
45+
CREATE OR MODIFY EXTERNAL ENTITIES FROM Module.Service;
46+
```
2847

2948
## Contract Browsing Statements
3049

docs/01-project/MDL_QUICK_REFERENCE.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ CREATE PERSISTENT ENTITY Module.Photo (
3838
| Create with extends | `CREATE PERSISTENT ENTITY Module.Name EXTENDS Parent.Entity (attrs);` | EXTENDS before `(` |
3939
| Create view entity | `CREATE VIEW ENTITY Module.Name (attrs) AS SELECT ...;` | OQL-backed read-only |
4040
| Create external entity | `CREATE EXTERNAL ENTITY Module.Name FROM ODATA CLIENT Module.Client (...) (attrs);` | From consumed OData |
41+
| Create external entities | `CREATE [OR MODIFY] EXTERNAL ENTITIES FROM Module.Client [INTO Module] [ENTITIES (...)];` | Bulk from $metadata |
4142
| Drop entity | `DROP ENTITY Module.Name;` | |
4243
| Describe entity | `DESCRIBE ENTITY Module.Name;` | Full MDL output |
4344
| Describe enumeration | `DESCRIBE ENUMERATION Module.Name;` | Full MDL output |
@@ -111,6 +112,7 @@ CREATE CONSTANT MyModule.EnableLogging TYPE Boolean DEFAULT true;
111112
| Show external entities | `SHOW EXTERNAL ENTITIES [IN Module];` | OData-backed entities |
112113
| Show external actions | `SHOW EXTERNAL ACTIONS [IN Module];` | Actions used in microflows |
113114
| Create external entity | `CREATE [OR MODIFY] EXTERNAL ENTITY Module.Name FROM ODATA CLIENT Module.Client (...) (attrs);` | |
115+
| Create external entities | `CREATE [OR MODIFY] EXTERNAL ENTITIES FROM Module.Client [INTO Module] [ENTITIES (...)];` | Bulk from $metadata |
114116
| Grant OData access | `GRANT ACCESS ON ODATA SERVICE Module.Name TO Module.Role, ...;` | |
115117
| Revoke OData access | `REVOKE ACCESS ON ODATA SERVICE Module.Name FROM Module.Role, ...;` | |
116118
| Show contract entities | `SHOW CONTRACT ENTITIES FROM Module.Client;` | Browse cached $metadata |

mdl-examples/doctype-tests/10-odata-examples.mdl

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,49 @@ SHOW EXTERNAL ACTIONS;
365365
SHOW EXTERNAL ACTIONS IN OdTest;
366366
/
367367

368+
-- ############################################################################
369+
-- LEVEL 10: CREATE EXTERNAL ENTITIES (bulk from contract)
370+
-- ############################################################################
371+
372+
/**
373+
* Level 10.1: Create all external entities from a consumed OData service.
374+
* Reads the service's cached $metadata and creates one external entity
375+
* per entity type in the domain model.
376+
* Prerequisite: The OData client must have cached metadata (fetched during CREATE).
377+
*
378+
* CREATE EXTERNAL ENTITIES FROM OdTest.SalesforceAPI;
379+
*/
380+
381+
/**
382+
* Level 10.2: Create external entities into a different module.
383+
* By default entities are created in the same module as the service.
384+
* Use INTO to redirect them.
385+
*
386+
* CREATE EXTERNAL ENTITIES FROM OdTest.SalesforceAPI INTO Integration;
387+
*/
388+
389+
/**
390+
* Level 10.3: Create only specific external entities by name.
391+
* Use the ENTITIES clause to filter which entity types to create.
392+
*
393+
* CREATE EXTERNAL ENTITIES FROM OdTest.SalesforceAPI ENTITIES (Account, Contact);
394+
*/
395+
396+
/**
397+
* Level 10.4: Idempotent bulk create — updates existing entities.
398+
*
399+
* CREATE OR MODIFY EXTERNAL ENTITIES FROM OdTest.SalesforceAPI;
400+
*/
401+
402+
/**
403+
* Level 10.5: Combine INTO, ENTITIES filter, and OR MODIFY.
404+
*
405+
* CREATE OR MODIFY EXTERNAL ENTITIES FROM OdTest.SalesforceAPI INTO Integration ENTITIES (Account, Contact);
406+
*/
407+
408+
-- NOTE: These commands require a live OData client with cached $metadata,
409+
-- so they are shown as comments above. The syntax is validated by mxcli check.
410+
368411
-- ############################################################################
369412
-- NOTE: CREATE ODATA CLIENT now auto-fetches and caches $metadata from the
370413
-- MetadataUrl. CONTRACT BROWSING commands (SHOW CONTRACT ENTITIES/ACTIONS)

mdl/ast/ast_odata.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,13 @@ type CreateExternalEntityStmt struct {
136136
}
137137

138138
func (s *CreateExternalEntityStmt) isStatement() {}
139+
140+
// CreateExternalEntitiesStmt represents: CREATE [OR MODIFY] EXTERNAL ENTITIES FROM Module.Service [INTO Module] [ENTITIES (Name1, Name2)]
141+
type CreateExternalEntitiesStmt struct {
142+
ServiceRef QualifiedName // FROM Module.Service
143+
TargetModule string // INTO Module (optional, defaults to service module)
144+
EntityNames []string // ENTITIES (Name1, Name2) filter (optional, imports all if empty)
145+
CreateOrModify bool // True if CREATE OR MODIFY was used
146+
}
147+
148+
func (s *CreateExternalEntitiesStmt) isStatement() {}

mdl/executor/cmd_contract.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -468,6 +468,137 @@ func edmToMendixType(p *mpr.EdmProperty) string {
468468
}
469469
}
470470

471+
// ============================================================================
472+
// CREATE EXTERNAL ENTITIES (bulk)
473+
// ============================================================================
474+
475+
// createExternalEntities handles CREATE [OR MODIFY] EXTERNAL ENTITIES FROM Module.Service [INTO Module] [ENTITIES (...)].
476+
// It reads entity types from the cached $metadata and creates external entities in the domain model.
477+
func (e *Executor) createExternalEntities(s *ast.CreateExternalEntitiesStmt) error {
478+
if e.writer == nil {
479+
return fmt.Errorf("not connected to a project in write mode")
480+
}
481+
482+
doc, svcQN, err := e.parseServiceContract(s.ServiceRef)
483+
if err != nil {
484+
return err
485+
}
486+
487+
// Build entity set lookup: entity type qualified name → entity set name
488+
esMap := make(map[string]string)
489+
for _, es := range doc.EntitySets {
490+
esMap[es.EntityType] = es.Name
491+
}
492+
493+
// Build filter set if entity names specified
494+
filterSet := make(map[string]bool)
495+
for _, name := range s.EntityNames {
496+
filterSet[strings.ToLower(name)] = true
497+
}
498+
499+
// Determine target module
500+
targetModule := s.TargetModule
501+
if targetModule == "" {
502+
targetModule = s.ServiceRef.Module
503+
}
504+
505+
var created, skipped, failed int
506+
507+
for _, schema := range doc.Schemas {
508+
for _, et := range schema.EntityTypes {
509+
// Apply entity name filter
510+
if len(filterSet) > 0 && !filterSet[strings.ToLower(et.Name)] {
511+
continue
512+
}
513+
514+
entitySetName := esMap[schema.Namespace+"."+et.Name]
515+
if entitySetName == "" {
516+
entitySetName = et.Name + "s" // fallback
517+
}
518+
519+
// Build attributes from properties
520+
var attrs []ast.Attribute
521+
for _, p := range et.Properties {
522+
// Skip key properties named ID (Mendix manages its own ID)
523+
isKey := false
524+
for _, k := range et.KeyProperties {
525+
if p.Name == k {
526+
isKey = true
527+
break
528+
}
529+
}
530+
if isKey && p.Name == "ID" {
531+
continue
532+
}
533+
534+
attrs = append(attrs, ast.Attribute{
535+
Name: p.Name,
536+
Type: edmToAstDataType(p),
537+
})
538+
}
539+
540+
stmt := &ast.CreateExternalEntityStmt{
541+
Name: ast.QualifiedName{Module: targetModule, Name: et.Name},
542+
ServiceRef: s.ServiceRef,
543+
EntitySet: entitySetName,
544+
RemoteName: et.Name,
545+
Countable: true,
546+
Attributes: attrs,
547+
CreateOrModify: s.CreateOrModify,
548+
}
549+
550+
if err := e.execCreateExternalEntity(stmt); err != nil {
551+
fmt.Fprintf(e.output, " FAILED: %s.%s — %v\n", targetModule, et.Name, err)
552+
failed++
553+
} else {
554+
created++
555+
}
556+
}
557+
}
558+
559+
if skipped > 0 || failed > 0 {
560+
fmt.Fprintf(e.output, "\nImported %d entities from %s (%d failed)\n", created, svcQN, failed)
561+
} else {
562+
fmt.Fprintf(e.output, "\nImported %d entities from %s into %s\n", created, svcQN, targetModule)
563+
}
564+
565+
return nil
566+
}
567+
568+
// edmToAstDataType converts an Edm property to an AST data type.
569+
func edmToAstDataType(p *mpr.EdmProperty) ast.DataType {
570+
switch p.Type {
571+
case "Edm.String":
572+
length := 200
573+
if p.MaxLength != "" && p.MaxLength != "max" {
574+
if n, err := fmt.Sscanf(p.MaxLength, "%d", &length); n == 0 || err != nil {
575+
length = 200
576+
}
577+
}
578+
return ast.DataType{Kind: ast.TypeString, Length: length}
579+
case "Edm.Int32":
580+
return ast.DataType{Kind: ast.TypeInteger}
581+
case "Edm.Int64":
582+
return ast.DataType{Kind: ast.TypeLong}
583+
case "Edm.Decimal":
584+
return ast.DataType{Kind: ast.TypeDecimal}
585+
case "Edm.Boolean":
586+
return ast.DataType{Kind: ast.TypeBoolean}
587+
case "Edm.DateTime", "Edm.DateTimeOffset", "Edm.Date":
588+
return ast.DataType{Kind: ast.TypeDateTime}
589+
case "Edm.Double", "Edm.Single":
590+
return ast.DataType{Kind: ast.TypeDecimal}
591+
case "Edm.Byte", "Edm.SByte", "Edm.Int16":
592+
return ast.DataType{Kind: ast.TypeInteger}
593+
case "Edm.Guid":
594+
return ast.DataType{Kind: ast.TypeString, Length: 36}
595+
case "Edm.Binary":
596+
return ast.DataType{Kind: ast.TypeString, Length: 200}
597+
default:
598+
return ast.DataType{Kind: ast.TypeString, Length: 200}
599+
}
600+
}
601+
471602
// ============================================================================
472603
// AsyncAPI Contract Commands
473604
// ============================================================================

mdl/executor/executor_dispatch.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,8 @@ func (e *Executor) executeInner(stmt ast.Statement) error {
196196
return e.dropRestClient(s)
197197
case *ast.CreateExternalEntityStmt:
198198
return e.execCreateExternalEntity(s)
199+
case *ast.CreateExternalEntitiesStmt:
200+
return e.createExternalEntities(s)
199201
case *ast.GrantODataServiceAccessStmt:
200202
return e.execGrantODataServiceAccess(s)
201203
case *ast.RevokeODataServiceAccessStmt:

0 commit comments

Comments
 (0)