// ==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);
}
});
})();