UNPKG

@voidagency/api-gateway-sdk

Version:

Node.js SDK for the DDEV Update Manager API - A powerful toolkit for managing DDEV instances, executing commands, running SQL queries, and PHP scripts with convenient template literal syntax

456 lines (410 loc) 15.1 kB
const got = require('got'); /** * DDEV Update Manager API SDK * A Node.js SDK for interacting with the DDEV Update Manager API */ class DdevUpdateManagerSDK { /** * Create a new SDK instance * @param {Object} options - Configuration options * @param {string} options.baseUrl - Base URL of the API (default: 'http://localhost:3000') * @param {string} options.apiKey - API key for authentication * @param {string} options.projectId - Optional project ID to set for this instance * @param {Object} options.requestOptions - Additional options to pass to got */ constructor(options = {}) { this.baseUrl = options.baseUrl || 'http://localhost:3000'; this.apiKey = options.apiKey; this.projectId = options.projectId || null; this.requestOptions = { timeout: { request: 30000 }, retry: { limit: 3 }, ...options.requestOptions }; if (!this.apiKey) { throw new Error('API key is required'); } } /** * Set the project ID for this SDK instance * @param {string} projectId - The project ID to set */ setProjectId(projectId) { if (!projectId) { throw new Error('Project ID is required'); } this.projectId = projectId; } /** * Get the current project ID * @returns {string|null} The current project ID */ getProjectId() { return this.projectId; } /** * Get instance by URL and set it as the current project * @param {string} url - The URL of the DDEV instance * @returns {Promise<string>} The instance name (projectId) */ async getInstanceByUrl(url) { if (!url) { throw new Error('URL is required'); } const instances = await this.listInstances(); const instance = instances.find(instance => instance.primary_url === url); if (!instance) { // Provide helpful debugging information const availableUrls = instances.map(inst => { const urls = []; if (inst.primary_url) urls.push(`primary_url: ${inst.primary_url}`); if (inst.httpurl) urls.push(`httpurl: ${inst.httpurl}`); if (inst.url) urls.push(`url: ${inst.url}`); if (inst.site_url) urls.push(`site_url: ${inst.site_url}`); if (inst.domain) urls.push(`domain: ${inst.domain}`); return `${inst.name}: [${urls.join(', ')}]`; }); throw new Error(`No DDEV instance found for URL: ${url}\nAvailable instances and their URLs:\n${availableUrls.join('\n')}`); } this.projectId = instance.name; return this.projectId; } /** * Helper method to get project ID from parameter or instance * @private * @param {string} projectId - Optional project ID parameter * @returns {string} The project ID to use */ _getProjectId(projectId) { const id = projectId || this.projectId; if (!id) { throw new Error('Project ID is required. Either pass it as a parameter or set it using setProjectId() or getInstanceByUrl()'); } return id; } /** * Create a got instance with default configuration * @private */ _createGotInstance() { return got.extend({ prefixUrl: this.baseUrl, headers: { 'X-API-KEY': this.apiKey, 'Content-Type': 'application/json' }, ...this.requestOptions }); } /** * Handle API errors and provide meaningful error messages * @private * @param {Error} error - The error from got * @param {string} operation - Description of the operation that failed * @returns {Error} Enhanced error with better message */ _handleApiError(error, operation) { // If it's a got HTTP error, try to extract more details if (error.response && error.response.body) { try { const errorBody = JSON.parse(error.response.body); // Build a comprehensive error message let errorMessage = `${operation} failed`; if (errorBody.shortMessage) { errorMessage += `: ${errorBody.shortMessage}`; } if (errorBody.stderr) { errorMessage += `\n\nSTDERR:\n${errorBody.stderr}`; } if (errorBody.stdout) { errorMessage += `\n\nSTDOUT:\n${errorBody.stdout}`; } if (errorBody.command) { errorMessage += `\n\nCommand: ${errorBody.command}`; } if (errorBody.exitCode) { errorMessage += `\nExit Code: ${errorBody.exitCode}`; } // Create a new error with the enhanced message const enhancedError = new Error(errorMessage); // enhancedError.originalError = error; enhancedError.statusCode = error.response.statusCode; enhancedError.errorDetails = errorBody; return enhancedError; } catch (parseError) { // If we can't parse the error body, fall back to the original error const enhancedError = new Error(`${operation} failed: ${error.message}`); enhancedError.originalError = error; enhancedError.statusCode = error.response.statusCode; return enhancedError; } } // For non-HTTP errors or errors without response body const enhancedError = new Error(`${operation} failed: ${error.message}`); enhancedError.originalError = error; return enhancedError; } /** * Check if an error is an API error with details * @param {Error} error - The error to check * @returns {boolean} True if the error has API details */ static isApiError(error) { return error && error.errorDetails && error.statusCode; } /** * Get error details from an API error * @param {Error} error - The error to extract details from * @returns {Object|null} Error details object or null if not an API error */ static getErrorDetails(error) { if (this.isApiError(error)) { return error.errorDetails; } return null; } /** * Get the original error from an enhanced API error * @param {Error} error - The enhanced error * @returns {Error|null} Original error or null if not an enhanced error */ static getOriginalError(error) { if (error && error.originalError) { return error.originalError; } return null; } /** * Get a simple hello message * @returns {Promise<string>} Hello message */ async getHello() { const client = this._createGotInstance(); try { const response = await client.get(''); return response.body; } catch (error) { throw this._handleApiError(error, 'Hello request'); } } /** * List all DDEV instances * @returns {Promise<Array>} Array of DDEV instances */ async listInstances() { const client = this._createGotInstance(); try { const response = await client.get('ddev/instances'); return JSON.parse(response.body); } catch (error) { throw this._handleApiError(error, 'List instances'); } } /** * Get detailed information about a specific DDEV project * @param {string} projectId - Optional project ID (uses instance projectId if not provided) * @returns {Promise<Object>} Project details */ async getProject(projectId) { const id = this._getProjectId(projectId); const client = this._createGotInstance(); try { const response = await client.get(`ddev/instances/${encodeURIComponent(id)}`); return JSON.parse(response.body); } catch (error) { throw this._handleApiError(error, `Get project ${id}`); } } /** * Start a DDEV project * @param {string} projectId - Optional project ID (uses instance projectId if not provided) * @returns {Promise<Object>} Updated project status */ async startProject(projectId) { const id = this._getProjectId(projectId); const client = this._createGotInstance(); try { const response = await client.get(`ddev/instances/${encodeURIComponent(id)}/start`); return JSON.parse(response.body); } catch (error) { throw this._handleApiError(error, `Start project ${id}`); } } /** * Execute a command in a DDEV project * @param {string} command - The command to execute * @param {string} projectId - Optional project ID (uses instance projectId if not provided) * @returns {Promise<Object>} Command execution result */ async execCommand(command, projectId) { if (!command) { throw new Error('Command is required'); } const id = this._getProjectId(projectId); const client = this._createGotInstance(); try { const response = await client.post(`ddev/instances/${encodeURIComponent(id)}/exec`, { json: { command } }); return JSON.parse(response.body); } catch (error) { throw this._handleApiError(error, `Execute command in project ${id}`); } } /** * Execute a SQL query in a DDEV project's database * @param {string} query - The SQL query to execute * @param {string} projectId - Optional project ID (uses instance projectId if not provided) * @returns {Promise<Object>} SQL query execution result */ async execSqlQuery(query, projectId) { if (!query) { throw new Error('SQL query is required'); } const id = this._getProjectId(projectId); const client = this._createGotInstance(); try { const response = await client.post(`ddev/instances/${encodeURIComponent(id)}/sql`, { json: { query } }); return JSON.parse(response.body); } catch (error) { throw this._handleApiError(error, `Execute SQL query in project ${id}`); } } /** * Run a PHP script in a DDEV project * @param {string} script - The PHP script code to execute * @param {string} projectId - Optional project ID (uses instance projectId if not provided) * @returns {Promise<Object>} PHP script execution result */ async runPhpScript(script, projectId) { if (!script) { throw new Error('PHP script is required'); } const id = this._getProjectId(projectId); const client = this._createGotInstance(); try { const response = await client.post(`ddev/instances/${encodeURIComponent(id)}/php`, { body: script, headers: { 'Content-Type': 'text/plain' } }); return JSON.parse(response.body); } catch (error) { throw this._handleApiError(error, `Run PHP script in project ${id}`); } } // Convenience methods for common operations /** * Get Drupal status using drush * @param {string} projectId - Optional project ID (uses instance projectId if not provided) * @returns {Promise<Object>} Drush status result */ async getDrupalStatus(projectId) { return this.execCommand('drush status', projectId); } /** * Clear Drupal cache using drush * @param {string} projectId - Optional project ID (uses instance projectId if not provided) * @returns {Promise<Object>} Cache clear result */ async clearDrupalCache(projectId) { return this.execCommand('drush cr', projectId); } /** * List files in the project directory * @param {string} path - Optional path to list (default: current directory) * @param {string} projectId - Optional project ID (uses instance projectId if not provided) * @returns {Promise<Object>} File listing result */ async listFiles(path = '.', projectId) { return this.execCommand(`ls -la ${path}`, projectId); } /** * Get user information from database * @param {number} uid - User ID (default: 1 for admin) * @param {string} projectId - Optional project ID (uses instance projectId if not provided) * @returns {Promise<Object>} User query result */ async getUserInfo(uid = 1, projectId) { const query = `SELECT uid, name, mail FROM users_field_data WHERE uid = ${uid}`; return this.execSqlQuery(query, projectId); } /** * Get list of installed modules * @param {string} projectId - Optional project ID (uses instance projectId if not provided) * @returns {Promise<Object>} Module list result */ async getInstalledModules(projectId) { const script = '$modules = Drupal::service("extension.list.module")->getAllInstalledInfo(); echo json_encode(array_keys($modules));'; return this.runPhpScript(script, projectId); } /** * Create a new node * @param {string} title - Node title * @param {string} type - Node type (default: 'article') * @param {string} projectId - Optional project ID (uses instance projectId if not provided) * @returns {Promise<Object>} Node creation result */ async createNode(title, type = 'article', projectId) { const script = `$node = Drupal\\node\\Entity\\Node::create(['type' => '${type}', 'title' => '${title}']); $node->save(); echo "Node created with ID: " . $node->id();`; return this.runPhpScript(script, projectId); } /** * Get PHP information * @param {string} projectId - Optional project ID (uses instance projectId if not provided) * @returns {Promise<Object>} PHP info result */ async getPhpInfo(projectId) { return this.runPhpScript('phpinfo();', projectId); } // Special alias methods using template literals for convenience /** * Execute command using template literal syntax * @param {string|TemplateStringsArray} command - The command to execute (without quotes) * @param {string} projectId - Optional project ID (uses instance projectId if not provided) * @returns {Promise<Object>} Command execution result * * @example * // Instead of: sdk.execCommand('ls -la', projectId) * // You can use: sdk.$exec`ls -la` */ async $exec(command, projectId) { // Handle template literal arrays const commandStr = Array.isArray(command) ? command.raw.join('') : command; return this.execCommand(commandStr, projectId); } /** * Execute SQL query using template literal syntax * @param {string|TemplateStringsArray} query - The SQL query to execute (without quotes) * @param {string} projectId - Optional project ID (uses instance projectId if not provided) * @returns {Promise<Object>} SQL query execution result * * @example * // Instead of: sdk.execSqlQuery('SELECT * FROM users', projectId) * // You can use: sdk.$sql`SELECT * FROM users` */ async $sql(query, projectId) { // Handle template literal arrays const queryStr = Array.isArray(query) ? query.raw.join('') : query; return this.execSqlQuery(queryStr, projectId); } /** * Run PHP script using template literal syntax * @param {string|TemplateStringsArray} script - The PHP script code to execute (without quotes) * @param {string} projectId - Optional project ID (uses instance projectId if not provided) * @returns {Promise<Object>} PHP script execution result * * @example * // Instead of: sdk.runPhpScript('echo "Hello";', projectId) * // You can use: sdk.$php`echo "Hello";` */ async $php(script, projectId) { // Handle template literal arrays const scriptStr = Array.isArray(script) ? script.raw.join('') : script; return this.runPhpScript(scriptStr, projectId); } } module.exports = DdevUpdateManagerSDK;