/**
* Functions for Dynamics 365 authentication and user identity
*/
// Import required types
import { ConfidentialClientApplication, } from "@azure/msal-node";
import dotenv from "dotenv";
// Load environment variables from .env file
dotenv.config();
export class Dynamics365 {
clientId;
clientSecret;
tenantId;
d365Url;
msalInstance;
/**
* @param clientId - Azure AD application client ID
* @param clientSecret - Azure AD application client secret
* @param tenantId - Azure AD tenant ID
* @param d365Url - Dynamics 365 instance URL (e.g., https://your-org.crm.dynamics.com)
*/
constructor(clientId, clientSecret, tenantId, d365Url) {
this.clientId = clientId;
this.clientSecret = clientSecret;
this.tenantId = tenantId;
this.d365Url = d365Url;
// Configure MSAL
const msalConfig = {
auth: {
clientId: this.clientId,
authority: `https://login.microsoftonline.com/${this.tenantId}`,
clientSecret: this.clientSecret,
},
};
// Initialize MSAL client
this.msalInstance = new ConfidentialClientApplication(msalConfig);
}
/**
* @returns Promise resolving to authentication result with token
*/
async authenticate() {
const tokenRequest = {
scopes: [`${new URL(this.d365Url).origin}/.default`],
};
try {
const response = await this.msalInstance.acquireTokenByClientCredential(tokenRequest);
if (response && response.accessToken) {
return response.accessToken;
}
else {
throw new Error("Token acquisition failed: response is null or invalid.");
}
}
catch (error) {
console.error("Token acquisition failed:", error);
throw new Error(`Failed to authenticate with Dynamics 365: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Makes an API request to Dynamics 365
* @param endpoint - The API endpoint (relative to the base URL)
* @param method - The HTTP method (e.g., "GET", "POST")
* @param body - The request body (optional, for POST/PUT requests)
* @param additionalHeaders - Additional headers to include in the request
* @returns Promise resolving to the API response
*/
async makeApiRequest(endpoint, method, body, additionalHeaders) {
const token = await this.authenticate();
const baseUrl = this.d365Url.endsWith("/")
? this.d365Url
: `${this.d365Url}/`;
const url = `${baseUrl}${endpoint}`;
const headers = {
Authorization: `Bearer ${token}`,
Accept: "application/json",
"Content-Type": "application/json",
"OData-MaxVersion": "4.0",
"OData-Version": "4.0",
...additionalHeaders,
};
try {
const response = await fetch(url, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
if (!response.ok) {
throw new Error(`API request failed with status: ${response.status}, message: ${await response.text()}`);
}
return await response.json();
}
catch (error) {
console.error(`API request to ${url} failed:`, error);
throw new Error(`Failed to make API request: ${error instanceof Error ? error.message : String(error)}`);
}
}
/**
* Makes a WhoAmI request to Dynamics 365 to get information about the currently logged-in user
* @returns Promise resolving to the user's information
*/
async makeWhoAmIRequest() {
const data = await this.makeApiRequest("api/data/v9.2/WhoAmI", "GET");
// If we want to get more details about the user, we can make an additional request
if (data && data.UserId) {
const userDetails = await this.makeApiRequest(`api/data/v9.2/systemusers(${data.UserId})`, "GET", undefined, { Prefer: 'odata.include-annotations="*"' });
data.UserName = userDetails.domainname;
data.FullName = userDetails.fullname;
}
return data;
}
/**
* Fetches accounts from Dynamics 365
* @returns Promise resolving to the list of accounts
*/
async getAccounts() {
return this.makeApiRequest("api/data/v9.2/accounts", "GET");
}
/**
* Fetches contacts for a given account from Dynamics 365
* @param accountId - The ID of the account for which to retrieve contacts
* @returns Promise resolving to the list of contacts
*/
async getAssociatedContacts(accountId) {
if (!accountId) {
throw new Error("Account ID is required to fetch contacts.");
}
const endpoint = `api/data/v9.2/contacts?$filter=_parentcustomerid_value eq ${accountId}`;
return this.makeApiRequest(endpoint, "GET");
}
/**
* Fetches opportunities for a given account from Dynamics 365
* @param accountId - The ID of the account for which to retrieve opportunities
* @returns Promise resolving to the list of opportunities
*/
async getAssociatedOpportunities(accountId) {
if (!accountId) {
throw new Error("Account ID is required to fetch opportunities.");
}
const endpoint = `api/data/v9.2/opportunities?$filter=_customerid_value eq ${accountId}`;
return this.makeApiRequest(endpoint, "GET");
}
/* create a new account in Dynamics 365
* @param accountData - The data for the new account
* @returns Promise resolving to the created account
*/
async createAccount(accountData) {
if (!accountData) {
throw new Error("Account data is required to create an account.");
}
const endpoint = "api/data/v9.2/accounts";
return this.makeApiRequest(endpoint, "POST", accountData);
}
/**
* Updates an existing account in Dynamics 365
* @param accountId - The ID of the account to update
* @param accountData - The updated data for the account
* @returns Promise resolving to the updated account
*/
async updateAccount(accountId, accountData) {
if (!accountId) {
throw new Error("Account ID is required to update an account.");
}
if (!accountData) {
throw new Error("Account data is required to update an account.");
}
const endpoint = `api/data/v9.2/accounts(${accountId})`;
return this.makeApiRequest(endpoint, "PATCH", accountData);
}
}