A Mendix SDK Primer — Part 2

Mendix SDK Primer Pt 2 main image shows a purple and coral background with a 3D gray box in the center that displays the Mendix logo on one side, an another side is some pretend code, and on a third side is a diagram of elements connected together.

This is the second blog post in a short series illustrating how the Mendix Platform/Model SDKs can give you programmatic access to your app’s model without using Mendix Studio or Mendix Studio Pro.

Creating apps and building the model

In the first SDK blog post I demonstrated how you can use a typescript script and NodeJS to access a Mendix app model and export information from the model into a JavaScript script. Now I’ll create a new Mendix app using a Platform/Model SDK script, build a new module in the app, and place some elements into that module.

A Mendix SDK Primer — Part 2_Image of a computer with an empty screen indicating a blank slate to start with

A New App

For a blog, the use-case has to be pretty simple, so I’ll create an app which mimics a very simplistic order entry scenario. The app will just have the following:

  • An order entity with an autonumber for the order ID, an order value, an order status, and a customer name, which can also hold a document (a PDF maybe) of the order received.
  • An order line entity with an order line ID, product name, and line value.
  • An association with cascaded deletion between the order and order line entities.
  • A non-persistent entity that might be used to search orders valued between user-entered maximum and minimum values.
  • An enumeration representing the order status.
  • Production level security configured with an admin role and a regular user role. I have made the persistent entities writable by admin but read-only for regular users just to illustrate how this is done.

The script will build all of this from scratch and also allow control over the Mendix version and starting app template that are used as a basis for the App that is created.

I expect we can build on this app in subsequent blog posts.

So to start: All the activities happen in an async function called main(). This allows us to use await modifiers to simply the code.

Create the app

This is simple and we just need a name for the app and an optional summary (description) and template ID. If you don’t specify the template ID then the default template for the latest available Mendix version will be used.

I am going to use the Blank Web App template for Mendix 9.18.1.

You can find the template ID for the template you want to use for the app by looking on the Mendix Marketplace and selecting the “Templates” option:

A Mendix SDK Primer — Part 2_Find the template you want to use by looking in the Mendix Marketplace

Click on the template you want and then click on the “Releases” tab inside the template’s page.

A Mendix SDK Primer — Part 2_image of the releases tab showing a template's page

If you open the individual releases you will find details relating to each, including the framework version which corresponds to the “Mendix version” that it is intended for. Not all Mendix versions are represented. If you need to use a version that is not shown, choose the latest framework version prior to the version you want and then upgrade the app in Studio Pro after the app has been built.

A Mendix SDK Primer — Part 2_Marketplace template release details

