# Development Guide for Aruba Central n8n Integration



## File Structure

Here's our structure:

```
.
├── ArubaCentral.node.ts
├── api/
│   ├── descriptions.ts             # Combines all domain descriptions
│   ├── [domain]/                   # Each API domain (e.g., monitoring)
│   │   ├── [domain].descriptions.ts  # Combines all resources' descriptions
│   │   ├── [domain].methods.ts       # Combines all resources' methods
│   │   └── [resource]/               # Each resource in the domain
│   │       ├── [resource].descriptions.ts  # Combines all operation descriptions
│   │       ├── [resource].methods.ts       # Exports all operations
│   │       ├── [resource].types.ts         # Type definitions
│   │       └── operations/                 # Individual operations
│   │           ├── [operation].descriptions.ts  # UI parameters
│   │           └── [operation].methods.ts       # Implementation
├── credentials/
├── helpers/
│   ├── apiRequest.ts               # API request handling
│   ├── deduplicate.ts              # Description deduplication
│   ├── errorHandler.ts             # Error handling
│   └── executeOperation.ts         # Operation execution
└── shared/
```

## Helper Function for Deduplication

First, let's create the deduplication helper:

```typescript
// helpers/deduplicate.ts
import { INodeProperties } from 'n8n-workflow';

/**
 * Adds descriptions to a target array, avoiding duplicates
 * 
 * @param target The target array to add descriptions to
 * @param source The source array of descriptions to add
 */
export function addDescriptions(target: INodeProperties[], source: INodeProperties[] | undefined): void {
  if (!Array.isArray(source) || source.length === 0) return;
  
  source.forEach(item => {
    const exists = target.some(existing => 
      existing.name === item.name && 
      existing.type === item.type &&
      JSON.stringify(existing.displayOptions) === JSON.stringify(item.displayOptions)
    );
    
    if (!exists) {
      target.push(item);
    }
  });
}
```

## Step-by-Step Guide for Implementing New Domains

### Step 1: Parse the API Documentation

Start by parsing the API documentation .json files to extract:
- Available endpoints
- Required parameters
- Response structures

Use this script to extract key information from API docs:

```javascript
// scripts/parse-api-docs.js
const fs = require('fs');
const path = require('path');

// Parse an API documentation file
function parseApiDoc(domainName) {
  try {
    const filePath = path.join(__dirname, `../api-docs/${domainName}.json`);
    const apiData = JSON.parse(fs.readFileSync(filePath, 'utf8'));
    
    const endpoints = [];
    
    // Extract paths from the Swagger/OpenAPI format
    Object.entries(apiData.paths || {}).forEach(([path, methods]) => {
      Object.entries(methods).forEach(([method, details]) => {
        endpoints.push({
          path,
          method: method.toUpperCase(),
          operationId: details.operationId || `${method}${path.replace(/\//g, '_').replace(/[{}]/g, '')}`,
          summary: details.summary || '',
          description: details.description || '',
          parameters: details.parameters || [],
          responses: details.responses || {},
        });
      });
    });
    
    // Group endpoints logically by resource
    const resources = {};
    
    endpoints.forEach(endpoint => {
      // Use heuristics to determine the resource from the path
      const pathParts = endpoint.path.split('/').filter(Boolean);
      const resourceName = pathParts[1] || 'default'; // Use 2nd part of path as resource name
      
      if (!resources[resourceName]) {
        resources[resourceName] = [];
      }
      
      resources[resourceName].push(endpoint);
    });
    
    return { resources, schemas: apiData.components?.schemas || {} };
  } catch (error) {
    console.error(`Error parsing API doc for ${domainName}:`, error);
    return { resources: {}, schemas: {} };
  }
}

// Example usage
const domainName = process.argv[2];
if (!domainName) {
  console.error('Please provide a domain name');
  process.exit(1);
}

const { resources, schemas } = parseApiDoc(domainName);
console.log(`Found ${Object.keys(resources).length} resources in domain ${domainName}`);
Object.entries(resources).forEach(([resource, endpoints]) => {
  console.log(`- ${resource}: ${endpoints.length} endpoints`);
});

