From 07968927fc570d82f874a7d112d3b8c524b243b2 Mon Sep 17 00:00:00 2001 From: "Peter.Morton" Date: Sat, 7 Mar 2026 12:14:45 -0600 Subject: [PATCH] Change googleDriveAdapterHelper.js to use return statement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reverted from object literal to function with return statement Rationale: - User requested explicit 'return' statement for clarity - Function wrapper with 'return' is more conventional and readable - Makes it clear the code returns an object when executed - Matches common module pattern expectations Structure: ```javascript (function() { // Define all classes and functions class DocumentCountExceededError extends Error {...} function generateRequestId() {...} // ... all other functions // Return object with all exports return { DocumentCountExceededError, generateRequestId, // ... all other functions }; })(); ``` Changes: - Wrapped entire file in IIFE: (function() { ... })() - Changed final expression from ({ ... }) to return { ... }; - Re-added 2-space indentation for content inside function - Updated header comment: 'Returns' → 'Function that returns' Benefits: ✅ Explicit return statement (clearer intent) ✅ Standard IIFE module pattern ✅ More conventional JavaScript style ✅ Easier to understand for new developers ✅ Same functionality as before Testing: ✓ Syntax validated ✓ Server starts successfully ✓ Module loads: 'Loaded global functions: googleDriveAdapterHelper' ✓ All function calls work correctly ✓ Request handling functional Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../googleDriveAdapterHelper.js | 555 +++++++++--------- 1 file changed, 278 insertions(+), 277 deletions(-) diff --git a/src/globalVariables/googleDriveAdapterHelper.js b/src/globalVariables/googleDriveAdapterHelper.js index c706049..dd33ce8 100644 --- a/src/globalVariables/googleDriveAdapterHelper.js +++ b/src/globalVariables/googleDriveAdapterHelper.js @@ -6,7 +6,7 @@ * * ARCHITECTURE: * - Loaded by server.js using vm.Script (same as proxy.js) - * - Returns a single object containing all helper functions + * - Function that returns a single object containing all helper functions * - Injected into globalVariableContext for access by proxy.js * - NO IMPORTS - All dependencies provided via VM context * @@ -17,296 +17,297 @@ * @returns {Object} Helpers object with all utility functions */ -/** - * Custom error for document count exceeding limit - */ -class DocumentCountExceededError extends Error { -constructor(count, limit) { - super(`Document count ${count} exceeds limit of ${limit}`); - this.name = "DocumentCountExceededError"; - this.count = count; - this.limit = limit; - this.statusCode = 413; -} -} - -// ============================================================================= -// Utility Functions -// ============================================================================= - -/** - * Generate a unique request ID for tracing - * Uses UUID v4 for uniqueness - * - * @returns {string} Request ID in format: req_ - */ -function generateRequestId() { -return `req_${crypto.randomUUID()}`; -} - -/** - * Validate document ID format - * Google Drive IDs are alphanumeric with hyphens and underscores - * - * @param {string} id - Document ID to validate - * @returns {boolean} True if valid - */ -function validateDocumentId(id) { - if (!id || typeof id !== "string") { - return false; +(function() { + /** + * Custom error for document count exceeding limit + */ + class DocumentCountExceededError extends Error { + constructor(count, limit) { + super(`Document count ${count} exceeds limit of ${limit}`); + this.name = "DocumentCountExceededError"; + this.count = count; + this.limit = limit; + this.statusCode = 413; } - - // Google Drive IDs are typically 8-128 characters - // Characters: a-z, A-Z, 0-9, -, _ - const pattern = /^[a-zA-Z0-9_-]{8,128}$/; - return pattern.test(id); -} - -/** - * Validate document count against limit - * - * @param {number} count - Document count - * @param {number} limit - Maximum allowed (default: 50000) - * @throws {DocumentCountExceededError} If count > limit - */ -function validateDocumentCount(count, limit = 50000) { - if (count > limit) { - throw new DocumentCountExceededError(count, limit); } -} - -// ============================================================================= -// XML Utilities -// ============================================================================= - -/** - * Escape special XML characters - * Prevents XML injection and ensures valid XML output - * - * @param {string} str - String to escape - * @returns {string} Escaped string safe for XML - */ -function escapeXml(str) { - if (!str) return ""; - - return str - .replace(/&/g, "&") - .replace(//g, ">") - .replace(/"/g, """) - .replace(/'/g, "'"); -} - -// ============================================================================= -// Error Mapping -// ============================================================================= - -/** - * Map Drive API error to HTTP status code and retry info - * - * Per specification: - * - 429: Rate limit - include Retry-After header - * - 503: Service unavailable - NO RETRY (fail immediately) - * - 401: Authentication failed - * - 500: Other errors - * - * @param {Error} error - Drive API error - * @returns {Object} { statusCode, retryAfter? } - */ -function mapDriveErrorToHttp(error) { - // Handle DocumentCountExceededError - if (error instanceof DocumentCountExceededError) { - return { statusCode: 413 }; + + // ============================================================================= + // Utility Functions + // ============================================================================= + + /** + * Generate a unique request ID for tracing + * Uses UUID v4 for uniqueness + * + * @returns {string} Request ID in format: req_ + */ + function generateRequestId() { + return `req_${crypto.randomUUID()}`; } - - // Extract status code from Drive API error - const statusCode = error.response?.status || error.code || 500; - - // Handle rate limiting (429) - if (statusCode === 429) { - // Extract Retry-After from response headers if present - const retryAfter = error.response?.headers?.["retry-after"]; - const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : 60; - - return { - statusCode: 429, - retryAfter: retryAfterSeconds, - }; + + /** + * Validate document ID format + * Google Drive IDs are alphanumeric with hyphens and underscores + * + * @param {string} id - Document ID to validate + * @returns {boolean} True if valid + */ + function validateDocumentId(id) { + if (!id || typeof id !== "string") { + return false; + } + + // Google Drive IDs are typically 8-128 characters + // Characters: a-z, A-Z, 0-9, -, _ + const pattern = /^[a-zA-Z0-9_-]{8,128}$/; + return pattern.test(id); } - - // Handle service unavailable (503) - NO RETRY per spec - if (statusCode === 503) { - return { statusCode: 503 }; + + /** + * Validate document count against limit + * + * @param {number} count - Document count + * @param {number} limit - Maximum allowed (default: 50000) + * @throws {DocumentCountExceededError} If count > limit + */ + function validateDocumentCount(count, limit = 50000) { + if (count > limit) { + throw new DocumentCountExceededError(count, limit); + } } - - // Handle authentication errors - if (statusCode === 401 || statusCode === 403) { - return { statusCode: statusCode }; + + // ============================================================================= + // XML Utilities + // ============================================================================= + + /** + * Escape special XML characters + * Prevents XML injection and ensures valid XML output + * + * @param {string} str - String to escape + * @returns {string} Escaped string safe for XML + */ + function escapeXml(str) { + if (!str) return ""; + + return str + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); } - - // All other errors map to 500 - return { statusCode: 500 }; -} - -// ============================================================================= -// Sitemap Functions -// ============================================================================= - -/** - * Transform Drive document to sitemap entry - * - * Creates RESTful URL in format: {baseUrl}/documents/{documentId} - * Per specification clarification #2. - * - * @param {Object} document - Drive API document - * @param {string} document.id - Document ID - * @param {string} document.modifiedTime - ISO 8601 timestamp - * @param {string} baseUrl - Base URL for the adapter - * @returns {Object} Sitemap entry { loc, lastmod } - */ -function toSitemapEntry(document, baseUrl) { - if (!document || !document.id) { - console.error("Invalid document for sitemap entry", { document }); - return null; + + // ============================================================================= + // Error Mapping + // ============================================================================= + + /** + * Map Drive API error to HTTP status code and retry info + * + * Per specification: + * - 429: Rate limit - include Retry-After header + * - 503: Service unavailable - NO RETRY (fail immediately) + * - 401: Authentication failed + * - 500: Other errors + * + * @param {Error} error - Drive API error + * @returns {Object} { statusCode, retryAfter? } + */ + function mapDriveErrorToHttp(error) { + // Handle DocumentCountExceededError + if (error instanceof DocumentCountExceededError) { + return { statusCode: 413 }; + } + + // Extract status code from Drive API error + const statusCode = error.response?.status || error.code || 500; + + // Handle rate limiting (429) + if (statusCode === 429) { + // Extract Retry-After from response headers if present + const retryAfter = error.response?.headers?.["retry-after"]; + const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : 60; + + return { + statusCode: 429, + retryAfter: retryAfterSeconds, + }; + } + + // Handle service unavailable (503) - NO RETRY per spec + if (statusCode === 503) { + return { statusCode: 503 }; + } + + // Handle authentication errors + if (statusCode === 401 || statusCode === 403) { + return { statusCode: statusCode }; + } + + // All other errors map to 500 + return { statusCode: 500 }; } - - // RESTful URL format: /documents/{documentId} - const loc = `${baseUrl}/documents/${encodeURIComponent(document.id)}`; - - // Format lastmod as ISO 8601 date (YYYY-MM-DD) - let lastmod; - if (document.modifiedTime) { - try { - const date = new Date(document.modifiedTime); - lastmod = date.toISOString().split("T")[0]; // Extract YYYY-MM-DD - } catch (error) { - console.error("Invalid modifiedTime for document", { - documentId: document.id, - modifiedTime: document.modifiedTime, - }); + + // ============================================================================= + // Sitemap Functions + // ============================================================================= + + /** + * Transform Drive document to sitemap entry + * + * Creates RESTful URL in format: {baseUrl}/documents/{documentId} + * Per specification clarification #2. + * + * @param {Object} document - Drive API document + * @param {string} document.id - Document ID + * @param {string} document.modifiedTime - ISO 8601 timestamp + * @param {string} baseUrl - Base URL for the adapter + * @returns {Object} Sitemap entry { loc, lastmod } + */ + function toSitemapEntry(document, baseUrl) { + if (!document || !document.id) { + console.error("Invalid document for sitemap entry", { document }); + return null; + } + + // RESTful URL format: /documents/{documentId} + const loc = `${baseUrl}/documents/${encodeURIComponent(document.id)}`; + + // Format lastmod as ISO 8601 date (YYYY-MM-DD) + let lastmod; + if (document.modifiedTime) { + try { + const date = new Date(document.modifiedTime); + lastmod = date.toISOString().split("T")[0]; // Extract YYYY-MM-DD + } catch (error) { + console.error("Invalid modifiedTime for document", { + documentId: document.id, + modifiedTime: document.modifiedTime, + }); + lastmod = new Date().toISOString().split("T")[0]; // Fallback to today + } + } else { lastmod = new Date().toISOString().split("T")[0]; // Fallback to today } - } else { - lastmod = new Date().toISOString().split("T")[0]; // Fallback to today + + return { loc, lastmod }; } - - return { loc, lastmod }; -} - -/** - * Transform array of Drive documents to sitemap entries - * - * @param {Array} documents - Array of Drive API documents - * @param {string} baseUrl - Base URL for the adapter - * @returns {Array} Array of sitemap entries - */ -function transformDocumentsToSitemapEntries(documents, baseUrl) { - if (!Array.isArray(documents)) { - console.error("Documents must be an array", { documents }); - return []; + + /** + * Transform array of Drive documents to sitemap entries + * + * @param {Array} documents - Array of Drive API documents + * @param {string} baseUrl - Base URL for the adapter + * @returns {Array} Array of sitemap entries + */ + function transformDocumentsToSitemapEntries(documents, baseUrl) { + if (!Array.isArray(documents)) { + console.error("Documents must be an array", { documents }); + return []; + } + + return documents + .map((doc) => toSitemapEntry(doc, baseUrl)) + .filter((entry) => entry !== null); } - - return documents - .map((doc) => toSitemapEntry(doc, baseUrl)) - .filter((entry) => entry !== null); -} - -/** - * Generate XML sitemap from sitemap entries - * - * Handles empty sitemap (0 documents) case - returns valid XML with empty urlset. - * - * @param {Array} sitemapEntries - Array of { loc, lastmod } objects - * @returns {string} Complete XML sitemap string - */ -function generateSitemapXML(sitemapEntries) { - let xml = '\n'; - xml += '\n'; - - // Handle empty sitemap - valid XML with no elements - if (!sitemapEntries || sitemapEntries.length === 0) { + + /** + * Generate XML sitemap from sitemap entries + * + * Handles empty sitemap (0 documents) case - returns valid XML with empty urlset. + * + * @param {Array} sitemapEntries - Array of { loc, lastmod } objects + * @returns {string} Complete XML sitemap string + */ + function generateSitemapXML(sitemapEntries) { + let xml = '\n'; + xml += '\n'; + + // Handle empty sitemap - valid XML with no elements + if (!sitemapEntries || sitemapEntries.length === 0) { + xml += ""; + return xml; + } + + for (const entry of sitemapEntries) { + xml += " \n"; + xml += ` ${escapeXml(entry.loc)}\n`; + xml += ` ${escapeXml(entry.lastmod)}\n`; + xml += " \n"; + } + xml += ""; + return xml; } - - for (const entry of sitemapEntries) { - xml += " \n"; - xml += ` ${escapeXml(entry.loc)}\n`; - xml += ` ${escapeXml(entry.lastmod)}\n`; - xml += " \n"; + + /** + * Main sitemap generation function + * + * Combines document transformation and XML generation. + * + * @param {Array} documents - Array of Drive API documents + * @param {string} baseUrl - Base URL for the adapter + * @returns {string} Complete XML sitemap + */ + function generateSitemap(documents, baseUrl) { + const entries = transformDocumentsToSitemapEntries(documents, baseUrl); + return generateSitemapXML(entries); } - - xml += ""; - - return xml; -} - -/** - * Main sitemap generation function - * - * Combines document transformation and XML generation. - * - * @param {Array} documents - Array of Drive API documents - * @param {string} baseUrl - Base URL for the adapter - * @returns {string} Complete XML sitemap - */ -function generateSitemap(documents, baseUrl) { - const entries = transformDocumentsToSitemapEntries(documents, baseUrl); - return generateSitemapXML(entries); -} - -// ============================================================================= -// Route Parsing -// ============================================================================= - -/** - * Parse route from request - * @param {string} method - HTTP method - * @param {string} url - Request URL - * @returns {Object} Route info or error - */ -function parseRoute(method, url) { - if (method !== "GET") { - return { route: null, error: "Method not allowed", statusCode: 405 }; + + // ============================================================================= + // Route Parsing + // ============================================================================= + + /** + * Parse route from request + * @param {string} method - HTTP method + * @param {string} url - Request URL + * @returns {Object} Route info or error + */ + function parseRoute(method, url) { + if (method !== "GET") { + return { route: null, error: "Method not allowed", statusCode: 405 }; + } + + const urlObj = new URL(url, "http://localhost"); + const path = urlObj.pathname; + + // Match any path containing 'sitemap.xml' + if (path.includes("sitemap.xml")) { + return { route: "sitemap" }; + } + + // All other paths return 404 + return { route: null, error: "Not found", statusCode: 404 }; } + // ============================================================================= + // Return helpers object with all functions + // ============================================================================= - const urlObj = new URL(url, "http://localhost"); - const path = urlObj.pathname; - - // Match any path containing 'sitemap.xml' - if (path.includes("sitemap.xml")) { - return { route: "sitemap" }; - } - -// All other paths return 404 -return { route: null, error: "Not found", statusCode: 404 }; -} - -// ============================================================================= -// Return helpers object with all functions -// ============================================================================= - -({ -// Error classes -DocumentCountExceededError, - -// Utilities -generateRequestId, -validateDocumentId, -validateDocumentCount, - -// XML -escapeXml, - -// Error mapping -mapDriveErrorToHttp, - -// Sitemap -toSitemapEntry, -transformDocumentsToSitemapEntries, -generateSitemapXML, -generateSitemap, - -// Routing -parseRoute, -}); + return { + // Error classes + DocumentCountExceededError, + + // Utilities + generateRequestId, + validateDocumentId, + validateDocumentCount, + + // XML + escapeXml, + + // Error mapping + mapDriveErrorToHttp, + + // Sitemap + toSitemapEntry, + transformDocumentsToSitemapEntries, + generateSitemapXML, + generateSitemap, + + // Routing + parseRoute, + }; +})();