Copy the UUID that is given against the release you want to use — this is the template ID which can be inserted into the createNewApp statement.

    import { MendixPlatformClient } from "mendixplatformsdk";
    import { projects, security, domainmodels, enumerations, texts } from 'mendixmodelsdk';
    
    main().catch(console.error);
    
    
    async function main()
    {
        const client = new MendixPlatformClient();
    
        // Create the app
        const app = await client.createNewApp("SDKTestApp", {summary: "Test App created using the Mendix SDKs", templateId: "5cdf7b82-e902-4b8d-b0dd-bdd939c9e82f"});
    
        console.log("App created with App ID: " + app.appId);

Next we create a working copy for the app and open the model so we can update it.

        // Open a working copy
        const workingCopy = await app.createTemporaryWorkingCopy("trunk");
        const model = await workingCopy.openModel();

Set up the module

Next we create the new app module and give it our chosen name, and initialize the security for the module. In this app we shall be working with two roles, one for administrator access and one for regular users. The way I have set this up is not very realistic as I’m just trying to show how you set up security in the SDK — administrators will have have write access to entities, where regular users will only have read access (with one exception).

        // Create a new module
        const module = projects.Module.createIn(model.allProjects()[0]);
        module.name = "SDKModule";
    
        // Set up module security
        const moduleSecurity = security.ModuleSecurity.createIn(module);
        const adminRole = security.ModuleRole.createIn(moduleSecurity);
        adminRole.name = "Admin";
        const userRole = security.ModuleRole.createIn(moduleSecurity);
        userRole.name = "User";

Finally, we need to create the domain model for the module and we are ready to start populating it.

S        // Create the module domain model
        const domainModel = domainmodels.DomainModel.createIn(module);

Create an enumeration

The “Order Status” on the order entity is to be an enumeration, so let’s create that first and give it four possible values: New, On Hold, In Progress, and Completed. To keep things simple we just provide US English captions for each value.

        // Make the Order Status enumeration with its values
        const statusEnumeration = enumerations.Enumeration.createIn(module);
        statusEnumeration.name = "ENUM_Status";
    
        const statusEnumerationValue1 = enumerations.EnumerationValue.createIn(statusEnumeration);
        statusEnumerationValue1.name = "_New";
        const statusEnumerationCaption1 = texts.Text.createInEnumerationValueUnderCaption(statusEnumerationValue1);
        const statusEnumerationTranslation1 = texts.Translation.create(model);
        statusEnumerationTranslation1.text = "New";
        statusEnumerationTranslation1.languageCode = "en_US";
        statusEnumerationCaption1.translations.push(statusEnumerationTranslation1);
    
        const statusEnumerationValue2 = enumerations.EnumerationValue.createIn(statusEnumeration);
        statusEnumerationValue2.name = "Hold";
        const statusEnumerationCaption2 = texts.Text.createInEnumerationValueUnderCaption(statusEnumerationValue2);
        const statusEnumerationTranslation2 = texts.Translation.create(model);
        statusEnumerationTranslation2.text = "On Hold";
        statusEnumerationTranslation2.languageCode = "en_US";
        statusEnumerationCaption2.translations.push(statusEnumerationTranslation2);
       
        const statusEnumerationValue3 = enumerations.EnumerationValue.createIn(statusEnumeration);
        statusEnumerationValue3.name = "Progress";
        const statusEnumerationCaption3 = texts.Text.createInEnumerationValueUnderCaption(statusEnumerationValue3);
        const statusEnumerationTranslation3 = texts.Translation.create(model);
        statusEnumerationTranslation3.text = "In Progress";
        statusEnumerationTranslation3.languageCode = "en_US";
        statusEnumerationCaption3.translations.push(statusEnumerationTranslation3);
       
        const statusEnumerationValue4 = enumerations.EnumerationValue.createIn(statusEnumeration);
        statusEnumerationValue4.name = "Completed";
        const statusEnumerationCaption4 = texts.Text.createInEnumerationValueUnderCaption(statusEnumerationValue4);
        const statusEnumerationTranslation4 = texts.Translation.create(model);
        statusEnumerationTranslation4.text = "Completed";
        statusEnumerationTranslation4.languageCode = "en_US";
        statusEnumerationCaption4.translations.push(statusEnumerationTranslation4);

OK, that was pretty straightforward so let’s get on with something a bit more involved.

Create entities

As we work our way through this you should see a clear correspondence between the code we have here and performing the same actions inside Studio Pro.

Let’s start with the order line entity. The entity creation is simple. The location dictates the position that the entity will take in the Studio Pro domain model diagram: where x specifies the horizontal position in diagram points measured across from the left of the diagram, and y specifies the vertical position in diagram points measured down from the top of the diagram. Both reflect the top-left corner of the entity box.

        // Create the Order Line Entity and attributes
        const orderLineEntity = domainmodels.Entity.createIn(domainModel);
        orderLineEntity.name = "OrderLine";
        orderLineEntity.location = { x: 100, y: 100};
        orderLineEntity.documentation = "The Order Line entity created using the Model SDK";

The first two attributes are also straightforward — one is an integer and the other is a decimal.

        const orderLineIdAttributeType = domainmodels.IntegerAttributeType.create(model);
        const orderLineIdAttributeDefault = domainmodels.StoredValue.create(model);
        orderLineIdAttributeDefault.defaultValue = "";
        const orderLineIdAttribute = domainmodels.Attribute.createIn(orderLineEntity);
        orderLineIdAttribute.name = "OrderLineId";
        orderLineIdAttribute.type = orderLineIdAttributeType;
        orderLineIdAttribute.value = orderLineIdAttributeDefault;
        orderLineIdAttribute.documentation = "The Id of this Order Line unique within an order created using the Model SDK";
    
        const orderLineValueAttributeType = domainmodels.DecimalAttributeType.create(model);
        const orderLineValueAttributeDefault = domainmodels.StoredValue.create(model);
        orderLineValueAttributeDefault.defaultValue = "";
        const orderLineValueAttribute = domainmodels.Attribute.createIn(orderLineEntity);
        orderLineValueAttribute.name = "OrderLineValue";
        orderLineValueAttribute.type = orderLineValueAttributeType;
        orderLineValueAttribute.value = orderLineValueAttributeDefault;
        orderLineValueAttribute.documentation = "The Value of this Order Line created using the Model SDK";

The third attribute has a few wrinkles added:

  • The attribute is a string and has a length (300) specified.
  • The attribute has a default value which is not blank: “Happy Days”.
  • The attribute has a mandatory validation rule applied. This needs an error message defined . Again, we just use US English for simplicity.
        const orderLineProductNameAttributeType = domainmodels.StringAttributeType.create(model);
        orderLineProductNameAttributeType.length = 300;
        const orderLineProductNameAttributeDefault = domainmodels.StoredValue.create(model);
        orderLineProductNameAttributeDefault.defaultValue = "Happy Days";
        const orderLineProductNameAttribute = domainmodels.Attribute.createIn(orderLineEntity);
        orderLineProductNameAttribute.name = "ProductName";
        orderLineProductNameAttribute.type = orderLineProductNameAttributeType;
        orderLineProductNameAttribute.value = orderLineProductNameAttributeDefault;
        orderLineProductNameAttribute.documentation = "The name of the Product the subject of this Order Line created using the Model SDK";
        const orderLineEntityValidationRule1 = domainmodels.ValidationRule.createIn(orderLineEntity);
        orderLineEntityValidationRule1.attribute = orderLineProductNameAttribute;
        domainmodels.RequiredRuleInfo.createIn(orderLineEntityValidationRule1);
        const orderLineEntityValidationRuleText = texts.Text.createInValidationRuleUnderErrorMessage(orderLineEntityValidationRule1);
        const orderLineEntityValidationRuleTranslation = texts.Translation.create(model);
        orderLineEntityValidationRuleTranslation.text = "A Product Name must be entered";
        orderLineEntityValidationRuleTranslation.languageCode = "en_US";
        orderLineEntityValidationRuleText.translations.push(orderLineEntityValidationRuleTranslation);

Next, we’ll create the order entity. This is a little different from order line as it is a specialization of System.FileDocument. As I mentioned in the SDK Blog Part 1, using system module elements is a little awkward as for security reasons, you cannot reference them directly in an SDK script. So we use a workaround. If the generalization entity was not in the system module then we could reference it directly.

        // Create the Order Entity and its attributes
        const orderEntity = domainmodels.Entity.createIn(domainModel);
        orderEntity.name = "Order";
        orderEntity.location = { x: 500, y: 100};
        orderEntity.documentation = "The Order Entity specializing from System.FileDocument created using the Model SDK";
        const orderEntityGeneralization = domainmodels.Generalization.createIn(orderEntity);
        (orderEntityGeneralization as any)["__generalization"].updateWithRawValue("System.FileDocument");

For now, we are not doing anything fancy with the attributes in order entity so the code is straightforward but note again there is a string attribute with a length specified (100), the AutoNumber OrderId attribute has a default starting value defined (set to ‘1’) and the OrderStatus (which uses the enumeration we created above) has its default value set (‘_New’).

        const orderIdAttributeType = domainmodels.AutoNumberAttributeType.create(model);
        const orderIdAttributeDefault = domainmodels.StoredValue.create(model);
        orderIdAttributeDefault.defaultValue = "1";
        const orderIdAttribute = domainmodels.Attribute.createIn(orderEntity);
        orderIdAttribute.name = "OrderId";
        orderIdAttribute.type = orderIdAttributeType;
        orderIdAttribute.value = orderIdAttributeDefault;
        orderIdAttribute.documentation = "The Id of this Order Id unique accross the system created using the Model SDK";
    
        const orderValueAttributeType = domainmodels.DecimalAttributeType.create(model);
        const orderValueAttributeDefault = domainmodels.StoredValue.create(model);
        orderValueAttributeDefault.defaultValue = "";
        const orderValueAttribute = domainmodels.Attribute.createIn(orderEntity);
        orderValueAttribute.name = "OrderValue";
        orderValueAttribute.type = orderValueAttributeType;
        orderValueAttribute.value = orderValueAttributeDefault;
        orderValueAttribute.documentation = "The Value of this Order created using the Model SDK";
    
        const customerNameAttributeType = domainmodels.StringAttributeType.create(model);
        customerNameAttributeType.length = 100;
        const customerNameAttributeDefault = domainmodels.StoredValue.create(model);
        customerNameAttributeDefault.defaultValue = "";
        const customerNameAttribute = domainmodels.Attribute.createIn(orderEntity);
        customerNameAttribute.name = "CustomerName";
        customerNameAttribute.type = customerNameAttributeType;
        customerNameAttribute.value = customerNameAttributeDefault;
        customerNameAttribute.documentation = "The name of the Customer who raised this Order created using the Model SDK";
    
        const orderStatusAttributeType = domainmodels.EnumerationAttributeType.create(model);
        orderStatusAttributeType.enumeration = statusEnumeration;
        const orderStatusAttributeDefault = domainmodels.StoredValue.create(model);
        orderStatusAttributeDefault.defaultValue = "_New";
        const orderStatusAttribute = domainmodels.Attribute.createIn(orderEntity);
        orderStatusAttribute.name = "OrderStatus";
        orderStatusAttribute.type = orderStatusAttributeType;
        orderStatusAttribute.value = orderStatusAttributeDefault;
        orderStatusAttribute.documentation = "The status of this Order created using the Model SDK";

Create the non-persistent OrderSearch entity and its attributes. The non-persistence is set using a simple flag.

        // Create the OrderSearch non-persistent Entity
        const orderSearchEntity = domainmodels.Entity.createIn(domainModel);
        orderSearchEntity.name = "OrderSearch";
        orderSearchEntity.location = { x: 900, y: 100};
        orderSearchEntity.documentation = "A non-persistent entity used for searching Orders created using the Model SDK";
        const orderSearchEntityNoGeneralization = domainmodels.NoGeneralization.createIn(orderSearchEntity);
        orderSearchEntityNoGeneralization.persistable = false;
    
        const orderSearchMinimumValueAttributeType = domainmodels.DecimalAttributeType.create(model);
        const orderSearchMinimumValueDefaultValue = domainmodels.StoredValue.create(model);
        orderSearchMinimumValueDefaultValue.defaultValue = "";
        const orderSearchMiniumuValueAttribute = domainmodels.Attribute.createIn(orderSearchEntity);
        orderSearchMiniumuValueAttribute.name = "MinimumValue";
        orderSearchMiniumuValueAttribute.type = orderSearchMinimumValueAttributeType;
        orderSearchMiniumuValueAttribute.value = orderSearchMinimumValueDefaultValue;
        orderSearchMiniumuValueAttribute.documentation = "The search order value minimum value created using the Model SDK";
    
        const orderSearchMaximumValueAttributeType = domainmodels.DecimalAttributeType.create(model);
        const orderSearchMaximumValueDefaultValue = domainmodels.StoredValue.create(model);
        orderSearchMaximumValueDefaultValue.defaultValue = "";
        const orderSearchMaxiumuValueAttribute = domainmodels.Attribute.createIn(orderSearchEntity);
        orderSearchMaxiumuValueAttribute.name = "MaximumValue";
        orderSearchMaxiumuValueAttribute.type = orderSearchMaximumValueAttributeType;
        orderSearchMaxiumuValueAttribute.value = orderSearchMaximumValueDefaultValue;
        orderSearchMaxiumuValueAttribute.documentation = "The search order value maximum value created using the Model SDK";

Finally, we need to create the association between the order and order line entities. The childConnection and parentConnection settings are similar to those for the entity location values, but instead of absolute positions in the diagram, they refer to percentages. So the x refers to how far along the width of the entity in the diagram, and the y refers to how far down the side of the entity in the diagram. Using these you can set the line representing the association to end at any point on the entity box.

When the association has been created, we set the deletion behavior so that associated OrderLine objects will be deleted when an order object is deleted.

        // Create the association between the OrderLine and Order Entities
        const association = domainmodels.Association.createIn(domainModel);
        association.name = "OrderLine_Order"; 
        association.child = orderEntity;
        association.parent = orderLineEntity;
        association.type = domainmodels.AssociationType.Reference;
        association.owner = domainmodels.AssociationOwner.Default;
        association.childConnection = { x: 0, y: 50};
        association.parentConnection = { x: 100, y: 50};
        association.documentation = "Association created using the Model SDK";
        const associationDeleteBehavior = domainmodels.AssociationDeleteBehavior.createIn(association);
        associationDeleteBehavior.childDeleteBehavior = domainmodels.DeletingBehavior.DeleteMeAndReferences;
        association.deleteBehavior = associationDeleteBehavior;

This should all give as a domain model diagram that looks like this:

A Mendix SDK Primer — Part 2_domain model diagram

Entity security settings

Now we need to execute a list of settings to control the security for each of the admin and user roles we created. Each attribute and association has to be set up separately and if the roles differ then each role has to be set up separately too. This is relatively straightforward, if rather protracted.

Also we have to define the settings for the inherited attributes from System.FileDocument using a similar workaround as before as the system module is not directly accessible.

        // Set up the security on the new module's Entities
        // Security settings for Order Line Entity for Admin user
        const orderLineEntityAdminAccessRule = domainmodels.AccessRule.createInEntityUnderAccessRules(orderLineEntity);
        orderLineEntityAdminAccessRule.allowCreate = true;
        orderLineEntityAdminAccessRule.allowDelete = true;
        orderLineEntityAdminAccessRule.defaultMemberAccessRights = domainmodels.MemberAccessRights.ReadWrite;
        orderLineEntityAdminAccessRule.moduleRoles.push(adminRole);
        
        const orderLineEntityAdminAccessOrderLineId = domainmodels.MemberAccess.createIn(orderLineEntityAdminAccessRule);
        orderLineEntityAdminAccessOrderLineId.attribute = orderLineIdAttribute;
        orderLineEntityAdminAccessOrderLineId.accessRights = domainmodels.MemberAccessRights.ReadWrite;
    
        const orderLineEntityAdminAccessProductName = domainmodels.MemberAccess.createIn(orderLineEntityAdminAccessRule);
        orderLineEntityAdminAccessProductName.attribute = orderLineProductNameAttribute;
        orderLineEntityAdminAccessProductName.accessRights = domainmodels.MemberAccessRights.ReadWrite;
    
        const orderLineEntityAdminAccessOrderLineValue = domainmodels.MemberAccess.createIn(orderLineEntityAdminAccessRule);
        orderLineEntityAdminAccessOrderLineValue.attribute = orderLineValueAttribute;
        orderLineEntityAdminAccessOrderLineValue.accessRights = domainmodels.MemberAccessRights.ReadWrite;
    
        const orderLineEntityAdminAccessAssociation = domainmodels.MemberAccess.createIn(orderLineEntityAdminAccessRule);
        orderLineEntityAdminAccessAssociation.association = association;
        orderLineEntityAdminAccessAssociation.accessRights = domainmodels.MemberAccessRights.ReadWrite;
    
        // Security settings for Order Line Entity for regular user
        const orderLineEntityUserAccessRule = domainmodels.AccessRule.createInEntityUnderAccessRules(orderLineEntity);
        orderLineEntityUserAccessRule.allowCreate = false;
        orderLineEntityUserAccessRule.allowDelete = false;
        orderLineEntityUserAccessRule.defaultMemberAccessRights = domainmodels.MemberAccessRights.ReadOnly;
        orderLineEntityUserAccessRule.moduleRoles.push(userRole);
        
        const orderLineEntityUserAccessOrderLineId = domainmodels.MemberAccess.createIn(orderLineEntityUserAccessRule);
        orderLineEntityUserAccessOrderLineId.attribute = orderLineIdAttribute;
        orderLineEntityUserAccessOrderLineId.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    
        const orderLineEntityUserAccessProductName = domainmodels.MemberAccess.createIn(orderLineEntityUserAccessRule);
        orderLineEntityUserAccessProductName.attribute = orderLineProductNameAttribute;
        orderLineEntityUserAccessProductName.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    
        const orderLineEntityUserAccessOrderLineValue = domainmodels.MemberAccess.createIn(orderLineEntityUserAccessRule);
        orderLineEntityUserAccessOrderLineValue.attribute = orderLineValueAttribute;
        orderLineEntityUserAccessOrderLineValue.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    
        const orderLineEntityUserAccessAssociation = domainmodels.MemberAccess.createIn(orderLineEntityUserAccessRule);
        orderLineEntityUserAccessAssociation.association = association;
        orderLineEntityUserAccessAssociation.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    
        // Security settings for Order Entity for Admin user
        const orderEntityAdminAccessRule = domainmodels.AccessRule.createInEntityUnderAccessRules(orderEntity);
        orderEntityAdminAccessRule.allowCreate = true;
        orderEntityAdminAccessRule.allowDelete = true;
        orderEntityAdminAccessRule.defaultMemberAccessRights = domainmodels.MemberAccessRights.ReadWrite;
        orderEntityAdminAccessRule.moduleRoles.push(adminRole);
        
        const orderEntityAdminAccessOrderId = domainmodels.MemberAccess.createIn(orderEntityAdminAccessRule);
        orderEntityAdminAccessOrderId.attribute = orderIdAttribute;
        orderEntityAdminAccessOrderId.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    
        const orderEntityAdminAccessOrderValue = domainmodels.MemberAccess.createIn(orderEntityAdminAccessRule);
        orderEntityAdminAccessOrderValue.attribute = orderValueAttribute;
        orderEntityAdminAccessOrderValue.accessRights = domainmodels.MemberAccessRights.ReadWrite;
    
        const orderEntityAdminAccessCustomerName = domainmodels.MemberAccess.createIn(orderEntityAdminAccessRule);
        orderEntityAdminAccessCustomerName.attribute = customerNameAttribute;
        orderEntityAdminAccessCustomerName.accessRights = domainmodels.MemberAccessRights.ReadWrite;
    
        const orderEntityAdminAccessOrderStatus = domainmodels.MemberAccess.createIn(orderEntityAdminAccessRule);
        orderEntityAdminAccessOrderStatus.attribute = orderStatusAttribute;
        orderEntityAdminAccessOrderStatus.accessRights = domainmodels.MemberAccessRights.ReadWrite;
    
        const orderEntityAdminAccessName = domainmodels.MemberAccess.createIn(orderEntityAdminAccessRule);
        (orderEntityAdminAccessName as any)["__attribute"].updateWithRawValue("System.FileDocument.Name");
        orderEntityAdminAccessName.accessRights = domainmodels.MemberAccessRights.ReadWrite;
    
        const orderEntityAdminAccessFileId = domainmodels.MemberAccess.createIn(orderEntityAdminAccessRule);
        (orderEntityAdminAccessFileId as any)["__attribute"].updateWithRawValue("System.FileDocument.FileID");
        orderEntityAdminAccessFileId.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    
        const orderEntityAdminAccessDeleteAfterDownload = domainmodels.MemberAccess.createIn(orderEntityAdminAccessRule);
        (orderEntityAdminAccessDeleteAfterDownload as any)["__attribute"].updateWithRawValue("System.FileDocument.DeleteAfterDownload");
        orderEntityAdminAccessDeleteAfterDownload.accessRights = domainmodels.MemberAccessRights.ReadWrite;
    
        const orderEntityAdminAccessContents = domainmodels.MemberAccess.createIn(orderEntityAdminAccessRule);
        (orderEntityAdminAccessContents as any)["__attribute"].updateWithRawValue("System.FileDocument.Contents");
        orderEntityAdminAccessContents.accessRights = domainmodels.MemberAccessRights.ReadWrite;
    
        const orderEntityAdminAccessHasContents = domainmodels.MemberAccess.createIn(orderEntityAdminAccessRule);
        (orderEntityAdminAccessHasContents as any)["__attribute"].updateWithRawValue("System.FileDocument.HasContents");
        orderEntityAdminAccessHasContents.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    
        const orderEntityAdminAccessSize = domainmodels.MemberAccess.createIn(orderEntityAdminAccessRule);
        (orderEntityAdminAccessSize as any)["__attribute"].updateWithRawValue("System.FileDocument.Size");
        orderEntityAdminAccessSize.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    
        // Security settings for Order Entity for regular user
        const orderEntityUserAccessRule = domainmodels.AccessRule.createInEntityUnderAccessRules(orderEntity);
        orderEntityUserAccessRule.allowCreate = false;
        orderEntityUserAccessRule.allowDelete = false;
        orderEntityUserAccessRule.defaultMemberAccessRights = domainmodels.MemberAccessRights.ReadOnly;
        orderEntityUserAccessRule.moduleRoles.push(userRole);
        
        const orderEntityUserAccessOrderId = domainmodels.MemberAccess.createIn(orderEntityUserAccessRule);
        orderEntityUserAccessOrderId.attribute = orderIdAttribute;
        orderEntityUserAccessOrderId.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    
        const orderEntityUserAccessOrderValue = domainmodels.MemberAccess.createIn(orderEntityUserAccessRule);
        orderEntityUserAccessOrderValue.attribute = orderValueAttribute;
        orderEntityUserAccessOrderValue.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    
        const orderEntityUserAccesCustomerName = domainmodels.MemberAccess.createIn(orderEntityUserAccessRule);
        orderEntityUserAccesCustomerName.attribute = customerNameAttribute;
        orderEntityUserAccesCustomerName.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    
        const orderEntityUserAccesOrderStatus = domainmodels.MemberAccess.createIn(orderEntityUserAccessRule);
        orderEntityUserAccesOrderStatus.attribute = orderStatusAttribute;
        orderEntityUserAccesOrderStatus.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    
        const orderEntityUserAccessName = domainmodels.MemberAccess.createIn(orderEntityUserAccessRule);
        (orderEntityUserAccessName as any)["__attribute"].updateWithRawValue("System.FileDocument.Name");
        orderEntityUserAccessName.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    
        const orderEntityUserAccessFileId = domainmodels.MemberAccess.createIn(orderEntityUserAccessRule);
        (orderEntityUserAccessFileId as any)["__attribute"].updateWithRawValue("System.FileDocument.FileID");
        orderEntityUserAccessFileId.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    
        const orderEntityUserAccessDeleteAfterDownload = domainmodels.MemberAccess.createIn(orderEntityUserAccessRule);
        (orderEntityUserAccessDeleteAfterDownload as any)["__attribute"].updateWithRawValue("System.FileDocument.DeleteAfterDownload");
        orderEntityUserAccessDeleteAfterDownload.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    
        const orderEntityUserAccessContents = domainmodels.MemberAccess.createIn(orderEntityUserAccessRule);
        (orderEntityUserAccessContents as any)["__attribute"].updateWithRawValue("System.FileDocument.Contents");
        orderEntityUserAccessContents.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    
        const orderEntityUserAccessHasContents = domainmodels.MemberAccess.createIn(orderEntityUserAccessRule);
        (orderEntityUserAccessHasContents as any)["__attribute"].updateWithRawValue("System.FileDocument.HasContents");
        orderEntityUserAccessHasContents.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    
        const orderEntityUserAccessSize = domainmodels.MemberAccess.createIn(orderEntityUserAccessRule);
        (orderEntityUserAccessSize as any)["__attribute"].updateWithRawValue("System.FileDocument.Size");
        orderEntityUserAccessSize.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    
        // Security settings for OrderSearch Entity for both Admin and regular users
        const orderSearchEntityUserAccessRule = domainmodels.AccessRule.createInEntityUnderAccessRules(orderSearchEntity);
        orderSearchEntityUserAccessRule.allowCreate = true;
        orderSearchEntityUserAccessRule.allowDelete = true;
        orderSearchEntityUserAccessRule.defaultMemberAccessRights = domainmodels.MemberAccessRights.ReadWrite;
        orderSearchEntityUserAccessRule.moduleRoles.push(adminRole);
        orderSearchEntityUserAccessRule.moduleRoles.push(userRole);
        
        const orderSearchEntityUserAccessMinimumValue = domainmodels.MemberAccess.createIn(orderSearchEntityUserAccessRule);
        orderSearchEntityUserAccessMinimumValue.attribute = orderSearchMiniumuValueAttribute;
        orderSearchEntityUserAccessMinimumValue.accessRights = domainmodels.MemberAccessRights.ReadWrite;
    
        const orderSearchEntityUserAccessMaximumValue = domainmodels.MemberAccess.createIn(orderSearchEntityUserAccessRule);
        orderSearchEntityUserAccessMaximumValue.attribute = orderSearchMaxiumuValueAttribute;
        orderSearchEntityUserAccessMaximumValue.accessRights = domainmodels.MemberAccessRights.ReadWrite;e

Completion

Nearly finished now! Next, we set the project to “production” (check everything) security level and take the module roles which we created earlier and map them to the project roles.

Then flush the changes and commit.

        // Set up the project level security roles. Set Production level security and put the module roles into the project roles
        const projectSecurity = await model.allProjectSecurities()[0].load();
        projectSecurity.securityLevel = security.SecurityLevel.CheckEverything;
        projectSecurity.checkSecurity = true;
    
        const adminRoleName = projectSecurity.adminUserRoleName;
        const projectAdminRole = projectSecurity.userRoles.find(t => t.name === adminRoleName);
        const projectUserRole = projectSecurity.userRoles.find(t => t.name === "User");
    
        if (projectAdminRole != undefined)
            projectAdminRole.moduleRoles.push(adminRole);
        if (projectUserRole != undefined)
            projectUserRole.moduleRoles.push(userRole);
    
        console.log("Committing changes to the repository");
    
        await model.flushChanges();
        await workingCopy.commitToRepository("trunk", {commitMessage: "App created and modified by my Create App Model SDK script"});
    
    }

