Mendix Model SDK, поддерживаемый Mendix Platform SDK, — это инструмент, который существует уже много лет. Его цель — предоставить разработчикам программный доступ к модели приложения Mendix без использования Studio или Studio Pro.

В этой короткой серии я проиллюстрирую, как можно использовать SDK для выполнения полезных операций с приложением. Этот первый пост предназначен для того, чтобы вы начали с настройки среды разработки с помощью NodeJS, создания скрипта TypeScript для использования SDK, а также выполнения этого скрипта.

Я не хочу, чтобы это было руководство по TypeScript/JavaScript (я вряд ли смогу это сделать, а в Интернете доступно множество хороших ресурсов), и я сосредоточусь на аспектах SDK , а не детали кода.

Почему вы можете захотеть использовать SDK?

Существует большое количество вариантов использования, которые может поддерживать Mendix Model SDK, и это лишь некоторые из них:

Извлечение деталей всей или части модели приложения для перевода на другой носитель.Например, вы можете извлечь информацию из модели для создания собственной документации, или вы можете извлечь логику из микропотоков для создания эквивалентов на другом языке, таком как JavaScript или C#.

Автоматическое обновление приложений для обеспечения соответствия стандартам разработки или безопасности. Например, вы можете захотеть применить общие стандарты именования микропотоков и нанопотоков или потребовать, чтобы сущности имели соответствующие минимальные разрешения на доступ.

Автоматическое создание кода, страниц, объектов и т. д. в приложении на основе параметризованного ввода. Например, вы можете автоматизировать процесс копирования структуры или схемы источника данных и встроить его в приложение Mendix.

Среда разработки

У разработчиков есть свои предпочтения в отношении инструментов. Вам понадобится как минимум NodeJS и редактор скриптов — я использую для редактирования Visual Studio Code, доступный в Visual Studio Code, так как мне нравится его поддержка TypeScript. Я не буду предлагать какие-либо причудливые установки и настройки для среды, а просто сделаю все просто — единая папка для хранения скриптов, которые я создаю.

Существует много документации, относящейся к SDK платформы и модели, доступной на странице документов ниже.



Установка NodeJS

Загрузите и установите последнюю стабильную версию NodeJS, которую можно найти в NodeJS со страницей загрузки на английском языке в NodeJS English Downloads.



Если у вас уже установлена ​​более ранняя версия NodeJS и вы хотите сохранить ее, вы можете использовать такой инструмент, как диспетчер версий NodeJS nvm, который позволит вам устанавливать несколько версий NodeJS и управлять ими, а также переключаться между ними. Выбор менеджеров пакетов описан в разделе Менеджеры пакетов NodeJS.

Создайте рабочую папку и инициализируйте ее

Затем мне нужно куда-то поместить свою работу, поэтому я создаю папку для работы. Затем я инициализирую ее с помощью диспетчера пакетов NodeJS и проверяю, установлен ли пакет TypeScript.

mkdir SDKBlog
cd SDKBlog
npm init --yes
npm install -g typescript

Затем переключитесь в редактор и создайте или отредактируйте файл в папке с именем package.json и измените этот файл, включив в него зависимости для пакетов SDK. Это должно выглядеть примерно так:

{
    "name": "sdkblog",
    "version": "1.0.0",
    "main": "index.js",
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "keywords": [],
    "author": "",
    "license": "ISC",
    "dependencies": {
        "mendixmodelsdk": "^4.56.0",
        "mendixplatformsdk": "^5.0.0"
    },
    "devDependencies": {},
    "description": ""
}

Загрузите пакеты SDK с помощью npm install. Это создаст подпапку с именем «node_modules», в которой будет храниться иерархия различных файлов пакетов.

npm install

Наконец, создайте или отредактируйте файл tsconfig.json файл, чтобы указать параметры компилятора и имя создаваемого файла TypeScript. Каждый раз, когда вы добавляете новый файл TypeScript в папку, вы можете добавить его в файл tsconfig.json, а затем, когда вы запустите команду компилятора TypeScript «tsc», она скомпилирует все файлы в JavaScript, чтобы их можно было выполнить.

{
    "compilerOptions" : {
        "module" : "commonjs",
        "target" : "es2020",
        "strict": true
    },
    "files" : [
        "showdocument.ts"
    ]
}