// Output to a structured file for further processing
fs.writeFileSync(
  path.join(__dirname, `../api-structure/${domainName}-structure.json`),
  JSON.stringify({ resources, schemas }, null, 2)
);
```

### Step 2: Generate Skeleton Files for a Domain

Create a script to generate the skeleton structure for a new domain:

```javascript
// scripts/generate-domain.js
const fs = require('fs');
const path = require('path');

function generateDomain(domainName, resources) {
  // Create the domain directory
  const domainDir = path.join(__dirname, `../src/api/${domainName}`);
  if (!fs.existsSync(domainDir)) {
    fs.mkdirSync(domainDir, { recursive: true });
  }
  
  // Generate domain descriptions file
  const descriptionsContent = `import { INodeProperties } from 'n8n-workflow';
import { addDescriptions } from '../../helpers/deduplicate';
${resources.map(r => `import { ${r}Descriptions } from './${r}/${r}.descriptions';`).join('\n')}

// Resource selection for the ${domainName} domain
export const ${domainName}ResourceSelector: INodeProperties = {
  displayName: 'Resource',
  name: 'resource',
  type: 'options',
  noDataExpression: true,
  displayOptions: {
    show: {
      domain: ['${domainName}'],
    },
  },
  options: [
${resources.map(r => `    {
      name: '${r.charAt(0).toUpperCase() + r.slice(1).replace(/([A-Z])/g, ' $1')}',
      value: '${r}',
      description: 'Work with ${r.replace(/([A-Z])/g, ' $1').toLowerCase()} information',
    },`).join('\n')}
  ],
  default: '${resources[0] || 'default'}',
};

// Combined descriptions for all resources in this domain
export const ${domainName}Descriptions: INodeProperties[] = [${domainName}ResourceSelector];

// Add each resource's descriptions
${resources.map(r => `addDescriptions(${domainName}Descriptions, ${r}Descriptions);`).join('\n')}
`;

  fs.writeFileSync(path.join(domainDir, `${domainName}.descriptions.ts`), descriptionsContent);
  
  // Generate domain methods file
  const methodsContent = `${resources.map(r => `import { ${r}Operations } from './${r}/${r}.methods';`).join('\n')}
import { logger } from '../../helpers/logger';

/**
 * All operations in the ${domainName} domain
 */
export const ${domainName}Operations = {
${resources.map(r => `  ${r}: ${r}Operations,`).join('\n')}
};

/**
 * Check if an operation exists in the ${domainName} domain
 * 
 * @param resource Resource name
 * @param operation Operation name
 * @returns boolean indicating if the operation exists
 */
export function hasOperation(resource: string, operation: string): boolean {
  if (!${domainName}Operations[resource]) {
    logger.debug('${domainName}:check', \`Resource "\${resource}" not found\`);
    return false;
  }
  
  if (!${domainName}Operations[resource][operation]) {
    logger.debug('${domainName}:check', \`Operation "\${operation}" not found in resource "\${resource}"\`);
    return false;
  }
  
  return true;
}
`;

  fs.writeFileSync(path.join(domainDir, `${domainName}.methods.ts`), methodsContent);
  
  // Generate directories and skeleton files for each resource
  resources.forEach(resourceName => {
    generateResource(domainName, resourceName);
  });
  
  console.log(`Generated domain: ${domainName}`);
}

