mirror of
https://github.com/czlonkowski/n8n-mcp.git
synced 2026-03-21 18:03:07 +00:00
Replaces the single-purpose n8n_create_data_table tool with a comprehensive n8n_manage_datatable tool covering all 10 n8n data table API endpoints: Table operations: createTable, listTables, getTable, updateTable, deleteTable Row operations: getRows, insertRows, updateRows, upsertRows, deleteRows - Filter system with and/or logic and 8 condition operators - Dry-run support for updateRows, upsertRows, deleteRows - Pagination, sorting, and full-text search for row listing - 9 new N8nApiClient methods for all data table endpoints - Shared error handler and consolidated Zod schemas - Comprehensive tool documentation with examples per action - 36 handler tests + 18 API client tests BREAKING: n8n_create_data_table removed. Use n8n_manage_datatable with action="createTable" instead. Based on work by @djakielski in PR #646. Co-Authored-By: Dominik Jakielski <dominik.jakielski@urlaubsguru.de> Conceived by Romuald Członkowski - https://www.aiadvisors.pl/en Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
727 lines
22 KiB
TypeScript
727 lines
22 KiB
TypeScript
import axios, { AxiosInstance, AxiosRequestConfig, InternalAxiosRequestConfig } from 'axios';
|
|
import { logger } from '../utils/logger';
|
|
import {
|
|
Workflow,
|
|
WorkflowListParams,
|
|
WorkflowListResponse,
|
|
Execution,
|
|
ExecutionListParams,
|
|
ExecutionListResponse,
|
|
Credential,
|
|
CredentialListParams,
|
|
CredentialListResponse,
|
|
Tag,
|
|
TagListParams,
|
|
TagListResponse,
|
|
HealthCheckResponse,
|
|
N8nVersionInfo,
|
|
Variable,
|
|
WebhookRequest,
|
|
WorkflowExport,
|
|
WorkflowImport,
|
|
SourceControlStatus,
|
|
SourceControlPullResult,
|
|
SourceControlPushResult,
|
|
DataTable,
|
|
DataTableColumn,
|
|
DataTableListParams,
|
|
DataTableRow,
|
|
DataTableRowListParams,
|
|
DataTableInsertRowsParams,
|
|
DataTableUpdateRowsParams,
|
|
DataTableUpsertRowParams,
|
|
DataTableDeleteRowsParams,
|
|
} from '../types/n8n-api';
|
|
import { handleN8nApiError, logN8nError } from '../utils/n8n-errors';
|
|
import { cleanWorkflowForCreate, cleanWorkflowForUpdate } from './n8n-validation';
|
|
import {
|
|
fetchN8nVersion,
|
|
cleanSettingsForVersion,
|
|
getCachedVersion,
|
|
} from './n8n-version';
|
|
|
|
export interface N8nApiClientConfig {
|
|
baseUrl: string;
|
|
apiKey: string;
|
|
timeout?: number;
|
|
maxRetries?: number;
|
|
}
|
|
|
|
export class N8nApiClient {
|
|
private client: AxiosInstance;
|
|
private maxRetries: number;
|
|
private baseUrl: string;
|
|
private versionInfo: N8nVersionInfo | null = null;
|
|
private versionPromise: Promise<N8nVersionInfo | null> | null = null;
|
|
|
|
constructor(config: N8nApiClientConfig) {
|
|
const { baseUrl, apiKey, timeout = 30000, maxRetries = 3 } = config;
|
|
|
|
this.maxRetries = maxRetries;
|
|
this.baseUrl = baseUrl;
|
|
|
|
// Ensure baseUrl ends with /api/v1
|
|
const apiUrl = baseUrl.endsWith('/api/v1')
|
|
? baseUrl
|
|
: `${baseUrl.replace(/\/$/, '')}/api/v1`;
|
|
|
|
this.client = axios.create({
|
|
baseURL: apiUrl,
|
|
timeout,
|
|
headers: {
|
|
'X-N8N-API-KEY': apiKey,
|
|
'Content-Type': 'application/json',
|
|
},
|
|
});
|
|
|
|
// Request interceptor for logging
|
|
this.client.interceptors.request.use(
|
|
(config: InternalAxiosRequestConfig) => {
|
|
logger.debug(`n8n API Request: ${config.method?.toUpperCase()} ${config.url}`, {
|
|
params: config.params,
|
|
data: config.data,
|
|
});
|
|
return config;
|
|
},
|
|
(error: unknown) => {
|
|
logger.error('n8n API Request Error:', error);
|
|
return Promise.reject(error);
|
|
}
|
|
);
|
|
|
|
// Response interceptor for logging
|
|
this.client.interceptors.response.use(
|
|
(response: any) => {
|
|
logger.debug(`n8n API Response: ${response.status} ${response.config.url}`);
|
|
return response;
|
|
},
|
|
(error: unknown) => {
|
|
const n8nError = handleN8nApiError(error);
|
|
logN8nError(n8nError, 'n8n API Response');
|
|
return Promise.reject(n8nError);
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get the n8n version, fetching it if not already cached.
|
|
* Uses promise-based locking to prevent concurrent requests.
|
|
*/
|
|
async getVersion(): Promise<N8nVersionInfo | null> {
|
|
// If we already have version info, return it
|
|
if (this.versionInfo) {
|
|
return this.versionInfo;
|
|
}
|
|
|
|
// If a fetch is already in progress, wait for it
|
|
if (this.versionPromise) {
|
|
return this.versionPromise;
|
|
}
|
|
|
|
// Start a new fetch with promise-based locking
|
|
this.versionPromise = this.fetchVersionOnce();
|
|
try {
|
|
this.versionInfo = await this.versionPromise;
|
|
return this.versionInfo;
|
|
} finally {
|
|
// Clear the promise so future calls can retry if needed
|
|
this.versionPromise = null;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Internal method to fetch version once
|
|
*/
|
|
private async fetchVersionOnce(): Promise<N8nVersionInfo | null> {
|
|
// Check if already cached globally
|
|
let version = getCachedVersion(this.baseUrl);
|
|
if (!version) {
|
|
// Fetch from server
|
|
version = await fetchN8nVersion(this.baseUrl);
|
|
}
|
|
return version;
|
|
}
|
|
|
|
/**
|
|
* Get cached version info without fetching
|
|
*/
|
|
getCachedVersionInfo(): N8nVersionInfo | null {
|
|
return this.versionInfo;
|
|
}
|
|
|
|
// Health check to verify API connectivity
|
|
async healthCheck(): Promise<HealthCheckResponse> {
|
|
try {
|
|
// Try the standard healthz endpoint (available on all n8n instances)
|
|
const baseUrl = this.client.defaults.baseURL || '';
|
|
const healthzUrl = baseUrl.replace(/\/api\/v\d+\/?$/, '') + '/healthz';
|
|
|
|
const response = await axios.get(healthzUrl, {
|
|
timeout: 5000,
|
|
validateStatus: (status) => status < 500
|
|
});
|
|
|
|
// Also fetch version info (will be cached)
|
|
const versionInfo = await this.getVersion();
|
|
|
|
if (response.status === 200 && response.data?.status === 'ok') {
|
|
return {
|
|
status: 'ok',
|
|
n8nVersion: versionInfo?.version,
|
|
features: {}
|
|
};
|
|
}
|
|
|
|
// If healthz doesn't work, fall back to API check
|
|
throw new Error('healthz endpoint not available');
|
|
} catch (error) {
|
|
// If healthz endpoint doesn't exist, try listing workflows with limit 1
|
|
// This is a fallback for older n8n versions
|
|
try {
|
|
await this.client.get('/workflows', { params: { limit: 1 } });
|
|
|
|
// Still try to get version
|
|
const versionInfo = await this.getVersion();
|
|
|
|
return {
|
|
status: 'ok',
|
|
n8nVersion: versionInfo?.version,
|
|
features: {}
|
|
};
|
|
} catch (fallbackError) {
|
|
throw handleN8nApiError(fallbackError);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Workflow Management
|
|
async createWorkflow(workflow: Partial<Workflow>): Promise<Workflow> {
|
|
try {
|
|
const cleanedWorkflow = cleanWorkflowForCreate(workflow);
|
|
const response = await this.client.post('/workflows', cleanedWorkflow);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async getWorkflow(id: string): Promise<Workflow> {
|
|
try {
|
|
const response = await this.client.get(`/workflows/${id}`);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async updateWorkflow(id: string, workflow: Partial<Workflow>): Promise<Workflow> {
|
|
try {
|
|
// Step 1: Basic cleaning (remove read-only fields, filter to known settings)
|
|
const cleanedWorkflow = cleanWorkflowForUpdate(workflow as Workflow);
|
|
|
|
// Step 2: Version-aware settings filtering for older n8n compatibility
|
|
// This prevents "additional properties" errors on n8n < 1.119.0
|
|
const versionInfo = await this.getVersion();
|
|
if (versionInfo) {
|
|
logger.debug(`Updating workflow with n8n version ${versionInfo.version}`);
|
|
// Apply version-specific filtering to settings
|
|
cleanedWorkflow.settings = cleanSettingsForVersion(
|
|
cleanedWorkflow.settings as Record<string, unknown>,
|
|
versionInfo
|
|
);
|
|
} else {
|
|
logger.warn('Could not determine n8n version, sending all known settings properties');
|
|
// Without version info, we send all known properties (might fail on old n8n)
|
|
}
|
|
|
|
// First, try PUT method (newer n8n versions)
|
|
try {
|
|
const response = await this.client.put(`/workflows/${id}`, cleanedWorkflow);
|
|
return response.data;
|
|
} catch (putError: any) {
|
|
// If PUT fails with 405 (Method Not Allowed), try PATCH
|
|
if (putError.response?.status === 405) {
|
|
logger.debug('PUT method not supported, falling back to PATCH');
|
|
const response = await this.client.patch(`/workflows/${id}`, cleanedWorkflow);
|
|
return response.data;
|
|
}
|
|
throw putError;
|
|
}
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async deleteWorkflow(id: string): Promise<Workflow> {
|
|
try {
|
|
const response = await this.client.delete(`/workflows/${id}`);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async transferWorkflow(id: string, destinationProjectId: string): Promise<void> {
|
|
try {
|
|
await this.client.put(`/workflows/${id}/transfer`, { destinationProjectId });
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async activateWorkflow(id: string): Promise<Workflow> {
|
|
try {
|
|
const response = await this.client.post(`/workflows/${id}/activate`, {});
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async deactivateWorkflow(id: string): Promise<Workflow> {
|
|
try {
|
|
const response = await this.client.post(`/workflows/${id}/deactivate`, {});
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lists workflows from n8n instance.
|
|
*
|
|
* @param params - Query parameters for filtering and pagination
|
|
* @returns Paginated list of workflows
|
|
*
|
|
* @remarks
|
|
* This method handles two response formats for backwards compatibility:
|
|
* - Modern (n8n v0.200.0+): {data: Workflow[], nextCursor?: string}
|
|
* - Legacy (older versions): Workflow[] (wrapped automatically)
|
|
*
|
|
* @see https://github.com/czlonkowski/n8n-mcp/issues/349
|
|
*/
|
|
async listWorkflows(params: WorkflowListParams = {}): Promise<WorkflowListResponse> {
|
|
try {
|
|
const response = await this.client.get('/workflows', { params });
|
|
return this.validateListResponse<Workflow>(response.data, 'workflows');
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
// Execution Management
|
|
async getExecution(id: string, includeData = false): Promise<Execution> {
|
|
try {
|
|
const response = await this.client.get(`/executions/${id}`, {
|
|
params: { includeData },
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Lists executions from n8n instance.
|
|
*
|
|
* @param params - Query parameters for filtering and pagination
|
|
* @returns Paginated list of executions
|
|
*
|
|
* @remarks
|
|
* This method handles two response formats for backwards compatibility:
|
|
* - Modern (n8n v0.200.0+): {data: Execution[], nextCursor?: string}
|
|
* - Legacy (older versions): Execution[] (wrapped automatically)
|
|
*
|
|
* @see https://github.com/czlonkowski/n8n-mcp/issues/349
|
|
*/
|
|
async listExecutions(params: ExecutionListParams = {}): Promise<ExecutionListResponse> {
|
|
try {
|
|
const response = await this.client.get('/executions', { params });
|
|
return this.validateListResponse<Execution>(response.data, 'executions');
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async deleteExecution(id: string): Promise<void> {
|
|
try {
|
|
await this.client.delete(`/executions/${id}`);
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
// Webhook Execution
|
|
async triggerWebhook(request: WebhookRequest): Promise<any> {
|
|
try {
|
|
const { webhookUrl, httpMethod, data, headers, waitForResponse = true } = request;
|
|
|
|
// SECURITY: Validate URL for SSRF protection (includes DNS resolution)
|
|
// See: https://github.com/czlonkowski/n8n-mcp/issues/265 (HIGH-03)
|
|
const { SSRFProtection } = await import('../utils/ssrf-protection');
|
|
const validation = await SSRFProtection.validateWebhookUrl(webhookUrl);
|
|
|
|
if (!validation.valid) {
|
|
throw new Error(`SSRF protection: ${validation.reason}`);
|
|
}
|
|
|
|
// Extract path from webhook URL
|
|
const url = new URL(webhookUrl);
|
|
const webhookPath = url.pathname;
|
|
|
|
// Make request directly to webhook endpoint
|
|
const config: AxiosRequestConfig = {
|
|
method: httpMethod,
|
|
url: webhookPath,
|
|
headers: {
|
|
...headers,
|
|
// Don't override API key header for webhook endpoints
|
|
'X-N8N-API-KEY': undefined,
|
|
},
|
|
data: httpMethod !== 'GET' ? data : undefined,
|
|
params: httpMethod === 'GET' ? data : undefined,
|
|
// Webhooks might take longer
|
|
timeout: waitForResponse ? 120000 : 30000,
|
|
};
|
|
|
|
// Create a new axios instance for webhook requests to avoid API interceptors
|
|
const webhookClient = axios.create({
|
|
baseURL: new URL('/', webhookUrl).toString(),
|
|
validateStatus: (status: number) => status < 500, // Don't throw on 4xx
|
|
});
|
|
|
|
const response = await webhookClient.request(config);
|
|
|
|
return {
|
|
status: response.status,
|
|
statusText: response.statusText,
|
|
data: response.data,
|
|
headers: response.headers,
|
|
};
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
// Credential Management
|
|
/**
|
|
* Lists credentials from n8n instance.
|
|
*
|
|
* @param params - Query parameters for filtering and pagination
|
|
* @returns Paginated list of credentials
|
|
*
|
|
* @remarks
|
|
* This method handles two response formats for backwards compatibility:
|
|
* - Modern (n8n v0.200.0+): {data: Credential[], nextCursor?: string}
|
|
* - Legacy (older versions): Credential[] (wrapped automatically)
|
|
*
|
|
* @see https://github.com/czlonkowski/n8n-mcp/issues/349
|
|
*/
|
|
async listCredentials(params: CredentialListParams = {}): Promise<CredentialListResponse> {
|
|
try {
|
|
const response = await this.client.get('/credentials', { params });
|
|
return this.validateListResponse<Credential>(response.data, 'credentials');
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async getCredential(id: string): Promise<Credential> {
|
|
try {
|
|
const response = await this.client.get(`/credentials/${id}`);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async createCredential(credential: Partial<Credential>): Promise<Credential> {
|
|
try {
|
|
const response = await this.client.post('/credentials', credential);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async updateCredential(id: string, credential: Partial<Credential>): Promise<Credential> {
|
|
try {
|
|
const response = await this.client.patch(`/credentials/${id}`, credential);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async deleteCredential(id: string): Promise<void> {
|
|
try {
|
|
await this.client.delete(`/credentials/${id}`);
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
// Tag Management
|
|
/**
|
|
* Lists tags from n8n instance.
|
|
*
|
|
* @param params - Query parameters for filtering and pagination
|
|
* @returns Paginated list of tags
|
|
*
|
|
* @remarks
|
|
* This method handles two response formats for backwards compatibility:
|
|
* - Modern (n8n v0.200.0+): {data: Tag[], nextCursor?: string}
|
|
* - Legacy (older versions): Tag[] (wrapped automatically)
|
|
*
|
|
* @see https://github.com/czlonkowski/n8n-mcp/issues/349
|
|
*/
|
|
async listTags(params: TagListParams = {}): Promise<TagListResponse> {
|
|
try {
|
|
const response = await this.client.get('/tags', { params });
|
|
return this.validateListResponse<Tag>(response.data, 'tags');
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async createTag(tag: Partial<Tag>): Promise<Tag> {
|
|
try {
|
|
const response = await this.client.post('/tags', tag);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async updateTag(id: string, tag: Partial<Tag>): Promise<Tag> {
|
|
try {
|
|
const response = await this.client.patch(`/tags/${id}`, tag);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async deleteTag(id: string): Promise<void> {
|
|
try {
|
|
await this.client.delete(`/tags/${id}`);
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async updateWorkflowTags(workflowId: string, tagIds: string[]): Promise<Tag[]> {
|
|
try {
|
|
const response = await this.client.put(`/workflows/${workflowId}/tags`, tagIds.filter(id => id).map(id => ({ id })));
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
// Source Control Management (Enterprise feature)
|
|
async getSourceControlStatus(): Promise<SourceControlStatus> {
|
|
try {
|
|
const response = await this.client.get('/source-control/status');
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async pullSourceControl(force = false): Promise<SourceControlPullResult> {
|
|
try {
|
|
const response = await this.client.post('/source-control/pull', { force });
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async pushSourceControl(
|
|
message: string,
|
|
fileNames?: string[]
|
|
): Promise<SourceControlPushResult> {
|
|
try {
|
|
const response = await this.client.post('/source-control/push', {
|
|
message,
|
|
fileNames,
|
|
});
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
// Variable Management (via Source Control API)
|
|
async getVariables(): Promise<Variable[]> {
|
|
try {
|
|
const response = await this.client.get('/variables');
|
|
return response.data.data || [];
|
|
} catch (error) {
|
|
// Variables might not be available in all n8n versions
|
|
logger.warn('Variables API not available, returning empty array');
|
|
return [];
|
|
}
|
|
}
|
|
|
|
async createVariable(variable: Partial<Variable>): Promise<Variable> {
|
|
try {
|
|
const response = await this.client.post('/variables', variable);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async updateVariable(id: string, variable: Partial<Variable>): Promise<Variable> {
|
|
try {
|
|
const response = await this.client.patch(`/variables/${id}`, variable);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async deleteVariable(id: string): Promise<void> {
|
|
try {
|
|
await this.client.delete(`/variables/${id}`);
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async createDataTable(params: { name: string; columns?: DataTableColumn[] }): Promise<DataTable> {
|
|
try {
|
|
const response = await this.client.post('/data-tables', params);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async listDataTables(params: DataTableListParams = {}): Promise<{ data: DataTable[]; nextCursor?: string | null }> {
|
|
try {
|
|
const response = await this.client.get('/data-tables', { params });
|
|
return this.validateListResponse<DataTable>(response.data, 'data-tables');
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async getDataTable(id: string): Promise<DataTable> {
|
|
try {
|
|
const response = await this.client.get(`/data-tables/${id}`);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async updateDataTable(id: string, params: { name: string }): Promise<DataTable> {
|
|
try {
|
|
const response = await this.client.patch(`/data-tables/${id}`, params);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async deleteDataTable(id: string): Promise<void> {
|
|
try {
|
|
await this.client.delete(`/data-tables/${id}`);
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async getDataTableRows(id: string, params: DataTableRowListParams = {}): Promise<{ data: DataTableRow[]; nextCursor?: string | null }> {
|
|
try {
|
|
const response = await this.client.get(`/data-tables/${id}/rows`, { params });
|
|
return this.validateListResponse<DataTableRow>(response.data, 'data-table-rows');
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async insertDataTableRows(id: string, params: DataTableInsertRowsParams): Promise<any> {
|
|
try {
|
|
const response = await this.client.post(`/data-tables/${id}/rows`, params);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async updateDataTableRows(id: string, params: DataTableUpdateRowsParams): Promise<any> {
|
|
try {
|
|
const response = await this.client.patch(`/data-tables/${id}/rows/update`, params);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async upsertDataTableRow(id: string, params: DataTableUpsertRowParams): Promise<any> {
|
|
try {
|
|
const response = await this.client.post(`/data-tables/${id}/rows/upsert`, params);
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
async deleteDataTableRows(id: string, params: DataTableDeleteRowsParams): Promise<any> {
|
|
try {
|
|
const response = await this.client.delete(`/data-tables/${id}/rows/delete`, { params });
|
|
return response.data;
|
|
} catch (error) {
|
|
throw handleN8nApiError(error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Validates and normalizes n8n API list responses.
|
|
* Handles both modern format {data: [], nextCursor?: string} and legacy array format.
|
|
*
|
|
* @param responseData - Raw response data from n8n API
|
|
* @param resourceType - Resource type for error messages (e.g., 'workflows', 'executions')
|
|
* @returns Normalized response in modern format
|
|
* @throws Error if response structure is invalid
|
|
*/
|
|
private validateListResponse<T>(
|
|
responseData: any,
|
|
resourceType: string
|
|
): { data: T[]; nextCursor?: string | null } {
|
|
// Validate response structure
|
|
if (!responseData || typeof responseData !== 'object') {
|
|
throw new Error(`Invalid response from n8n API for ${resourceType}: response is not an object`);
|
|
}
|
|
|
|
// Handle legacy case where API returns array directly (older n8n versions)
|
|
if (Array.isArray(responseData)) {
|
|
logger.warn(
|
|
`n8n API returned array directly instead of {data, nextCursor} object for ${resourceType}. ` +
|
|
'Wrapping in expected format for backwards compatibility.'
|
|
);
|
|
return {
|
|
data: responseData,
|
|
nextCursor: null
|
|
};
|
|
}
|
|
|
|
// Validate expected format {data: [], nextCursor?: string}
|
|
if (!Array.isArray(responseData.data)) {
|
|
const keys = Object.keys(responseData).slice(0, 5);
|
|
const keysPreview = keys.length < Object.keys(responseData).length
|
|
? `${keys.join(', ')}...`
|
|
: keys.join(', ');
|
|
throw new Error(
|
|
`Invalid response from n8n API for ${resourceType}: expected {data: [], nextCursor?: string}, ` +
|
|
`got object with keys: [${keysPreview}]`
|
|
);
|
|
}
|
|
|
|
return responseData;
|
|
}
|
|
} |