Получите токен личного доступа

Вам нужно будет зайти на сайт Mendix Warden в Mendix Warden. Оказавшись там, вам нужно будет войти в систему, используя свои учетные данные портала разработчиков Mendix.

Создайте токен личного доступа для доступа к функциям репозитория, например:

Сохраните сгенерированный токен в переменной среды с именем MENDIX_TOKEN. Инструкции о том, как это сделать, доступны на Странице настройки Mendix PAT.

Выполнив это, теперь вы должны быть готовы к использованию SDK.

Приложение к скрипту JavaScript

Сценарий, который я напишу здесь, является полезным инструментом, который вы можете использовать при работе с Mendix SDK.

Он извлечет модель из существующего приложения Mendix и найдет документ в модели, которую вы укажете, и выведет определение этого документа в виде кода JavaScript.

Поверьте мне, когда вы начнете работать с Mendix SDK, вы, вероятно, будете использовать его снова и снова, поскольку нет ничего лучше, чем взглянуть на существующий пример, чтобы лучше понять, как использовать SDK и модель приложения.

Сценарий находится на Github, а ссылка на проект Github находится в конце этого сообщения в блоге ниже.

Предварительные

Сценарий открывается, ожидая, что командная строка будет содержать псевдоним проекта (любое имя, которое вы хотите) и полное имя документа Mendix (микропоток/форма/перечисление) — так что это будет просто имя модуля, если вы хотите извлечь модель предметной области или имя модуля плюс точка плюс имя документа. Если вы обращаетесь к другому проекту в первый раз, вам нужно будет добавить идентификатор приложения в командную строку (взятый на вкладке «Общие» на странице портала Mendix Developer для приложения) и имя ветки, если вы не хотите использовать ветку по умолчанию.

import { JavaScriptSerializer } from "mendixmodelsdk";
import { MendixPlatformClient, OnlineWorkingCopy } from "mendixplatformsdk";
import * as fs from "fs";
// Usage: node showdocument.js nickname documentname appID branch
//   nickname is your own name for the app
//   documentname if the qualified name (module.document) of the document to serialize
//   appID is the appID for the app (taken from the Mendix developer portal page)
//   branch is the name of the branch to use
//
// The appID and branch are only needed when setting up a new working copy
//
// The appID, branch name and working copy ID are saved in a file called nickname.workingcopy in the
// current folder so they can be used next time if possible
//
const args = process.argv.slice(2);
main(args);
async function main(args: string[])
{
    var appID = "";
    var branch = "";
    var documentname = "";
    if (args.length < 1)
    {
        console.log(`Need at least a nickname and document name on the command line`);
        return;
    }
    const nickname = args[0].split(' ').join('');
    documentname = args[1];
    if (args.length > 2)
        appID = args[2];
    if (args.length > 3)
        branch = args[3];
    const workingCopyFile = nickname + '.workingcopy';
    var wcFile;
    var wcID;
    try
    {
        wcFile = fs.readFileSync(workingCopyFile).toString();
        appID = wcFile.split(':')[0];
        branch = wcFile.split(':')[1];
        wcID = wcFile.split(':')[2];
    }
    catch
    {
        wcFile = "";
        wcID = "";
        if (appID === "")
        {
            console.log("Need an appID on the command line if no workingcopy file is present for the nickname");
            return;
        }
    }

Когда скрипт будет запущен, он создаст файл с именем, которое вы дали + «.workingcopy», и в нем будет храниться идентификатор приложения, имя ветки и сгенерированный идентификатор рабочей копии. Это делается для того, чтобы при следующем запуске скрипта для того же приложения (псевдоним) он считывал идентификатор рабочей копии, который вы создали последним, и использовал его снова. Это делает процесс намного быстрее.

    const client = new MendixPlatformClient();
    var workingCopy:OnlineWorkingCopy;
    const app = client.getApp(appID);
    var useBranch = branch;
    if (wcID != "")
    {
        try
        {
            console.log("Opening existing working copy");
            workingCopy = app.getOnlineWorkingCopy(wcID);
        }
        catch (e)
        {
            console.log(`Failed to get existing working copy ${wcID}: ${e}`);
            wcID = ""
        }
    }
    if (wcID === "")
    {
        const repository = app.getRepository();
        if ((branch === "") || (branch === "trunk") || (branch === "main"))
        {
            const repositoryInfo = await repository.getInfo();
            if (repositoryInfo.type === "svn")
                useBranch = "trunk";
            else
                useBranch = "main";
        }
        try
        {
            workingCopy = await app.createTemporaryWorkingCopy(useBranch);
            wcID = workingCopy.workingCopyId;
        }
        catch (e)
        {
            console.log(`Failed to create new working copy for app ${appID}, branch ${useBranch}: ${e}`);
            return;
        }
    }
    fs.writeFileSync(workingCopyFile, `${appID}:${useBranch}:${wcID}`);

Использование SDK

Создав/открыв рабочую копию, скрипт теперь открывает модель для приложения, находит указанную модель предметной области или документ, вызывает десериализатор JavaScript и записывает вывод в консоль.

Обратите внимание, что сценарий предполагает, что имя документа без символа точки является именем модуля, и вы хотите извлечь модель предметной области для этого модуля. В противном случае предоставленное имя рассматривается как полное имя документа (module.document).

    const model = await workingCopy!.openModel();
    console.log(`Opening ${documentname}`);
    if (documentname.split(".").length <= 1)
    {
        const domainmodelinterfaces = model.allDomainModels().filter(dm => dm.containerAsModule.name === documentname);
        if (domainmodelinterfaces.length < 1)
            console.log(`Cannot find domain model for ${document}`);
        else
        {
            try
            {
                const domainmodelinterface = domainmodelinterfaces[0];
                const domainmodel = await domainmodelinterface.load();
                 console.log(JavaScriptSerializer.serializeToJs(domainmodel));
            }
            catch(e)
            {
                console.log(`Error occured: ${e}`);
            }
        }
    }
    else
    {
        const documentinterfaces = model.allDocuments().filter(doc => doc.qualifiedName === documentname);
        if (documentinterfaces.length < 1)
            console.log(`Cannot find document for ${document}`);
        else
        {
            try
            {
                const documentinterface = documentinterfaces[0];
                const document = await documentinterface.load();
                console.log(JavaScriptSerializer.serializeToJs(document));
            }
            catch(e)
            {
                console.log(`Error occured: ${e}`);
            }
        }
    }
}

