Remove IIFE wrapper from googleDriveAdapterHelper.js

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>
This commit is contained in:
2026-03-07 12:06:13 -06:00
parent 470f760b9b
commit c094d4d472
2 changed files with 254 additions and 258 deletions

View File

@@ -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: **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 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 - 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 - Contains ONLY pure utilities, NO business logic or state
### I. Zero External Imports or Exports from `src/proxyScripts/proxy.js` (NON-NEGOTIABLE) ### I. Zero External Imports or Exports from `src/proxyScripts/proxy.js` (NON-NEGOTIABLE)
@@ -291,11 +291,11 @@ config/
**googleDriveAdapterHelper.js Pattern**: **googleDriveAdapterHelper.js Pattern**:
- MUST be loaded using `vm.Script` (same isolation as proxy.js) - 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) - MUST have ZERO imports/exports (pure vm.Script execution)
- Loaded by `loadGlobalVariables()` which scans for both JSON and JS files - Loaded by `loadGlobalVariables()` which scans for both JSON and JS files
- Filename determines global key: `googleDriveAdapterHelper.js``globalVariableContext.helpers` - Filename determines global key: `googleDriveAdapterHelper.js``globalVariableContext.googleDriveAdapterHelper`
- Injected as `helpers` global object into VM context - Injected as `googleDriveAdapterHelper` global object into VM context
- Contains ONLY pure utilities: validators, formatters, XML, error mappers - Contains ONLY pure utilities: validators, formatters, XML, error mappers
- MUST NOT contain: authentication, API calls, state, business decisions - MUST NOT contain: authentication, API calls, state, business decisions
- Executed in context with full access to globalVMContext and globalVariableContext - Executed in context with full access to globalVMContext and globalVariableContext
@@ -441,7 +441,6 @@ script.runInContext(context);
10. **googleDriveAdapterHelper** - Pure utility functions object (OPTIONAL) 10. **googleDriveAdapterHelper** - Pure utility functions object (OPTIONAL)
- Purpose: Extracted helper functions for code organization - Purpose: Extracted helper functions for code organization
- Source: `src/globalVariables/googleDriveAdapterHelper.js` loaded via `vm.Script` - 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 - Loading: server.js loads via `loadGlobalVariables()` at startup
- Generic Loading Pattern: - Generic Loading Pattern:
- All .js files in globalVariables/ are loaded automatically - All .js files in globalVariables/ are loaded automatically

View File

