mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 02:21:13 +00:00
feat(Data Table Node): Add Update, Upsert operations (no-changelog) (#18820)
This commit is contained in:
@@ -3,7 +3,9 @@ import { NodeOperationError } from 'n8n-workflow';
|
||||
|
||||
import * as row from './row/Row.resource';
|
||||
|
||||
type DataTableNodeType = AllEntities<{ row: 'insert' | 'get' | 'deleteRows' }>;
|
||||
type DataTableNodeType = AllEntities<{
|
||||
row: 'insert' | 'get' | 'deleteRows' | 'update' | 'upsert';
|
||||
}>;
|
||||
|
||||
export async function router(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const operationResult: INodeExecutionData[] = [];
|
||||
|
||||
@@ -3,9 +3,11 @@ import type { INodeProperties } from 'n8n-workflow';
|
||||
import * as deleteRows from './delete.operation';
|
||||
import * as get from './get.operation';
|
||||
import * as insert from './insert.operation';
|
||||
import * as update from './update.operation';
|
||||
import * as upsert from './upsert.operation';
|
||||
import { DATA_TABLE_ID_FIELD } from '../../common/fields';
|
||||
|
||||
export { insert, get, deleteRows };
|
||||
export { insert, get, deleteRows, update, upsert };
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
@@ -49,6 +51,18 @@ export const description: INodeProperties[] = [
|
||||
description: 'Insert a new row',
|
||||
action: 'Insert row',
|
||||
},
|
||||
{
|
||||
name: 'Update',
|
||||
value: update.FIELD,
|
||||
description: 'Update row(s) matching certain fields',
|
||||
action: 'Update row(s)',
|
||||
},
|
||||
{
|
||||
name: 'Upsert',
|
||||
value: upsert.FIELD,
|
||||
description: 'Update row(s), or insert if there is no match',
|
||||
action: 'Upsert row(s)',
|
||||
},
|
||||
],
|
||||
default: 'insert',
|
||||
},
|
||||
@@ -79,4 +93,6 @@ export const description: INodeProperties[] = [
|
||||
...deleteRows.description,
|
||||
...insert.description,
|
||||
...get.description,
|
||||
...update.description,
|
||||
...upsert.description,
|
||||
];
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import type {
|
||||
IDisplayOptions,
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { COLUMNS } from '../../common/fields';
|
||||
import { dataObjectToApiInput, getDataTableProxyExecute } from '../../common/utils';
|
||||
import { getAddRow, makeAddRow } from '../../common/addRow';
|
||||
import { getDataTableProxyExecute } from '../../common/utils';
|
||||
|
||||
export const FIELD: string = 'insert';
|
||||
|
||||
@@ -18,34 +17,16 @@ const displayOptions: IDisplayOptions = {
|
||||
},
|
||||
};
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
{
|
||||
...COLUMNS,
|
||||
displayOptions,
|
||||
},
|
||||
];
|
||||
export const description: INodeProperties[] = [makeAddRow(FIELD, displayOptions)];
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const items = this.getInputData();
|
||||
|
||||
const dataStoreProxy = await getDataTableProxyExecute(this, index);
|
||||
const dataMode = this.getNodeParameter('columns.mappingMode', index) as string;
|
||||
|
||||
let data: IDataObject;
|
||||
const row = getAddRow(this, index);
|
||||
|
||||
if (dataMode === 'autoMapInputData') {
|
||||
data = items[index].json;
|
||||
} else {
|
||||
const fields = this.getNodeParameter('columns.value', index, {}) as IDataObject;
|
||||
|
||||
data = fields;
|
||||
}
|
||||
|
||||
const rows = dataObjectToApiInput(data, this.getNode(), index);
|
||||
|
||||
const insertedRowIds = await dataStoreProxy.insertRows([rows]);
|
||||
return insertedRowIds.map((x) => ({ json: { id: x } }));
|
||||
const insertedRows = await dataStoreProxy.insertRows([row]);
|
||||
return insertedRows.map((json) => ({ json }));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import {
|
||||
NodeOperationError,
|
||||
type IDisplayOptions,
|
||||
type IExecuteFunctions,
|
||||
type INodeExecutionData,
|
||||
type INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { makeAddRow, getAddRow } from '../../common/addRow';
|
||||
import { executeSelectMany, getSelectFields } from '../../common/selectMany';
|
||||
import { getDataTableProxyExecute } from '../../common/utils';
|
||||
|
||||
export const FIELD: string = 'update';
|
||||
|
||||
const displayOptions: IDisplayOptions = {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: [FIELD],
|
||||
},
|
||||
};
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
...getSelectFields(displayOptions),
|
||||
makeAddRow(FIELD, displayOptions),
|
||||
];
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const dataStoreProxy = await getDataTableProxyExecute(this, index);
|
||||
|
||||
const row = getAddRow(this, index);
|
||||
|
||||
const matches = await executeSelectMany(this, index, dataStoreProxy, true);
|
||||
|
||||
const result = [];
|
||||
for (const x of matches) {
|
||||
const updatedRows = await dataStoreProxy.updateRows({
|
||||
data: row,
|
||||
filter: { id: x.json.id },
|
||||
});
|
||||
if (updatedRows.length !== 1) {
|
||||
throw new NodeOperationError(this.getNode(), 'invariant broken');
|
||||
}
|
||||
|
||||
// The input object gets updated in the api call, somehow
|
||||
// And providing this column to the backend causes an unexpected column error
|
||||
// So let's just re-delete the field until we have a more aligned API
|
||||
delete row['updatedAt'];
|
||||
|
||||
result.push(updatedRows[0]);
|
||||
}
|
||||
|
||||
return result.map((json) => ({ json }));
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
import {
|
||||
NodeOperationError,
|
||||
type IDisplayOptions,
|
||||
type IExecuteFunctions,
|
||||
type INodeExecutionData,
|
||||
type INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { makeAddRow, getAddRow } from '../../common/addRow';
|
||||
import { executeSelectMany, getSelectFields } from '../../common/selectMany';
|
||||
import { getDataTableProxyExecute } from '../../common/utils';
|
||||
|
||||
export const FIELD: string = 'upsert';
|
||||
|
||||
const displayOptions: IDisplayOptions = {
|
||||
show: {
|
||||
resource: ['row'],
|
||||
operation: [FIELD],
|
||||
},
|
||||
};
|
||||
|
||||
export const description: INodeProperties[] = [
|
||||
...getSelectFields(displayOptions),
|
||||
makeAddRow(FIELD, displayOptions),
|
||||
];
|
||||
|
||||
export async function execute(
|
||||
this: IExecuteFunctions,
|
||||
index: number,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const dataStoreProxy = await getDataTableProxyExecute(this, index);
|
||||
|
||||
const row = getAddRow(this, index);
|
||||
|
||||
const matches = await executeSelectMany(this, index, dataStoreProxy, true);
|
||||
|
||||
// insert
|
||||
if (matches.length === 0) {
|
||||
const result = await dataStoreProxy.insertRows([row]);
|
||||
return result.map((json) => ({ json }));
|
||||
}
|
||||
|
||||
// update
|
||||
const result = [];
|
||||
for (const match of matches) {
|
||||
const updatedRows = await dataStoreProxy.updateRows({
|
||||
data: row,
|
||||
filter: { id: match.json.id },
|
||||
});
|
||||
if (updatedRows.length !== 1) {
|
||||
throw new NodeOperationError(this.getNode(), 'invariant broken');
|
||||
}
|
||||
|
||||
// The input object gets updated in the api call, somehow
|
||||
// And providing this column to the backend causes an unexpected column error
|
||||
// So let's just re-delete the field until we have a more aligned API
|
||||
delete row['updatedAt'];
|
||||
|
||||
result.push(updatedRows[0]);
|
||||
}
|
||||
|
||||
return result.map((json) => ({ json }));
|
||||
}
|
||||
55
packages/nodes-base/nodes/DataTable/common/addRow.ts
Normal file
55
packages/nodes-base/nodes/DataTable/common/addRow.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IDisplayOptions,
|
||||
IExecuteFunctions,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { DATA_TABLE_ID_FIELD } from './fields';
|
||||
import { dataObjectToApiInput } from './utils';
|
||||
|
||||
export function makeAddRow(operation: string, displayOptions: IDisplayOptions) {
|
||||
return {
|
||||
displayName: 'Columns',
|
||||
name: 'columns',
|
||||
type: 'resourceMapper',
|
||||
default: {
|
||||
mappingMode: 'defineBelow',
|
||||
value: null,
|
||||
},
|
||||
noDataExpression: true,
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: [`${DATA_TABLE_ID_FIELD}.value`],
|
||||
resourceMapper: {
|
||||
valuesLabel: `Columns to ${operation}`,
|
||||
resourceMapperMethod: 'getDataTables',
|
||||
mode: 'add',
|
||||
fieldWords: {
|
||||
singular: 'column',
|
||||
plural: 'columns',
|
||||
},
|
||||
addAllFields: true,
|
||||
multiKeyMatch: true,
|
||||
},
|
||||
},
|
||||
displayOptions,
|
||||
} satisfies INodeProperties;
|
||||
}
|
||||
|
||||
export function getAddRow(ctx: IExecuteFunctions, index: number) {
|
||||
const items = ctx.getInputData();
|
||||
const dataMode = ctx.getNodeParameter('columns.mappingMode', index) as string;
|
||||
|
||||
let data: IDataObject;
|
||||
|
||||
if (dataMode === 'autoMapInputData') {
|
||||
data = items[index].json;
|
||||
} else {
|
||||
const fields = ctx.getNodeParameter('columns.value', index, {}) as IDataObject;
|
||||
|
||||
data = fields;
|
||||
}
|
||||
|
||||
return dataObjectToApiInput(data, ctx.getNode(), index);
|
||||
}
|
||||
@@ -2,36 +2,11 @@ import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
export const DATA_TABLE_ID_FIELD = 'dataTableId';
|
||||
|
||||
export const COLUMNS = {
|
||||
displayName: 'Columns',
|
||||
name: 'columns',
|
||||
type: 'resourceMapper',
|
||||
default: {
|
||||
mappingMode: 'defineBelow',
|
||||
value: null,
|
||||
},
|
||||
noDataExpression: true,
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: [`${DATA_TABLE_ID_FIELD}.value`],
|
||||
resourceMapper: {
|
||||
resourceMapperMethod: 'getDataTables',
|
||||
mode: 'add',
|
||||
fieldWords: {
|
||||
singular: 'column',
|
||||
plural: 'columns',
|
||||
},
|
||||
addAllFields: 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',
|
||||
'Whether the operation should only be simulated, returning the rows that would have been affected',
|
||||
} satisfies INodeProperties;
|
||||
|
||||
@@ -11,7 +11,10 @@ import type { FilterType } from './constants';
|
||||
import { DATA_TABLE_ID_FIELD } from './fields';
|
||||
import { buildGetManyFilter, isFieldArray, isMatchType } from './utils';
|
||||
|
||||
export function getSelectFields(displayOptions: IDisplayOptions): INodeProperties[] {
|
||||
export function getSelectFields(
|
||||
displayOptions: IDisplayOptions,
|
||||
requireCondition = false,
|
||||
): INodeProperties[] {
|
||||
return [
|
||||
{
|
||||
displayName: 'Must Match',
|
||||
@@ -36,6 +39,7 @@ export function getSelectFields(displayOptions: IDisplayOptions): INodePropertie
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
minRequiredFields: requireCondition ? 1 : 0,
|
||||
},
|
||||
displayOptions,
|
||||
default: {},
|
||||
@@ -105,9 +109,14 @@ export async function executeSelectMany(
|
||||
ctx: IExecuteFunctions,
|
||||
index: number,
|
||||
dataStoreProxy: IDataStoreProjectService,
|
||||
rejectEmpty = false,
|
||||
): Promise<Array<{ json: DataStoreRowReturn }>> {
|
||||
const filter = getSelectFilter(ctx, index);
|
||||
|
||||
if (rejectEmpty && filter.filters.length === 0) {
|
||||
throw new NodeOperationError(ctx.getNode(), 'At least one condition is required');
|
||||
}
|
||||
|
||||
let take = 1000;
|
||||
const result: Array<{ json: DataStoreRowReturn }> = [];
|
||||
let totalCount = undefined;
|
||||
|
||||
Reference in New Issue
Block a user