Примеры

Перед запуском скрипта или после того, как вы изменили скрипт, вы должны скомпилировать его из TypeScript в JavaScript с помощью команды «tsc». Это будет работать при условии, что имя файла TypeScript включено в tsconfig.json, как описано ранее.

tsc

В противном случае вы можете скомпилировать конкретный файл TypeScript с помощью такой команды:

tsc showdocument.ts

Во-первых, я вытащил модель домена из модели администрирования с помощью следующей команды. Я выбрал «Фред» в качестве своего псевдонима для приложения.

node showdocument.js fred Administration 8252db0e-6235-40a5-9502-36e324c618d7

Вывод может быть очень длинным, поэтому я покажу только его часть.

    var generalization1 = domainmodels.Generalization.create(model);
    // Note: this is an unsupported internal property of the Model SDK which is subject to change.
     generalization1.__generalization.updateWithRawValue("System.User");
    var stringAttributeType1 = domainmodels.StringAttributeType.create(model);
    var storedValue1 = domainmodels.StoredValue.create(model);
    var fullName1 = domainmodels.Attribute.create(model);
    fullName1.name = "FullName";
    fullName1.type = stringAttributeType1;   // Note: for this property a default value is defined.
    fullName1.value = storedValue1;   // Note: for this property a default value is defined.
    var stringAttributeType2 = domainmodels.StringAttributeType.create(model);
    var storedValue2 = domainmodels.StoredValue.create(model);
    var email1 = domainmodels.Attribute.create(model);
    email1.name = "Email";
    email1.type = stringAttributeType2;   // Note: for this property a default value is defined.
    email1.value = storedValue2;   // Note: for this property a default value is defined.
    var booleanAttributeType1 = domainmodels.BooleanAttributeType.create(model);
    var storedValue3 = domainmodels.StoredValue.create(model);
    storedValue3.defaultValue = "true";
    var isLocalUser1 = domainmodels.Attribute.create(model);
    isLocalUser1.name = "IsLocalUser";
    isLocalUser1.type = booleanAttributeType1;   // Note: for this property a default value is defined.
    isLocalUser1.value = storedValue3;   // Note: for this property a default value is defined.
    var memberAccess1 = domainmodels.MemberAccess.create(model);
    memberAccess1.attribute = model.findAttributeByQualifiedName("Administration.Account.FullName");
    memberAccess1.accessRights = domainmodels.MemberAccessRights.ReadWrite;
    var memberAccess2 = domainmodels.MemberAccess.create(model);
    memberAccess2.attribute = model.findAttributeByQualifiedName("Administration.Account.Email");
    memberAccess2.accessRights = domainmodels.MemberAccessRights.ReadWrite;
    var memberAccess3 = domainmodels.MemberAccess.create(model);
    memberAccess3.attribute = model.findAttributeByQualifiedName("Administration.Account.IsLocalUser");
    memberAccess3.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    var accessRule1 = domainmodels.AccessRule.create(model);
    accessRule1.memberAccesses.push(memberAccess1);
    accessRule1.memberAccesses.push(memberAccess2);
    accessRule1.memberAccesses.push(memberAccess3);
    accessRule1.moduleRoles.push(model.findModuleRoleByQualifiedName("Administration.Administrator"));
    accessRule1.allowCreate = true;
    accessRule1.allowDelete = true;
    var memberAccess4 = domainmodels.MemberAccess.create(model);
    memberAccess4.attribute = model.findAttributeByQualifiedName("Administration.Account.FullName");
    memberAccess4.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    var memberAccess5 = domainmodels.MemberAccess.create(model);
    memberAccess5.attribute = model.findAttributeByQualifiedName("Administration.Account.Email");
    memberAccess5.accessRights = domainmodels.MemberAccessRights.ReadOnly;
    var memberAccess6 = domainmodels.MemberAccess.create(model);
    memberAccess6.attribute = model.findAttributeByQualifiedName("Administration.Account.IsLocalUser");
    var accessRule2 = domainmodels.AccessRule.create(model);
    accessRule2.memberAccesses.push(memberAccess4);
    accessRule2.memberAccesses.push(memberAccess5);
    accessRule2.memberAccesses.push(memberAccess6);
    accessRule2.moduleRoles.push(model.findModuleRoleByQualifiedName("Administration.User"));
    accessRule2.defaultMemberAccessRights = domainmodels.MemberAccessRights.ReadOnly;
    var memberAccess7 = domainmodels.MemberAccess.create(model);
    memberAccess7.attribute = model.findAttributeByQualifiedName("Administration.Account.FullName");
    memberAccess7.accessRights = domainmodels.MemberAccessRights.ReadWrite;
    var memberAccess8 = domainmodels.MemberAccess.create(model);
    memberAccess8.attribute = model.findAttributeByQualifiedName("Administration.Account.Email");
    var memberAccess9 = domainmodels.MemberAccess.create(model);
    memberAccess9.attribute = model.findAttributeByQualifiedName("Administration.Account.IsLocalUser");
    var accessRule3 = domainmodels.AccessRule.create(model);
    accessRule3.memberAccesses.push(memberAccess7);
    accessRule3.memberAccesses.push(memberAccess8);
    accessRule3.memberAccesses.push(memberAccess9);
    accessRule3.moduleRoles.push(model.findModuleRoleByQualifiedName("Administration.User"));
    accessRule3.xPathConstraint = "[id='[%CurrentUser%]']";
    var account1 = domainmodels.Entity.create(model);
    account1.name = "Account";
    account1.location = {"x":220,"y":140};
    account1.generalization = generalization1;   // Note: for this property a default value is defined.
    account1.attributes.push(fullName1);
    account1.attributes.push(email1);
    account1.attributes.push(isLocalUser1);
    account1.accessRules.push(accessRule1);
    account1.accessRules.push(accessRule2);
    account1.accessRules.push(accessRule3);

