A Mendix Introduction au SDK — Partie 1
Le Mendix Modèle SDK, pris en charge par le Mendix Platform SDK est un outil qui existe depuis des années. Son objectif est de fournir aux développeurs un accès programmatique au modèle d'un Mendix application, sans utiliser Studio ou Studio Pro.
Dans cette courte série, je vais illustrer comment les SDK peuvent être utilisés pour effectuer des opérations utiles sur une application. Ce premier article est destiné à vous aider à démarrer mise en place d'un environnement de développement en utilisant NodeJS, création d'un script TypeScript pour utiliser le SDK ainsi que l'exécution de ce script.
Je n'ai pas l'intention que ce soit un tutoriel TypeScript/JavaScript (je ne suis pas vraiment bien placé pour le faire et il existe de nombreuses ressources intéressantes disponibles en ligne) et Je me concentrerai sur les aspects du SDK plutôt que les détails du code.

Pourquoi pourriez-vous vouloir utiliser le SDK ?
Il existe un grand nombre de cas d'utilisation pour lesquels Mendix Le Model SDK peut prendre en charge, pour n'en citer que quelques-uns :
L'extraction des détails de tout ou partie d'un modèle d'application pour traduction dans un autre support. Par exemple, vous souhaiterez peut-être extraire des informations du modèle pour produire votre propre documentation, ou vous souhaiterez peut-être extraire la logique des microflux. construire des équivalents dans une autre langue comme JavaScript ou C#.
La mise à jour automatisée des applications pour garantir la conformité aux normes de développement ou de sécurité. Par exemple, vous souhaiterez peut-être appliquer des normes de dénomination microflow/nanoflow communes ou exiger que les entités disposent d'autorisations d'accès minimales appropriées.
La création automatisée de code, de pages, d'entités, etc. dans une application à partir d'entrées paramétrées. Par exemple, vous souhaiterez peut-être automatiser le processus de copie de la structure ou du schéma d'une source de données et l'intégrer dans un Mendix app.
Environnement de développement
Les développeurs ont leurs propres préférences en matière d'outils. Vous aurez besoin de NodeJS et d'un éditeur de script au minimum. J'utilise Visual Studio Code pour l'édition, disponible sur Visual Studio Code, car j'aime son support TypeScript. Je ne suggérerai pas de configuration sophistiquée pour l'environnement, mais je garderai les choses simples : un seul dossier pour contenir les scripts que je crée.
Il existe de nombreuses documentations relatives aux SDK de plate-forme et de modèle disponibles sur la page de documentation : Mendix Kit de développement logiciel (SDK) de la plateforme.
Installation de NodeJS
Téléchargez et installez la dernière version stable de NodeJS qui se trouve à l'adresse NodeJS avec la page de téléchargement en anglais sur Téléchargements de NodeJS en anglais.
Si vous avez déjà installé une version antérieure de NodeJS et que vous souhaitez la conserver, vous pouvez utiliser un outil tel que NodeJS Version Manager 'nvm' qui vous permettra d'installer et de gérer plusieurs versions de NodeJS et de basculer entre elles. Une sélection de gestionnaires de paquets est décrite à Gestionnaires de paquets NodeJS.
Créer un dossier de travail et l'initialiser
Ensuite, j'ai besoin d'un endroit où placer mon travail, je crée donc un dossier dans lequel travailler. Ensuite, je l'initialise avec le gestionnaire de packages NodeJS et je m'assure que le package TypeScript est installé.
mkdir SDKBlog cd SDKBlog npm init --yes npm install -g typescript
Passez ensuite à votre éditeur et créez ou modifiez un fichier dans le dossier appelé package.json et modifiez ce fichier pour inclure les dépendances des packages SDK. Cela devrait ressembler à ceci :
{
"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": ""
}
Téléchargez les packages SDK en utilisant npm install. Cela créera un sous-dossier appelé «node_modules" où une hiérarchie de différents fichiers de packages sera stockée.
npm installer
Enfin, des créer ou modifier un tsconfig.json filet pour spécifier les options du compilateur et le nom du fichier TypeScript en cours de création. Chaque fois que vous ajoutez un nouveau fichier TypeScript au dossier, vous pouvez l'ajouter au fichier tsconfig.json, puis lorsque vous exécutez la commande du compilateur TypeScript 'tsc', il compilera tous les fichiers en JavaScript afin qu'ils puissent être exécutés.
{
"compilerOptions" : {
"module" : "commonjs",
"target" : "es2020",
"strict": true
},
"files" : [
"showdocument.ts"
]
}
Obtenez un jeton d'accès personnel
Vous devrez aller à la Mendix Site du gardien à Mendix Gardien. Une fois sur place, vous devrez vous connecter avec votre Mendix Informations d'identification du portail des développeurs.
Créez un jeton d'accès personnel pour accéder aux fonctions du référentiel, par exemple :