So with this script saved into a NodeJS initialized folder (see SDK Blog 1 for how to do that) as a TypeScript file (I called mine ‘createapp.ts’), you can now compile the TypeScript into JavaScript using tsc and use node to execute the JavaScript generated.

With this script run you should now have an app created with an added module and a populated domain model. OK it doesn’t do much yet — we’ll start to put some meat on the bones in part three of this blog series.

A Mendix SDK Primer — Part 2_image of a laptop with a red screen and a pirate flag indicating something didn't work out

Errors

Some words of warning born from experience when using the SDKs to update apps. The SDK system does not always report things that are wrong. When you make a mistake, you could learn of that fact at different points in the process:

  • The TypeScript compiler reports issues when compiling to JavaScript.
  • When you run the JavaScript using node then issues are reported back to you.
  • When you open the app in Studio Pro errors/issues may be reported in the errors tab, or what you created/changed looks wrong.
  • If it’s really bad, then Studio Pro may report exceptions during the open or fail to open the app at all and bite the dust.

This leads me to say: always have a fallback position. Make a backup copy of the project if it’s an existing project. Copy the project and work on the copy. Work on a branch. Maybe all of those…

A Mendix SDK Primer — Part 2_image of a wooden surface with several cards on it, one card contains the phrase- do as I say not as I do

