@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
JavaScript
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;