A Mendix SDK 入门 — 第 1 部分 | Mendix

跳到主要内容

A Mendix SDK 入门 — 第一部分

A Mendix SDK 入门 — 第一部分

此 Mendix 模型 SDK,由 Mendix Platform SDK 是一个已经存在多年的工具。它的目的是为开发人员提供对 Mendix 应用程序,无需使用 Studio 或 Studio Pro。

在这个简短的系列中,我将说明如何使用 SDK 对应用程序执行有用的操作。第一篇文章将帮助您入门 设置开发环境 使用 NodeJS 创建 TypeScript 脚本 使用 SDK 以及执行该脚本。

我并不打算把这篇文章当成一个 TypeScript/JavaScript 教程(我不太适合做这个,而且网上也有很多很好的资源) 我将重点介绍 SDK 方面 而不是代码细节。

您为什么想要使用 SDK?

有大量的用例 Mendix 模型 SDK 可以支持以下几种:

提取应用程序模型的全部或部分细节,以便转换到不同的媒介中。 例如,您可能希望从模型中提取信息来生成自己的文档,或者您可能希望提取微流中的逻辑 用另一种语言建立对应物 例如 JavaScript 或 C#。

自动更新应用程序以强制遵守开发或安全标准。 例如,您可能希望强制执行通用的微流/纳流命名标准或要求实体应用适当的最低访问权限。

通过参数化输入在应用程序中自动创建代码、页面、实体等。 例如,您可能希望自动复制数据源的结构或模式,并将其构建到 Mendix 应用程序。

开发环境

开发人员在工具方面有自己的偏好。您至少需要 NodeJS 和脚本编辑器——我使用 Visual Studio Code 进行编辑,可从 Visual Studio代码因为我喜欢它的 TypeScript 支持。我不会建议对环境进行任何花哨的设置和配置,而只是保持简单 — 一个文件夹来保存我构建的脚本。

文档页面上有大量与平台和模型 SDK 相关的文档: Mendix 平台SDK.

NodeJS 安装

下载并安装最新的稳定版 NodeJS,可在以下网址找到: 的NodeJS 英文下载页面为 NodeJS 英文下载.

如果您已经安装了早期版本的 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": ""
}

使用 npm install 下载 SDK 包。这将创建一个名为“节点模块' 其中将存储各种包文件的层次结构。

npm安装

最后, 创建或编辑 tsconfig.json 文件 指定编译器选项和正在创建的 TypeScript 文件的名称。每次将新的 TypeScript 文件添加到文件夹时,您都可以将其添加到 tsconfig.json 文件中,然后当您运行 TypeScript 编译器命令“tsc”时,它会将所有文件编译为 JavaScript,以便可以执行它们。

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

获取个人访问令牌

你需要去 Mendix 监狱长站点 Mendix 看守。到达那里后,您需要使用您的 Mendix 开发者门户凭证。

创建个人访问令牌来访问存储库功能,例如:

将生成的令牌保存在名为 MENDIX_TOKEN。有关如何操作的说明,请访问 Mendix PAT 设置页面.

完成此操作后 您现在就可以使用 SDK 了。

应用程序到 JavaScript 脚本

我将在这里编写的脚本是一个有用的工具,你可以在做任何 Mendix SDK 工作。

它将模型从现有的 Mendix 应用程序并在您指定的模型中找到一个文档,并将该文档的定义输出为 JavaScript 代码。

相信我,当你开始使用 Mendix 您可能会一次又一次地使用 SDK,因为没有什么比目睹现有示例更能增强您对如何使用 SDK 和应用程序模型的理解。

该脚本位于 Github 上,Github 项目的链接位于下面这篇博文的末尾。

预赛

该脚本通过期望命令行保存项目昵称(任何你想要的名字)和限定名称来打开 Mendix document (microflow/form/enumeration) — 因此,如果您想要提取域模型,那么这将是模块名称,或者模块名称加上句点加上文档名称。如果您是第一次访问其他项目,则需要将应用程序 ID 添加到命令行(从 Mendix 应用程序的开发者门户页面)以及分支名称(如果您不想使用默认分支)。

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);
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' 命名的文件,并在其中存储应用程序 ID、分支名称和生成的工作副本 ID。这样,下次您为同一应用程序(昵称)运行脚本时,它将读取您上次创建的工作副本 ID 并再次使用该 ID。这使得该过程更快。

    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}`);
            }
        }
 

例子

在运行脚本之前或修改脚本之后,您应该使用命令“tsc”将其从 TypeScript 编译为 JavaScript。只要您在 tsconfig.json 中包含了前面所述的 TypeScript 文件名,此方法便可行。

tsc

否则,您可以使用以下命令编译特定的 TypeScript 文件:

tsc showdocument.ts

首先,我使用以下命令拉取了管理模型的域模型。我选择“fred”作为应用程序的昵称。

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

输出可能很长,因此我仅显示其中的一部分。

   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 微流。请注意,不需要应用程序 ID,因为它可以使用刚刚创建的现有工作副本。

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 非常适合向您展示如何对模型执行类似的操作。对吗?好吧,差不多。

生成的代码几乎总是可以正常工作,但有些操作无法按照编写脚本更新模型的方式呈现。例如,我发现生成的代码 无法针对模型执行 脚本使用 系统 模块。

SDK 不能用于访问 系统 模块用于读取或写入,这是一个复杂的问题。有一些解决方法允许您引用模块中的某些内容 系统 模块,例如,在上面脚本顶部的 Administration 域模型中,创建了一个 domainmodels.Generalization,它引用 System.User,稍后在脚本底部创建名为 Account 的专业化时会用到它。脚本中给出的语法是正确的,但目前如果您尝试运行它,它将被拒绝。您需要替换:

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

使用:

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

 

设置微流调用参数并通过专业化引用系统实体中的属性时也存在类似情况。我预计其他地方也会出现此类问题。这些问题正在等待解决,但目前您需要使用一种解决方法,例如上面的这个👆。

结语

希望您觉得这篇博文和脚本有用。在我的下一篇 SDK 博文中, 我将编写一个脚本来创建一个全新的应用程序,并向其中添加一个模块 以及领域模型和一些简单的支持文档。

用于生成此博客文章的文件夹和脚本以及示例命令的输出可在以下位置找到: GitHub上。

选择你的语言