UNPKG

@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
"use strict"; 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