Mendix SDK for finding module usages

0
Hi,   I’m trying to use the Mendix SDK to automate the task of analysing usages of modules within a Mendix model. In Studio Pro I can right-click a module and find “usages by” and “usages from” the different modules, which is great, but I would like to automate this a little bit as it is quite time-consuming trying to track usages using Studio Pro this way. So far, I got this, which is only a script that outputs all documents within each module: ----------------------------------------------------------------------------- import { MendixPlatformClient } from "mendixplatformsdk";   async function main() {     const client = new MendixPlatformClient();     const app = client.getApp(“<MyAppId>");     const repositoryInfo = app.getRepository();     console.log(repositoryInfo);     const workingCopy = await app.createTemporaryWorkingCopy("main");     const model = workingCopy.openModel();         // Iterate over the modules in the project     (await model).allModules().forEach((module) => {       console.log('Module: ' + module.name);             // Retrieve documents of each module         const documents = module.documents;         const folders = module.folders;         documents.forEach((document) => {         console.log(' - Document: ' + document.name);         folders.forEach((folder) => {           const subDocuments = folder.documents;           subDocuments.forEach((subDocument) => {             console.log(' - Document: ' + subDocument.name);       });     });   }); })   }   main().catch(console.error); ----------------------------------------------------------------------------- And I got this output: Any help in developing this script further to actually show usages next to each document would be great. I’ve rummaged around the Mendix SDK documents, but couldn’t immediately find anything relevant, and I could very likely be trying to look for something that doesn’t exist, so do let me know if this is possible at all using the SDK or perhaps any other tool. https://apidocs.rnd.mendix.com/platformsdk/latest/index.html https://apidocs.rnd.mendix.com/modelsdk/latest/index.html Thank you so much for the help in advance.
asked
1 answers
1

If anyone is still looking for a solution to this, I managed to get around with this slightly convoluted in my opinion bit of code. It makes use mainly of the reverse-engineering method JavaScriptSerializer.serializeToJs() the SDK provides which neatly contains all references of objects of a document.  

import { microflows } from "mendixmodelsdk";
import { pages } from "mendixmodelsdk";
import { JavaScriptSerializer } from "mendixmodelsdk";
import { domainmodels } from "mendixmodelsdk";
import { documenttemplates } from "mendixmodelsdk";

import { MendixPlatformClient } from "mendixplatformsdk";

async function main() {
  const client = new MendixPlatformClient();
  const app = client.getApp("XXXXXX"); //Replace XXXXXX with your app ID that you get from the developer portal
  const workingCopy = await app.createTemporaryWorkingCopy("main");
  const model = await workingCopy.openModel();
  const modules = model.allModules();

  const usages = new Map<string, string[]>();

  // Add module names to usages map
  modules.forEach((module) => {
    usages.set(module.name, []);
  })

  // Get all page interfaces
  const pageInterfaces = model.allPages();

  // Get page usages
  (await getPageUsages(pageInterfaces, usages));

  // Get all snippet interfaces
  const snippetInterfaces = model.allSnippets();

  // Get snippet usages
  (await getSnippetUsages(snippetInterfaces, usages));

  // Get all domain model interfaces
  const domainModelInterfaces = model.allDomainModels();

  // Get domain model usages
  (await getDomainModelUsages(domainModelInterfaces, usages));

  // Get all layout interfaces
  const layoutInterfaces = model.allLayouts();

  // Get layout usages
  (await getLayoutUsages(layoutInterfaces, usages));

  // Get all document template interfaces
  const documentTemplateInterfaces = model.allDocumentTemplates();

  // Get document template usages
  (await getDocumentTemplateUsages(documentTemplateInterfaces, usages));

  // Get all nanoflow interfaces
  const nanoflowInterfaces = model.allNanoflows();

  // Get nanoflow usages
  (await getNanoflowUsages(nanoflowInterfaces, usages));

  // Get all microflow interfaces
  const microInterfaces = model.allMicroflowBases();

  // Get and print microflow usages
  console.log(await getMicroflowUsages(microInterfaces, usages));

}

async function getMicroflowUsages(microInterfaces: microflows.IMicroflowBase[], usages: Map<string, string[]>) {
  console.log(`Getting microflow usages...`)
  // Initialize variable to hold module name to which microflow belongs
  var microModule: any;

  for (const microInterface of microInterfaces) {
    // Load microflow to get all properties
    const micro = await microInterface.load();
    // Derive module name microflow belongs to from fully qualified name which would be for example Login_EndUser.ACT_Signup
    microModule = micro.qualifiedName?.substring(0, micro.qualifiedName.indexOf("."));

    // Using a for-of loop to exclude microModule from "usages" map so usages refer to those of other modules
    // and we don't get the result that the module is using itself
    const filteredKeys: string[] = [];
    for (const key of usages.keys()) {
      if (key !== microModule) {
        filteredKeys.push(key);
      }
    }

    for (const action of micro.objectCollection.objects) {
      // Get JSON for microflow
      const json = action.toJSON();
      // console.log(json); // <------ use if you need to find out something this script is reporting but Studio Pro isn't
      // Convert it to a String for searching
      const jsonString = JSON.stringify(json);

      for (const usagesKey of filteredKeys) {
        // Search for a reference in action of microflow to Module Name which is represented with each key in map "usages" 
        // and append to value array if it isn't already in there
        if (jsonString.includes(usagesKey) && !usages.get(microModule)?.includes(usagesKey)) {
          usages.get(microModule)?.push(usagesKey);
        }
      }
    }
  }
  return usages;
}

async function getPageUsages(pageInterfaces: pages.IPage[], usages: Map<string, string[]>) {
  console.log(`Getting page usages...`)
  // Initialize variable to hold module name to which page belongs
  var pageModule: any;

  for (const pageInterface of pageInterfaces) {
    // Load page to get all properties
    const page = await pageInterface.load();
    // Derive module name to which page belongs from fully qualified name, which would be for example Login_EndUser.Login_Page
    pageModule = page.qualifiedName?.substring(0, page.qualifiedName.indexOf("."));
    // Get reverse engineered JavaScript that generates page which seems to be the easiest way to 
    // get all different widgets and containers for the page
    const js = JavaScriptSerializer.serializeToJs(page);
    // Convert it to a String for searching
    const jsString = JSON.stringify(js);

    // Using a for-of loop to exclude pageModule from "usages" map so usages refer to those of other modules
    // and we don't get the result that the module is using itself
    const filteredKeys: string[] = [];
    for (const key of usages.keys()) {
      if (key !== pageModule) {
        filteredKeys.push(key);
      }
    }

    for (const usagesKey of filteredKeys) {
      // Search for a reference in page JavaScript to Module with name of usagesKey
      // and append to value array if it isn't already in there
      if (jsString.includes(usagesKey) && !usages.get(pageModule)?.includes(usagesKey)) {
        usages.get(pageModule)?.push(usagesKey);
      }
    }
  }
  return usages;
}

async function getSnippetUsages(snippetInterfaces: pages.ISnippet[], usages: Map<string, string[]>) {
  console.log(`Getting snippet usages...`)
  // Initialize variable to hold module name to which snippet belongs
  var snippetModule: any;

  for (const snippetInterface of snippetInterfaces) {
    // Load snippet to get all properties
    const snippet = await snippetInterface.load();
    // Derive module name to which snippet belongs from fully qualified name, which would be for example ContentEditor_Core.SNIP_MobileButton01
    snippetModule = snippet.qualifiedName?.substring(0, snippet.qualifiedName.indexOf("."));
    // Get reverse engineered JavaScript that generates snippet
    const js = JavaScriptSerializer.serializeToJs(snippet);
    // Convert it to a String for searching
    const jsString = JSON.stringify(js);

    // Using a for-of loop to exclude snippetModule from "usages" map so usages refer to those of other modules
    // and we don't get the result that the module is using itself
    const filteredKeys: string[] = [];
    for (const key of usages.keys()) {
      if (key !== snippetModule) {
        filteredKeys.push(key);
      }
    }

    for (const usagesKey of filteredKeys) {
      // Search for a reference in snippet JavaScript to Module with name of usagesKey
      // and append to value array if it isn't already in there
      if (jsString.includes(usagesKey) && !usages.get(snippetModule)?.includes(usagesKey)) {
        usages.get(snippetModule)?.push(usagesKey);
      }
    }
  }
  return usages;
}

async function getDomainModelUsages(domainModelInterfaces: domainmodels.IDomainModel[], usages: Map<string, string[]>) {
  console.log(`Getting domain model usages...`)
  // Initialize variable to hold module name to which domain model belongs
  var domainModelModule: any;

  for (const domainModelInterface of domainModelInterfaces) {
    // Load domain model to get all properties
    const domainModel = await domainModelInterface.load();
    // Get module name from container name
    domainModelModule = domainModel.containerAsModule.name;
    // Get reverse engineered JavaScript that generates domain model
    const js = JavaScriptSerializer.serializeToJs(domainModel);
    // Convert it to a String for searching
    const jsString = JSON.stringify(js);
    
    // Using a for-of loop to exclude domainModelModule from "usages" map so usages refer to those of other modules
    // and we don't get the result that the module is using itself
    const filteredKeys: string[] = [];
    for (const key of usages.keys()) {
      if (key !== domainModelModule) {
        filteredKeys.push(key);
      }
    }

    for (const usagesKey of filteredKeys) {
      // Search for a reference in domain module JavaScript to Module with name of usagesKey
      // and append to value array if it isn't already in there
      if (jsString.includes(usagesKey) && !usages.get(domainModelModule)?.includes(usagesKey)) {
        usages.get(domainModelModule)?.push(usagesKey);
      }
    }
  }
  return usages;
}

async function getNanoflowUsages(nanoflowInterfaces: microflows.INanoflow[], usages: Map<string, string[]>) {
  console.log(`Getting nanoflow usages...`)
  // Initialize variable to hold module name to which nanoflow belongs
  var nanoflowModule: any;

  for (const nanoflowInterface of nanoflowInterfaces) {
    // Load nanoflow to get all properties
    const nanoflow = await nanoflowInterface.load();
    // Derive module name to which nanoflow belongs from fully qualified name, which would be for example QRManager_EndUser.ACT_Group_CreatWizard_ShortcutValidation
    nanoflowModule = nanoflow.qualifiedName?.substring(0, nanoflow.qualifiedName.indexOf("."));
    // Get reverse engineered JavaScript that generates nanoflow
    const js = JavaScriptSerializer.serializeToJs(nanoflow);
    // Convert it to a String for searching
    const jsString = JSON.stringify(js);

    // Using a for-of loop to exclude nanoflowModule from "usages" map so usages refer to those of other modules
    // and we don't get the result that the module is using itself
    const filteredKeys: string[] = [];
    for (const key of usages.keys()) {
      if (key !== nanoflowModule) {
        filteredKeys.push(key);
      }
    }

    for (const usagesKey of filteredKeys) {
      // Search for a reference in nanoflow JavaScript to Module with name of usagesKey
      // and append to value array if it isn't already in there
      if (jsString.includes(usagesKey) && !usages.get(nanoflowModule)?.includes(usagesKey)) {
        usages.get(nanoflowModule)?.push(usagesKey);
      }
    }
  }
  return usages;
}

async function getLayoutUsages(layoutInterfaces: pages.ILayout[], usages: Map<string, string[]>) {
  console.log(`Getting layout usages...`)
  // Initialize variable to hold module name to which layout belongs
  var layoutModule: any;

  for (const layoutInterface of layoutInterfaces) {
    // Load layout to get all properties
    const layout = await layoutInterface.load();
    // Derive module name to which layout belongs from fully qualified name
    layoutModule = layout.qualifiedName?.substring(0, layout.qualifiedName.indexOf("."));
    // Get reverse engineered JavaScript that generates layout
    const js = JavaScriptSerializer.serializeToJs(layout);
    // Convert it to a String for searching
    const jsString = JSON.stringify(js);

    // Using a for-of loop to exclude layoutModule from "usages" map so usages refer to those of other modules
    // and we don't get the result that the module is using itself
    const filteredKeys: string[] = [];
    for (const key of usages.keys()) {
      if (key !== layoutModule) {
        filteredKeys.push(key);
      }
    }

    for (const usagesKey of filteredKeys) {
      // Search for a reference in layout JavaScript to Module with name of usagesKey
      // and append to value array if it isn't already in there
      if (jsString.includes(usagesKey) && !usages.get(layoutModule)?.includes(usagesKey)) {
        usages.get(layoutModule)?.push(usagesKey);
      }
    }
  }
  return usages;
}


async function getDocumentTemplateUsages(documentTemplateInterfaces: documenttemplates.IDocumentTemplate[], usages: Map<string, string[]>) {
  console.log(`Getting document template usages...`)
  // Initialize variable to hold module name to which documentTemplate belongs
  var documentTemplateModule: any;

  for (const documentTemplateInterface of documentTemplateInterfaces) {
    // Load documentTemplate to get all properties
    const documentTemplate = await documentTemplateInterface.load();
    // Derive module name to which documentTemplate belongs from fully qualified name
    documentTemplateModule = documentTemplate.qualifiedName?.substring(0, documentTemplate.qualifiedName.indexOf("."));
    // Get reverse engineered JavaScript that generates documentTemplate
    const js = JavaScriptSerializer.serializeToJs(documentTemplate);
    // Convert it to a String for searching
    const jsString = JSON.stringify(js);

    // Using a for-of loop to exclude documentTemplateModule from "usages" map so usages refer to those of other modules
    // and we don't get the result that the module is using itself
    const filteredKeys: string[] = [];
    for (const key of usages.keys()) {
      if (key !== documentTemplateModule) {
        filteredKeys.push(key);
      }
    }

    for (const usagesKey of filteredKeys) {
      // Search for a reference in documentTemplate JavaScript to Module with name of usagesKey
      // and append to value array if it isn't already in there
      if (jsString.includes(usagesKey) && !usages.get(documentTemplateModule)?.includes(usagesKey)) {
        usages.get(documentTemplateModule)?.push(usagesKey);
      }
    }
  }
  return usages;
}

main().catch(console.error);

 

It eventually outputs a neat map with all modules and their references to other modules, similar to what the “Find other module usages by this module” function in Studio Pro would do.

PS > node .\script.js 
Creating temporary working copy for branch 'main'...
Successfully created temporary working copy with id '90ab5b0c-0906-4987-9222-6c0be19221b7' based on branch 'main'
Getting page usages...
Getting snippet usages...
Getting domain model usages...
Getting layout usages...
Getting document template usages...
Getting nanoflow usages...
Getting microflow usages...
Map(34) {
  'ForgotPassword' => [
    'Atlas_Core',
    'Email_Connector',
    'Administration',
    'Encryption',
    'DeepLink',
    'MxModelReflection',
    'Login_EndUser'
  ],
  'ModuleTemplate_EndUser' => [
    'ModuleTemplate_Core',
    'DonQR_Theming',
    'Atlas_Core',
    'ContentEditor_Core'
  ],
  'FeedbackModule' => [],
  'Atlas_Web_Content' => [],
  'ExcelImporter' => [ 'Atlas_Core', 'MxModelReflection' ],
  'DragAndDrop_Foundation' => [],
  'Administration' => [ 'Atlas_Core' ],
  'AppSetup_Foundation' => [ 'DeepLink' ],
  'QRManager_EndUser' => [
    'QRGenerator_Core',
    'ContentEditor_Core',
    'QRManager_Core',
    'DonQR_Theming',
    'ForgotPassword',
    'Atlas_Core',
    'XLSReport',
    'ImageCrop',
    'DragAndDrop_Foundation',
    'NanoflowCommons',
    'WebActions',
    'CommunityCommons',
    'QRGenerator_Foundation',
    'Login_Core'
  ],
  'CommunityCommons' => [],
  'Atlas_Core' => [],
  'QRGenerator_Foundation' => [],
  'WebActions' => [],
  'QRConnect_EndUser' => [
    'ContentEditor_Core',
    'QRManager_Core',
    'DonQR_Theming',
    'QRGenerator_Core',
    'RESTDeeplinkModule',
    'CommunityCommons'
  ],
  'XLSReport' => [ 'Atlas_Core', 'MxModelReflection' ],
  'QRGenerator_Core' => [
    'QRManager_Core',
    'QRGenerator_Foundation',
    'CommunityCommons',
    'AppSetup_Foundation',
    'Barcode'
  ],
  'RESTDeeplinkModule' => [ 'DeepLink' ],
  'AppSetup_EndUser' => [
    'QRManager_Core',
    'DonQR_Theming',
    'Atlas_Core',
    'ImageCrop',
    'TaskQueueHelpers',
    'ContentEditor_Core'
  ],
  'Barcode' => [],
  'Encryption' => [],
  'Login_EndUser' => [ 'Administration', 'Login_Core', 'DonQR_Theming', 'ForgotPassword' ],
  'Login_Core' => [ 'Administration' ],
  'DomainModel_MockUp' => [],
  'DeepLink' => [],
  'Email_Connector' => [ 'Atlas_Core', 'Encryption', 'MxModelReflection', 'ForgotPassword' ],
  'ContentEditor_Core' => [
    'Atlas_Core',
    'DonQR_Theming',
    'QRGenerator_Core',
    'ImageCrop',
    'QRManager_Core',
    'ModuleTemplate_Core',
    'NanoflowCommons',
    'CommunityCommons',
    'DragAndDrop_Foundation'
  ],
  'ImageCrop' => [],
  'QRManager_Core' => [
    'QRGenerator_Core',
    'Login_Core',
    'DonQR_Theming',
    'QRManager_EndUser',
    'XLSReport',
    'CommunityCommons',
    'ContentEditor_Core'
  ],
  'ModuleTemplate_Core' => [],
  'NanoflowCommons' => [],
  'DonQR_Theming' => [ 'Login_Core', 'Atlas_Core', 'QRManager_EndUser' ],
  'DataWidgets' => [],
  'MxModelReflection' => [],
  'TaskQueueHelpers' => [ 'Atlas_Core' ]
}


Would be interested to know if anyone can come up with any more efficient way to do this.

answered