dataverse-type-gen
Version:
TypeScript type generator for Dataverse entities and metadata
1,350 lines (1,337 loc) • 228 kB
JavaScript
'use strict';
var identity = require('@azure/identity');
var crypto = require('crypto');
var fs = require('fs');
var path = require('path');
var tsMorph = require('ts-morph');
var commander = require('commander');
var chalk = require('chalk');
var readline = require('readline');
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
var crypto__default = /*#__PURE__*/_interopDefault(crypto);
var chalk__default = /*#__PURE__*/_interopDefault(chalk);
var __defProp = Object.defineProperty;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
}) : x)(function(x) {
if (typeof require !== "undefined") return require.apply(this, arguments);
throw Error('Dynamic require of "' + x + '" is not supported');
});
var __esm = (fn, res) => function __init() {
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
};
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
// src/processors/index.ts
var processors_exports = {};
__export(processors_exports, {
extractOptionSetsFromAttributes: () => extractOptionSetsFromAttributes,
extractRelatedEntityNames: () => extractRelatedEntityNames,
filterAttributesByType: () => filterAttributesByType,
filterAuxiliaryAttributes: () => filterAuxiliaryAttributes,
getLookupAttributes: () => getLookupAttributes,
getOptionSetAttributes: () => getOptionSetAttributes,
getPrimaryAttributes: () => getPrimaryAttributes,
groupAttributesByType: () => groupAttributesByType,
mergeOptionSets: () => mergeOptionSets,
processAttributeMetadata: () => processAttributeMetadata,
processEntityMetadata: () => processEntityMetadata,
processGlobalOptionSet: () => processGlobalOptionSet,
processOptionSet: () => processOptionSet,
validateProcessedMetadata: () => validateProcessedMetadata
});
function extractDisplayName(localizedLabel) {
if (!localizedLabel) return "";
const label = localizedLabel.UserLocalizedLabel?.Label || localizedLabel.LocalizedLabels?.[0]?.Label || "";
return label;
}
function extractRequiredLevel(requiredLevel) {
if (!requiredLevel?.Value) return "None";
switch (requiredLevel.Value) {
case "SystemRequired":
return "SystemRequired";
case "ApplicationRequired":
return "ApplicationRequired";
case "Recommended":
return "Recommended";
default:
return "None";
}
}
function processEntityMetadata(entityMetadata, options) {
const processed = {
logicalName: entityMetadata.LogicalName,
schemaName: entityMetadata.SchemaName,
displayName: extractDisplayName(entityMetadata.DisplayName) || entityMetadata.SchemaName,
description: extractDisplayName(entityMetadata.Description),
primaryIdAttribute: entityMetadata.PrimaryIdAttribute || "",
primaryNameAttribute: entityMetadata.PrimaryNameAttribute || "",
entitySetName: entityMetadata.EntitySetName || "",
isCustomEntity: entityMetadata.IsCustomEntity,
attributes: [],
optionSets: [],
expandableProperties: [],
oneToManyRelationships: [],
manyToOneRelationships: [],
manyToManyRelationships: [],
relatedEntities: {}
};
if (entityMetadata.Attributes) {
const allProcessedAttributes = entityMetadata.Attributes.map((attr) => processAttributeMetadata(attr));
processed.attributes = allProcessedAttributes;
processed.optionSets = extractOptionSetsFromAttributes(entityMetadata.Attributes);
const lookupFields = processed.attributes.filter((attr) => attr.attributeType === "Lookup").map((attr) => attr.schemaName);
processed.expandableProperties = [...lookupFields];
}
if (entityMetadata.OneToManyRelationships) {
processed.oneToManyRelationships = entityMetadata.OneToManyRelationships.map((rel) => ({
schemaName: rel.SchemaName,
referencedEntity: rel.ReferencedEntity,
referencedAttribute: rel.ReferencedAttribute,
referencingEntity: rel.ReferencingEntity,
referencingAttribute: rel.ReferencingAttribute,
isCustomRelationship: rel.IsCustomRelationship
}));
const oneToManyNames = processed.oneToManyRelationships.map((rel) => rel.schemaName);
processed.expandableProperties.push(...oneToManyNames);
}
if (entityMetadata.ManyToOneRelationships) {
processed.manyToOneRelationships = entityMetadata.ManyToOneRelationships.map((rel) => ({
schemaName: rel.SchemaName,
referencedEntity: rel.ReferencedEntity,
referencedAttribute: rel.ReferencedAttribute,
referencingEntity: rel.ReferencingEntity,
referencingAttribute: rel.ReferencingAttribute,
isCustomRelationship: rel.IsCustomRelationship
}));
}
if (entityMetadata.ManyToManyRelationships) {
processed.manyToManyRelationships = entityMetadata.ManyToManyRelationships.map((rel) => ({
schemaName: rel.SchemaName,
entity1LogicalName: rel.Entity1LogicalName,
entity1IntersectAttribute: rel.Entity1IntersectAttribute,
entity2LogicalName: rel.Entity2LogicalName,
entity2IntersectAttribute: rel.Entity2IntersectAttribute,
intersectEntityName: rel.IntersectEntityName,
isCustomRelationship: rel.IsCustomRelationship
}));
const manyToManyNames = processed.manyToManyRelationships.map((rel) => rel.schemaName);
processed.expandableProperties.push(...manyToManyNames);
}
processed.relatedEntities = buildRelatedEntitiesInfo(processed, options?.excludeSystemAuditRelationships);
return processed;
}
function buildRelatedEntitiesInfo(primaryEntity, excludeSystemAuditRelationships = false) {
const relatedEntities = {};
const systemAuditPatterns = [
/^lk_.*_createdby$/,
/^lk_.*_modifiedby$/,
/^lk_.*_createdonbehalfby$/,
/^lk_.*_modifiedonbehalfby$/,
/^user_.*$/,
/^systemuser_.*$/,
/^createdby_.*$/,
/^modifiedby_.*$/,
/^createdonbehalfby_.*$/,
/^modifiedonbehalfby_.*$/
];
for (const attr of primaryEntity.attributes) {
if (attr.attributeType === "Lookup" && attr.targets) {
for (const targetEntity of attr.targets) {
const relationshipName = attr.logicalName.replace(/_value$/, "");
if (relatedEntities[relationshipName]) {
continue;
}
relatedEntities[relationshipName] = {
relationshipName,
targetEntityLogicalName: targetEntity,
targetEntitySetName: `${targetEntity}s`,
// Will be corrected when we have actual metadata
relationshipType: "ManyToOne"
};
}
}
}
for (const rel of primaryEntity.oneToManyRelationships) {
if (excludeSystemAuditRelationships && primaryEntity.logicalName === "systemuser") {
const isSystemAuditRelationship = systemAuditPatterns.some(
(pattern) => pattern.test(rel.schemaName)
);
if (isSystemAuditRelationship) {
continue;
}
}
if (relatedEntities[rel.schemaName]) {
continue;
}
relatedEntities[rel.schemaName] = {
relationshipName: rel.schemaName,
targetEntityLogicalName: rel.referencingEntity,
targetEntitySetName: `${rel.referencingEntity}s`,
// Will be corrected when we have actual metadata
relationshipType: "OneToMany"
};
}
for (const rel of primaryEntity.manyToManyRelationships) {
const targetEntity = rel.entity1LogicalName === primaryEntity.logicalName ? rel.entity2LogicalName : rel.entity1LogicalName;
if (relatedEntities[rel.schemaName]) {
continue;
}
relatedEntities[rel.schemaName] = {
relationshipName: rel.schemaName,
targetEntityLogicalName: targetEntity,
targetEntitySetName: `${targetEntity}s`,
// Will be corrected when we have actual metadata
relationshipType: "ManyToMany"
};
}
return relatedEntities;
}
function extractRelatedEntityNames(metadata) {
const relatedEntityNames = /* @__PURE__ */ new Set();
for (const relatedEntity of Object.values(metadata.relatedEntities)) {
relatedEntityNames.add(relatedEntity.targetEntityLogicalName);
}
return Array.from(relatedEntityNames);
}
function processAttributeMetadata(attributeMetadata) {
const processed = {
logicalName: attributeMetadata.LogicalName,
schemaName: attributeMetadata.SchemaName,
displayName: extractDisplayName(attributeMetadata.DisplayName) || attributeMetadata.SchemaName,
description: extractDisplayName(attributeMetadata.Description),
attributeType: attributeMetadata.AttributeType,
isCustomAttribute: attributeMetadata.IsCustomAttribute,
isValidForCreate: attributeMetadata.IsValidForCreate,
isValidForRead: attributeMetadata.IsValidForRead,
isValidForUpdate: attributeMetadata.IsValidForUpdate,
requiredLevel: extractRequiredLevel(attributeMetadata.RequiredLevel),
isPrimaryId: attributeMetadata.IsPrimaryId || false,
isPrimaryName: attributeMetadata.IsPrimaryName || false,
attributeOf: attributeMetadata.AttributeOf
};
const odataType = attributeMetadata["@odata.type"];
const normalizedODataType = odataType?.startsWith("#") ? odataType.substring(1) : odataType;
switch (normalizedODataType) {
case "Microsoft.Dynamics.CRM.PicklistAttributeMetadata":
processPicklistAttribute(processed, attributeMetadata);
break;
case "Microsoft.Dynamics.CRM.MultiSelectPicklistAttributeMetadata":
processMultiSelectPicklistAttribute(processed, attributeMetadata);
break;
case "Microsoft.Dynamics.CRM.LookupAttributeMetadata":
processLookupAttribute(processed, attributeMetadata);
break;
case "Microsoft.Dynamics.CRM.StateAttributeMetadata":
case "Microsoft.Dynamics.CRM.StatusAttributeMetadata":
processStateStatusAttribute(processed, attributeMetadata);
break;
case "Microsoft.Dynamics.CRM.StringAttributeMetadata":
processStringAttribute(processed, attributeMetadata);
break;
case "Microsoft.Dynamics.CRM.IntegerAttributeMetadata":
case "Microsoft.Dynamics.CRM.BigIntAttributeMetadata":
case "Microsoft.Dynamics.CRM.DecimalAttributeMetadata":
case "Microsoft.Dynamics.CRM.DoubleAttributeMetadata":
case "Microsoft.Dynamics.CRM.MoneyAttributeMetadata":
processNumericAttribute(processed, attributeMetadata);
break;
case "Microsoft.Dynamics.CRM.DateTimeAttributeMetadata":
processDateTimeAttribute(processed, attributeMetadata);
break;
}
return processed;
}
function processPicklistAttribute(processed, attr) {
if (attr.OptionSet) {
processed.optionSetName = attr.OptionSet.Name || `${processed.logicalName}_options`;
}
if (attr.GlobalOptionSet) {
processed.globalOptionSet = attr.GlobalOptionSet;
}
}
function processMultiSelectPicklistAttribute(processed, attr) {
processed.isMultiSelect = true;
if (attr.OptionSet) {
processed.optionSetName = attr.OptionSet.Name || `${processed.logicalName}_options`;
}
if (attr.GlobalOptionSet) {
processed.globalOptionSet = attr.GlobalOptionSet;
}
}
function processLookupAttribute(processed, attr) {
processed.targets = attr.Targets || [];
processed.format = attr.Format;
}
function processStateStatusAttribute(processed, attr) {
if (attr.OptionSet) {
processed.optionSetName = attr.OptionSet.Name || `${processed.logicalName}_options`;
}
}
function processStringAttribute(processed, attr) {
if (attr.MaxLength !== void 0) {
processed.maxLength = attr.MaxLength;
}
if (attr.Format) {
processed.format = attr.Format;
}
}
function processNumericAttribute(processed, attr) {
if (attr.MinValue !== void 0) {
processed.minValue = attr.MinValue;
}
if (attr.MaxValue !== void 0) {
processed.maxValue = attr.MaxValue;
}
if (attr.Precision !== void 0) {
processed.precision = attr.Precision;
}
}
function processDateTimeAttribute(processed, attr) {
if (attr.Format) {
processed.format = attr.Format;
}
}
function extractOptionSetsFromAttributes(attributes) {
const optionSets = [];
for (const attr of attributes) {
const odataType = attr["@odata.type"];
const normalizedODataType = odataType?.startsWith("#") ? odataType.substring(1) : odataType;
if (normalizedODataType?.includes("PicklistAttributeMetadata") || normalizedODataType?.includes("StateAttributeMetadata") || normalizedODataType?.includes("StatusAttributeMetadata")) {
const picklistAttr = attr;
if (picklistAttr.OptionSet && !picklistAttr.GlobalOptionSet) {
const processedOptionSet = processOptionSet(picklistAttr.OptionSet, attr.LogicalName);
if (processedOptionSet) {
optionSets.push(processedOptionSet);
}
}
}
}
return optionSets;
}
function processOptionSet(optionSetMetadata, fallbackName) {
if (!optionSetMetadata?.Options) return null;
const processed = {
name: optionSetMetadata.Name || fallbackName || "unknown_optionset",
displayName: extractDisplayName(optionSetMetadata.DisplayName) || optionSetMetadata.Name || fallbackName || "Unknown",
description: extractDisplayName(optionSetMetadata.Description),
isGlobal: optionSetMetadata.IsGlobal || false,
isCustom: optionSetMetadata.IsCustomOptionSet || optionSetMetadata.IsCustomOptionSet || false,
options: []
};
processed.options = optionSetMetadata.Options.map((option) => ({
value: option.Value,
label: extractDisplayName(option.Label) || option.Value.toString(),
description: extractDisplayName(option.Description)
}));
return processed;
}
function processGlobalOptionSet(globalOptionSet) {
return {
name: globalOptionSet.Name,
displayName: extractDisplayName(globalOptionSet.DisplayName) || globalOptionSet.Name,
description: extractDisplayName(globalOptionSet.Description),
isGlobal: globalOptionSet.IsGlobal,
isCustom: globalOptionSet.IsCustomOptionSet,
options: globalOptionSet.Options.map((option) => ({
value: option.Value,
label: extractDisplayName(option.Label) || option.Value.toString(),
description: extractDisplayName(option.Description)
}))
};
}
function mergeOptionSets(optionSets) {
const uniqueOptionSets = /* @__PURE__ */ new Map();
for (const optionSet of optionSets) {
const key = optionSet.name;
if (!uniqueOptionSets.has(key)) {
uniqueOptionSets.set(key, optionSet);
}
}
return Array.from(uniqueOptionSets.values());
}
function filterAttributesByType(attributes, types) {
return attributes.filter((attr) => types.includes(attr.attributeType));
}
function getPrimaryAttributes(attributes) {
return {
primaryId: attributes.find((attr) => attr.isPrimaryId),
primaryName: attributes.find((attr) => attr.isPrimaryName)
};
}
function groupAttributesByType(attributes) {
const groups = {};
for (const attr of attributes) {
if (!groups[attr.attributeType]) {
groups[attr.attributeType] = [];
}
groups[attr.attributeType].push(attr);
}
return groups;
}
function getLookupAttributes(attributes) {
return attributes.filter((attr) => attr.attributeType === "Lookup" && attr.targets).map((attr) => ({ ...attr, targets: attr.targets }));
}
function getOptionSetAttributes(attributes) {
return attributes.filter(
(attr) => attr.attributeType === "Picklist" || attr.attributeType === "MultiSelectPicklist" || attr.attributeType === "State" || attr.attributeType === "Status"
);
}
function filterAuxiliaryAttributes(attributes) {
return attributes.filter((attr) => !attr.attributeOf);
}
function validateProcessedMetadata(metadata) {
const errors = [];
const warnings = [];
if (!metadata.logicalName) {
errors.push("Missing logical name");
}
if (!metadata.schemaName) {
errors.push("Missing schema name");
}
if (!metadata.primaryIdAttribute) {
warnings.push("Missing primary ID attribute");
}
if (!metadata.primaryNameAttribute) {
warnings.push("Missing primary name attribute");
}
if (metadata.attributes.length === 0) {
warnings.push("No attributes found");
}
const logicalNames = /* @__PURE__ */ new Set();
for (const attr of metadata.attributes) {
if (logicalNames.has(attr.logicalName)) {
errors.push(`Duplicate attribute logical name: ${attr.logicalName}`);
}
logicalNames.add(attr.logicalName);
}
for (const optionSet of metadata.optionSets) {
if (optionSet.options.length === 0) {
warnings.push(`Option set '${optionSet.name}' has no options`);
}
const values = /* @__PURE__ */ new Set();
for (const option of optionSet.options) {
if (values.has(option.value)) {
errors.push(`Duplicate option value in '${optionSet.name}': ${option.value}`);
}
values.add(option.value);
}
}
return {
isValid: errors.length === 0,
errors,
warnings
};
}
var init_processors = __esm({
"src/processors/index.ts"() {
}
});
// src/error-logger.ts
async function advancedLog(response, requestUrl, requestMethod, requestBody) {
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
const headers = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
const requestId = headers["request-id"] || headers["x-ms-request-id"] || headers["activityid"];
let errorDetails = {
status: response.status,
statusText: response.statusText,
url: requestUrl || response.url,
method: requestMethod || "GET",
headers,
timestamp,
requestId
};
try {
const responseClone = response.clone();
const responseText = await responseClone.text();
errorDetails.body = responseText;
if (responseText) {
try {
const errorResponse = JSON.parse(responseText);
if (errorResponse.error) {
errorDetails.error = errorResponse.error;
}
} catch (parseError) {
console.warn("Failed to parse error response as JSON:", parseError);
}
}
} catch (textError) {
console.warn("Failed to read response body:", textError);
}
console.error("\u{1F6A8} Dataverse API Error Details:");
console.error("\u2501".repeat(60));
console.error(`\u23F0 Timestamp: ${errorDetails.timestamp}`);
console.error(`\u{1F310} URL: ${errorDetails.url}`);
console.error(`\u{1F4CB} Method: ${errorDetails.method}`);
console.error(`\u{1F4CA} Status: ${errorDetails.status} ${errorDetails.statusText}`);
if (errorDetails.requestId) {
console.error(`\u{1F194} Request ID: ${errorDetails.requestId}`);
}
if (errorDetails.error) {
console.error("\u274C Error Details:");
console.error(` Code: ${errorDetails.error.code}`);
console.error(` Message: ${errorDetails.error.message}`);
if (errorDetails.error["@Microsoft.PowerApps.CDS.ErrorDetails.OperationStatus"]) {
console.error(` Operation Status: ${errorDetails.error["@Microsoft.PowerApps.CDS.ErrorDetails.OperationStatus"]}`);
}
if (errorDetails.error["@Microsoft.PowerApps.CDS.ErrorDetails.SubErrorCode"]) {
console.error(` Sub Error Code: ${errorDetails.error["@Microsoft.PowerApps.CDS.ErrorDetails.SubErrorCode"]}`);
}
if (errorDetails.error["@Microsoft.PowerApps.CDS.HelpLink"]) {
console.error(` Help Link: ${errorDetails.error["@Microsoft.PowerApps.CDS.HelpLink"]}`);
}
if (errorDetails.error["@Microsoft.PowerApps.CDS.TraceText"]) {
console.error(` Trace Text:`);
console.error(` ${errorDetails.error["@Microsoft.PowerApps.CDS.TraceText"]}`);
}
if (errorDetails.error["@Microsoft.PowerApps.CDS.InnerError.Message"]) {
console.error(` Inner Error: ${errorDetails.error["@Microsoft.PowerApps.CDS.InnerError.Message"]}`);
}
} else if (errorDetails.body) {
console.error("\u{1F4C4} Raw Response Body:");
console.error(errorDetails.body);
}
console.error("\u{1F4CB} Request Headers:");
Object.entries(headers).forEach(([key, value]) => {
if (key.toLowerCase().includes("authorization") || key.toLowerCase().includes("cookie")) {
console.error(` ${key}: [REDACTED]`);
} else {
console.error(` ${key}: ${value}`);
}
});
if (requestBody) {
console.error("\u{1F4E4} Request Body:");
console.error(requestBody);
}
console.error("\u2501".repeat(60));
return errorDetails;
}
function getErrorCodeDescription(code) {
const errorDescriptions = {
"0x80040265": "Generic custom business logic error",
"0x80040333": "SQL timeout expired",
"0x80040888": "Invalid query parameter",
"0x8004431A": "Cannot add or act on behalf of another user privilege",
"0x80044150": "Principal privilege denied",
"0x80040220": "Cannot assign to disabled user",
"0x80040225": "Cannot assign to inactive business unit",
"0x80040237": "Cannot delete due to association",
"0x80040216": "Cannot create activity for inactive campaign",
"0x80040274": "Duplicate record",
"0x80040264": "Invalid argument",
"0x80040217": "Invalid operation",
"0x80040266": "Not supported",
"0x80040313": "Concurrency version mismatch",
"0x80040201": "Access denied",
"0x80040277": "Attribute permission read is missing",
"0x80040278": "Attribute permission update is missing during update",
"0x80040234": "Attribute privilege create is missing",
"0x80040231": "Cannot act on behalf of another user",
"0x80040230": "CRM security error",
"0x80040200": "Invalid access rights",
"0x80040276": "Privilege create is disabled for organization",
"0x80040221": "Privilege denied"
};
return errorDescriptions[code] || "Unknown error code";
}
function getStatusCodeDescription(status) {
const statusDescriptions = {
400: "Bad Request - Invalid request syntax or unsupported query parameters",
401: "Unauthorized - Authentication failed or insufficient permissions",
403: "Forbidden - Access denied or insufficient privileges",
404: "Not Found - Resource does not exist",
405: "Method Not Allowed - HTTP method not supported for this resource",
412: "Precondition Failed - Concurrency or duplicate record issues",
413: "Payload Too Large - Request size exceeds limits",
429: "Too Many Requests - API rate limits exceeded",
501: "Not Implemented - Operation not supported",
503: "Service Unavailable - Dataverse service temporarily unavailable"
};
return statusDescriptions[status] || "Unknown status code";
}
var memoryCache = /* @__PURE__ */ new Map();
function generateCacheKey(url, options = {}) {
const normalizedUrl = url.toLowerCase().trim();
const method = (options.method || "GET").toUpperCase();
const relevantHeaders = {};
if (options.headers) {
const headers = options.headers;
for (const [key, value] of Object.entries(headers)) {
const lowerKey = key.toLowerCase();
if (lowerKey.includes("accept") || lowerKey.includes("prefer")) {
relevantHeaders[lowerKey] = value;
}
}
}
const keyData = {
url: normalizedUrl,
method,
headers: relevantHeaders
};
return crypto__default.default.createHash("sha256").update(JSON.stringify(keyData)).digest("hex");
}
async function getFromCache(url, options = {}) {
const cacheKey = generateCacheKey(url, options);
const memoryHit = memoryCache.get(cacheKey);
if (memoryHit) {
return new Response(memoryHit.body, {
status: memoryHit.status,
statusText: memoryHit.statusText,
headers: new Headers(memoryHit.headers)
});
}
return null;
}
async function saveToCache(url, options = {}, response) {
const cacheKey = generateCacheKey(url, options);
if (response.ok && response.status === 200) {
const body = await response.clone().text();
const headers = {};
response.headers.forEach((value, key) => {
headers[key] = value;
});
const cachedResponse = {
body,
status: response.status,
statusText: response.statusText,
headers
};
memoryCache.set(cacheKey, cachedResponse);
}
}
function clearMemoryCache() {
const clearedCount = memoryCache.size;
memoryCache.clear();
return clearedCount;
}
function getMemoryCacheStatus() {
return {
entryCount: memoryCache.size
};
}
var TokenManager = class {
tokenCache = /* @__PURE__ */ new Map();
credential;
bufferTimeMs = 5 * 60 * 1e3;
// Refresh 5 minutes before expiry
constructor(credential) {
this.credential = credential || new identity.DefaultAzureCredential();
}
/**
* Get a cached token or acquire a new one
* This method is the key to performance - it ensures we only get 1 token per resource per run
*/
async getToken(resourceUrl) {
const normalizedUrl = this.normalizeResourceUrl(resourceUrl);
const cacheKey = normalizedUrl;
const cached = this.tokenCache.get(cacheKey);
if (cached && this.isTokenValid(cached)) {
return cached.token;
}
try {
const baseUrl = normalizedUrl.endsWith("/") ? normalizedUrl.slice(0, -1) : normalizedUrl;
const scope = `${baseUrl}/.default`;
const tokenResponse = await this.credential.getToken([scope]);
if (!tokenResponse?.token) {
throw new Error("No token received from credential");
}
const cachedToken = {
token: tokenResponse.token,
expiresAt: tokenResponse.expiresOnTimestamp,
resourceUrl: normalizedUrl
};
this.tokenCache.set(cacheKey, cachedToken);
return tokenResponse.token;
} catch (error) {
throw new Error(
`Failed to acquire Azure access token for resource: ${resourceUrl}
Error: ${error instanceof Error ? error.message : String(error)}
Please authenticate via one of these methods:
\u2022 Azure CLI: az login
\u2022 Environment variables: AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID
\u2022 Managed Identity (when running in Azure)`
);
}
}
/**
* Pre-acquire token for a resource to avoid delays during API calls
*/
async preAcquireToken(resourceUrl) {
await this.getToken(resourceUrl);
}
/**
* Check if a cached token is still valid (with buffer time)
*/
isTokenValid(cached) {
const now = Date.now();
const expiresWithBuffer = cached.expiresAt - this.bufferTimeMs;
return now < expiresWithBuffer;
}
/**
* Normalize resource URL for consistent caching
*/
normalizeResourceUrl(resourceUrl) {
if (!resourceUrl) {
throw new Error("Resource URL is required for authentication");
}
try {
const url = new URL(resourceUrl);
return `${url.protocol}//${url.host}`;
} catch {
throw new Error(`Invalid resource URL: ${resourceUrl}`);
}
}
/**
* Clear all cached tokens (useful for testing)
*/
clearCache() {
this.tokenCache.clear();
}
/**
* Get cache statistics
*/
getCacheStats() {
const totalTokens = this.tokenCache.size;
let validTokens = 0;
for (const cached of this.tokenCache.values()) {
if (this.isTokenValid(cached)) {
validTokens++;
}
}
return { totalTokens, validTokens };
}
};
var globalTokenManager = null;
function getTokenManager(credential) {
if (!globalTokenManager) {
globalTokenManager = new TokenManager(credential);
}
return globalTokenManager;
}
async function preAcquireGlobalToken(resourceUrl, credential) {
const manager = getTokenManager(credential);
await manager.preAcquireToken(resourceUrl);
}
// src/auth/index.ts
async function getAzureToken(options) {
const { resourceUrl, credential } = options;
try {
const normalizedUrl = validateResourceUrl(resourceUrl);
const useCredential = credential || new identity.DefaultAzureCredential();
const baseUrl = normalizedUrl.endsWith("/") ? normalizedUrl.slice(0, -1) : normalizedUrl;
const scope = `${baseUrl}/.default`;
const tokenResponse = await useCredential.getToken([scope]);
return tokenResponse?.token || null;
} catch (error) {
throw new Error(
`Failed to acquire Azure access token for resource: ${resourceUrl}
Error: ${error instanceof Error ? error.message : String(error)}
Please authenticate via one of these methods:
\u2022 Azure CLI: az login
\u2022 Environment variables: AZURE_CLIENT_ID, AZURE_CLIENT_SECRET, AZURE_TENANT_ID
\u2022 Managed Identity (when running in Azure)`
);
}
}
function validateResourceUrl(resourceUrl) {
if (!resourceUrl) {
throw new Error("Resource URL is required for authentication");
}
try {
const url = new URL(resourceUrl);
return `${url.protocol}//${url.host}`;
} catch {
throw new Error(`Invalid resource URL: ${resourceUrl}`);
}
}
function createAuthenticatedFetcher(resourceUrl, options = {}) {
const { maxRetries = 3, baseDelay = 1e3, credential } = options;
return async function authenticatedFetch(url, requestOptions = {}) {
const fullUrl = url.startsWith("http") ? url : `${resourceUrl.replace(/\/$/, "")}${url}`;
let lastError = null;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const tokenManager = getTokenManager(credential);
const accessToken = await tokenManager.getToken(resourceUrl);
if (!accessToken) {
throw new Error("Failed to acquire access token");
}
if (!requestOptions.method || requestOptions.method.toUpperCase() === "GET") {
const cachedResponse = await getFromCache(fullUrl, requestOptions);
if (cachedResponse) {
return cachedResponse;
}
}
const headers = new Headers(requestOptions.headers);
headers.set("Authorization", `Bearer ${accessToken}`);
headers.set("Accept", "application/json");
headers.set("Content-Type", "application/json");
headers.set("Prefer", 'odata.include-annotations="*"');
const response = await fetch(fullUrl, {
...requestOptions,
headers
});
if (response.ok && response.headers.get("content-type")?.includes("application/json")) {
}
if (response.ok && (!requestOptions.method || requestOptions.method.toUpperCase() === "GET")) {
await saveToCache(fullUrl, requestOptions, response.clone());
}
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get("Retry-After") || "5") * 1e3;
if (attempt < maxRetries) {
await new Promise((resolve3) => setTimeout(resolve3, retryAfter));
continue;
}
}
if (!response.ok) {
await advancedLog(response, fullUrl, requestOptions.method || "GET", requestOptions.body);
}
return response;
} catch (error) {
lastError = error instanceof Error ? error : new Error(String(error));
if (attempt < maxRetries) {
const delay = baseDelay * Math.pow(2, attempt);
await new Promise((resolve3) => setTimeout(resolve3, delay));
}
}
}
throw lastError || new Error("Authentication failed after all retries");
};
}
// src/metadata-client.ts
function getAuthenticatedFetch() {
const dataverseUrl = process.env.DATAVERSE_INSTANCE;
if (!dataverseUrl) {
throw new Error("DATAVERSE_INSTANCE environment variable is required");
}
return createAuthenticatedFetcher(dataverseUrl);
}
async function getEntityDefinitions(options = {}) {
const baseUrl = "/api/data/v9.1/EntityDefinitions";
const params = new URLSearchParams();
if (options.select && options.select.length > 0) {
params.append("$select", options.select.join(","));
}
if (options.filter) {
params.append("$filter", options.filter);
}
if (options.expand && options.expand.length > 0) {
params.append("$expand", options.expand.join(","));
}
const url = params.toString() ? `${baseUrl}?${params.toString()}` : baseUrl;
const authenticatedFetch = getAuthenticatedFetch();
const response = await authenticatedFetch(url, {
method: "GET"
});
if (!response.ok) {
await advancedLog(response, url, "GET");
const statusDescription = getStatusCodeDescription(response.status);
throw new Error(`Failed to fetch entity definitions: ${response.status} ${response.statusText} - ${statusDescription}`);
}
const data = await response.json();
return data.value;
}
async function getEntityDefinition(logicalName) {
const results = await getEntityDefinitions({
filter: `LogicalName eq '${logicalName}'`,
select: ["LogicalName", "SchemaName", "DisplayName", "Description", "HasActivities", "HasFeedback", "HasNotes", "IsActivity", "IsCustomEntity", "OwnershipType", "PrimaryIdAttribute", "PrimaryNameAttribute", "EntitySetName"],
expand: ["Attributes"]
});
return results.length > 0 ? results[0] : null;
}
// src/client/concurrent-queue.ts
var ConcurrentRequestQueue = class {
activeRequests = 0;
maxConcurrent;
queue = [];
// eslint-disable-line @typescript-eslint/no-explicit-any
rateLimitInfo = {};
requestHistory = [];
totalRequestsCounter = 0;
// Track total API calls made
lastProgressLog = 0;
// Track when we last logged progress
constructor(maxConcurrent = 25) {
const platformMaxConcurrent = process.platform === "win32" ? 15 : 25;
this.maxConcurrent = Math.min(maxConcurrent, platformMaxConcurrent);
}
/**
* Execute a request with automatic queuing and retry
*/
async execute(request, options = {}) {
const { maxRetries = 3, priority = 0 } = options;
return new Promise((resolve3, reject) => {
const queuedRequest = {
execute: async () => {
try {
this.activeRequests++;
this.totalRequestsCounter++;
this.recordRequest();
const response = await request();
this.updateRateLimitInfo(response);
if (response.status === 429) {
const retryAfter = this.parseRetryAfter(response);
if (queuedRequest.retryCount < queuedRequest.maxRetries) {
console.warn(`Rate limited (429), retrying after ${retryAfter}ms...`);
await this.delay(retryAfter);
queuedRequest.retryCount++;
this.queue.unshift(queuedRequest);
return;
} else {
throw new Error(`Rate limit exceeded after ${maxRetries} retries`);
}
}
if (!response.ok) {
if (options.url) {
await advancedLog(response, options.url, options.method || "GET");
}
throw new Error(`Request failed: ${response.status} ${response.statusText}`);
}
const result = await response.json();
resolve3(result);
} catch (error) {
if (queuedRequest.retryCount < queuedRequest.maxRetries && this.isRetryableError(error)) {
console.warn(`Request failed, retrying (${queuedRequest.retryCount + 1}/${queuedRequest.maxRetries})...`, error);
queuedRequest.retryCount++;
await this.delay(Math.pow(2, queuedRequest.retryCount) * 1e3);
this.queue.unshift(queuedRequest);
} else {
reject(error);
}
} finally {
this.activeRequests--;
this.processNext();
}
},
resolve: resolve3,
reject,
retryCount: 0,
maxRetries,
priority
};
const insertIndex = this.queue.findIndex((req) => req.priority < priority);
if (insertIndex === -1) {
this.queue.push(queuedRequest);
} else {
this.queue.splice(insertIndex, 0, queuedRequest);
}
this.processNext();
});
}
/**
* Process the next request in the queue if capacity allows
*/
async processNext() {
if (this.shouldThrottle()) {
const delay = this.calculateThrottleDelay();
if (delay > 0) {
setTimeout(() => this.processNext(), delay);
return;
}
}
if (this.activeRequests >= this.maxConcurrent || this.queue.length === 0) {
return;
}
const request = this.queue.shift();
if (request) {
request.execute().catch((error) => {
console.error("Queue request execution failed:", error);
});
}
}
/**
* Update rate limit information from response headers
*/
updateRateLimitInfo(response) {
const remainingRequests = response.headers.get("x-ms-ratelimit-burst-remaining-xrm-requests");
const remainingDuration = response.headers.get("x-ms-ratelimit-time-remaining-xrm-requests");
const retryAfter = response.headers.get("Retry-After");
if (remainingRequests) {
this.rateLimitInfo.remainingRequests = parseInt(remainingRequests, 10);
}
if (remainingDuration) {
this.rateLimitInfo.remainingDuration = parseInt(remainingDuration, 10);
}
if (retryAfter) {
this.rateLimitInfo.retryAfter = parseInt(retryAfter, 10);
}
}
/**
* Parse Retry-After header value
*/
parseRetryAfter(response) {
const retryAfter = response.headers.get("Retry-After");
if (!retryAfter) return 1e3;
const seconds = parseInt(retryAfter, 10);
return isNaN(seconds) ? 1e3 : seconds * 1e3;
}
/**
* Record request timestamp for rate limiting
*/
recordRequest() {
const now = Date.now();
this.requestHistory.push(now);
const fiveMinutesAgo = now - 5 * 60 * 1e3;
while (this.requestHistory.length > 0 && this.requestHistory[0] < fiveMinutesAgo) {
this.requestHistory.shift();
}
}
/**
* Check if we should throttle requests based on rate limits
*/
shouldThrottle() {
if (this.rateLimitInfo.remainingRequests !== void 0) {
return this.rateLimitInfo.remainingRequests < 100;
}
const requestsInLast5Minutes = this.requestHistory.length;
return requestsInLast5Minutes > 5e3;
}
/**
* Calculate delay for throttling
*/
calculateThrottleDelay() {
if (this.rateLimitInfo.retryAfter) {
return this.rateLimitInfo.retryAfter * 1e3;
}
if (this.rateLimitInfo.remainingDuration && this.rateLimitInfo.remainingRequests) {
const averageDelay = this.rateLimitInfo.remainingDuration / this.rateLimitInfo.remainingRequests;
return Math.max(averageDelay * 1e3, 100);
}
const requestsInLast5Minutes = this.requestHistory.length;
if (requestsInLast5Minutes > 5e3) {
return Math.min((requestsInLast5Minutes - 5e3) * 10, 5e3);
}
return 0;
}
/**
* Check if an error is retryable
*/
isRetryableError(error) {
if (error instanceof Error) {
const message = error.message.toLowerCase();
return message.includes("timeout") || message.includes("network") || message.includes("connection") || message.includes("429");
}
return false;
}
/**
* Delay utility
*/
delay(ms) {
return new Promise((resolve3) => setTimeout(resolve3, ms));
}
/**
* Get current queue status
*/
getStatus() {
return {
activeRequests: this.activeRequests,
queuedRequests: this.queue.length,
rateLimitInfo: { ...this.rateLimitInfo },
requestsInLast5Minutes: this.requestHistory.length,
totalRequestsMade: this.totalRequestsCounter
};
}
/**
* Log final statistics
*/
logFinalStats() {
}
/**
* Wait for all active requests to complete
*/
async waitForCompletion() {
while (this.activeRequests > 0 || this.queue.length > 0) {
await this.delay(100);
}
}
};
var globalRequestQueue = new ConcurrentRequestQueue();
// src/client/or-filter-batching.ts
function getAuthenticatedFetch2() {
const dataverseUrl = process.env.DATAVERSE_INSTANCE;
if (!dataverseUrl) {
throw new Error("DATAVERSE_INSTANCE environment variable is required");
}
return createAuthenticatedFetcher(dataverseUrl);
}
var OR_BATCH_SIZE = 15;
async function fetchEntitiesWithOrFilter(entityNames, select = [
"LogicalName",
"SchemaName",
"DisplayName",
"Description",
"HasActivities",
"HasFeedback",
"HasNotes",
"IsActivity",
"IsCustomEntity",
"OwnershipType",
"PrimaryIdAttribute",
"PrimaryNameAttribute",
"EntitySetName"
], onProgress) {
if (entityNames.length === 0) {
return [];
}
const allEntities = [];
let processedCount = 0;
for (let i = 0; i < entityNames.length; i += OR_BATCH_SIZE) {
const batch = entityNames.slice(i, i + OR_BATCH_SIZE);
try {
const batchEntities = await fetchEntityBatchWithOrFilter(batch, select);
allEntities.push(...batchEntities);
processedCount += batch.length;
if (onProgress) {
const lastEntityInBatch = batch[batch.length - 1];
onProgress(processedCount, entityNames.length, lastEntityInBatch);
}
} catch (error) {
console.warn(`Failed to fetch entity batch [${batch.join(", ")}]:`, error);
console.log(`Falling back to individual requests for batch: ${batch.join(", ")}`);
for (const entityName of batch) {
try {
const entity = await fetchSingleEntityWithQueue(entityName, select);
if (entity) {
allEntities.push(entity);
}
processedCount++;
if (onProgress) {
onProgress(processedCount, entityNames.length, entityName);
}
} catch (singleError) {
console.warn(`Failed to fetch individual entity '${entityName}':`, singleError);
processedCount++;
if (onProgress) {
onProgress(processedCount, entityNames.length, entityName);
}
}
}
}
}
return allEntities;
}
async function fetchEntityBatchWithOrFilter(entityNames, select) {
const orConditions = entityNames.map((name) => `LogicalName eq '${name}'`);
const filter = orConditions.join(" or ");
const params = new URLSearchParams();
if (select.length > 0) {
params.append("$select", select.join(","));
}
params.append("$filter", filter);
const url = `/api/data/v9.1/EntityDefinitions?${params.toString()}`;
const response = await globalRequestQueue.execute(
() => getAuthenticatedFetch2()(url, { method: "GET" }),
{
url,
method: "GET",
priority: 1
// High priority for entity metadata
}
);
return response.value || [];
}
async function fetchSingleEntityWithQueue(entityLogicalName, select) {
try {
const params = new URLSearchParams();
if (select.length > 0) {
params.append("$select", select.join(","));
}
const url = `/api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')${params.toString() ? `?${params.toString()}` : ""}`;
const response = await globalRequestQueue.execute(
() => getAuthenticatedFetch2()(url, { method: "GET" }),
{
url,
method: "GET",
priority: 1
}
);
return response;
} catch (error) {
if (error instanceof Error && error.message.includes("404")) {
return null;
}
throw error;
}
}
// src/client/index.ts
function getAuthenticatedFetch3() {
const dataverseUrl = process.env.DATAVERSE_INSTANCE;
if (!dataverseUrl) {
throw new Error("DATAVERSE_INSTANCE environment variable is required");
}
return createAuthenticatedFetcher(dataverseUrl);
}
var COMPONENT_TYPES = {
ENTITY: 1,
ATTRIBUTE: 2,
RELATIONSHIP: 10,
OPTION_SET: 9
// Add more as needed
};
async function fetchEntityMetadata(entityLogicalName, options = {}) {
const {
includeAttributes = true,
includeRelationships = false,
select = [
"LogicalName",
"SchemaName",
"DisplayName",
"Description",
"HasActivities",
"HasFeedback",
"HasNotes",
"IsActivity",
"IsCustomEntity",
"OwnershipType",
"PrimaryIdAttribute",
"PrimaryNameAttribute",
"EntitySetName"
]
} = options;
try {
const params = new URLSearchParams();
if (select.length > 0) {
params.append("$select", select.join(","));
}
const expandItems = [];
if (includeAttributes) {
expandItems.push("Attributes");
}
if (includeRelationships) {
expandItems.push("OneToManyRelationships", "ManyToOneRelationships", "ManyToManyRelationships");
}
if (expandItems.length > 0) {
params.append("$expand", expandItems.join(","));
}
const url = `/api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')${params.toString() ? `?${params.toString()}` : ""}`;
const authenticatedFetch = getAuthenticatedFetch3();
const response = await authenticatedFetch(url, { method: "GET" });
if (response.status === 404) {
return null;
}
if (!response.ok) {
await advancedLog(response, url, "GET");
const statusDescription = getStatusCodeDescription(response.status);
throw new Error(`Failed to fetch entity metadata for '${entityLogicalName}': ${response.status} ${response.statusText} - ${statusDescription}`);
}
const entityData = await response.json();
if (includeAttributes && entityData) {
try {
const detailedAttributes = await fetchEntityAttributes(entityLogicalName);
entityData.Attributes = detailedAttributes;
} catch (error) {
console.warn(`Failed to fetch detailed attributes for ${entityLogicalName}, using basic attributes:`, error);
}
}
return entityData;
} catch (error) {
if (error instanceof Error) {
throw new Error(`Error fetching entity metadata for '${entityLogicalName}': ${error.message}`);
}
throw error;
}
}
async function fetchAllEntityMetadata(options = {}) {
console.log(`\u{1F30D} Fetching ALL entities from Dataverse for complete type safety...`);
try {
const allEntities = await fetchAllEntities({
select: [
"LogicalName",
"SchemaName",
"DisplayName",
"Description",
"HasActivities",
"HasFeedback",
"HasNotes",
"IsActivity",
"IsCustomEntity",
"OwnershipType",
"PrimaryIdAttribute",
"PrimaryNameAttribute",
"EntitySetName"
]
});
console.log(`Found ${allEntities.length} total entities in Dataverse`);
if (options.includeAttributes) {
const entitiesWithAttributes = await fetchMultipleEntities(
allEntities.map((e) => e.LogicalName),
{ ...options, onProgress: options.onProgress }
);
console.log(`\u2705 Successfully fetched detailed metadata for ${entitiesWithAttributes.length} entities`);
return entitiesWithAttributes;
}
return allEntities;
} catch (error) {
if (error instanceof Error) {
throw new Error(`Error fetching all entity metadata: ${error.message}`);
}
throw error;
}
}
async function fetchMultipleEntitiesWithRelated(entityNames, options = {}) {
const { includeRelatedEntities = false, ...baseOptions } = options;
console.log(`Fetching metadata for ${entityNames.length} primary entities...`);
const primaryEntities = await fetchMultipleEntities(entityNames, {
...baseOptions,
includeRelationships: includeRelatedEntities
});
const relatedEntities = /* @__PURE__ */ new Map();
if (includeRelatedEntities && primaryEntities.length > 0) {
const { processEntityMetadata: processEntityMetadata2, extractRelatedEntityNames: extractRelatedEntityNames2 } = await Promise.resolve().then(() => (init_processors(), processors_exports));
const allRelatedEntityNames = /* @__PURE__ */ new Set();
for (const entity of primaryEntities) {
const processed = processEntityMetadata2(entity);
const relatedNames = extractRelatedEntityNames2(processed);
relatedNames.forEach((name) => allRelatedEntityNames.add(name));
}
entityNames.forEach((name) => allRelatedEntityNames.delete(name));
const relatedEntityArray = Array.from(allRelatedEntityNames);
if (relatedEntityArray.length > 0) {
console.log(`Discovering ${relatedEntityArray.length} related entities...`);
console.log(`Fetching metadata for related entities...`);
const relatedEntitiesArray = await fetchMultipleEntities(relatedEntityArray, baseOptions);
for (const entity of relatedEntitiesArray) {
relatedEntities.set(entity.LogicalName, entity);
}
console.log(`\u2705 Successfully fetched ${relatedEntitiesArray.length} related entities`);
}
}
return {
primaryEntities,
relatedEntities
};
}
async function fetchEntityAttributes(entityLogicalName, attributeTypes = [
"Microsoft.Dynamics.CRM.PicklistAttributeMetadata",
"Microsoft.Dynamics.CRM.MultiSelectPicklistAttributeMetadata",
"Microsoft.Dynamics.CRM.StateAttributeMetadata",
"Microsoft.Dynamics.CRM.StatusAttributeMetadata",
"Microsoft.Dynamics.CRM.LookupAttributeMetadata"
]) {
try {
const basicUrl = `/api/data/v9.2/EntityDefinitions(LogicalName='${entityLogicalName}')/Attributes?$select=LogicalName,SchemaName,DisplayName,Description,AttributeType,IsCustomAttribute,IsValidForCreate,IsValidForRead,IsValidForUpdate,RequiredLevel,IsPrimaryId,IsPrimaryName,AttributeOf`;
const basicData = await globalRequestQueue.execute(
() => getAuthenticatedFetch3()(basicUrl, { method: "GET" }),
{
url: basicUrl,
method: "GET",
priority: 2
// High priority for basic attributes
}
);
const basicAttributes = basicData.value;
const existingAttributeTypes = /* @__PURE__ */ new Set();
for (const attr of basicAttributes) {
if (attr.AttributeType === "Picklist") {
existingAttributeTypes.add("Microsoft.Dynamics.CRM.PicklistAttributeMetadata");
} else if (attr.AttributeType === "MultiSelectPicklist") {
existingAttributeTypes.add("Microsoft.Dynamics.CRM.MultiSelectPicklistAttributeMetadata");
} else if (attr.AttributeType === "State") {
existingAttributeTypes.add("Microsof