@@ -17,299 +17,296 @@
* @returns {Object} Helpers object with all utility functions * @returns {Object} Helpers object with all utility functions
*/ */
// Wrap in IIFE that returns helpers object /**
(function createHelpers() { * Custom error for document count exceeding limit
/** */
* Custom error for document count exceeding limit class DocumentCountExceededError extends Error {
*/ constructor(count, limit) {
class DocumentCountExceededError extends Error { super(`Document count ${count} exceeds limit of ${limit}`);
constructor(count, limit) { this.name = "DocumentCountExceededError";
super(`Document count ${count} exceeds limit of ${limit}`); this.count = count;
this.name = "DocumentCountExceededError"; this.limit = limit;
this.count = count; this.statusCode = 413;
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_<uuid>
*/
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
// Utility Functions // 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 * Validate document count against limit
* Uses UUID v4 for uniqueness *
* * @param {number} count - Document count
* @returns {string} Request ID in format: req_<uuid> * @param {number} limit - Maximum allowed (default: 50000)
*/ * @throws {DocumentCountExceededError} If count > limit
function generateRequestId() { */
return `req_${crypto.randomUUID()}`; 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
}
// =============================================================================
// 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
* Validate document ID format const statusCode = error.response?.status || error.code || 500;
* 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 // Handle rate limiting (429)
// Characters: a-z, A-Z, 0-9, -, _ if (statusCode === 429) {
const pattern = /^[a-zA-Z0-9_-]{8,128}$/; // Extract Retry-After from response headers if present
return pattern.test(id); 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
* Validate document count against limit if (statusCode === 503) {
* return { statusCode: 503 };
* @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
// XML Utilities if (statusCode === 401 || statusCode === 403) {
// ============================================================================= return { statusCode: statusCode };
/**
* 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, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&apos;");
} }
// ============================================================================= // All other errors map to 500
// Error Mapping return { statusCode: 500 };
// ============================================================================= }
/** // =============================================================================
* Map Drive API error to HTTP status code and retry info // Sitemap Functions
* // =============================================================================
* 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; * Transform Drive document to sitemap entry
*
// Handle rate limiting (429) * Creates RESTful URL in format: {baseUrl}/documents/{documentId}
if (statusCode === 429) { * Per specification clarification #2.
// Extract Retry-After from response headers if present *
const retryAfter = error.response?.headers?.["retry-after"]; * @param {Object} document - Drive API document
const retryAfterSeconds = retryAfter ? parseInt(retryAfter, 10) : 60; * @param {string} document.id - Document ID
* @param {string} document.modifiedTime - ISO 8601 timestamp
return { * @param {string} baseUrl - Base URL for the adapter
statusCode: 429, * @returns {Object} Sitemap entry { loc, lastmod }
retryAfter: retryAfterSeconds, */
}; function toSitemapEntry(document, baseUrl) {
} if (!document || !document.id) {
console.error("Invalid document for sitemap entry", { document });
// Handle service unavailable (503) - NO RETRY per spec return null;
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}
// Sitemap Functions const loc = `${baseUrl}/documents/${encodeURIComponent(document.id)}`;
// =============================================================================
/** // Format lastmod as ISO 8601 date (YYYY-MM-DD)
* Transform Drive document to sitemap entry let lastmod;
* if (document.modifiedTime) {
* Creates RESTful URL in format: {baseUrl}/documents/{documentId} try {
* Per specification clarification #2. const date = new Date(document.modifiedTime);
* lastmod = date.toISOString().split("T")[0]; // Extract YYYY-MM-DD
* @param {Object} document - Drive API document } catch (error) {
* @param {string} document.id - Document ID console.error("Invalid modifiedTime for document", {
* @param {string} document.modifiedTime - ISO 8601 timestamp documentId: document.id,
* @param {string} baseUrl - Base URL for the adapter modifiedTime: document.modifiedTime,
* @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 lastmod = new Date().toISOString().split("T")[0]; // Fallback to today
} }
} else {
return { loc, lastmod }; lastmod = new Date().toISOString().split("T")[0]; // Fallback to today
} }
/** return { loc, lastmod };
* Transform array of Drive documents to sitemap entries }
*
* @param {Array<Object>} documents - Array of Drive API documents
* @param {string} baseUrl - Base URL for the adapter
* @returns {Array<Object>} 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)) * Transform array of Drive documents to sitemap entries
.filter((entry) => entry !== null); *
* @param {Array<Object>} documents - Array of Drive API documents
* @param {string} baseUrl - Base URL for the adapter
* @returns {Array<Object>} Array of sitemap entries
*/
function transformDocumentsToSitemapEntries(documents, baseUrl) {
if (!Array.isArray(documents)) {
console.error("Documents must be an array", { documents });
return [];
} }
/** return documents
* Generate XML sitemap from sitemap entries .map((doc) => toSitemapEntry(doc, baseUrl))
* .filter((entry) => entry !== null);
* Handles empty sitemap (0 documents) case - returns valid XML with empty urlset. }
*
* @param {Array<Object>} sitemapEntries - Array of { loc, lastmod } objects
* @returns {string} Complete XML sitemap string
*/
function generateSitemapXML(sitemapEntries) {
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
// Handle empty sitemap - valid XML with no <url> elements /**
if (!sitemapEntries || sitemapEntries.length === 0) { * Generate XML sitemap from sitemap entries
xml += "</urlset>"; *
return xml; * Handles empty sitemap (0 documents) case - returns valid XML with empty urlset.
} *
* @param {Array<Object>} sitemapEntries - Array of { loc, lastmod } objects
for (const entry of sitemapEntries) { * @returns {string} Complete XML sitemap string
xml += " <url>\n"; */
xml += ` <loc>${escapeXml(entry.loc)}</loc>\n`; function generateSitemapXML(sitemapEntries) {
xml += ` <lastmod>${escapeXml(entry.lastmod)}</lastmod>\n`; let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
xml += " </url>\n"; xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">\n';
}
// Handle empty sitemap - valid XML with no <url> elements
if (!sitemapEntries || sitemapEntries.length === 0) {
xml += "</urlset>"; xml += "</urlset>";
return xml; return xml;
} }
/** for (const entry of sitemapEntries) {
* Main sitemap generation function xml += " <url>\n";
* xml += ` <loc>${escapeXml(entry.loc)}</loc>\n`;
* Combines document transformation and XML generation. xml += ` <lastmod>${escapeXml(entry.lastmod)}</lastmod>\n`;
* xml += " </url>\n";
* @param {Array<Object>} 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 += "</urlset>";
// Route Parsing
// =============================================================================
/** return 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 };
}
const urlObj = new URL(url, "http://localhost"); /**
const path = urlObj.pathname; * Main sitemap generation function
*
* Combines document transformation and XML generation.
*
* @param {Array<Object>} 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")) { // Route Parsing
return { route: "sitemap" }; // =============================================================================
}
// 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 };
} }
// ============================================================================= const urlObj = new URL(url, "http://localhost");
// Return helpers object with all functions const path = urlObj.pathname;
// =============================================================================
return { // Match any path containing 'sitemap.xml'
// Error classes if (path.includes("sitemap.xml")) {
DocumentCountExceededError, return { route: "sitemap" };
}
// Utilities // All other paths return 404
generateRequestId, return { route: null, error: "Not found", statusCode: 404 };
validateDocumentId, }
validateDocumentCount,
// XML // =============================================================================
escapeXml, // Return helpers object with all functions
// =============================================================================
// Error mapping ({
mapDriveErrorToHttp, // Error classes
DocumentCountExceededError,
// Sitemap // Utilities
toSitemapEntry, generateRequestId,
transformDocumentsToSitemapEntries, validateDocumentId,
generateSitemapXML, validateDocumentCount,
generateSitemap,
// Routing // XML
parseRoute, escapeXml,
};
})(); // Error mapping
mapDriveErrorToHttp,
// Sitemap
toSitemapEntry,
transformDocumentsToSitemapEntries,
generateSitemapXML,
generateSitemap,
// Routing
parseRoute,
});