Затем я вытащил микропоток ChangeMyPassword из модели, выполнив следующую команду. Обратите внимание, что идентификатор приложения не требуется, так как он может использовать существующую рабочую копию, которая только что была создана.

node showdocument.js fred Administration.ChangeMyPassword

Этот вывод еще длиннее, поэтому я снова просто вырезаю часть результатов.

    var expressionSplitCondition1 = microflows.ExpressionSplitCondition.create(model);
    expressionSplitCondition1.expression = "$AccountPasswordData/NewPassword = $AccountPasswordData/ConfirmPassword";
    var exclusiveSplit1 = microflows.ExclusiveSplit.create(model);
    exclusiveSplit1.relativeMiddlePoint = {"x":430,"y":200};
    exclusiveSplit1.size = {"width":130,"height":80};
    exclusiveSplit1.splitCondition = expressionSplitCondition1;   // Note: for this property a default value is defined.
    exclusiveSplit1.caption = "Passwords equal?";
    var translation1 = texts.Translation.create(model);
    translation1.languageCode = "en_US";
    translation1.text = "The new passwords do not match.";
    var translation2 = texts.Translation.create(model);
    translation2.languageCode = "nl_NL";
    translation2.text = "De nieuwe wachtwoorden komen niet overeen.";
    var text1 = texts.Text.create(model);
    text1.translations.push(translation1);
    text1.translations.push(translation2);
    var textTemplate1 = microflows.TextTemplate.create(model);
    textTemplate1.text = text1;   // Note: for this property a default value is defined.
    var showMessageAction1 = microflows.ShowMessageAction.create(model);
    showMessageAction1.template = textTemplate1;   // Note: for this property a default value is defined.
    showMessageAction1.type = microflows.ShowMessageType.Error;
    var actionActivity1 = microflows.ActionActivity.create(model);
    actionActivity1.relativeMiddlePoint = {"x":430,"y":75};
    actionActivity1.size = {"width":120,"height":60};
    actionActivity1.action = showMessageAction1;
    var endEvent1 = microflows.EndEvent.create(model);
    endEvent1.relativeMiddlePoint = {"x":430,"y":-20};
    endEvent1.size = {"width":20,"height":20};
    var startEvent1 = microflows.StartEvent.create(model);
    startEvent1.relativeMiddlePoint = {"x":-220,"y":200};
    startEvent1.size = {"width":20,"height":20};
    var closeFormAction1 = microflows.CloseFormAction.create(model);
    var actionActivity2 = microflows.ActionActivity.create(model);
    actionActivity2.relativeMiddlePoint = {"x":1110,"y":200};
    actionActivity2.size = {"width":120,"height":60};
    actionActivity2.action = closeFormAction1;
    var endEvent2 = microflows.EndEvent.create(model);
    endEvent2.relativeMiddlePoint = {"x":1230,"y":200};
    endEvent2.size = {"width":20,"height":20};
    var memberChange1 = microflows.MemberChange.create(model);
    // Note: this is an unsupported internal property of the Model SDK which is subject to change.
    memberChange1.__attribute.updateWithRawValue("System.User.Password");
    memberChange1.value = "$AccountPasswordData/NewPassword";
    var changeObjectAction1 = microflows.ChangeObjectAction.create(model);
    changeObjectAction1.items.push(memberChange1);
    changeObjectAction1.refreshInClient = true;
    changeObjectAction1.commit = microflows.CommitEnum.Yes;
    changeObjectAction1.changeVariableName = "Account";
    var actionActivity3 = microflows.ActionActivity.create(model);
    actionActivity3.relativeMiddlePoint = {"x":620,"y":200};
    actionActivity3.size = {"width":120,"height":60};
    actionActivity3.action = changeObjectAction1;
    actionActivity3.caption = "Save password";
    actionActivity3.autoGenerateCaption = false;
    var expressionSplitCondition2 = microflows.ExpressionSplitCondition.create(model);
    expressionSplitCondition2.expression = "$OldPasswordOkay";
    var exclusiveSplit2 = microflows.ExclusiveSplit.create(model);
    exclusiveSplit2.relativeMiddlePoint = {"x":230,"y":200};
    exclusiveSplit2.size = {"width":120,"height":80};
    exclusiveSplit2.splitCondition = expressionSplitCondition2;   // Note: for this property a default value is defined.
    exclusiveSplit2.caption = "Old password okay?";
    var endEvent3 = microflows.EndEvent.create(model);
    endEvent3.relativeMiddlePoint = {"x":230,"y":-20};
    endEvent3.size = {"width":20,"height":20};
    var basicCodeActionParameterValue1 = microflows.BasicCodeActionParameterValue.create(model);
    basicCodeActionParameterValue1.argument = "$Account/Name";
    var javaActionParameterMapping1 = microflows.JavaActionParameterMapping.create(model);
    // Note: this is an unsupported internal property of the Model SDK which is subject to change.
    javaActionParameterMapping1.__parameter.updateWithRawValue("System.VerifyPassword.userName");
    javaActionParameterMapping1.parameterValue = basicCodeActionParameterValue1;   // Note: for this property a default value is defined.
    var basicCodeActionParameterValue2 = microflows.BasicCodeActionParameterValue.create(model);
    basicCodeActionParameterValue2.argument = "$AccountPasswordData/OldPassword";
    var javaActionParameterMapping2 = microflows.JavaActionParameterMapping.create(model);
    // Note: this is an unsupported internal property of the Model SDK which is subject to change.
    javaActionParameterMapping2.__parameter.updateWithRawValue("System.VerifyPassword.password");
    javaActionParameterMapping2.parameterValue = basicCodeActionParameterValue2;   // Note: for this property a default value is defined.