function generateResource(domainName, resourceName) {
  const resourceDir = path.join(__dirname, `../src/api/${domainName}/${resourceName}`);
  const operationsDir = path.join(resourceDir, 'operations');
  
  if (!fs.existsSync(operationsDir)) {
    fs.mkdirSync(operationsDir, { recursive: true });
  }
  
  // Generate resource descriptions file
  const descriptionsContent = `import { INodeProperties } from 'n8n-workflow';
import { addDescriptions } from '../../../helpers/deduplicate';
// Import operation descriptions here
// Example: import { getDetailsDescription } from './operations/getDetails.descriptions';

/**
 * Operation selector for the ${resourceName} resource
 */
export const ${resourceName}OperationSelector: INodeProperties = {
  displayName: 'Operation',
  name: 'operation',
  type: 'options',
  noDataExpression: true,
  displayOptions: {
    show: {
      resource: ['${resourceName}'],
      domain: ['${domainName}'],
    },
  },
  options: [
    // Add operations here
    // Example:
    // {
    //   name: 'Get Details',
    //   value: 'getDetails',
    //   description: 'Get details for a specific item',
    //   action: 'Get details',
    // },
  ],
  default: '',
};

// Combined descriptions for all operations in this resource
export const ${resourceName}Descriptions: INodeProperties[] = [${resourceName}OperationSelector];

// Add each operation's descriptions
// Example: addDescriptions(${resourceName}Descriptions, getDetailsDescription);
`;

  fs.writeFileSync(path.join(resourceDir, `${resourceName}.descriptions.ts`), descriptionsContent);
  
  // Generate resource methods file
  const methodsContent = `import { IExecuteFunctions } from 'n8n-workflow';
import { logger } from '../../../helpers/logger';
// Import operation methods here
// Example: import { getDetails } from './operations/getDetails.methods';

/**
 * All operations available for the ${resourceName} resource
 */
export const ${resourceName}Operations = {
  // Add operations here
  // Example: getDetails,
};

/**
 * Check if a ${resourceName} operation exists
 * 
 * @param operation Operation name to check
 * @returns boolean indicating if the operation exists
 */
export function has${resourceName.charAt(0).toUpperCase() + resourceName.slice(1)}Operation(operation: string): boolean {
  const exists = !!${resourceName}Operations[operation as keyof typeof ${resourceName}Operations];
  logger.debug('${domainName}:${resourceName}:check', \`Operation "\${operation}" \${exists ? 'exists' : 'does not exist'}\`);
  return exists;
}
`;

  fs.writeFileSync(path.join(resourceDir, `${resourceName}.methods.ts`), methodsContent);
  
  // Generate resource types file
  const typesContent = `/**
 * Type definitions for the ${resourceName} resource
 */

// Example:
// export interface ${resourceName.charAt(0).toUpperCase() + resourceName.slice(1)} {
//   id: string;
//   name: string;
//   [key: string]: any;
// }

// Validation function
export function validate${resourceName.charAt(0).toUpperCase() + resourceName.slice(1)}Response<T>(response: any): response is T {
  if (response === null || typeof response !== 'object') {
    return false;
  }
  
  return true;
}
`;

  fs.writeFileSync(path.join(resourceDir, `${resourceName}.types.ts`), typesContent);
  
  console.log(`Generated resource: ${resourceName}`);
}

// Example usage
const domainName = process.argv[2];
const resources = process.argv.slice(3);

if (!domainName || resources.length === 0) {
  console.error('Usage: node generate-domain.js domainName resource1 resource2 ...');
  process.exit(1);
}

generateDomain(domainName, resources);
```

### Step 3: Generate Operation Files

Create a script to generate operation files based on API endpoints:

```javascript
// scripts/generate-operations.js
const fs = require('fs');
const path = require('path');

