feat: replace n8n_create_data_table with n8n_manage_datatable (10 actions)

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>
This commit is contained in:
czlonkowski
2026-03-21 18:50:29 +01:00
parent 4a9e3c7ec0
commit 4e2da6c652
16 changed files with 1572 additions and 374 deletions

View File

@@ -1353,6 +1353,310 @@ describe('N8nApiClient', () => {
});
});
describe('listDataTables', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should list data tables successfully', async () => {
const response = { data: [{ id: 'dt-1', name: 'Table One' }], nextCursor: 'abc' };
mockAxiosInstance.get.mockResolvedValue({ data: response });
const result = await client.listDataTables({ limit: 10, cursor: 'xyz' });
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/data-tables', { params: { limit: 10, cursor: 'xyz' } });
expect(result).toEqual(response);
});
it('should handle error', async () => {
const error = {
message: 'Request failed',
response: { status: 500, data: { message: 'Internal server error' } },
};
await mockAxiosInstance.simulateError('get', error);
try {
await client.listDataTables();
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nServerError);
expect((err as N8nServerError).statusCode).toBe(500);
}
});
});
describe('getDataTable', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should get data table successfully', async () => {
const table = { id: 'dt-1', name: 'My Table', columns: [] };
mockAxiosInstance.get.mockResolvedValue({ data: table });
const result = await client.getDataTable('dt-1');
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/data-tables/dt-1');
expect(result).toEqual(table);
});
it('should handle 404 error', async () => {
const error = {
message: 'Request failed',
response: { status: 404, data: { message: 'Data table not found' } },
};
await mockAxiosInstance.simulateError('get', error);
try {
await client.getDataTable('dt-nonexistent');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nNotFoundError);
expect((err as N8nNotFoundError).statusCode).toBe(404);
}
});
});
describe('updateDataTable', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should update data table successfully', async () => {
const updated = { id: 'dt-1', name: 'Renamed' };
mockAxiosInstance.patch.mockResolvedValue({ data: updated });
const result = await client.updateDataTable('dt-1', { name: 'Renamed' });
expect(mockAxiosInstance.patch).toHaveBeenCalledWith('/data-tables/dt-1', { name: 'Renamed' });
expect(result).toEqual(updated);
});
it('should handle error', async () => {
const error = {
message: 'Request failed',
response: { status: 400, data: { message: 'Invalid name' } },
};
await mockAxiosInstance.simulateError('patch', error);
try {
await client.updateDataTable('dt-1', { name: '' });
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nValidationError);
expect((err as N8nValidationError).statusCode).toBe(400);
}
});
});
describe('deleteDataTable', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should delete data table successfully', async () => {
mockAxiosInstance.delete.mockResolvedValue({ data: {} });
await client.deleteDataTable('dt-1');
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/data-tables/dt-1');
});
it('should handle 404 error', async () => {
const error = {
message: 'Request failed',
response: { status: 404, data: { message: 'Data table not found' } },
};
await mockAxiosInstance.simulateError('delete', error);
try {
await client.deleteDataTable('dt-nonexistent');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nNotFoundError);
expect((err as N8nNotFoundError).statusCode).toBe(404);
}
});
});
describe('getDataTableRows', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should get data table rows with params', async () => {
const response = { data: [{ id: 1, email: 'a@b.com' }], nextCursor: null };
mockAxiosInstance.get.mockResolvedValue({ data: response });
const params = { limit: 50, sortBy: 'email:asc', search: 'john' };
const result = await client.getDataTableRows('dt-1', params);
expect(mockAxiosInstance.get).toHaveBeenCalledWith('/data-tables/dt-1/rows', { params });
expect(result).toEqual(response);
});
it('should handle error', async () => {
const error = {
message: 'Request failed',
response: { status: 500, data: { message: 'Internal server error' } },
};
await mockAxiosInstance.simulateError('get', error);
try {
await client.getDataTableRows('dt-1');
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nServerError);
expect((err as N8nServerError).statusCode).toBe(500);
}
});
});
describe('insertDataTableRows', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should insert data table rows successfully', async () => {
const insertResult = { insertedCount: 2 };
mockAxiosInstance.post.mockResolvedValue({ data: insertResult });
const params = { data: [{ email: 'a@b.com' }, { email: 'c@d.com' }], returnType: 'count' as const };
const result = await client.insertDataTableRows('dt-1', params);
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/data-tables/dt-1/rows', params);
expect(result).toEqual(insertResult);
});
it('should handle 400 error', async () => {
const error = {
message: 'Request failed',
response: { status: 400, data: { message: 'Invalid row data' } },
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.insertDataTableRows('dt-1', { data: [{}] });
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nValidationError);
expect((err as N8nValidationError).message).toBe('Invalid row data');
expect((err as N8nValidationError).statusCode).toBe(400);
}
});
});
describe('updateDataTableRows', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should update data table rows successfully', async () => {
const updateResult = { updatedCount: 3 };
mockAxiosInstance.patch.mockResolvedValue({ data: updateResult });
const params = {
filter: { type: 'and' as const, filters: [{ columnName: 'status', condition: 'eq' as const, value: 'old' }] },
data: { status: 'new' },
};
const result = await client.updateDataTableRows('dt-1', params);
expect(mockAxiosInstance.patch).toHaveBeenCalledWith('/data-tables/dt-1/rows/update', params);
expect(result).toEqual(updateResult);
});
it('should handle error', async () => {
const error = {
message: 'Request failed',
response: { status: 500, data: { message: 'Internal server error' } },
};
await mockAxiosInstance.simulateError('patch', error);
try {
await client.updateDataTableRows('dt-1', {
filter: { type: 'and', filters: [{ columnName: 'id', condition: 'eq', value: 1 }] },
data: { name: 'test' },
});
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nServerError);
expect((err as N8nServerError).statusCode).toBe(500);
}
});
});
describe('upsertDataTableRow', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should upsert data table row successfully', async () => {
const upsertResult = { action: 'updated', row: { id: 1, email: 'a@b.com' } };
mockAxiosInstance.post.mockResolvedValue({ data: upsertResult });
const params = {
filter: { type: 'and' as const, filters: [{ columnName: 'email', condition: 'eq' as const, value: 'a@b.com' }] },
data: { score: 15 },
};
const result = await client.upsertDataTableRow('dt-1', params);
expect(mockAxiosInstance.post).toHaveBeenCalledWith('/data-tables/dt-1/rows/upsert', params);
expect(result).toEqual(upsertResult);
});
it('should handle error', async () => {
const error = {
message: 'Request failed',
response: { status: 400, data: { message: 'Invalid upsert params' } },
};
await mockAxiosInstance.simulateError('post', error);
try {
await client.upsertDataTableRow('dt-1', {
filter: { type: 'and', filters: [{ columnName: 'id', condition: 'eq', value: 1 }] },
data: { name: 'test' },
});
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nValidationError);
expect((err as N8nValidationError).statusCode).toBe(400);
}
});
});
describe('deleteDataTableRows', () => {
beforeEach(() => {
client = new N8nApiClient(defaultConfig);
});
it('should delete data table rows successfully', async () => {
const deleteResult = { deletedCount: 2 };
mockAxiosInstance.delete.mockResolvedValue({ data: deleteResult });
const params = { filter: '{"type":"and","filters":[]}', dryRun: false };
const result = await client.deleteDataTableRows('dt-1', params);
expect(mockAxiosInstance.delete).toHaveBeenCalledWith('/data-tables/dt-1/rows/delete', { params });
expect(result).toEqual(deleteResult);
});
it('should handle error', async () => {
const error = {
message: 'Request failed',
response: { status: 500, data: { message: 'Internal server error' } },
};
await mockAxiosInstance.simulateError('delete', error);
try {
await client.deleteDataTableRows('dt-1', { filter: '{}' });
expect.fail('Should have thrown an error');
} catch (err) {
expect(err).toBeInstanceOf(N8nServerError);
expect((err as N8nServerError).statusCode).toBe(500);
}
});
});
describe('interceptors', () => {
let requestInterceptor: any;
let responseInterceptor: any;