diff --git a/src/globalVariables/googleDriveAdapterHelper.js b/src/globalVariables/googleDriveAdapterHelper.js index dd33ce8..c706049 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) - * - Function that returns a single object containing all helper functions + * - 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,297 +17,296 @@ * @returns {Object} Helpers object with all utility functions */ -(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; +/** + * 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; } + + // 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); } - - // ============================================================================= - // 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()}`; +} + +// ============================================================================= +// 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 }; } - - /** - * 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); + + // 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 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 service unavailable (503) - NO RETRY per spec + if (statusCode === 503) { + return { statusCode: 503 }; } - - // ============================================================================= - // 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, "'"); + + // Handle authentication errors + if (statusCode === 401 || statusCode === 403) { + return { statusCode: statusCode }; } - - // ============================================================================= - // 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 }; + + // 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; } - - // ============================================================================= - // 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 { + + // 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 } - - return { loc, lastmod }; + } else { + lastmod = new Date().toISOString().split("T")[0]; // Fallback to today } - - /** - * 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 { 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 []; } - - /** - * 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"; - } - + + 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) { 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 }; - } - - 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 - // ============================================================================= - return { - // Error classes - DocumentCountExceededError, - - // Utilities - generateRequestId, - validateDocumentId, - validateDocumentCount, - - // XML - escapeXml, - - // Error mapping - mapDriveErrorToHttp, - - // Sitemap - toSitemapEntry, - transformDocumentsToSitemapEntries, - generateSitemapXML, - generateSitemap, - - // Routing - parseRoute, - }; -})(); + for (const entry of sitemapEntries) { + xml += " \n"; + xml += ` ${escapeXml(entry.loc)}\n`; + xml += ` ${escapeXml(entry.lastmod)}\n`; + xml += " \n"; + } + + 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 }; + } + + 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, +});