function generateOperation(domainName, resourceName, operationName, method, endpoint, params = []) {
  const operationsDir = path.join(__dirname, `../src/api/${domainName}/${resourceName}/operations`);
  
  if (!fs.existsSync(operationsDir)) {
    fs.mkdirSync(operationsDir, { recursive: true });
  }
  
  // Generate operation method file
  const methodsContent = `import { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { apiRequest } from '../../../../helpers/apiRequest';
import { logger } from '../../../../helpers/logger';
import { handleApiError } from '../../../../helpers/errorHandler';

/**
 * ${operationName.replace(/([A-Z])/g, ' $1').trim()}
 * 
 * @param this The n8n execution context
 * @returns Formatted API response
 */
export async function ${operationName}(this: IExecuteFunctions): Promise<INodeExecutionData[]> {
  try {
    // Get parameters
${params.map(param => `    const ${param.name} = this.getNodeParameter('${param.name}', 0) ${param.type === 'boolean' ? 'as boolean' : param.type === 'number' ? 'as number' : 'as string'};`).join('\n')}
    
    // Construct API endpoint
    const endpoint = \`${endpoint.replace(/{([^}]+)}/g, '${$1}')}\`;
    
    // Make API request
    const response = await apiRequest.call(this, '${method}', endpoint);
    
    // Format and return response
    return [{ json: response }];
  } catch (error) {
    logger.error('${domainName}:${resourceName}:${operationName}:error', { message: error.message });
    return handleApiError.call(this, error, 'Failed to execute ${operationName.replace(/([A-Z])/g, ' $1').toLowerCase()}');
  }
}
`;

  fs.writeFileSync(path.join(operationsDir, `${operationName}.methods.ts`), methodsContent);
  
  // Generate operation descriptions file
  const descriptionsContent = `import { INodeProperties } from 'n8n-workflow';

export const ${operationName}Description: INodeProperties[] = [
${params.map(param => `  {
    displayName: '${param.name.charAt(0).toUpperCase() + param.name.slice(1).replace(/([A-Z])/g, ' $1')}',
    name: '${param.name}',
    type: '${param.type || 'string'}',
    ${param.required ? 'required: true,' : ''}
    default: ${param.type === 'boolean' ? 'false' : param.type === 'number' ? '0' : "''"},
    displayOptions: {
      show: {
        resource: ['${resourceName}'],
        domain: ['${domainName}'],
        operation: ['${operationName}'],
      },
    },
    description: '${param.description || ''}',
  },`).join('\n')}
];
`;

  fs.writeFileSync(path.join(operationsDir, `${operationName}.descriptions.ts`), descriptionsContent);
  
  // Update resource descriptions file to include the new operation
  const resourceDescPath = path.join(__dirname, `../src/api/${domainName}/${resourceName}/${resourceName}.descriptions.ts`);
  let resourceDescContent = fs.readFileSync(resourceDescPath, 'utf8');
  
  // Add import for the operation description
  if (!resourceDescContent.includes(`import { ${operationName}Description }`)) {
    const importPos = resourceDescContent.indexOf('// Import operation descriptions here');
    const importLine = `import { ${operationName}Description } from './operations/${operationName}.descriptions';\n`;
    resourceDescContent = resourceDescContent.slice(0, importPos) + importLine + resourceDescContent.slice(importPos);
  }
  
  // Add operation to the options array
  const optionsPos = resourceDescContent.indexOf('  options: [');
  const opLinePos = resourceDescContent.indexOf('  ],', optionsPos);
  const operationOption = `    {
      name: '${operationName.replace(/([A-Z])/g, ' $1').trim()}',
      value: '${operationName}',
      description: '${operationName.replace(/([A-Z])/g, ' $1').toLowerCase()}',
      action: '${operationName.replace(/([A-Z])/g, ' $1').toLowerCase()}',
    },\n`;
  resourceDescContent = resourceDescContent.slice(0, opLinePos) + operationOption + resourceDescContent.slice(opLinePos);
  
  // Add description to the addDescriptions section
  const addDescPos = resourceDescContent.indexOf('// Add each operation\'s descriptions');
  const addDescLine = `addDescriptions(${resourceName}Descriptions, ${operationName}Description);\n`;
  resourceDescContent = resourceDescContent.slice(0, addDescPos) + addDescLine + resourceDescContent.slice(addDescPos);
  
  fs.writeFileSync(resourceDescPath, resourceDescContent);
  
  // Update resource methods file to include the new operation
  const resourceMethodsPath = path.join(__dirname, `../src/api/${domainName}/${resourceName}/${resourceName}.methods.ts`);
  let resourceMethodsContent = fs.readFileSync(resourceMethodsPath, 'utf8');
  
  // Add import for the operation method
  if (!resourceMethodsContent.includes(`import { ${operationName} }`)) {
    const importPos = resourceMethodsContent.indexOf('// Import operation methods here');
    const importLine = `import { ${operationName} } from './operations/${operationName}.methods';\n`;
    resourceMethodsContent = resourceMethodsContent.slice(0, importPos) + importLine + resourceMethodsContent.slice(importPos);
  }
  
  // Add operation to the operations object
  const opsPos = resourceMethodsContent.indexOf('export const');
  const opBracketPos = resourceMethodsContent.indexOf('{', opsPos);
  const opCloseBracketPos = resourceMethodsContent.indexOf('};', opBracketPos);
  const operationLine = `  ${operationName},\n`;
  resourceMethodsContent = resourceMethodsContent.slice(0, opCloseBracketPos) + operationLine + resourceMethodsContent.slice(opCloseBracketPos);
  
  fs.writeFileSync(resourceMethodsPath, resourceMethodsContent);
  
  console.log(`Generated operation: ${operationName}`);
}

