Microsoft Power Platform/Dynamics 365 CE - Generate TypeScript Definitions

Automatically creates TypeScript type definitions compatible with @types/xrm by extracting form attributes and controls from Dynamics 365/Power Platform model-driven applications.

You will need to install an extension such as Tampermonkey, Greasemonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install a user script manager extension to install this script.

(I already have a user script manager, let me install it!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==UserScript==
// @name         Microsoft Power Platform/Dynamics 365 CE - Generate TypeScript Definitions
// @namespace    https://github.com/gncnpk/xrm-generate-ts-overloads
// @author       Gavin Canon-Phratsachack (https://github.com/gncnpk)
// @version      1.998
// @license      MIT
// @description  Automatically creates TypeScript type definitions compatible with @types/xrm by extracting form attributes and controls from Dynamics 365/Power Platform model-driven applications.
// @match        https://*.dynamics.com/main.aspx?appid=*&pagetype=entityrecord&etn=*&id=*
// @grant        none
// ==/UserScript==

(function () {
  "use strict";

  const groupItemsByType = (items) => {
    return Object.entries(items).reduce((acc, [itemName, itemType]) => {
      if (!acc[itemType]) {
        acc[itemType] = [];
      }
      acc[itemType].push(itemName);
      return acc;
    }, {});
  };
  const stripNonAlphaNumeric = (str) => {
    return str.replace(/\W/g, "");
  };
  // Create a button element and style it to be fixed in the bottom-right corner.
  const btn = document.createElement("button");
  btn.textContent = "Generate TypeScript Definitions";
  btn.style.position = "fixed";
  btn.style.bottom = "20px";
  btn.style.right = "20px";
  btn.style.padding = "10px";
  btn.style.backgroundColor = "#007ACC";
  btn.style.color = "#fff";
  btn.style.border = "none";
  btn.style.borderRadius = "5px";
  btn.style.cursor = "pointer";
  btn.style.zIndex = 10000;
  document.body.appendChild(btn);

  btn.addEventListener("click", () => {
    // Mapping objects for Xrm attribute and control types.
    var attributeTypeMapping = {
      boolean: "Xrm.Attributes.BooleanAttribute",
      datetime: "Xrm.Attributes.DateAttribute",
      decimal: "Xrm.Attributes.NumberAttribute",
      double: "Xrm.Attributes.NumberAttribute",
      integer: "Xrm.Attributes.NumberAttribute",
      lookup: "Xrm.Attributes.LookupAttribute",
      memo: "Xrm.Attributes.StringAttribute",
      money: "Xrm.Attributes.NumberAttribute",
      multiselectoptionset: "Xrm.Attributes.MultiselectOptionSetAttribute",
      optionset: "Xrm.Attributes.OptionSetAttribute",
      string: "Xrm.Attributes.StringAttribute",
    };

    var controlTypeMapping = {
      standard: "Xrm.Controls.StandardControl",
      iframe: "Xrm.Controls.IframeControl",
      lookup: "Xrm.Controls.LookupControl",
      optionset: "Xrm.Controls.OptionSetControl",
      "customsubgrid:MscrmControls.Grid.GridControl":
        "Xrm.Controls.GridControl",
      subgrid: "Xrm.Controls.GridControl",
      timelinewall: "Xrm.Controls.TimelineWall",
      quickform: "Xrm.Controls.QuickFormControl",
      formcomponent: "Xrm.FormContext",
    };

    var specificControlTypeMapping = {
      boolean: "Xrm.Controls.BooleanControl",
      datetime: "Xrm.Controls.DateControl",
      decimal: "Xrm.Controls.NumberControl",
      double: "Xrm.Controls.NumberControl",
      integer: "Xrm.Controls.NumberControl",
      lookup: "Xrm.Controls.LookupControl",
      memo: "Xrm.Controls.StringControl",
      money: "Xrm.Controls.NumberControl",
      multiselectoptionset: "Xrm.Controls.MultiselectOptionSetControl",
      optionset: "Xrm.Controls.OptionSetControl",
      string: "Xrm.Controls.StringControl",
    };

    // Object to hold the type information.
    const typeInfo = {
      subGrids: {},
      quickViews: {},
      formAttributes: {},
      formControls: {},
      subForms: {},
      possibleEnums: [],
      formTabs: {},
      formEnums: {},
    };

    class Form {
      constructor() {
        this.attributes = {};
        this.controls = {};
        this.enums = {};
        this.subGrids = {};
        this.quickViews = {};
      }
    }
    class Subgrid {
      constructor() {
        this.attributes = {};
        this.enums = {};
      }
    }
    class QuickForm {
      constructor() {
        this.attributes = {};
        this.controls = {};
        this.enums = {};
      }
    }

    class Tab {
      constructor() {
        this.sections = {};
      }
    }

    const currentFormName = stripNonAlphaNumeric(
      Xrm.Page.ui.formSelector.getCurrentItem().getLabel()
    );

    // Loop through all controls on the form.
    function getControls(formContext, controlObject) {
      if (
        typeof formContext !== "undefined" &&
        formContext &&
        typeof formContext.getControl === "function"
      ) {
        formContext.getControl().forEach((ctrl) => {
          const ctrlType = ctrl.getControlType();
          const mappedType = controlTypeMapping[ctrlType];
          if (mappedType) {
            controlObject[ctrl.getName()] = mappedType;
          }
        });
      } else {
        alert("Xrm.Page is not available on this page.");
        return;
      }
    }

    getControls(Xrm.Page, typeInfo.formControls);

    // Loop through all tabs and sections on the form.
    if (typeof Xrm.Page.ui.tabs.get === "function") {
      Xrm.Page.ui.tabs.get().forEach((tab) => {
        let formTab = (typeInfo.formTabs[stripNonAlphaNumeric(tab.getName())] =
          new Tab());
        tab.sections.forEach((section) => {
          formTab.sections[
            stripNonAlphaNumeric(section.getName())
          ] = `${stripNonAlphaNumeric(section.getName())}_section`;
        });
      });
    }

    // Loop through all attributes on the form.
    function getAttributes(
      formContext,
      attributesObject,
      controlsObject,
      enumsObject
    ) {
      if (typeof formContext.getAttribute === "function") {
        formContext.getAttribute().forEach((attr) => {
          const attrType = attr.getAttributeType();
          const attrName = attr.getName();
          const mappedType = attributeTypeMapping[attrType];
          const mappedControlType = specificControlTypeMapping[attrType];
          if (mappedType) {
            attributesObject[attrName] = mappedType;
            attr.controls.forEach((ctrl) => {
              controlsObject[ctrl.getName()] = mappedControlType;
            });
          }
          if (
            attr.getAttributeType() === "optionset" &&
            attr.controls.get().length > 0
          ) {
            const enumValues = attr.getOptions();
            if (enumValues) {
              enumsObject[attrName] = { attribute: "", values: [] };
              enumsObject[attrName].values = enumValues;
              enumsObject[attrName].attribute = attrName;
              attributesObject[attrName] = `${attrName}_attribute`;
            }
          }
        });
      }
    }
    getAttributes(
      Xrm.Page,
      typeInfo.formAttributes,
      typeInfo.formControls,
      typeInfo.formEnums
    );

    // Loop through all subgrids on the form.
    function getSubGrids(formContext, subGridsObject, controlsObject) {
      if (typeof formContext.getControl === "function") {
        formContext.getControl().forEach((ctrl) => {
          if (
            ctrl.getControlType() === "subgrid" ||
            ctrl.getControlType() ===
              "customsubgrid:MscrmControls.Grid.GridControl"
          ) {
            const gridRow = ctrl.getGrid().getRows().get(0);
            const gridName = ctrl.getName();
            let subgrid = (subGridsObject[gridName] = new Subgrid());
            controlsObject[gridName] = `${gridName}_gridcontrol`;
            if (gridRow !== null) {
              gridRow.data.entity.attributes.forEach((attr) => {
                const attrType = attr.getAttributeType();
                const attrName = attr.getName();
                const mappedType = attributeTypeMapping[attrType];
                if (mappedType) {
                  subgrid.attributes[attrName] = mappedType;
                }
                if (
                  attr.getAttributeType() === "optionset" &&
                  attr.controls.get().length > 0
                ) {
                  const enumValues = attr.getOptions();
                  if (enumValues) {
                    subgrid.enums[attrName] = { attribute: "", values: [] };
                    subgrid.enums[attrName].values = enumValues;
                    subgrid.enums[attrName].attribute = attrName;
                    subgrid.attributes[attrName] = `${attrName}_attribute`;
                  }
                }
              });
            }
          }
        });
      }
    }
    getSubGrids(Xrm.Page, typeInfo.subGrids, typeInfo.formControls);

    if (typeof Xrm.Page.getControl === "function") {
      Xrm.Page.getControl().forEach((ctrl) => {
        if (ctrl.getControlType() === "formcomponent") {
          let formObject = (typeInfo.subForms[`${ctrl.getName()}`] =
            new Form());
          try {
            getControls(ctrl, formObject.controls);
            getAttributes(
              ctrl,
              formObject.attributes,
              formObject.controls,
              formObject.enums
            );
            getSubGrids(ctrl, formObject.subGrids, formObject.controls);
            getQuickViews(ctrl, formObject.quickViews, formObject.controls);
          } catch {}
        }
      });
    }
    function generateEnums(possibleEnums, enumsObject) {
      for (const [originalEnumName, enumValues] of Object.entries(
        enumsObject
      )) {
        if (possibleEnums.includes(originalEnumName)) {
          continue;
        }
        possibleEnums.push(originalEnumName);
        let enumName = `${originalEnumName}_enum`;
        let enumTemplate = [];
        let textLiteralTypes = [];
        let valueLiteralTypes = [];
        for (const enumValue of enumValues.values) {
          enumTemplate.push(
            `   ${enumValue.text.replace(/\W/g, "").replace(/[0-9]/g, "")} = ${
              enumValue.value
            }`
          );
          textLiteralTypes.push(`"${enumValue.text}"`);
          valueLiteralTypes.push(
            `${enumName}.${enumValue.text
              .replace(/\W/g, "")
              .replace(/[0-9]/g, "")}`
          );
        }
        outputTS += `
const enum ${enumName} {
${enumTemplate.join(",\n")}
}
`;
        outputTS += `
interface ${enumValues.attribute}_value extends Xrm.OptionSetValue {
text: ${textLiteralTypes.join(" | ")};
value: ${valueLiteralTypes.join(" | ")};  
}
`;
        outputTS += `
interface ${enumValues.attribute}_attribute extends Xrm.Attributes.OptionSetAttribute<${enumName}> {
   `;
        valueLiteralTypes.forEach((value, index) => {
          outputTS += `getOption(value: ${value}): {text: ${textLiteralTypes[index]}, value: ${value}};\n`;
        });
        outputTS += `getOption(value: ${enumValues.attribute}_value['value']): ${enumValues.attribute}_value | null;
  getOptions(): ${enumValues.attribute}_value[];
  getSelectedOption(): ${enumValues.attribute}_value | null;
  getValue(): ${enumName} | null;
  setValue(value: ${enumName} | null): void;
  getText(): ${enumValues.attribute}_value['text'] | null;
}
`;
      }
    }

    function generateLiteralsTypesUnionsAndCollection(
      unionAndCollectionName,
      literalsAndTypesObject,
      defaultType = "unknown",
      generateCollection = true,
      useLiteralAndAppendToType = ""
    ) {
      if (generateCollection) {
        outputTS += `
  interface ${unionAndCollectionName} extends Xrm.Collection.ItemCollection<${unionAndCollectionName}_types> {`;
        if (useLiteralAndAppendToType) {
          for (const [literal, type] of Object.entries(
            literalsAndTypesObject
          )) {
            outputTS += `get(itemName: "${literal}"): ${literal}${useLiteralAndAppendToType};\n`;
          }
        } else {
          for (const [type, literal] of Object.entries(
            groupItemsByType(literalsAndTypesObject)
          )) {
            outputTS += `get(itemName: "${literal.join('" | "')}"): ${type};\n`;
          }
        }
        outputTS += `
      get(itemName: ${unionAndCollectionName}_literals): ${unionAndCollectionName}_types;
      get(itemNameOrIndex: string | number): ${unionAndCollectionName} | null;
      get(delegate?): ${unionAndCollectionName}_types[];
      }
  `;
      }
      if (useLiteralAndAppendToType) {
        outputTS += `
  type ${unionAndCollectionName}_types = ${new Set(
          Object.keys(literalsAndTypesObject)
        )
          .map((literal) => `${literal}${useLiteralAndAppendToType}`)
          .join(" | ")}${
          Object.keys(literalsAndTypesObject).length === 0 ? defaultType : ""
        };\n`;
      } else {
        outputTS += `
  type ${unionAndCollectionName}_types = ${new Set(
          Object.values(literalsAndTypesObject)
        )
          .map((type) => `${type}`)
          .join(" | ")}${
          Object.keys(literalsAndTypesObject).length === 0 ? defaultType : ""
        };\n`;
      }

      outputTS += `
  type ${unionAndCollectionName}_literals = ${new Set(
        Object.keys(literalsAndTypesObject)
      )
        .map((literal) => `"${literal}"`)
        .join(" | ")}${
        Object.keys(literalsAndTypesObject).length === 0 ? '""' : ""
      };\n`;
    }

    function generateSubgridTypes(subgridName) {
      outputTS += `
  interface ${subgridName}_entity extends Xrm.Entity {
    attributes: ${subgridName}_attributes;
  }
  interface ${subgridName}_data extends Xrm.Data {
    entity: ${subgridName}_entity;
  }
  
  interface ${subgridName}_gridrow extends Xrm.Controls.Grid.GridRow {
    data: ${subgridName}_data;
  }
  
  interface ${subgridName}_grid extends Xrm.Controls.Grid {
    getRows(): Xrm.Collection.ItemCollection<${subgridName}_gridrow>;
  }
  
  interface ${subgridName}_gridcontrol extends Xrm.Controls.GridControl {
    getGrid(): ${subgridName}_grid;
  }`;
    }
    function generateContext(
      formName,
      contextType,
      attributesObject,
      controlsObject,
      uiType = null
    ) {
      let contextSuffix;
      if (contextType === "Xrm.FormContext") {
        contextSuffix = `context`;
      } else if (contextType === "Xrm.Controls.QuickFormControl") {
        contextSuffix = `quickformcontrol`;
      }
      outputTS += `
      interface ${formName}_${contextSuffix} extends ${contextType} {`;
      if (uiType) {
        outputTS += `ui: ${uiType};`;
      }
      if (attributesObject) {
        for (const [attrType, attrNames] of Object.entries(
          groupItemsByType(attributesObject)
        )) {
          outputTS += `getAttribute(attributeName: "${attrNames.join(
            '" | "'
          )}"): ${attrType};\n`;
        }
        outputTS += `getAttribute(attributeName: ${formName}_attributes_literals): ${formName}_attributes_types;`;
        outputTS += `getAttribute(attributeNameOrIndex: string | number): Xrm.Attributes.Attribute | null;`;
        outputTS += `getAttribute(delegateFunction?): ${formName}_attributes[];`;
      }
      if (controlsObject) {
        for (const [controlType, controlNames] of Object.entries(
          groupItemsByType(controlsObject)
        )) {
          outputTS += `getControl(controlName: "${controlNames.join(
            '" | "'
          )}"): ${controlType};\n`;
        }
        outputTS += `getControl(controlName: ${formName}_controls_literals): ${formName}_controls_types;`;
        outputTS += `getControl(controlNameOrIndex: string | number): Xrm.Controls.Control | null;`;
        outputTS += `getControl(delegateFunction?): ${formName}_controls_types[];`;
      }
      outputTS += `}`;
      outputTS += `
  interface ${formName}_eventcontext extends Xrm.Events.EventContext {
    getFormContext(): ${formName}_${contextSuffix};
}`;
    }
    // Loop through all Quick View controls and attributes on the form.
    function getQuickViews(formContext, quickViewsObject, controlsObject) {
      if (typeof formContext.ui.quickForms.get === "function") {
        formContext.ui.quickForms.get().forEach((ctrl) => {
          const quickViewName = ctrl.getName();
          let quickView = (quickViewsObject[quickViewName] = new QuickForm());
          controlsObject[quickViewName] = `${quickViewName}_quickformcontrol`;
          ctrl.getControl().forEach((subCtrl) => {
            if (typeof subCtrl.getAttribute !== "function") {
              return;
            }
            const subCtrlAttrType = subCtrl.getAttribute().getAttributeType();
            const mappedControlType =
              specificControlTypeMapping[subCtrlAttrType] ??
              controlTypeMapping[subCtrl.getControlType()];
            if (mappedControlType) {
              quickView.controls[subCtrl.getName()] = mappedControlType;
            }
          });
          ctrl.getAttribute().forEach((attr) => {
            const attrType = attr.getAttributeType();
            const attrName = attr.getName();
            const mappedType = attributeTypeMapping[attrType];
            if (mappedType) {
              quickView.attributes[attrName] = mappedType;
            }
            if (attrType === "optionset" && attr.controls.get().length > 0) {
              const enumValues = attr.getOptions();
              if (enumValues) {
                quickView.enums[attrName] = { attribute: "", values: [] };
                quickView.enums[attrName].values = enumValues;
                quickView.enums[attrName].attribute = attrName;
                quickView.attributes[attrName] = `${attrName}_attribute`;
              }
            }
          });
        });
      }
    }
    getQuickViews(Xrm.Page, typeInfo.quickViews, typeInfo.formControls);

    // Build the TypeScript overload string.
    let outputTS = `// These TypeScript definitions were generated automatically on: ${new Date().toDateString()}\n`;
    generateEnums(typeInfo.possibleEnums, typeInfo.formEnums);
    for (let [subgridName, subgrid] of Object.entries(typeInfo.subGrids)) {
      subgridName = subgridName.replace(/\W/g, "");
      generateEnums(typeInfo.possibleEnums, subgrid.enums);
      generateLiteralsTypesUnionsAndCollection(
        `${subgridName}_attributes`,
        subgrid.attributes,
        "Xrm.Attributes.Attribute",
        true
      );
      generateSubgridTypes(subgridName);
      generateContext(`${subgridName}`, `Xrm.FormContext`, subgrid.attributes);
    }
    for (const [quickViewName, quickView] of Object.entries(
      typeInfo.quickViews
    )) {
      generateEnums(typeInfo.possibleEnums, quickView.enums);
      generateLiteralsTypesUnionsAndCollection(
        `${quickViewName}_attributes`,
        quickView.attributes,
        "Xrm.Attributes.Attribute",
        false
      );
      generateLiteralsTypesUnionsAndCollection(
        `${quickViewName}_controls`,
        quickView.controls,
        "Xrm.Controls.Control",
        false
      );
      generateContext(
        `${quickViewName}`,
        `Xrm.Controls.QuickFormControl`,
        quickView.attributes,
        quickView.controls
      );
    }

    for (const [tabName, tab] of Object.entries(typeInfo.formTabs)) {
      outputTS += `
      type ${tabName}_sections_literals = ${new Set(Object.keys(tab.sections))
        .map((sectionName) => `"${sectionName}"`)
        .join(" | ")}${Object.keys(tab.sections).length === 0 ? '""' : ""};\n`;
      outputTS += `
interface ${tabName}_sections extends Xrm.Collection.ItemCollection<Xrm.Controls.Section> {`;
      outputTS += `get(itemName: ${tabName}_sections_literals): Xrm.Controls.Section;\n`;
      outputTS += `get(itemNameOrIndex: string | number): Xrm.Controls.Section | null;\n`;
      outputTS += `get(delegate?): Xrm.Controls.Section[];\n`;
      outputTS += `}`;

      outputTS += `
interface ${tabName}_tab extends Xrm.Controls.Tab {
  sections: ${tabName}_sections;
    }`;
    }
    generateLiteralsTypesUnionsAndCollection(
      `${currentFormName}_tabs`,
      typeInfo.formTabs,
      "Xrm.Controls.Tab",
      true,
      "_tab"
    );
    generateLiteralsTypesUnionsAndCollection(
      `${currentFormName}_quickforms`,
      typeInfo.quickViews,
      "Xrm.Controls.QuickFormControl",
      true,
      "_quickformcontrol"
    );
    generateLiteralsTypesUnionsAndCollection(
      `${currentFormName}_controls`,
      typeInfo.formControls,
      "Xrm.Controls.Control",
      false
    );
    generateLiteralsTypesUnionsAndCollection(
      `${currentFormName}_attributes`,
      typeInfo.formAttributes,
      "Xrm.Attributes.Attribute",
      false
    );
    outputTS += `
    interface ${currentFormName}_ui extends Xrm.Ui {
      quickForms: ${currentFormName}_quickforms | null;
      tabs: ${currentFormName}_tabs;
    }
    `;
    generateContext(
      currentFormName,
      `Xrm.FormContext`,
      typeInfo.formAttributes,
      typeInfo.formControls,
      `${currentFormName}_ui`
    );

    for (const [formName, formObject] of Object.entries(typeInfo.subForms)) {
      generateEnums(typeInfo.possibleEnums, formObject.enums);
      for (let [subgridName, subgrid] of Object.entries(formObject.subGrids)) {
        subgridName = subgridName.replace(/\W/g, "");
        generateEnums(typeInfo.possibleEnums, subgrid.enums);
        generateLiteralsTypesUnionsAndCollection(
          `${subgridName}_attributes`,
          subgrid.attributes,
          "Xrm.Attributes.Attribute",
          true
        );
        generateSubgridTypes(subgridName);
        generateContext(
          `${subgridName}`,
          `Xrm.FormContext`,
          subgrid.attributes
        );
      }
      for (const [quickViewName, quickView] of Object.entries(
        formObject.quickViews
      )) {
        generateEnums(typeInfo.possibleEnums, quickView.enums);
        generateLiteralsTypesUnionsAndCollection(
          `${quickViewName}_attributes`,
          quickView.attributes,
          "Xrm.Attributes.Attribute",
          false
        );
        generateLiteralsTypesUnionsAndCollection(
          `${quickViewName}_controls`,
          quickView.controls,
          "Xrm.Controls.Control",
          false
        );
        generateContext(
          `${quickViewName}`,
          `Xrm.Controls.QuickFormControl`,
          quickView.attributes,
          quickView.controls
        );
      }
      generateLiteralsTypesUnionsAndCollection(
        `${formName}_attributes`,
        formObject.attributes,
        "Xrm.Attributes.Attribute",
        false
      );
      generateLiteralsTypesUnionsAndCollection(
        `${formName}_controls`,
        formObject.controls,
        "Xrm.Controls.Control",
        false
      );
      generateContext(
        formName,
        `Xrm.FormContext`,
        formObject.attributes,
        formObject.controls
      );
    }

    // Create a new window with a textarea showing the output.
    // The textarea is set to readonly to prevent editing.
    const w = window.open(
      "",
      "_blank",
      "width=600,height=400,menubar=no,toolbar=no,location=no,resizable=yes"
    );
    if (w) {
      w.document.write(
        "<html><head><title>TypeScript Definitions</title></head><body>"
      );
      w.document.write(
        '<textarea readonly style="width:100%; height:90%;">' +
          outputTS +
          "</textarea>"
      );
      w.document.write("</body></html>");
      w.document.close();
    } else {
      // Fallback to prompt if popups are blocked.
      prompt("Copy the TypeScript definition:", outputTS);
    }
  });
})();