@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
832 lines (675 loc) • 21 kB
Markdown
# DDEV Update Manager API SDK
A Node.js SDK for interacting with the DDEV Update Manager API, built with the `got` HTTP client library.
## Installation
```bash
npm install @voidagency/api-gateway-sdk
```
## Quick Start
```javascript
const DdevUpdateManagerSDK = require('@voidagency/api-gateway-sdk');
// Initialize the SDK with your API key
const sdk = new DdevUpdateManagerSDK({
apiKey: 'your-api-key-here',
baseUrl: 'http://localhost:3000' // optional, defaults to localhost:3000
});
// List all DDEV instances
const instances = await sdk.listInstances();
console.log(instances);
```
## Configuration
The SDK constructor accepts the following options:
- `apiKey` (required): Your API key for authentication
- `baseUrl` (optional): Base URL of the API (default: `http://localhost:3000`)
- `projectId` (optional): Set a default project ID for this SDK instance
- `requestOptions` (optional): Additional options to pass to the `got` HTTP client
```javascript
const sdk = new DdevUpdateManagerSDK({
apiKey: 'your-api-key',
baseUrl: 'https://api.example.com',
projectId: 'my-drupal-project', // Set default project ID
requestOptions: {
timeout: { request: 60000 }, // 60 second timeout
retry: { limit: 5 } // 5 retry attempts
}
});
```
## Setting Project ID
You can set the project ID in three different ways:
### Method 1: Constructor Option
```javascript
const sdk = new DdevUpdateManagerSDK({
apiKey: 'your-api-key',
projectId: 'my-drupal-project'
});
```
### Method 2: Manual Setting
```javascript
const sdk = new DdevUpdateManagerSDK({ apiKey: 'your-api-key' });
sdk.setProjectId('my-drupal-project');
```
### Method 3: By URL
```javascript
const sdk = new DdevUpdateManagerSDK({ apiKey: 'your-api-key' });
const projectId = await sdk.getInstanceByUrl('https://my-drupal-project.ddev.site');
// projectId is now set to 'my-drupal-project'
```
Once a project ID is set, you can use methods without passing the projectId parameter:
```javascript
// With projectId set on instance
const sdk = new DdevUpdateManagerSDK({
apiKey: 'your-api-key',
projectId: 'my-drupal-project'
});
// No need to pass projectId to methods
await sdk.getDrupalStatus();
await sdk.clearDrupalCache();
await sdk.execCommand('drush status');
```
## API Methods
### Core Methods
#### `getHello()`
Get a simple hello message from the API.
```javascript
const message = await sdk.getHello();
console.log(message); // "Hello World!"
```
#### `listInstances()`
List all DDEV instances.
```javascript
const instances = await sdk.listInstances();
console.log(instances);
// [
// {
// name: "my-drupal-project",
// status: "running",
// type: "drupal9",
// httpurl: "https://my-drupal-project.ddev.site"
// }
// ]
```
#### `getProject(projectId?)`
Get detailed information about a specific DDEV project.
```javascript
// With explicit projectId
const project = await sdk.getProject('my-drupal-project');
// With instance projectId set
const sdk = new DdevUpdateManagerSDK({
apiKey: 'your-api-key',
projectId: 'my-drupal-project'
});
const project = await sdk.getProject(); // Uses instance projectId
console.log(project);
// {
// name: "my-drupal-project",
// status: "running",
// type: "drupal9",
// php_version: "8.1",
// database_type: "mysql",
// database_version: "8.0"
// }
```
#### `startProject(projectId?)`
Start a DDEV project.
```javascript
// With explicit projectId
const result = await sdk.startProject('my-drupal-project');
// With instance projectId set
const result = await sdk.startProject(); // Uses instance projectId
console.log(result);
// {
// name: "my-drupal-project",
// status: "running"
// }
```
#### `execCommand(command, projectId?)`
Execute a command in a DDEV project.
```javascript
// With explicit projectId
const result = await sdk.execCommand('drush status', 'my-drupal-project');
// With instance projectId set
const result = await sdk.execCommand('drush status'); // Uses instance projectId
console.log(result);
// {
// stdout: "Drupal version : 9.5.0\nPHP version : 8.1.0\nDatabase system : MySQL 8.0",
// stderr: "",
// exitCode: 0
// }
```
#### `execSqlQuery(query, projectId?)`
Execute a SQL query in a DDEV project's database.
```javascript
// With explicit projectId
const result = await sdk.execSqlQuery('SELECT uid, name, mail FROM users_field_data WHERE uid = 1', 'my-drupal-project');
// With instance projectId set
const result = await sdk.execSqlQuery('SELECT uid, name, mail FROM users_field_data WHERE uid = 1'); // Uses instance projectId
console.log(result);
// {
// stdout: {
// rows: [
// {
// uid: 1,
// name: "admin",
// mail: "admin@example.com"
// }
// ]
// },
// stderr: "",
// exitCode: 0
// }
```
#### `runPhpScript(script, projectId?)`
Run a PHP script in a DDEV project (Drupal will be automatically bootstrapped).
```javascript
// With explicit projectId
const script = `
$user = \Drupal\user\Entity\User::load(1);
echo $user->getEmail();
`;
const result = await sdk.runPhpScript(script, 'my-drupal-project');
// With instance projectId set
const result = await sdk.runPhpScript(script); // Uses instance projectId
console.log(result);
// {
// stdout: "admin@example.com",
// stderr: "",
// exitCode: 0
// }
```
### Template Literal Aliases
For even more convenient syntax, you can use template literal aliases:
#### `$exec(command, projectId?)`
Execute command using template literal syntax.
```javascript
// Instead of: sdk.execCommand('ls -la', projectId)
await sdk.$exec`ls -la`
await sdk.$exec('ls -la') // Also works with regular strings
```
#### `$sql(query, projectId?)`
Execute SQL query using template literal syntax.
```javascript
// Instead of: sdk.execSqlQuery('SELECT * FROM users', projectId)
await sdk.$sql`SELECT id FROM users`
await sdk.$sql('SELECT * FROM users') // Also works with regular strings
```
#### `$php(script, projectId?)`
Run PHP script using template literal syntax.
```javascript
// Instead of: sdk.runPhpScript('echo "Hello";', projectId)
await sdk.$php`echo "Hello from PHP!";`
// Multi-line scripts work great with template literals
await sdk.$php`
$term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
$terms = $term_storage->loadByProperties(['vid' => 'tags']);
$count = count($terms);
echo "Found {$count} taxonomy terms";
`
```
### Project ID Management Methods
#### `setProjectId(projectId)`
Set the project ID for this SDK instance.
```javascript
sdk.setProjectId('my-drupal-project');
```
#### `getProjectId()`
Get the current project ID.
```javascript
const projectId = sdk.getProjectId();
console.log(projectId); // "my-drupal-project" or null if not set
```
#### `getInstanceByUrl(url)`
Get instance name (projectId) by URL and set it as the current project.
```javascript
const projectId = await sdk.getInstanceByUrl('https://my-drupal-project.ddev.site');
console.log(projectId); // "my-drupal-project"
// The projectId is now set on the SDK instance
```
### Convenience Methods
The SDK also provides convenience methods for common operations:
#### `getDrupalStatus(projectId?)`
Get Drupal status using drush.
```javascript
const status = await sdk.getDrupalStatus('my-drupal-project');
// or with instance projectId: await sdk.getDrupalStatus();
```
#### `clearDrupalCache(projectId?)`
Clear Drupal cache using drush.
```javascript
const result = await sdk.clearDrupalCache('my-drupal-project');
// or with instance projectId: await sdk.clearDrupalCache();
```
#### `listFiles(path, projectId?)`
List files in the project directory.
```javascript
const files = await sdk.listFiles('/var/www/html', 'my-drupal-project');
// or with instance projectId: await sdk.listFiles('/var/www/html');
```
#### `getUserInfo(uid, projectId?)`
Get user information from database.
```javascript
const user = await sdk.getUserInfo(1, 'my-drupal-project');
// or with instance projectId: await sdk.getUserInfo(1);
```
#### `getInstalledModules(projectId?)`
Get list of installed modules.
```javascript
const modules = await sdk.getInstalledModules('my-drupal-project');
// or with instance projectId: await sdk.getInstalledModules();
```
#### `createNode(title, type, projectId?)`
Create a new node.
```javascript
const result = await sdk.createNode('My New Article', 'article', 'my-drupal-project');
// or with instance projectId: await sdk.createNode('My New Article', 'article');
```
#### `getPhpInfo(projectId?)`
Get PHP information.
```javascript
const phpInfo = await sdk.getPhpInfo('my-drupal-project');
// or with instance projectId: await sdk.getPhpInfo();
```
## Error Handling
The SDK provides comprehensive error handling with detailed error information from the API gateway. All errors are enhanced with additional context to help you debug issues effectively.
### Basic Error Handling
```javascript
try {
const result = await sdk.runPhpScript('echo "Hello";');
console.log('Success:', result);
} catch (error) {
console.error('Error:', error.message);
}
```
### Enhanced Error Information
The SDK automatically extracts detailed error information from API responses and provides it in a structured format:
```javascript
try {
const result = await sdk.runPhpScript(`
<?php
// This will cause a syntax error
if (true {
echo "Missing closing brace";
}
`);
} catch (error) {
console.log('Error Message:', error.message);
console.log('Status Code:', error.statusCode);
// Check if it's an API error with details
if (DdevUpdateManagerSDK.isApiError(error)) {
const details = DdevUpdateManagerSDK.getErrorDetails(error);
console.log('Error Details:', details);
// Access specific error information
console.log('Short Message:', details.shortMessage);
console.log('STDERR:', details.stderr);
console.log('STDOUT:', details.stdout);
console.log('Command:', details.command);
console.log('Exit Code:', details.exitCode);
}
}
```
### Error Types and Handling
#### API Errors (HTTP 4xx/5xx)
These errors contain detailed information from the API gateway:
```javascript
try {
await sdk.execCommand('invalid-command');
} catch (error) {
if (DdevUpdateManagerSDK.isApiError(error)) {
const details = DdevUpdateManagerSDK.getErrorDetails(error);
// Handle specific error types
switch (error.statusCode) {
case 401:
console.log('Authentication failed - check your API key');
break;
case 404:
console.log('Project not found - check the project ID');
break;
case 500:
console.log('Server error - check the API gateway logs');
if (details.stderr) {
console.log('Error output:', details.stderr);
}
break;
}
// Handle command-specific errors
if (details.exitCode === 1) {
console.log('Command failed with exit code 1 - likely a syntax or runtime error');
} else if (details.exitCode === 127) {
console.log('Command not found - check if the command exists in the DDEV environment');
}
}
}
```
#### Network/Connection Errors
These occur when the SDK can't reach the API gateway:
```javascript
try {
await sdk.getHello();
} catch (error) {
if (!DdevUpdateManagerSDK.isApiError(error)) {
console.log('Network/connection error:', error.message);
// This could be a timeout, DNS resolution failure, etc.
}
}
```
### Error Handling Utilities
The SDK provides static utility methods for working with errors:
#### `DdevUpdateManagerSDK.isApiError(error)`
Check if an error is an API error with details:
```javascript
if (DdevUpdateManagerSDK.isApiError(error)) {
// This error has API details (statusCode, errorDetails, etc.)
const details = DdevUpdateManagerSDK.getErrorDetails(error);
}
```
#### `DdevUpdateManagerSDK.getErrorDetails(error)`
Extract error details from an API error:
```javascript
const details = DdevUpdateManagerSDK.getErrorDetails(error);
if (details) {
console.log('Command:', details.command);
console.log('Exit Code:', details.exitCode);
console.log('STDERR:', details.stderr);
}
```
#### `DdevUpdateManagerSDK.getOriginalError(error)`
Get the original error from an enhanced API error:
```javascript
const originalError = DdevUpdateManagerSDK.getOriginalError(error);
if (originalError) {
console.log('Original error:', originalError.message);
}
```
### Common Error Scenarios
#### PHP Syntax Errors
```javascript
try {
await sdk.runPhpScript(`
<?php
if (true { // Missing closing brace
echo "Hello";
}
`);
} catch (error) {
if (DdevUpdateManagerSDK.isApiError(error)) {
const details = DdevUpdateManagerSDK.getErrorDetails(error);
console.log('PHP Syntax Error:', details.stderr);
// Output: Parse error: syntax error, unexpected token "{" in Standard input code on line 2
}
}
```
#### Command Not Found
```javascript
try {
await sdk.execCommand('non-existent-command');
} catch (error) {
if (DdevUpdateManagerSDK.isApiError(error)) {
const details = DdevUpdateManagerSDK.getErrorDetails(error);
if (details.exitCode === 127) {
console.log('Command not found:', details.command);
}
}
}
```
#### Invalid Project ID
```javascript
try {
await sdk.getProject('non-existent-project');
} catch (error) {
if (error.statusCode === 404) {
console.log('Project not found - check the project ID');
}
}
```
#### Authentication Errors
```javascript
try {
await sdk.listInstances();
} catch (error) {
if (error.statusCode === 401) {
console.log('Authentication failed - check your API key');
}
}
```
### Best Practices
#### 1. Always Use Try-Catch
```javascript
async function safeOperation() {
try {
return await sdk.execCommand('drush status');
} catch (error) {
console.error('Operation failed:', error.message);
throw error; // Re-throw if you want to handle it at a higher level
}
}
```
#### 2. Check Error Types
```javascript
async function handleErrors(error) {
if (DdevUpdateManagerSDK.isApiError(error)) {
// Handle API errors with details
const details = DdevUpdateManagerSDK.getErrorDetails(error);
console.log('API Error:', details.shortMessage);
} else {
// Handle network/connection errors
console.log('Network Error:', error.message);
}
}
```
#### 3. Log Error Details for Debugging
```javascript
try {
await sdk.runPhpScript(complexPhpCode);
} catch (error) {
console.error('Error:', error.message);
if (DdevUpdateManagerSDK.isApiError(error)) {
const details = DdevUpdateManagerSDK.getErrorDetails(error);
console.error('Full error details:', JSON.stringify(details, null, 2));
}
}
```
#### 4. Create Error Handling Wrappers
```javascript
async function safeExecute(operation, projectId) {
try {
return await operation(projectId);
} catch (error) {
if (DdevUpdateManagerSDK.isApiError(error)) {
const details = DdevUpdateManagerSDK.getErrorDetails(error);
// Handle specific error types
if (details.exitCode === 1) {
console.log('Command failed with exit code 1 - likely a syntax or runtime error');
if (details.stderr) {
console.log('Error output:', details.stderr);
}
}
}
throw error;
}
}
// Usage
try {
await safeExecute(sdk.getDrupalStatus.bind(sdk), 'my-project');
} catch (error) {
console.log('Error handled by wrapper');
}
```
### Error Handling with Template Literals
The template literal methods also benefit from the same error handling:
```javascript
try {
await sdk.$php`
<?php
echo "Hello from PHP!";
// This will cause a syntax error
if (true {
echo "Missing closing brace";
}
`;
} catch (error) {
if (DdevUpdateManagerSDK.isApiError(error)) {
const details = DdevUpdateManagerSDK.getErrorDetails(error);
console.log('PHP syntax error in template literal:', details.stderr);
}
}
```
For more examples, see the `error-handling-example.js` file in this package.
## Examples
### Complete Workflow Example with Project ID
```javascript
const DdevUpdateManagerSDK = require('api-gateway-sdk');
async function manageDdevProject() {
// Set project ID in constructor
const sdk = new DdevUpdateManagerSDK({
apiKey: 'your-api-key',
baseUrl: 'http://localhost:3000',
projectId: 'my-drupal-project'
});
try {
// List all instances
const instances = await sdk.listInstances();
console.log('Available instances:', instances);
// Get project details (no projectId needed)
const project = await sdk.getProject();
console.log('Project details:', project);
// Start project if not running (no projectId needed)
if (project.status !== 'running') {
await sdk.startProject();
console.log('Project started');
}
// Get Drupal status (no projectId needed)
const status = await sdk.getDrupalStatus();
console.log('Drupal status:', status);
// Clear cache (no projectId needed)
await sdk.clearDrupalCache();
console.log('Cache cleared');
// Get installed modules (no projectId needed)
const modules = await sdk.getInstalledModules();
console.log('Installed modules:', modules);
} catch (error) {
console.error('Error:', error.message);
}
}
manageDdevProject();
```
### Template Literal Aliases Example
```javascript
async function templateLiteralExample() {
const sdk = new DdevUpdateManagerSDK({
apiKey: 'your-api-key',
projectId: 'my-drupal-project'
});
// Execute commands with template literals
const files = await sdk.$exec`ls -la`;
console.log('Files:', files.stdout);
// SQL queries with template literals
const users = await sdk.$sql`SELECT uid, name FROM users_field_data WHERE uid = 1`;
console.log('Users:', users.stdout);
// PHP scripts with template literals
const result = await sdk.$php`
$term_storage = \Drupal::entityTypeManager()->getStorage('taxonomy_term');
$terms = $term_storage->loadByProperties(['vid' => 'tags']);
$count = count($terms);
echo "Found {$count} taxonomy terms";
`;
console.log('PHP result:', result.stdout);
}
```
### Project ID by URL Example
```javascript
async function projectByUrlExample() {
const sdk = new DdevUpdateManagerSDK({
apiKey: 'your-api-key'
});
// Set project ID by URL
const projectId = await sdk.getInstanceByUrl('https://my-drupal-project.ddev.site');
console.log('Project ID:', projectId); // "my-drupal-project"
// Now use methods without projectId
const status = await sdk.getDrupalStatus();
const cache = await sdk.clearDrupalCache();
// Use template literals
const files = await sdk.$exec`pwd`;
const users = await sdk.$sql`SELECT COUNT(*) as count FROM users_field_data`;
}
```
### Database Operations Example
```javascript
async function databaseOperations() {
const sdk = new DdevUpdateManagerSDK({
apiKey: 'your-api-key',
projectId: 'my-drupal-project'
});
// Get user information (no projectId needed)
const user = await sdk.getUserInfo(1);
console.log('Admin user:', user);
// Custom SQL query with template literal
const result = await sdk.$sql`
SELECT nid, title, created
FROM node_field_data
WHERE type = 'article'
ORDER BY created DESC
LIMIT 5
`;
console.log('Recent articles:', result);
}
```
### PHP Script Execution Example
```javascript
async function phpOperations() {
const sdk = new DdevUpdateManagerSDK({
apiKey: 'your-api-key',
projectId: 'my-drupal-project'
});
// Create a new vocabulary with template literal
const vocabResult = await sdk.$php`
$vocabulary = \Drupal\taxonomy\Entity\Vocabulary::create([
'name' => 'Test Vocabulary',
'vid' => 'test_vocabulary'
]);
$vocabulary->save();
echo "Vocabulary created: " . $vocabulary->id();
`;
console.log('Vocabulary creation result:', vocabResult);
// Get node count with template literal
const countResult = await sdk.$php`
$query = \Drupal::entityQuery('node')->accessCheck(FALSE);
$count = $query->count()->execute();
echo "Total nodes: " . $count;
`;
console.log('Node count result:', countResult);
}
```
## TypeScript Support
The SDK is written in JavaScript but can be used with TypeScript. You can create type definitions for better IntelliSense:
```typescript
interface DdevInstance {
name: string;
status: string;
type: string;
httpurl: string;
}
interface ProjectDetails extends DdevInstance {
php_version: string;
database_type: string;
database_version: string;
}
interface CommandResult {
stdout: string;
stderr: string;
exitCode: number;
}
interface SqlResult {
stdout: {
rows: any[];
hasData: boolean;
executionTime: string;
affectedItemsCount: number;
warningsCount: number;
warnings: string[];
info: string;
autoIncrementValue: number;
};
stderr: string;
exitCode: number;
}
```
## License
ISC