Итак, эти фрагменты показывают, что выходные данные могут быть довольно обширными, и они могут намекать на некоторую сложность, которая может возникнуть при использовании SDK, хотя, если вы знакомы с использованием JavaScript/TypeScript, вы, вероятно, будете чувствовать себя намного комфортнее.

Подобные команды можно использовать для извлечения определений для различных типов документов в приложении Mendix. JavaScriptSerializer может предоставить вам код, который можно использовать напрямую (или после модификации) для обновления того или иного приложения, что чрезвычайно полезно. Но…

Немного осторожности

Использование JavaScriptSerializer для извлечения части вашей модели и преобразования ее в JavaScript отлично подходит для того, чтобы показать вам, как вы можете выполнять аналогичные действия с моделью. Верно? Ну, почти.

Сгенерированный код будет работать почти все время, но есть некоторые действия, которые не будут отображаться точно так, как вы должны написать скрипт, обновляющий модель. Например, я обнаружил, что сгенерированный код не работает с моделью, в которой скрипт использует модуль System.

SDK нельзя использовать для доступа к модулю System для чтения или записи, что усложняет работу. Существуют обходные пути, которые позволяют вам ссылаться на что-либо в модуле System, например, в приведенной выше модели домена администрирования в верхней части скрипта создается domainmodels.Generalization, который ссылается на System.User. и который позже используется при создании специализации с именем Account в нижней части скрипта. Синтаксис сценария правильный, но в настоящее время он будет отклонен, если вы попытаетесь запустить его. Вам необходимо заменить:

generalization1.__generalization.updateWithRawValue("System.User");

с:

(generalization1 as any)["_generalization"].updateWithRawValue("System.User");

Есть аналогичные случаи с настройкой параметров вызова Microflow и обращением к атрибутам в Системных сущностях через специализации. Я ожидаю, что есть и другие места, где может возникнуть такая проблема. Эти вопросы находятся в очереди на исправление, но на данный момент вам нужно будет использовать обходной путь, такой как этот чуть выше👆.

Краткое содержание

Я надеюсь, что вы найдете этот пост в блоге и скрипт полезными. В следующем посте о SDK я напишу скрипт для создания совершенно нового приложения и добавлю в него модуль вместе с моделью предметной области и несколько простых сопроводительных документов.

Папка и сценарий, использованные для создания этого сообщения в блоге, а также выходные данные примеров команд доступны на GitHub.



Читать далее









От издателя —

Если вам понравилась эта статья, вы можете найти больше похожих на нашей Medium странице. Для просмотра отличных видео и прямых трансляций вы можете посетить MxLive или наше сообщество Страница YouTubee.

Для создателей, желающих начать работу, вы можете зарегистрировать бесплатную учетную запись и получить мгновенный доступ к обучению в нашей Академии.

Заинтересованы в более активном участии в нашем сообществе? Присоединяйтесь к нам в нашем Канале сообщества Slack.