この短いシリーズでは、SDKを使用してアプリに対して便利な操作を実行する方法を説明します。この最初の投稿は、 開発環境の設定 NodeJSを使用してTypeScriptスクリプトを作成する SDKを使用するには そのスクリプトを実行することもできます。
これはTypeScript/JavaScriptのチュートリアルではありません(私にはその立場があまりありませんし、オンラインで利用できる優れたリソースはたくさんあります)。 SDKの側面に焦点を当てます コードの詳細ではなく。

SDK を使用する理由は何でしょうか?
多数のユースケースがあり、 Mendix モデル SDK は、次のような機能をサポートできます。
アプリ モデルの全体または一部の詳細を抽出し、別のメディアに翻訳します。 たとえば、モデルから情報を抽出して独自のドキュメントを作成したり、マイクロフローのロジックを抽出したりしたい場合があります。 他の言語で同等のものを構築する JavaScript や C# など。
開発またはセキュリティ標準への準拠を強化するためにアプリを自動的に更新します。 たとえば、共通のマイクロフロー/ナノフロー命名標準を適用したり、エンティティに適切な最小限のアクセス権限を適用することを要求したりする場合があります。
パラメータ化された入力からアプリ内のコード、ページ、エンティティなどを自動的に作成します。 たとえば、データソースの構造やスキーマをコピーするプロセスを自動化し、それを Mendix アプリ。
開発環境
開発者はツールに関して独自の好みを持っています。最低限NodeJSとスクリプトエディタが必要です。私は編集にVisual Studio Codeを使用しています。 Visual Studio Code、TypeScript のサポートが気に入っているからです。環境のセットアップや構成については特に提案しませんが、シンプルに、つまり、作成したスクリプトを格納するフォルダーを 1 つだけにします。
ドキュメント ページには、プラットフォーム SDK とモデル 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パッケージをダウンロードします。. これにより、「node_modules' さまざまなパッケージ ファイルの階層が保存されます。
npmインストール
最後に、 tsconfig.jsonを作成または編集する file コンパイラ オプションと、作成される TypeScript ファイルの名前を指定します。新しい TypeScript ファイルをフォルダーに追加するたびに、それを tsconfig.json ファイルに追加できます。その後、TypeScript コンパイラ コマンド 'tsc' を実行すると、すべてのファイルが JavaScript にコンパイルされ、実行できるようになります。
{
"compilerOptions" : {
"module" : "commonjs",
"target" : "es2020",
"strict": true
},
"files" : [
"showdocument.ts"
]
}
個人アクセストークンを取得する
あなたは、 Mendix 監視員サイト Mendix ウォーデン. そこに到着したら、 Mendix 開発者ポータルの資格情報。
リポジトリ機能にアクセスするための個人アクセス トークンを作成します。例:

生成されたトークンを環境変数に保存します。 メンディックストークンこれを行う方法については、 Mendix PAT セットアップページ.
これを完了すると これで SDK を使用する準備が整いました。
アプリからJavaScriptスクリプトへ
ここで書くスクリプトは、何かを行うときに使える便利なツールです。 Mendix SDK の作業。
既存のモデルからモデルをプルダウンします Mendix アプリは、指定したモデル内のドキュメントを検索し、そのドキュメントの定義を JavaScript コードとして出力します。
信じてください、あなたが Mendix SDK はおそらく何度も使用することになるでしょう。SDK とアプリのモデルの使い方についての理解を深めるには、既存の例をざっと見るのが一番です。
スクリプトは Github にあり、Github プロジェクトへのリンクはこのブログ投稿の最後にあります。
予選
スクリプトは、コマンドラインにプロジェクトのニックネーム(任意の名前)とプロジェクトの修飾名を入力することで開始されます。 Mendix ドキュメント(マイクロフロー/フォーム/列挙)—ドメインモデルを抽出したい場合はモジュール名のみ、またはモジュール名とピリオドとドキュメント名を組み合わせたものになります。初めて別のプロジェクトにアクセスする場合は、コマンドラインにアプリ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 が読み込まれ、再び使用されます。これにより、プロセスが大幅に高速化されます。
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 のドメイン モデルでは、System.User を参照する domainmodels.Generalization が作成され、後でスクリプトの下部にある Account という特殊化を作成するときに使用されます。スクリプトで指定された構文は正しいですが、現在実行しようとすると拒否されます。次のものを置き換える必要があります。
generalization1.__generalization.updateWithRawValue("System.User");
と:
(generalization1 as any)["_generalization"].updateWithRawValue("System.User");
Microflow 呼び出しパラメータの設定や、特殊化によるシステム エンティティの属性の参照にも同様のインスタンスがあります。この種の問題が発生する可能性のある場所は他にもあると思います。これらの問題は修正待ちですが、当面は上記のような回避策を使用する必要があります👆。
製品概要
このブログ記事とスクリプトがお役に立てば幸いです。次のSDK記事では、 新しいアプリを作成するためのスクリプトを書いて、モジュールを追加します ドメイン モデルといくつかの簡単なサポート ドキュメントも含まれています。
このブログ投稿を作成するために使用されたフォルダとスクリプト、およびサンプルコマンドの出力は、以下から入手できます。 GitHubの。