// Example usage
const domainName = process.argv[2];
const resourceName = process.argv[3];
const operationName = process.argv[4];
const method = process.argv[5] || 'GET';
const endpoint = process.argv[6] || `/api/v2/${resourceName}`;
const paramsStr = process.argv[7] || '[]';

try {
  const params = JSON.parse(paramsStr);
  
  if (!domainName || !resourceName || !operationName) {
    console.error('Usage: node generate-operations.js domainName resourceName operationName [method] [endpoint] [params]');
    process.exit(1);
  }
  
  generateOperation(domainName, resourceName, operationName, method, endpoint, params);
} catch (error) {
  console.error('Error:', error);
  process.exit(1);
}
```

## Workflow for Implementing a New Domain

Follow this process to implement a new domain:

1. **Analyze API Documentation**
   ```bash
   node scripts/parse-api-docs.js monitoring
   ```

2. **Generate Domain Structure**
   ```bash
   node scripts/generate-domain.js monitoring ap client site
   ```

3. **Generate Operations for Each Resource**
   ```bash
   node scripts/generate-operations.js monitoring site getSites GET "/monitoring/v1/sites" '[{"name":"limit","type":"number","description":"Max number of results"},{"name":"offset","type":"number","description":"Results offset"}]'
   ```

4. **Update Domain Reference in api/descriptions.ts**
   - Import the new domain descriptions
   - Add the domain to the domain selector options
   - Add the domain's descriptions to the allDescriptions array

5. **Implement Operation Logic**
   - Refine the generated operation methods with specific logic
   - Add proper error handling and response formatting
   - Add type definitions based on API responses

## Best Practices for Implementation

1. **Type Safety**
   - Define interfaces for all request and response structures
   - Use validation functions to verify API responses
   - Provide type guards for complex response handling

2. **Operation Naming**
   - Use camelCase for operation names (`getSites`, `createUser`)
   - Follow consistent operation type prefixes (get, list, create, update, delete)
   - Group similar operations together

3. **Parameter Organization**
   - Required parameters should be top-level
   - Optional parameters should go in "Additional Fields" collections
   - Use appropriate n8n parameter types (string, number, boolean, options, multiOptions)

4. **Error Handling**
   - Use the `handleApiError` helper for consistent error formatting
   - Add domain, resource, and operation context to all error logs
   - Provide user-friendly error messages

5. **Code Documentation**
   - Add JSDoc comments to all functions
   - Document parameters and return types
   - Include examples for complex operations

## Maintenance Approach

To maintain and extend the integration over time:

1. **Keep Domain Separation**
   - Each API domain gets its own directory
   - Each resource gets its own subdirectory
   - Each operation gets its own files

2. **Update Versions**
   - When the API version changes, update the endpoint paths
   - Consider adding version-specific handlers for backwards compatibility
   - Document version compatibility in operation comments

3. **Track API Changes**
   - Use a changelog to document API changes
   - Add deprecation notices for endpoints that will be removed
   - Provide migration paths for users

4. **Test Coverage**
   - Unit test individual operations
   - Integration test resource and domain combinations
   - End-to-end test critical paths

By following this structure and process, you can efficiently implement the remaining 30+ API domains in a consistent, maintainable way that scales with the complexity of the Aruba Central API.