fix: resolve 5 bugs in n8n_manage_datatable row operations and error handling

Fix critical issues found during staging QA testing:

1. data parameter serialization: MCP transport sends JSON as strings —
   added z.preprocess coercers (coerceJsonArray, coerceJsonObject,
   coerceJsonFilter) to parse string→JSON before Zod validation

2. filter/sortBy URL encoding: n8n API requires URL-encoded query params —
   added encodeURIComponent() for filter and sortBy in getRows/deleteRows

3. json column type: n8n API only accepts string|number|boolean|date —
   removed json from enum in types, Zod schema, tool definition, and docs

4. 404 error messages: N8nNotFoundError was wrapping API messages in
   "Resource with ID <message> not found" — now passes through cleanly

5. Unit test expectations updated for URL-encoded filter/sortBy values

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 22:03:07 +01:00
parent be3d07dbdc
commit b0279bd11f
8 changed files with 53 additions and 25 deletions

View File

@@ -429,12 +429,12 @@ describe('Data Table Handlers (n8n_manage_datatable)', () => {
expect(mockApiClient.getDataTableRows).toHaveBeenCalledWith('dt-1', {
limit: 50,
sortBy: 'name:asc',
sortBy: encodeURIComponent('name:asc'),
search: 'john',
});
});
it('should serialize object filter to JSON string', async () => {
it('should serialize object filter to URL-encoded JSON string', async () => {
mockApiClient.getDataTableRows.mockResolvedValue({ data: [], nextCursor: null });
const objectFilter = {
@@ -448,20 +448,21 @@ describe('Data Table Handlers (n8n_manage_datatable)', () => {
});
expect(mockApiClient.getDataTableRows).toHaveBeenCalledWith('dt-1', {
filter: JSON.stringify(objectFilter),
filter: encodeURIComponent(JSON.stringify(objectFilter)),
});
});
it('should pass through string filter as-is', async () => {
it('should URL-encode string filter', async () => {
mockApiClient.getDataTableRows.mockResolvedValue({ data: [], nextCursor: null });
const filterStr = '{"type":"and","filters":[]}';
await handlers.handleGetRows({
tableId: 'dt-1',
filter: '{"type":"and","filters":[]}',
filter: filterStr,
});
expect(mockApiClient.getDataTableRows).toHaveBeenCalledWith('dt-1', {
filter: '{"type":"and","filters":[]}',
filter: encodeURIComponent(filterStr),
});
});
});
@@ -680,11 +681,11 @@ describe('Data Table Handlers (n8n_manage_datatable)', () => {
message: 'Rows deleted successfully',
});
expect(mockApiClient.deleteDataTableRows).toHaveBeenCalledWith('dt-1', {
filter: JSON.stringify({ type: 'and', ...filter }),
filter: encodeURIComponent(JSON.stringify({ type: 'and', ...filter })),
});
});
it('should serialize filter object to JSON string for API call', async () => {
it('should URL-encode serialized filter for API call', async () => {
mockApiClient.deleteDataTableRows.mockResolvedValue({ deletedCount: 1 });
const filter = {
@@ -698,7 +699,7 @@ describe('Data Table Handlers (n8n_manage_datatable)', () => {
await handlers.handleDeleteRows({ tableId: 'dt-1', filter });
expect(mockApiClient.deleteDataTableRows).toHaveBeenCalledWith('dt-1', {
filter: JSON.stringify(filter),
filter: encodeURIComponent(JSON.stringify(filter)),
});
});
@@ -719,7 +720,7 @@ describe('Data Table Handlers (n8n_manage_datatable)', () => {
expect(result.success).toBe(true);
expect(result.message).toBe('Dry run: rows matched for deletion (no changes applied)');
expect(mockApiClient.deleteDataTableRows).toHaveBeenCalledWith('dt-1', {
filter: JSON.stringify({ type: 'and', ...filter }),
filter: encodeURIComponent(JSON.stringify({ type: 'and', ...filter })),
dryRun: true,
});
});