Files
n8n-enterprise-unlocked/packages/nodes-base/nodes/Google/Sheet/v2/actions/sheet/appendOrUpdate.operation.ts
Milorad FIlipović 04cfa548af feat(editor): Implement Resource Mapper component (#6207)
*  scaffolding
*  finished scaffolding
*  renamed types
*  updated subtitle
*  renamed functions file, UI updates
*  query parameters fixes, ui updates, refactoring
*  fixes for credentials test, setup for error parsing
*  rlc for schema and table, error handling tweaks
*  delete operation, new options
*  columns loader
*  linter fixes
*  where clauses setup
*  logic for processing where clauses
*  select operation
*  refactoring
*  data mode for insert and update, wip
*  data mapping, insert update, skip on conflict option
*  select columns with spaces fix
*  update operation update, wip
*  finished update operation
*  upsert operation
*  ui fixes
* Copy updates.
* Copy updates.
*  option to convert empty strings to nulls, schema checks
*  UI requested updates
*  ssh setup WIP
*  fixes, ssh WIP
*  ssh fixes, credentials
*  credentials testing update
*  uncaught error fix
*  clean up
*  address in use fix
*  improved error message
*  tests setup
*  unit tests wip
*  config files clean up
*  utils unit tests
*  refactoring
*  setup for testing operations, tests for deleteTable operation
*  executeQuery and insert operations tests
*  select, update, upsert operations tests
*  runQueries tests setup
*  hint to query
* Copy updates.
*  ui fixes
*  clean up
*  error message update
*  ui update
* Minor tweaks to query params decription.
* feat(Google Sheets Node): Implement Resource mapper in Google Sheets node (#5752)
*  Added initial resource mapping support in google sheets node
*  Wired mapping API endpoint with node-specific logic for fetching mapping fields
*  Implementing mapping fields logic for google sheets
*  Updating Google Sheets execute methods to support resource mapper fields
* 🚧 Added initial version of `ResourceLocator` component
* 👌 Added `update` mode to resource mapper modes
* 👌 Addressing PR feedback
* 👌 Removing leftover const reference
* 👕 Fixing lint errors
*  singlton for conections
*  credentials test fix, clean up
* feat(Postgres Node): Add resource mapper to new version of Postgres node (#5814)
*  scaffolding
*  finished scaffolding
*  renamed types
*  updated subtitle
*  renamed functions file, UI updates
*  query parameters fixes, ui updates, refactoring
*  fixes for credentials test, setup for error parsing
*  rlc for schema and table, error handling tweaks
*  delete operation, new options
*  columns loader
*  linter fixes
*  where clauses setup
*  logic for processing where clauses
*  select operation
*  refactoring
*  data mode for insert and update, wip
*  data mapping, insert update, skip on conflict option
*  select columns with spaces fix
*  update operation update, wip
*  finished update operation
*  upsert operation
*  ui fixes
* Copy updates.
* Copy updates.
*  option to convert empty strings to nulls, schema checks
*  UI requested updates
*  ssh setup WIP
*  fixes, ssh WIP
*  ssh fixes, credentials
*  credentials testing update
*  uncaught error fix
*  clean up
*  address in use fix
*  improved error message
*  tests setup
*  unit tests wip
*  config files clean up
*  utils unit tests
*  refactoring
*  setup for testing operations, tests for deleteTable operation
*  executeQuery and insert operations tests
*  select, update, upsert operations tests
*  runQueries tests setup
*  hint to query
* Copy updates.
*  ui fixes
*  clean up
*  error message update
*  ui update
* Minor tweaks to query params decription.
*  Updated Postgres node to use resource mapper component
*  Implemented postgres <-> resource mapper type mapping
*  Updated Postgres node execution to use resource mapper fields in v3
* 🔥 Removing unused import
---------
Co-authored-by: Michael Kret <michael.k@radency.com>
Co-authored-by: Giulio Andreini <g.andreini@gmail.com>

* feat(core): Resource editor componend P0 (#5970)
*  Added inital value of mapping mode dropdown
*  Finished mapping mode selector
*  Finished implementing mapping mode selector
*  Implemented 'Columns to match on' dropdown
*  Implemented `loadOptionsDependOn` support in resource mapper
*  Implemented initial version of mapping fields
*  Implementing dependant fields watcher in new component setup
*  Generating correct resource mapper field types. Added `supportAutoMap` to node specification and UI. Not showing fields with `display=false`. Pre-selecting matching columns if it's the only one
*  Handling matching columns correctly in UI
*  Saving and loading resourceMapper values in component
*  Implemented proper data saving and loading
*  ResourceMapper component refactor, fixing value save/load
*  Refactoring MatchingColumnSelect component. Updating Sheets node to use single key match and Postgres to use multi key
*  Updated Google Sheets node to work with the new UI
*  Updating Postgres Node to work with new UI
*  Additional loading indicator that shown if there is no mapping mode selector
*  Removing hard-coded values, fixing matching columns ordering, refactoring
*  Updating field names in nodes
*  Fixing minor UI issues
*  Implemented matching fields filter logic
*  Moving loading label outside of fields list
*  Added initial unit tests for resource mapper
*  Finished default rendering test
*  Test refactoring
*  Finished unit tests
* 🔨 Updating the way i18n is used in resource mapper components
* ✔️ Fixing value to match on logic for postgres node
*  Hiding mapping fields when auto-map mode is selected
*  Syncing selected mapping mode between components
*  Fixing dateTime input rendering and adding update check to Postgres node
*  Properly handling database connections. Sending null for empty string values.
* 💄 Updated wording in the error message for non-existing rows
*  Fixing issues with selected matching values
* ✔️ Updating unit tests after matching logic update
*  Updating matching columns when new fields are loaded
*  Defaulting to null for empty parameter values
*  Allowing zero as valid value for number imputs
*  Updated list of types that use datepicker as widger
*  Using text inputs for time types
*  Initial mapping field rework
*  Added new component for mapping fields, moved bit of logic from root component to matching selector, fixing some lint errors
*  Added tooltip for columns that cannot be deleted
*  Saving deleted values in parameter value
*  Implemented control to add/remove mapping fields
*  Syncing field list with add field dropdown when changing dependent values
*  Not showing removed fields in matching columns selector. Updating wording in matching columns selector description
*  Implementing disabled states for add/remove all fields options
*  Saving removed columns separately, updating copy
*  Implemented resource mapper values validation
*  Updated validation logic and error input styling
*  Validating resource mapper fields when new nodes are added
*  Using node field words in validation, refactoring resource mapper component
*  Implemented schema syncing and add/remove all fields
*  Implemented custom parameter actions
*  Implemented loading indicator in parameter options
* 🔨 Removing unnecessary constants and vue props
*  Handling default values properly
*  Fixing validation logic
* 👕 Fixing lint errors
*  Fixing type issues
*  Not showing fields by default if `addAllFields` is set to `false`
*  Implemented field type validation in resource mapper
*  Updated casing in copy, removed all/remove all option from bottom menu
*  Added auto mapping mode notice
*  Added support for more types in validation
*  Added support for enumerated values
*  Fixing imports after merging
*  Not showing removed fields in matching columns selector. Refactoring validation logic.
* 👕 Fixing imports
* ✔️ Updating unit tests
*  Added resource mapper schema tests
*  Removing `match` from resource mapper field definition, fixing matching columns loading
*  Fixed schema merging
*  update operation return data fix
*  review
* 🐛 Added missing import
* 💄 Updating parameter actions icon based on the ui review
* 💄 Updating word capitalisation in tooltips
* 💄 Added empty state to mapping fields list
* 💄 Removing asterisk from fields, updating tooltips for matching fields
*  Preventing matching fields from being removed by 'Remove All option'
*  Not showing hidden fields in the `Add field` dropdown
*  Added support for custom matching columns labels
*  query optimization
*  fix
*  Optimizing Postgres node enumeration logic
*  Added empty state for matching columns
*  Only fully loading fields if there is no schema fetched
*  Hiding mapping fields if there is no matching columns available in the schema
* ✔️ Fixing minor issues
*  Implemented runtime type validation
* 🔨 Refactoring validation logic
*  Implemented required check, added more custom messages
*  Skipping boolean type in required check
* Type check improvements
*  Only reloading fields if dependent values actually change
*  Adding item index to validation error title
*  Updating Postgres fetching logic, using resource mapper mode to determine if a field can be deleted
*  Resetting field values when adding them via the addAll option
*  Using minor version (2.2) for new Postgres node
*  Implemented proper date validation and type casting
* 👕 Consolidating typing
*  Added unit tests for type validations
* 👌 Addressing front-end review comments
*  More refactoring to address review changes
*  Updating leftover props
*  Added fallback for ISO dates with invalid timezones
* Added timestamp to datetime test cases
*  Reseting matching columns if operation changes
*  Not forcing auto-increment fields to be filled in in Postgres node. Handling null values
* 💄 Added a custom message for invalid dates
*  Better handling of JSON values
*  Updating codemirror readonly stauts based on component property, handling objects in json validation
* Deleting leftover console.log
*  Better time validation
*  Fixing build error after merging
* 👕 Fixing lint error
*  Updating node configuration values
*  Handling postgres arrays better
*  Handling SQL array syntax
*  Updating time validation rules to include timezone
*  Sending expressions that resolve to `null` or `undefined` by the resource mapper to delete cell content in Google Sheets
*  Allowing removed fields to be selected for match
*  Updated the query for fetching unique columns and primary keys
*  Optimizing the unique query
*  Setting timezone to all parsed dates
*  Addressing PR review feedback
*  Configuring Sheets node for production, minor vue component update
* New cases added to the TypeValidation test.
*  Tweaking validation rules for arrays/objects and updating test cases
---------
Co-authored-by: Michael Kret <michael.k@radency.com>
Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
2023-05-31 11:56:09 +02:00

393 lines
10 KiB
TypeScript

import type { IExecuteFunctions, IDataObject, INodeExecutionData } from 'n8n-workflow';
import type {
ISheetUpdateData,
SheetProperties,
ValueInputOption,
ValueRenderOption,
} from '../../helpers/GoogleSheets.types';
import { NodeOperationError } from 'n8n-workflow';
import type { GoogleSheet } from '../../helpers/GoogleSheet';
import { untilSheetSelected } from '../../helpers/GoogleSheets.utils';
import { cellFormat, handlingExtraData, locationDefine } from './commonDescription';
export const description: SheetProperties = [
{
displayName: 'Data Mode',
name: 'dataMode',
type: 'options',
options: [
{
name: 'Auto-Map Input Data to Columns',
value: 'autoMapInputData',
description: 'Use when node input properties match destination column names',
},
{
name: 'Map Each Column Below',
value: 'defineBelow',
description: 'Set the value for each destination column',
},
{
name: 'Nothing',
value: 'nothing',
description: 'Do not send anything',
},
],
displayOptions: {
show: {
resource: ['sheet'],
operation: ['appendOrUpdate'],
'@version': [3],
},
hide: {
...untilSheetSelected,
},
},
default: 'defineBelow',
description: 'Whether to insert the input data this node receives in the new row',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased, n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Column to match on',
name: 'columnToMatchOn',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
typeOptions: {
loadOptionsDependsOn: ['sheetName.value'],
loadOptionsMethod: 'getSheetHeaderRowAndSkipEmpty',
},
default: '',
hint: "Used to find the correct row to update. Doesn't get changed.",
displayOptions: {
show: {
resource: ['sheet'],
operation: ['appendOrUpdate'],
'@version': [3],
},
hide: {
...untilSheetSelected,
},
},
},
{
displayName: 'Value of Column to Match On',
name: 'valueToMatchOn',
type: 'string',
default: '',
displayOptions: {
show: {
resource: ['sheet'],
operation: ['appendOrUpdate'],
dataMode: ['defineBelow'],
'@version': [3],
},
hide: {
...untilSheetSelected,
},
},
},
{
displayName: 'Values to Send',
name: 'fieldsUi',
placeholder: 'Add Field',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
resource: ['sheet'],
operation: ['appendOrUpdate'],
dataMode: ['defineBelow'],
'@version': [3],
},
hide: {
...untilSheetSelected,
},
},
default: {},
options: [
{
displayName: 'Field',
name: 'values',
values: [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
displayName: 'Column',
name: 'column',
type: 'options',
description:
'Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>',
typeOptions: {
loadOptionsDependsOn: ['sheetName.value', 'columnToMatchOn'],
loadOptionsMethod: 'getSheetHeaderRowAndAddColumn',
},
default: '',
},
{
displayName: 'Column Name',
name: 'columnName',
type: 'string',
default: '',
displayOptions: {
show: {
column: ['newColumn'],
},
},
},
{
displayName: 'Value',
name: 'fieldValue',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Columns',
name: 'columns',
type: 'resourceMapper',
noDataExpression: true,
default: {
mappingMode: 'defineBelow',
value: null,
},
required: true,
typeOptions: {
loadOptionsDependsOn: ['sheetName.value'],
resourceMapper: {
resourceMapperMethod: 'getMappingColumns',
mode: 'upsert',
fieldWords: {
singular: 'column',
plural: 'columns',
},
addAllFields: true,
multiKeyMatch: false,
},
},
displayOptions: {
show: {
resource: ['sheet'],
operation: ['appendOrUpdate'],
'@version': [4],
},
hide: {
...untilSheetSelected,
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
resource: ['sheet'],
operation: ['appendOrUpdate'],
},
hide: {
...untilSheetSelected,
},
},
options: [...cellFormat, ...locationDefine, ...handlingExtraData],
},
];
export async function execute(
this: IExecuteFunctions,
sheet: GoogleSheet,
sheetName: string,
sheetId: string,
): Promise<INodeExecutionData[]> {
const items = this.getInputData();
const valueInputMode = this.getNodeParameter('options.cellFormat', 0, 'RAW') as ValueInputOption;
const range = `${sheetName}!A:Z`;
const options = this.getNodeParameter('options', 0, {});
const valueRenderMode = (options.valueRenderMode || 'UNFORMATTED_VALUE') as ValueRenderOption;
const locationDefineOption = (options.locationDefine as IDataObject)?.values as IDataObject;
let headerRow = 0;
let firstDataRow = 1;
if (locationDefineOption) {
if (locationDefineOption.headerRow) {
headerRow = parseInt(locationDefineOption.headerRow as string, 10) - 1;
}
if (locationDefineOption.firstDataRow) {
firstDataRow = parseInt(locationDefineOption.firstDataRow as string, 10) - 1;
}
}
let columnNames: string[] = [];
const sheetData = await sheet.getData(sheetName, 'FORMATTED_VALUE');
if (sheetData === undefined || sheetData[headerRow] === undefined) {
throw new NodeOperationError(
this.getNode(),
`Could not retrieve the column names from row ${headerRow + 1}`,
);
}
columnNames = sheetData[headerRow];
const nodeVersion = this.getNode().typeVersion;
const newColumns = new Set<string>();
const columnsToMatchOn: string[] =
nodeVersion < 4
? [this.getNodeParameter('columnToMatchOn', 0) as string]
: (this.getNodeParameter('columns.matchingColumns', 0) as string[]);
const dataMode =
nodeVersion < 4
? (this.getNodeParameter('dataMode', 0) as string)
: (this.getNodeParameter('columns.mappingMode', 0) as string);
// TODO: Add support for multiple columns to match on in the next overhaul
const keyIndex = columnNames.indexOf(columnsToMatchOn[0]);
const columnValues = await sheet.getColumnValues(
range,
keyIndex,
firstDataRow,
valueRenderMode,
sheetData,
);
const updateData: ISheetUpdateData[] = [];
const appendData: IDataObject[] = [];
const mappedValues: IDataObject[] = [];
for (let i = 0; i < items.length; i++) {
if (dataMode === 'nothing') continue;
const data: IDataObject[] = [];
if (dataMode === 'autoMapInputData') {
const handlingExtraDataOption = (options.handlingExtraData as string) || 'insertInNewColumn';
if (handlingExtraDataOption === 'ignoreIt') {
data.push(items[i].json);
}
if (handlingExtraDataOption === 'error') {
Object.keys(items[i].json).forEach((key) => {
if (!columnNames.includes(key)) {
throw new NodeOperationError(this.getNode(), 'Unexpected fields in node input', {
itemIndex: i,
description: `The input field '${key}' doesn't match any column in the Sheet. You can ignore this by changing the 'Handling extra data' field, which you can find under 'Options'.`,
});
}
});
data.push(items[i].json);
}
if (handlingExtraDataOption === 'insertInNewColumn') {
Object.keys(items[i].json).forEach((key) => {
if (!columnNames.includes(key)) {
newColumns.add(key);
}
});
data.push(items[i].json);
}
} else {
const valueToMatchOn =
nodeVersion < 4
? (this.getNodeParameter('valueToMatchOn', i) as string)
: (this.getNodeParameter(`columns.value[${columnsToMatchOn[0]}]`, i) as string);
if (nodeVersion < 4) {
const valuesToSend = this.getNodeParameter('fieldsUi.values', i, []) as IDataObject[];
if (!valuesToSend?.length) {
throw new NodeOperationError(
this.getNode(),
"At least one value has to be added under 'Values to Send'",
);
}
const fields = valuesToSend.reduce((acc, entry) => {
if (entry.column === 'newColumn') {
const columnName = entry.columnName as string;
if (!columnNames.includes(columnName)) {
newColumns.add(columnName);
}
acc[columnName] = entry.fieldValue as string;
} else {
acc[entry.column as string] = entry.fieldValue as string;
}
return acc;
}, {} as IDataObject);
fields[columnsToMatchOn[0]] = valueToMatchOn;
data.push(fields);
} else {
const mappingValues = this.getNodeParameter('columns.value', i) as IDataObject;
if (Object.keys(mappingValues).length === 0) {
throw new NodeOperationError(
this.getNode(),
"At least one value has to be added under 'Values to Send'",
);
}
// Setting empty values to empty string so that they are not ignored by the API
Object.keys(mappingValues).forEach((key) => {
if (mappingValues[key] === undefined || mappingValues[key] === null) {
mappingValues[key] = '';
}
});
data.push(mappingValues);
mappedValues.push(mappingValues);
}
}
if (newColumns.size) {
await sheet.updateRows(
sheetName,
[columnNames.concat([...newColumns])],
(options.cellFormat as ValueInputOption) || 'RAW',
headerRow + 1,
);
}
const preparedData = await sheet.prepareDataForUpdateOrUpsert(
data,
columnsToMatchOn[0],
range,
headerRow,
firstDataRow,
valueRenderMode,
true,
[columnNames.concat([...newColumns])],
columnValues,
);
updateData.push(...preparedData.updateData);
appendData.push(...preparedData.appendData);
}
if (updateData.length) {
await sheet.batchUpdate(updateData, valueInputMode);
}
if (appendData.length) {
await sheet.appendEmptyRowsOrColumns(sheetId, 1, 0);
const lastRow = sheetData.length + 1;
await sheet.appendSheetData(
appendData,
range,
headerRow + 1,
valueInputMode,
false,
[columnNames.concat([...newColumns])],
lastRow,
);
}
if (nodeVersion < 4 || dataMode === 'autoMapInputData') {
return items;
} else {
return this.helpers.returnJsonArray(mappedValues);
}
}