이 짧은 시리즈에서는 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 패키지를 다운로드하세요.. 이렇게 하면 '라는 하위 폴더가 생성됩니다.node_modules' 다양한 패키지 파일의 계층이 저장되는 곳입니다.
npm 설치
마지막으로, tsconfig.json을 생성하거나 편집합니다. 파일 컴파일러 옵션과 생성되는 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 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를 읽어서 다시 사용하기 위한 것입니다. 이렇게 하면 프로세스가 훨씬 더 빨라집니다.
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의.