mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(Data Table Node): Add Delete operation (no-changelog) (#18785)
This commit is contained in:
@@ -3,7 +3,7 @@ import { NodeOperationError } from 'n8n-workflow';
|
|||||||
|
|
||||||
import * as row from './row/Row.resource';
|
import * as row from './row/Row.resource';
|
||||||
|
|
||||||
type DataTableNodeType = AllEntities<{ row: 'insert' | 'get' }>;
|
type DataTableNodeType = AllEntities<{ row: 'insert' | 'get' | 'deleteRows' }>;
|
||||||
|
|
||||||
export async function router(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
export async function router(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
const operationResult: INodeExecutionData[] = [];
|
const operationResult: INodeExecutionData[] = [];
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import type { INodeProperties } from 'n8n-workflow';
|
import type { INodeProperties } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import * as deleteRows from './delete.operation';
|
||||||
import * as get from './get.operation';
|
import * as get from './get.operation';
|
||||||
import * as insert from './insert.operation';
|
import * as insert from './insert.operation';
|
||||||
import { DATA_TABLE_ID_FIELD } from '../../common/fields';
|
import { DATA_TABLE_ID_FIELD } from '../../common/fields';
|
||||||
|
|
||||||
export { insert, get };
|
export { insert, get, deleteRows };
|
||||||
|
|
||||||
export const description: INodeProperties[] = [
|
export const description: INodeProperties[] = [
|
||||||
{
|
{
|
||||||
@@ -24,12 +25,12 @@ export const description: INodeProperties[] = [
|
|||||||
// description: 'Create a new record, or update the current one if it already exists (upsert)',
|
// description: 'Create a new record, or update the current one if it already exists (upsert)',
|
||||||
// action: 'Create or update a row',
|
// action: 'Create or update a row',
|
||||||
// },
|
// },
|
||||||
// {
|
{
|
||||||
// name: 'Delete',
|
name: 'Delete',
|
||||||
// value: 'delete',
|
value: deleteRows.FIELD,
|
||||||
// description: 'Delete a row',
|
description: 'Delete row(s)',
|
||||||
// action: 'Delete a row',
|
action: 'Delete row(s)',
|
||||||
// },
|
},
|
||||||
{
|
{
|
||||||
name: 'Get',
|
name: 'Get',
|
||||||
value: get.FIELD,
|
value: get.FIELD,
|
||||||
@@ -52,7 +53,7 @@ export const description: INodeProperties[] = [
|
|||||||
default: 'insert',
|
default: 'insert',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
displayName: 'Data Store',
|
displayName: 'Data Table',
|
||||||
name: DATA_TABLE_ID_FIELD,
|
name: DATA_TABLE_ID_FIELD,
|
||||||
type: 'resourceLocator',
|
type: 'resourceLocator',
|
||||||
default: { mode: 'list', value: '' },
|
default: { mode: 'list', value: '' },
|
||||||
@@ -75,7 +76,7 @@ export const description: INodeProperties[] = [
|
|||||||
],
|
],
|
||||||
displayOptions: { show: { resource: ['row'] } },
|
displayOptions: { show: { resource: ['row'] } },
|
||||||
},
|
},
|
||||||
|
...deleteRows.description,
|
||||||
...insert.description,
|
...insert.description,
|
||||||
...get.description,
|
...get.description,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import {
|
||||||
|
NodeOperationError,
|
||||||
|
type IDisplayOptions,
|
||||||
|
type IExecuteFunctions,
|
||||||
|
type INodeExecutionData,
|
||||||
|
type INodeProperties,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import { DRY_RUN } from '../../common/fields';
|
||||||
|
import { executeSelectMany, getSelectFields } from '../../common/selectMany';
|
||||||
|
import { getDataTableProxyExecute } from '../../common/utils';
|
||||||
|
|
||||||
|
// named `deleteRows` since `delete` is a reserved keyword
|
||||||
|
export const FIELD: string = 'deleteRows';
|
||||||
|
|
||||||
|
const displayOptions: IDisplayOptions = {
|
||||||
|
show: {
|
||||||
|
resource: ['row'],
|
||||||
|
operation: [FIELD],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const description: INodeProperties[] = [
|
||||||
|
...getSelectFields(displayOptions),
|
||||||
|
{
|
||||||
|
displayName: 'Options',
|
||||||
|
name: 'options',
|
||||||
|
type: 'collection',
|
||||||
|
default: {},
|
||||||
|
placeholder: 'Add option',
|
||||||
|
options: [DRY_RUN],
|
||||||
|
displayOptions,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function execute(
|
||||||
|
this: IExecuteFunctions,
|
||||||
|
index: number,
|
||||||
|
): Promise<INodeExecutionData[]> {
|
||||||
|
const dataStoreProxy = await getDataTableProxyExecute(this, index);
|
||||||
|
|
||||||
|
const dryRun = this.getNodeParameter(`options.${DRY_RUN.name}`, index, false);
|
||||||
|
|
||||||
|
if (typeof dryRun !== 'boolean') {
|
||||||
|
throw new NodeOperationError(
|
||||||
|
this.getNode(),
|
||||||
|
`unexpected input ${JSON.stringify(dryRun)} for boolean dryRun`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const matches = await executeSelectMany(this, index, dataStoreProxy);
|
||||||
|
|
||||||
|
if (!dryRun) {
|
||||||
|
const success = await dataStoreProxy.deleteRows(matches.map((x) => x.json.id));
|
||||||
|
if (!success) {
|
||||||
|
throw new NodeOperationError(this.getNode(), `failed to delete rows for index ${index}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return matches;
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ import type {
|
|||||||
INodeProperties,
|
INodeProperties,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { getSelectFields, getSelectFilter } from '../../common/selectMany';
|
import { executeSelectMany, getSelectFields } from '../../common/selectMany';
|
||||||
import { getDataTableProxyExecute } from '../../common/utils';
|
import { getDataTableProxyExecute } from '../../common/utils';
|
||||||
|
|
||||||
export const FIELD: string = 'get';
|
export const FIELD: string = 'get';
|
||||||
@@ -25,26 +25,5 @@ export async function execute(
|
|||||||
): Promise<INodeExecutionData[]> {
|
): Promise<INodeExecutionData[]> {
|
||||||
const dataStoreProxy = await getDataTableProxyExecute(this, index);
|
const dataStoreProxy = await getDataTableProxyExecute(this, index);
|
||||||
|
|
||||||
let take = 1000;
|
return await executeSelectMany(this, index, dataStoreProxy);
|
||||||
const result: INodeExecutionData[] = [];
|
|
||||||
|
|
||||||
const filter = getSelectFilter(this, index);
|
|
||||||
|
|
||||||
do {
|
|
||||||
const response = await dataStoreProxy.getManyRowsAndCount({
|
|
||||||
skip: result.length,
|
|
||||||
take,
|
|
||||||
filter,
|
|
||||||
});
|
|
||||||
const data = response.data.map((json) => ({ json }));
|
|
||||||
|
|
||||||
// Optimize common path of <1000 results
|
|
||||||
if (response.count === response.data.length) {
|
|
||||||
return data;
|
|
||||||
}
|
|
||||||
|
|
||||||
result.push.apply(result, data);
|
|
||||||
take = Math.min(take, response.count - result.length);
|
|
||||||
} while (take > 0);
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import type { INodeProperties } from 'n8n-workflow';
|
|||||||
|
|
||||||
export const DATA_TABLE_ID_FIELD = 'dataTableId';
|
export const DATA_TABLE_ID_FIELD = 'dataTableId';
|
||||||
|
|
||||||
export const COLUMNS: INodeProperties = {
|
export const COLUMNS = {
|
||||||
displayName: 'Columns',
|
displayName: 'Columns',
|
||||||
name: 'columns',
|
name: 'columns',
|
||||||
type: 'resourceMapper',
|
type: 'resourceMapper',
|
||||||
@@ -25,4 +25,13 @@ export const COLUMNS: INodeProperties = {
|
|||||||
multiKeyMatch: true,
|
multiKeyMatch: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
} satisfies INodeProperties;
|
||||||
|
|
||||||
|
export const DRY_RUN = {
|
||||||
|
displayName: 'Dry Run',
|
||||||
|
name: 'dryRun',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
'Whether the delete operation should only be simulated, returning the rows that would have been deleted',
|
||||||
|
} satisfies INodeProperties;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import {
|
import { NodeOperationError } from 'n8n-workflow';
|
||||||
NodeOperationError,
|
import type {
|
||||||
type IDisplayOptions,
|
DataStoreRowReturn,
|
||||||
type IExecuteFunctions,
|
IDataStoreProjectService,
|
||||||
type INodeProperties,
|
IDisplayOptions,
|
||||||
|
IExecuteFunctions,
|
||||||
|
INodeProperties,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import type { FilterType } from './constants';
|
import type { FilterType } from './constants';
|
||||||
@@ -98,3 +100,41 @@ export function getSelectFilter(ctx: IExecuteFunctions, index: number) {
|
|||||||
|
|
||||||
return buildGetManyFilter(fields, matchType);
|
return buildGetManyFilter(fields, matchType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function executeSelectMany(
|
||||||
|
ctx: IExecuteFunctions,
|
||||||
|
index: number,
|
||||||
|
dataStoreProxy: IDataStoreProjectService,
|
||||||
|
): Promise<Array<{ json: DataStoreRowReturn }>> {
|
||||||
|
const filter = getSelectFilter(ctx, index);
|
||||||
|
|
||||||
|
let take = 1000;
|
||||||
|
const result: Array<{ json: DataStoreRowReturn }> = [];
|
||||||
|
let totalCount = undefined;
|
||||||
|
do {
|
||||||
|
const response = await dataStoreProxy.getManyRowsAndCount({
|
||||||
|
skip: result.length,
|
||||||
|
take,
|
||||||
|
filter,
|
||||||
|
});
|
||||||
|
const data = response.data.map((json) => ({ json }));
|
||||||
|
|
||||||
|
// Optimize common path of <1000 results
|
||||||
|
if (response.count === response.data.length) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (totalCount !== undefined && response.count !== totalCount) {
|
||||||
|
throw new NodeOperationError(
|
||||||
|
ctx.getNode(),
|
||||||
|
'synchronization error: result count changed during pagination',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
totalCount = response.count;
|
||||||
|
|
||||||
|
result.push.apply(result, data);
|
||||||
|
take = Math.min(take, response.count - result.length);
|
||||||
|
} while (take > 0);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,15 +1,24 @@
|
|||||||
import type { IExecuteFunctions } from 'n8n-workflow';
|
import {
|
||||||
|
type INode,
|
||||||
|
NodeOperationError,
|
||||||
|
type IDataStoreProjectService,
|
||||||
|
type IExecuteFunctions,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
import { execute } from '../../../actions/row/get.operation';
|
import type { FieldEntry } from '../../common/constants';
|
||||||
import type { FieldEntry } from '../../../common/constants';
|
import { ANY_FILTER } from '../../common/constants';
|
||||||
import { ANY_FILTER } from '../../../common/constants';
|
import { DATA_TABLE_ID_FIELD } from '../../common/fields';
|
||||||
import { DATA_TABLE_ID_FIELD } from '../../../common/fields';
|
import { executeSelectMany } from '../../common/selectMany';
|
||||||
|
|
||||||
describe('Data Table get Operation', () => {
|
describe('selectMany utils', () => {
|
||||||
let mockExecuteFunctions: IExecuteFunctions;
|
let mockExecuteFunctions: IExecuteFunctions;
|
||||||
const getManyRowsAndCount = jest.fn();
|
const getManyRowsAndCount = jest.fn();
|
||||||
|
const dataStoreProxy = jest.mocked<IDataStoreProjectService>({
|
||||||
|
getManyRowsAndCount,
|
||||||
|
} as unknown as IDataStoreProjectService);
|
||||||
const dataTableId = 2345;
|
const dataTableId = 2345;
|
||||||
let filters: FieldEntry[];
|
let filters: FieldEntry[];
|
||||||
|
const node = { id: 1 } as unknown as INode;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
filters = [
|
filters = [
|
||||||
@@ -20,8 +29,7 @@ describe('Data Table get Operation', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
mockExecuteFunctions = {
|
mockExecuteFunctions = {
|
||||||
getNode: jest.fn().mockReturnValue({}),
|
getNode: jest.fn().mockReturnValue(node),
|
||||||
getInputData: jest.fn().mockReturnValue([{}]),
|
|
||||||
getNodeParameter: jest.fn().mockImplementation((field) => {
|
getNodeParameter: jest.fn().mockImplementation((field) => {
|
||||||
switch (field) {
|
switch (field) {
|
||||||
case DATA_TABLE_ID_FIELD:
|
case DATA_TABLE_ID_FIELD:
|
||||||
@@ -32,23 +40,18 @@ describe('Data Table get Operation', () => {
|
|||||||
return ANY_FILTER;
|
return ANY_FILTER;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
helpers: {
|
|
||||||
getDataStoreProxy: jest.fn().mockReturnValue({
|
|
||||||
getManyRowsAndCount,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
} as unknown as IExecuteFunctions;
|
} as unknown as IExecuteFunctions;
|
||||||
|
|
||||||
jest.clearAllMocks();
|
jest.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('execute', () => {
|
describe('executeSelectMany', () => {
|
||||||
it('should get a few rows', async () => {
|
it('should get a few rows', async () => {
|
||||||
// ARRANGE
|
// ARRANGE
|
||||||
getManyRowsAndCount.mockReturnValue({ data: [{ id: 1 }], count: 1 });
|
getManyRowsAndCount.mockReturnValue({ data: [{ id: 1 }], count: 1 });
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
const result = await execute.call(mockExecuteFunctions, 0);
|
const result = await executeSelectMany(mockExecuteFunctions, 0, dataStoreProxy);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
expect(result).toEqual([{ json: { id: 1 } }]);
|
expect(result).toEqual([{ json: { id: 1 } }]);
|
||||||
@@ -72,7 +75,7 @@ describe('Data Table get Operation', () => {
|
|||||||
filters = [];
|
filters = [];
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
const result = await execute.call(mockExecuteFunctions, 0);
|
const result = await executeSelectMany(mockExecuteFunctions, 0, dataStoreProxy);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
expect(result.length).toBe(2345);
|
expect(result.length).toBe(2345);
|
||||||
@@ -84,10 +87,31 @@ describe('Data Table get Operation', () => {
|
|||||||
getManyRowsAndCount.mockReturnValue({ data: [{ id: 1, colA: null }], count: 1 });
|
getManyRowsAndCount.mockReturnValue({ data: [{ id: 1, colA: null }], count: 1 });
|
||||||
|
|
||||||
// ACT
|
// ACT
|
||||||
const result = await execute.call(mockExecuteFunctions, 0);
|
const result = await executeSelectMany(mockExecuteFunctions, 0, dataStoreProxy);
|
||||||
|
|
||||||
// ASSERT
|
// ASSERT
|
||||||
expect(result).toEqual([{ json: { id: 1, colA: null } }]);
|
expect(result).toEqual([{ json: { id: 1, colA: null } }]);
|
||||||
});
|
});
|
||||||
|
it('should panic if pagination gets out of sync', async () => {
|
||||||
|
// ARRANGE
|
||||||
|
getManyRowsAndCount.mockReturnValueOnce({
|
||||||
|
data: Array.from({ length: 1000 }, (_, k) => ({ id: k })),
|
||||||
|
count: 2345,
|
||||||
|
});
|
||||||
|
getManyRowsAndCount.mockReturnValueOnce({
|
||||||
|
data: Array.from({ length: 1000 }, (_, k) => ({ id: k + 1000 })),
|
||||||
|
count: 2344,
|
||||||
|
});
|
||||||
|
|
||||||
|
filters = [];
|
||||||
|
|
||||||
|
// ACT ASSERT
|
||||||
|
await expect(executeSelectMany(mockExecuteFunctions, 0, dataStoreProxy)).rejects.toEqual(
|
||||||
|
new NodeOperationError(
|
||||||
|
node,
|
||||||
|
'synchronization error: result count changed during pagination',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
Reference in New Issue
Block a user