From c094d4d4727acad947695bfe8804e957da2b07c1 Mon Sep 17 00:00:00 2001 From: "Peter.Morton" Date: Sat, 7 Mar 2026 12:06:13 -0600 Subject: [PATCH] Remove IIFE wrapper from googleDriveAdapterHelper.js MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changed from IIFE pattern to direct object literal evaluation Rationale: - Global variable functions should evaluate to an object directly - IIFE wrapper was unnecessary complexity - Simpler pattern: code evaluates to object, not function that returns object - Matches intended architecture for vm.Script module loading Changes: 1. Removed IIFE wrapper: - Before: (function createHelpers() { ... return {...} })() - After: ({ ... }) 2. Removed function indentation (2 spaces throughout file) 3. Updated constitution.md: - Pattern description: 'IIFE returning object' → 'evaluates to a single object' - Clarified: 'not wrapped in IIFE' - Updated all pattern references Structure: - File defines classes and functions at top level - Final expression is object literal referencing all functions - When executed via vm.Script, evaluates to the object - Object is assigned to globalVariableContext.googleDriveAdapterHelper Benefits: ✅ Simpler pattern (no function wrapper) ✅ Clearer intent (direct object evaluation) ✅ Matches architecture description ✅ Easier to understand and maintain ✅ Same functionality, cleaner implementation Before (IIFE): After (Object Literal): Testing: ✓ Syntax validated ✓ Server starts successfully ✓ Module loads: 'Loaded global functions: googleDriveAdapterHelper' ✓ All function calls work correctly ✓ Request handling works as expected Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .specify/memory/constitution.md | 11 +- .../googleDriveAdapterHelper.js | 501 +++++++++--------- 2 files changed, 254 insertions(+), 258 deletions(-) diff --git a/.specify/memory/constitution.md b/.specify/memory/constitution.md index 975bcb6..b4e10c1 100644 --- a/.specify/memory/constitution.md +++ b/.specify/memory/constitution.md @@ -204,9 +204,9 @@ Follow-up TODOs: **Helper Functions Pattern**: Pure utility functions (XML escaping, validation, formatting, routing) MAY be extracted to `src/globalVariables/googleDriveAdapterHelper.js` to improve readability and maintainability. The helpers module: - MUST be loaded via `vm.Script` (same isolation as proxy.js) -- MUST return a single object with all helper functions +- MUST evaluate to a single JavaScript object with all helper functions - MUST have ZERO imports/exports -- Is injected as `helpers` global object into VM context +- Is injected as `googleDriveAdapterHelper` global object into VM context - Contains ONLY pure utilities, NO business logic or state ### I. Zero External Imports or Exports from `src/proxyScripts/proxy.js` (NON-NEGOTIABLE) @@ -291,11 +291,11 @@ config/ **googleDriveAdapterHelper.js Pattern**: - MUST be loaded using `vm.Script` (same isolation as proxy.js) -- MUST return single object with all helper functions via IIFE +- MUST evaluate to a single JavaScript object (not wrapped in IIFE) - MUST have ZERO imports/exports (pure vm.Script execution) - Loaded by `loadGlobalVariables()` which scans for both JSON and JS files -- Filename determines global key: `googleDriveAdapterHelper.js` → `globalVariableContext.helpers` -- Injected as `helpers` global object into VM context +- Filename determines global key: `googleDriveAdapterHelper.js` → `globalVariableContext.googleDriveAdapterHelper` +- Injected as `googleDriveAdapterHelper` global object into VM context - Contains ONLY pure utilities: validators, formatters, XML, error mappers - MUST NOT contain: authentication, API calls, state, business decisions - Executed in context with full access to globalVMContext and globalVariableContext @@ -441,7 +441,6 @@ script.runInContext(context); 10. **googleDriveAdapterHelper** - Pure utility functions object (OPTIONAL) - Purpose: Extracted helper functions for code organization - Source: `src/globalVariables/googleDriveAdapterHelper.js` loaded via `vm.Script` - - Pattern: IIFE returning object with all helper functions - Loading: server.js loads via `loadGlobalVariables()` at startup - Generic Loading Pattern: - All .js files in globalVariables/ are loaded automatically diff --git a/src/globalVariables/googleDriveAdapterHelper.js b/src/globalVariables/googleDriveAdapterHelper.js index 52daa06..c706049 100644 --- a/src/globalVariables/googleDriveAdapterHelper.js +++ b/src/globalVariables/googleDriveAdapterHelper.js @@ -17,299 +17,296 @@ * @returns {Object} Helpers object with all utility functions */ -// Wrap in IIFE that returns helpers object -(function createHelpers() { - /** - * 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; } - // ============================================================================= - // Utility Functions - // ============================================================================= + // 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); +} - /** - * 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 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 }; } - /** - * 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; - } + // Extract status code from Drive API error + const statusCode = error.response?.status || error.code || 500; - // 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 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 - // ============================================================================= + // All other errors map to 500 + return { statusCode: 500 }; +} - /** - * 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 }; - } +// ============================================================================= +// Sitemap Functions +// ============================================================================= - // 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 }; +/** + * 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 - // ============================================================================= + // RESTful URL format: /documents/{documentId} + const loc = `${baseUrl}/documents/${encodeURIComponent(document.id)}`; - /** - * 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 { + // 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 { loc, lastmod }; +} - return documents - .map((doc) => toSitemapEntry(doc, baseUrl)) - .filter((entry) => entry !== null); +/** + * 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'; + return documents + .map((doc) => toSitemapEntry(doc, baseUrl)) + .filter((entry) => entry !== null); +} - // 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"; - } +/** + * 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); + for (const entry of sitemapEntries) { + xml += " \n"; + xml += ` ${escapeXml(entry.loc)}\n`; + xml += ` ${escapeXml(entry.lastmod)}\n`; + xml += " \n"; } - // ============================================================================= - // Route Parsing - // ============================================================================= + xml += ""; - /** - * 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 }; - } + return xml; +} - const urlObj = new URL(url, "http://localhost"); - const path = urlObj.pathname; +/** + * 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); +} - // Match any path containing 'sitemap.xml' - if (path.includes("sitemap.xml")) { - return { route: "sitemap" }; - } +// ============================================================================= +// Route Parsing +// ============================================================================= - // All other paths return 404 - return { route: null, error: "Not found", statusCode: 404 }; +/** + * 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 }; } - // ============================================================================= - // Return helpers object with all functions - // ============================================================================= + const urlObj = new URL(url, "http://localhost"); + const path = urlObj.pathname; - return { - // Error classes - DocumentCountExceededError, + // Match any path containing 'sitemap.xml' + if (path.includes("sitemap.xml")) { + return { route: "sitemap" }; + } - // Utilities - generateRequestId, - validateDocumentId, - validateDocumentCount, +// All other paths return 404 +return { route: null, error: "Not found", statusCode: 404 }; +} - // XML - escapeXml, +// ============================================================================= +// Return helpers object with all functions +// ============================================================================= - // Error mapping - mapDriveErrorToHttp, +({ +// Error classes +DocumentCountExceededError, - // Sitemap - toSitemapEntry, - transformDocumentsToSitemapEntries, - generateSitemapXML, - generateSitemap, +// Utilities +generateRequestId, +validateDocumentId, +validateDocumentCount, - // Routing - parseRoute, - }; -})(); +// XML +escapeXml, + +// Error mapping +mapDriveErrorToHttp, + +// Sitemap +toSitemapEntry, +transformDocumentsToSitemapEntries, +generateSitemapXML, +generateSitemap, + +// Routing +parseRoute, +});