@kraveir0/webapi-proxy-interceptor
Version:
Drop-in replacement for PCF WebAPI that automatically routes calls to your local development proxy.
450 lines • 19.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProxiedWebAPI = void 0;
exports.createWebAPI = createWebAPI;
const config_1 = require("./config");
const logger_1 = require("./logger");
require("./types");
/**
* Proxied WebAPI Implementation
* A drop-in replacement for PCF context.webAPI that routes calls through a local development proxy
*/
class ProxiedWebAPI {
constructor(config) {
this.entityMetadataCache = new Map();
this.config = (0, config_1.mergeConfig)(config);
this.logger = new logger_1.Logger(this.config);
this.logger.log("🚀 ProxiedWebAPI initialized", {
proxyUrl: this.config.proxyUrl,
environment: this.isLocalDevelopment() ? "LOCAL_DEV" : "LIVE",
});
}
/**
* Creates a record
* @param entityLogicalName The logical name of the entity
* @param data The record data
*/
async createRecord(entityLogicalName, data) {
this.logger.log(`📝 createRecord: ${entityLogicalName}`, data);
try {
const entitySetName = await this.getEntitySetName(entityLogicalName);
const url = this.buildUrl(entitySetName);
const response = await this.makeRequest(url, {
method: "POST",
body: JSON.stringify(data),
});
this.logger.log(`✅ createRecord successful: ${entityLogicalName} (${entitySetName})`);
return response;
}
catch (error) {
this.logger.error(`❌ createRecord failed: ${entityLogicalName}`, error);
throw this.handleError("createRecord", error);
}
}
/**
* Updates a record
* @param entityLogicalName The logical name of the entity
* @param id The record ID
* @param data The update data
*/
async updateRecord(entityLogicalName, id, data) {
this.logger.log(`✏️ updateRecord: ${entityLogicalName}(${id})`, data);
try {
const entitySetName = await this.getEntitySetName(entityLogicalName);
const url = this.buildUrl(`${entitySetName}(${id})`);
const response = await this.makeRequest(url, {
method: "PATCH",
body: JSON.stringify(data),
});
this.logger.log(`✅ updateRecord successful: ${entityLogicalName} (${entitySetName})(${id})`);
return response;
}
catch (error) {
this.logger.error(`❌ updateRecord failed: ${entityLogicalName}(${id})`, error);
throw this.handleError("updateRecord", error);
}
}
/**
* Deletes a record
* @param entityLogicalName The logical name of the entity
* @param id The record ID
*/
async deleteRecord(entityLogicalName, id) {
this.logger.log(`🗑️ deleteRecord: ${entityLogicalName}(${id})`);
try {
const entitySetName = await this.getEntitySetName(entityLogicalName);
const url = this.buildUrl(`${entitySetName}(${id})`);
const response = await this.makeRequest(url, {
method: "DELETE",
});
this.logger.log(`✅ deleteRecord successful: ${entityLogicalName} (${entitySetName})(${id})`);
return response;
}
catch (error) {
this.logger.error(`❌ deleteRecord failed: ${entityLogicalName}(${id})`, error);
throw this.handleError("deleteRecord", error);
}
}
/**
* Retrieves a single record
* @param entityLogicalName The logical name of the entity
* @param id The record ID
* @param options Query options
*/
async retrieveRecord(entityLogicalName, id, options) {
this.logger.log(`📄 retrieveRecord: ${entityLogicalName}(${id})`, options);
try {
const entitySetName = await this.getEntitySetName(entityLogicalName);
let url = this.buildUrl(`${entitySetName}(${id})`);
if (options) {
const params = new URLSearchParams();
if (options.select)
params.append("$select", options.select.join(","));
if (options.expand) {
options.expand.forEach((expand) => {
params.append("$expand", expand.property + (expand.select ? `($select=${expand.select.join(",")})` : ""));
});
}
if (params.toString()) {
url += `?${params.toString()}`;
}
}
const response = await this.makeRequest(url, { method: "GET" });
this.logger.log(`✅ retrieveRecord successful: ${entityLogicalName} (${entitySetName})(${id})`);
return response;
}
catch (error) {
this.logger.error(`❌ retrieveRecord failed: ${entityLogicalName}(${id})`, error);
throw this.handleError("retrieveRecord", error);
}
}
/**
* Retrieves multiple records
* @param entityLogicalName The logical name of the entity
* @param query OData query string
* @param maxPageSize Maximum page size
*/
async retrieveMultipleRecords(entityLogicalName, query, maxPageSize) {
var _a;
this.logger.log(`📋 retrieveMultipleRecords: ${entityLogicalName}`, { query, maxPageSize });
try {
const entitySetName = await this.getEntitySetName(entityLogicalName);
let url = this.buildUrl(entitySetName);
if (query) {
url += `${query}`;
}
const response = await this.makeRequest(url, { method: "GET" });
const result = {
entities: response.value || [],
nextLink: response["@odata.nextLink"] || null,
fetchXmlPagingCookie: response["@Microsoft.Dynamics.CRM.fetchxmlpagingcookie"] || null,
totalRecordCount: response["@odata.count"] || null,
maxPageSize: maxPageSize || null,
};
this.logger.log(`✅ retrieveMultipleRecords successful: ${entityLogicalName} (${entitySetName})`, `Found ${((_a = response.value) === null || _a === void 0 ? void 0 : _a.length) || 0} records`);
return result;
}
catch (error) {
this.logger.error(`❌ retrieveMultipleRecords failed: ${entityLogicalName}`, error);
throw this.handleError("retrieveMultipleRecords", error);
}
}
/**
* Queries Dataverse metadata to get the correct entity set name for a logical name
* @param logicalName The logical name of the entity
* @returns The entity set name (schema name) used in REST API endpoints
*/
async getEntitySetNameFromMetadata(logicalName) {
var _a, _b, _c;
// Check cache first
if (this.entityMetadataCache.has(logicalName)) {
const cached = this.entityMetadataCache.get(logicalName);
this.logger.log(`📋 Using cached entity set name: ${logicalName} → ${cached.entitySetName}`);
return cached.entitySetName;
}
try {
this.logger.log(`🔍 Querying metadata for entity: ${logicalName}`);
// Query the EntityDefinitions metadata endpoint
const metadataUrl = `${this.config.proxyUrl.replace(/\/$/, "")}/api/data/v9.2/EntityDefinitions?$filter=LogicalName eq '${logicalName}'&$select=LogicalName,EntitySetName,DisplayName`;
const response = await this.makeRequest(metadataUrl, { method: "GET" });
this.logger.log(`📊 Metadata query response for ${logicalName}:`, {
totalResults: ((_a = response.value) === null || _a === void 0 ? void 0 : _a.length) || 0,
results: response.value || [],
url: metadataUrl,
});
if (response.value && response.value.length > 0) {
const entity = response.value[0];
const metadata = {
logicalName: entity.LogicalName,
entitySetName: entity.EntitySetName,
displayName: ((_c = (_b = entity.DisplayName) === null || _b === void 0 ? void 0 : _b.UserLocalizedLabel) === null || _c === void 0 ? void 0 : _c.Label) || entity.LogicalName,
};
// Cache the result
this.entityMetadataCache.set(logicalName, metadata);
this.logger.log(`✅ Found entity set name: ${logicalName} → ${metadata.entitySetName}`, metadata);
return metadata.entitySetName;
}
else {
this.logger.warn(`⚠️ No metadata found for entity: ${logicalName}, falling back to pluralization`);
const fallbackName = this.fallbackToPluralization(logicalName);
this.logger.log(`🔄 Fallback pluralization result: ${logicalName} → ${fallbackName}`);
return fallbackName;
}
}
catch (error) {
this.logger.warn(`⚠️ Failed to query metadata for ${logicalName}, falling back to pluralization:`, error);
const fallbackName = this.fallbackToPluralization(logicalName);
this.logger.log(`🔄 Fallback pluralization result: ${logicalName} → ${fallbackName}`);
return fallbackName;
}
}
/**
* Fallback pluralization logic when metadata query fails
* @param logicalName The logical name to pluralize
* @returns The pluralized form
*/
fallbackToPluralization(logicalName) {
// Static mapping for common entities
const commonMappings = {
account: "accounts",
contact: "contacts",
lead: "leads",
opportunity: "opportunities",
quote: "quotes",
order: "salesorders",
invoice: "invoices",
product: "products",
systemuser: "systemusers",
team: "teams",
businessunit: "businessunits",
role: "roles",
workflow: "workflows",
solution: "solutions",
publisher: "publishers",
annotation: "annotations",
email: "emails",
phonecall: "phonecalls",
task: "tasks",
appointment: "appointments",
incident: "incidents",
contract: "contracts",
queue: "queues",
subject: "subjects",
currency: "transactioncurrencies",
territory: "territories",
calendar: "calendars",
campaign: "campaigns",
list: "lists",
service: "services",
site: "sites",
equipment: "equipments",
facility: "facilities",
};
// Check static mappings first
if (commonMappings[logicalName.toLowerCase()]) {
return commonMappings[logicalName.toLowerCase()];
}
// Handle custom entities (typically start with new_, cr_, etc.)
if (logicalName.includes("_")) {
const parts = logicalName.split("_");
const prefix = parts[0];
const entityName = parts.slice(1).join("_");
this.logger.log(`🔍 Pluralizing custom entity: ${logicalName}`, {
prefix,
entityName,
originalParts: parts,
});
// Check if the entity name is already plural (specific patterns)
const alreadyPluralPatterns = [
/projects$/i, // ends with 'projects'
/activities$/i, // ends with 'activities'
/entities$/i, // ends with 'entities'
/properties$/i, // ends with 'properties'
/categories$/i, // ends with 'categories'
/histories$/i, // ends with 'histories'
/ies$/i, // ends with 'ies'
/ves$/i, // ends with 'ves' (like wolves)
/children$/i, // children
/people$/i, // people
/data$/i, // data
/information$/i, // information
];
const isAlreadyPlural = alreadyPluralPatterns.some((pattern) => pattern.test(entityName));
// Custom entity pluralization rules - apply to the entity part only
let pluralizedEntityName;
if (isAlreadyPlural) {
// If already plural, don't pluralize further
pluralizedEntityName = entityName;
this.logger.log(`📝 Entity name appears to be already plural: ${entityName}`);
}
else if (entityName.endsWith("y")) {
pluralizedEntityName = entityName.slice(0, -1) + "ies";
}
else if (entityName.endsWith("s") || entityName.endsWith("x") || entityName.endsWith("z") || entityName.endsWith("ch") || entityName.endsWith("sh")) {
pluralizedEntityName = entityName + "es";
}
else {
pluralizedEntityName = entityName + "s";
}
const result = `${prefix}_${pluralizedEntityName}`;
this.logger.log(`📝 Custom entity pluralization: ${logicalName} → ${result}`);
return result;
}
// Standard English pluralization
if (logicalName.endsWith("y")) {
return logicalName.slice(0, -1) + "ies";
}
else if (logicalName.endsWith("s") || logicalName.endsWith("x") || logicalName.endsWith("z") || logicalName.endsWith("ch") || logicalName.endsWith("sh")) {
return logicalName + "es";
}
else {
return logicalName + "s";
}
}
/**
* Gets the entity set name, either from cache, metadata query, or fallback pluralization
* @param entityLogicalName The logical name of the entity
* @returns Promise that resolves to the entity set name
*/
async getEntitySetName(entityLogicalName) {
// For paths that already contain special characters, don't convert
if (entityLogicalName.includes("(") || entityLogicalName.includes("/") || entityLogicalName.includes("$")) {
return entityLogicalName;
}
try {
return await this.getEntitySetNameFromMetadata(entityLogicalName);
}
catch (error) {
this.logger.warn(`Failed to get entity set name for ${entityLogicalName}, using fallback:`, error);
return this.fallbackToPluralization(entityLogicalName);
}
}
/**
* Builds the full URL for the request
*/
buildUrl(path) {
const baseUrl = this.config.proxyUrl.replace(/\/$/, "");
return `${baseUrl}/api/data/v9.2/${path}`;
}
/**
* Makes an HTTP request to the proxy
*/
async makeRequest(url, options) {
const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), this.config.timeout);
try {
// Build comprehensive OData headers that match native WebAPI behavior
const headers = {
"Content-Type": "application/json",
Accept: "application/json",
// Include OData annotations for formatted values and metadata
Prefer: "odata.include-annotations=*",
// OData version header for compatibility
"OData-Version": "4.0",
"OData-MaxVersion": "4.0",
// Ensure we get all the OData metadata
"Accept-Charset": "UTF-8",
...this.config.customHeaders,
};
this.logger.log(`🌐 Making request to: ${url}`, {
method: options.method,
headers: Object.keys(headers),
});
const response = await fetch(url, {
...options,
headers,
signal: controller.signal,
});
clearTimeout(timeoutId);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// Handle empty responses (like DELETE operations)
if (response.status === 204 || !response.headers.get("content-length")) {
return {};
}
const result = await response.json();
this.logger.log(`📦 Response received from proxy`, result);
return result;
}
catch (error) {
clearTimeout(timeoutId);
throw error;
}
}
/**
* Handles and formats errors
*/
handleError(methodName, error) {
const isLocalDev = this.isLocalDevelopment();
if (isLocalDev) {
return new Error(`ProxiedWebAPI ${methodName} failed in local development.\n` + `Please ensure your local development proxy is running at: ${this.config.proxyUrl}\n` + `Original error: ${error.message || error}`);
}
else {
return new Error(`ProxiedWebAPI ${methodName} failed.\n` + `Error: ${error.message || error}`);
}
}
/**
* Checks if we're in local development environment
*/
isLocalDevelopment() {
return (window.location.port === "8181" || // PCF test harness
window.location.port === "3000" ||
window.location.port === "4200" ||
window.location.port === "5173" ||
window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1" ||
window.location.href.includes("pcf") ||
window.location.href.includes("harness") ||
document.title.includes("PCF") ||
document.title.includes("Test Harness"));
}
/**
* Gets the current configuration
*/
getConfig() {
return { ...this.config };
}
/**
* Updates the configuration
*/
updateConfig(newConfig) {
this.config = (0, config_1.mergeConfig)({ ...this.config, ...newConfig });
this.logger = new logger_1.Logger(this.config);
this.logger.log("Configuration updated", this.config);
}
}
exports.ProxiedWebAPI = ProxiedWebAPI;
/**
* Factory function to create the appropriate WebAPI implementation
* @param context The PCF context (optional, used to detect environment)
* @param config Configuration for the proxy
* @returns ProxiedWebAPI for local development, or the original context.webAPI for live environments
*/
function createWebAPI(context, config) {
const isLocalDev = isLocalDevelopment();
if (isLocalDev) {
console.log("🔧 Local development detected - using ProxiedWebAPI");
return new ProxiedWebAPI(config);
}
else {
console.log("🌍 Live environment detected - using original WebAPI");
return (context === null || context === void 0 ? void 0 : context.webAPI) || null;
}
}
/**
* Helper function to detect local development environment
*/
function isLocalDevelopment() {
return (window.location.port === "8181" || // PCF test harness
window.location.port === "3000" ||
window.location.port === "4200" ||
window.location.port === "5173" ||
window.location.hostname === "localhost" ||
window.location.hostname === "127.0.0.1" ||
window.location.href.includes("pcf") ||
window.location.href.includes("harness") ||
document.title.includes("PCF") ||
document.title.includes("Test Harness"));
}
//# sourceMappingURL=interceptor.js.map