Enregistrez le jeton généré dans une variable d’environnement appelée JETON_MENDIX. Des instructions sur la façon de procéder sont disponibles sur le Mendix Page de configuration PAT.
Ayant terminé ceci vous devriez maintenant être prêt à utiliser les SDK.
Application vers script JavaScript
Le script que je vais écrire ici est un outil utile que vous pouvez utiliser lorsque vous faites n'importe quoi Mendix Travail du SDK.
Il extraira le modèle d'un existant Mendix app et recherchez un document dans le modèle que vous spécifiez et générez la définition de ce document sous forme de code JavaScript.
Croyez-moi, lorsque vous commencez à travailler avec le Mendix Vous utiliserez probablement le SDK à maintes reprises, car il n'y a rien de mieux que d'examiner un exemple existant pour améliorer votre compréhension de la manière d'utiliser le SDK et le modèle d'une application.
Le script est sur Github et le lien vers le projet Github se trouve à la fin de cet article de blog ci-dessous.
Préliminaires
Le script s'ouvre en attendant que la ligne de commande contienne un surnom de projet (n'importe quel nom que vous voulez) et le nom qualifié du projet. Mendix document (microflow/form/enumeration) — il s'agira donc simplement du nom du module si vous souhaitez extraire le modèle de domaine, ou du nom du module plus un point plus le nom du document. Si vous accédez à un autre projet pour la première fois, vous devrez ajouter l'ID de l'application à la ligne de commande (extrait de l'onglet Général de l' Mendix Page du portail du développeur pour l'application) et un nom de branche si vous ne souhaitez pas utiliser la branche par défaut.
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;
}
}
Lorsque le script est exécuté, il crée un fichier nommé avec le pseudonyme que vous avez donné + '.workingcopy', et il y stocke l'ID de l'application, le nom de la branche et l'ID de la copie de travail générée. Cela permet que, la prochaine fois que vous exécutez le script pour la même application (pseudo), il lise l'ID de la copie de travail que vous avez créé en dernier et l'utilise à nouveau. Cela rend le processus beaucoup plus rapide.
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}`);
Utilisation du SDK
Après avoir créé/ouvert la copie de travail, le script ouvre maintenant le modèle de l'application, recherche le modèle de domaine ou le document spécifié, appelle le désérialiseur JavaScript et écrit la sortie sur la console.
Notez que le script suppose qu'un nom de document sans point est un nom de module et que vous souhaitez extraire le modèle de domaine de ce module. Sinon, le nom fourni est considéré comme un nom de document qualifié (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}`);
}
}
Exemples
Avant d'exécuter le script, ou après l'avoir modifié, vous devez le compiler à partir de TypeScript en JavaScript à l'aide de la commande « tsc ». Cela fonctionnera à condition que le nom du fichier TypeScript soit inclus dans le fichier tsconfig.json comme décrit précédemment.
tsc
Sinon, vous pouvez compiler un fichier TypeScript spécifique à l'aide d'une commande comme :
tsc showdocument.ts
Tout d'abord, j'ai extrait le modèle de domaine du modèle d'administration à l'aide de la commande suivante. J'ai choisi « fred » comme surnom pour l'application.
node showdocument.js fred Administration 8252db0e-6235-40a5-9502-36e324c618d7>/code>
Le résultat peut être très long, je n'en montrerai donc qu'une partie.
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);
J'ai ensuite extrait le microflux ChangeMyPassword du modèle en exécutant la commande suivante. Notez qu'aucun ID d'application n'était nécessaire car il pouvait utiliser la copie de travail existante qui venait d'être créée.
node showdocument.js fred Administration.ChangeMyPassword
Cette sortie est encore plus longue, donc encore une fois je vais simplement couper une partie des résultats.
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.
OK, donc ces extraits montrent que la sortie peut être assez étendue et peuvent donner une idée de la complexité qui peut résulter de l'utilisation du SDK, bien que si vous êtes familier avec l'utilisation de JavaScript/TypeScript, vous vous sentirez probablement beaucoup plus à l'aise.
Des commandes similaires peuvent être utilisées pour extraire les définitions des différents types de documents dans un Mendix app. JavaScriptSerializer peut vous fournir du code qui peut être utilisé directement (ou après modification) pour mettre à jour cette application ou une autre, ce qui est extrêmement utile. Mais…
Un peu de prudence
Utiliser JavaScriptSerializer pour extraire une partie de votre modèle et la convertir en JavaScript est idéal pour vous montrer comment vous pouvez effectuer des actions similaires sur un modèle. N'est-ce pas ? Enfin presque.
Le code généré fonctionnera presque tout le temps, mais certaines actions ne seront pas rendues exactement de la même manière que vous devez écrire un script pour mettre à jour un modèle. Par exemple, j'ai constaté que le code généré n'est pas exécutable sur un modèle où le script utilise le Système module.
Le SDK ne peut pas être utilisé pour accéder au Système module de lecture ou d'écriture, ce qui est une complication. Il existe des solutions de contournement qui vous permettent de faire référence à quelque chose dans le Système module, donc par exemple dans le modèle de domaine d'Administration ci-dessus en haut du script un domainmodels.Generalization est créé qui fait référence à System.User et qui est utilisé plus tard lors de la création de la spécialisation nommée Account en bas du script. La syntaxe donnée dans le script est correcte mais sera actuellement rejetée si vous essayez de l'exécuter. Vous devez remplacer :
generalization1.__generalization.updateWithRawValue("System.User");
avec:
(generalization1 as any)["_generalization"].updateWithRawValue("System.User");
Il existe des cas similaires avec la définition des paramètres d'appel Microflow et la référence aux attributs dans les entités système via des spécialisations. Je suppose qu'il existe d'autres endroits où ce type de problème peut survenir. Ces problèmes sont en attente de résolution, mais pour le moment, vous devrez utiliser une solution de contournement, comme celle-ci juste au-dessus👆.
Résumé
J'espère que vous trouverez cet article de blog et ce script utiles. Dans mon prochain article sur le SDK, J'écrirai un script pour créer une toute nouvelle application et y ajouter un module accompagné d'un modèle de domaine et de quelques documents de support simples.
Le dossier et le script utilisés pour produire cet article de blog, ainsi que la sortie des exemples de commandes, sont disponibles sur GitHub.