Do as I say, not as I do

OK so many experienced developers are probably cringing at the code above. I’m sure it is obvious that these scripts can get very long and repetitive (domain model changes are simple compared to setting up forms and microflows for example), but I like to keep things as clear as is reasonable, particularly for a blog post. This involves using meaningful variable names and avoiding complex syntax.

The first thing we can do is to use functions which will help to make the script shorter, for example like this:

        createEnumerationOption(model, statusEnumeration, "_New", "New");
        createEnumerationOption(model, statusEnumeration, "Hold", "On Hold");
        createEnumerationOption(model, statusEnumeration, "Progress", "In Progress");
        createEnumerationOption(model, statusEnumeration, "Completed", "Completed");
    
    .....
    
    function createEnumerationOption(model: IModel, enumeration: enumerations.Enumeration, name: string, caption_en_US: string)
    {
        const value = enumerations.EnumerationValue.createIn(enumeration);
        value.name = name;
        const caption = texts.Text.createInEnumerationValueUnderCaption(value);
        const translation = texts.Translation.create(model);
        translation.text = caption_en_US;
        translation.languageCode = "en_US";
        caption.translations.push(translation);
    }

You will probably see the value of building a library of functions and/or classes that you can include and use in a variety of SDK scripts that you write.

But long scripts can also run into performance issues for other reasons. Three things I have learned to reduce memory overhead and improve speed:

  • Frequently call FlushChanges. This moves the changes you have made so far from your local NodeJS environment to the Mendix ModelServer reducing your local memory footprint.
  • Use locally scoped variables where you can — using functions makes it easy to do this. Locally scoped variables are born, live, and die in the block you define and so reduce the accumulation of data that is carried around in the rest of the script. Each variable reference in your script requires a lookup of some sort in the JavaScript internals and the more variables you have defined the longer that takes (and the more memory is consumed).
  • Keep variable names short, or at least shorter, as this will improve execution times and reduce memory overhead in long scripts. Obviously there is a trade-off between brevity and clarity. I have an app which writes TypeScript files to modify apps for me and I can set it to produce meaningful variable names (for debugging) or short variable names (for production) by flicking a switch. Alternatively you might choose to use a separate tool to minify the generated JavaScript before running it, but I have not tried this, so I have no idea how well it works.

In the Github repository mentioned below, I have included two scripts, one simplistic script that is shown in this blog (‘createapp.ts’) and an alternative script that implements some of the changes I have suggested here (‘createapp2.ts’) . Each script produces the same app but the second one is significantly shorter and, I would think, is easier to follow.

So that concludes this post. The scripts are available on Github at https://github.com/Adrian-Preston/SDKBlog2.

I will continue by developing some functionality in part three  — coming soon!