mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-23 12:49:09 +00:00
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>
This commit is contained in:
committed by
GitHub
parent
5ae1124106
commit
04cfa548af
@@ -11,7 +11,7 @@ export class GoogleSheets extends VersionedNodeType {
|
||||
name: 'googleSheets',
|
||||
icon: 'file:googleSheets.svg',
|
||||
group: ['input', 'output'],
|
||||
defaultVersion: 3,
|
||||
defaultVersion: 4,
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Read, update and write data to Google Sheets',
|
||||
};
|
||||
@@ -20,6 +20,7 @@ export class GoogleSheets extends VersionedNodeType {
|
||||
1: new GoogleSheetsV1(baseDescription),
|
||||
2: new GoogleSheetsV1(baseDescription),
|
||||
3: new GoogleSheetsV2(baseDescription),
|
||||
4: new GoogleSheetsV2(baseDescription),
|
||||
};
|
||||
|
||||
super(nodeVersions, baseDescription);
|
||||
|
||||
@@ -5,7 +5,7 @@ import type {
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { versionDescription } from './actions/versionDescription';
|
||||
import { credentialTest, listSearch, loadOptions } from './methods';
|
||||
import { credentialTest, listSearch, loadOptions, resourceMapping } from './methods';
|
||||
import { router } from './actions/router';
|
||||
|
||||
export class GoogleSheetsV2 implements INodeType {
|
||||
@@ -22,6 +22,7 @@ export class GoogleSheetsV2 implements INodeType {
|
||||
loadOptions,
|
||||
credentialTest,
|
||||
listSearch,
|
||||
resourceMapping,
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions) {
|
||||
|
||||
@@ -30,6 +30,7 @@ export const description: SheetProperties = [
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['append'],
|
||||
'@version': [3],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
@@ -48,6 +49,7 @@ export const description: SheetProperties = [
|
||||
show: {
|
||||
operation: ['append'],
|
||||
dataMode: ['autoMapInputData'],
|
||||
'@version': [3],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
@@ -68,6 +70,7 @@ export const description: SheetProperties = [
|
||||
resource: ['sheet'],
|
||||
operation: ['append'],
|
||||
dataMode: ['defineBelow'],
|
||||
'@version': [3],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
@@ -101,6 +104,40 @@ export const description: SheetProperties = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Columns',
|
||||
name: 'columns',
|
||||
type: 'resourceMapper',
|
||||
noDataExpression: true,
|
||||
default: {
|
||||
mappingMode: 'defineBelow',
|
||||
value: null,
|
||||
},
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['sheetName.value'],
|
||||
resourceMapper: {
|
||||
resourceMapperMethod: 'getMappingColumns',
|
||||
mode: 'add',
|
||||
fieldWords: {
|
||||
singular: 'column',
|
||||
plural: 'columns',
|
||||
},
|
||||
addAllFields: true,
|
||||
multiKeyMatch: false,
|
||||
},
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['append'],
|
||||
'@version': [4],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
@@ -156,7 +193,11 @@ export async function execute(
|
||||
sheetId: string,
|
||||
): Promise<INodeExecutionData[]> {
|
||||
const items = this.getInputData();
|
||||
const dataMode = this.getNodeParameter('dataMode', 0) as string;
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
const dataMode =
|
||||
nodeVersion < 4
|
||||
? (this.getNodeParameter('dataMode', 0) as string)
|
||||
: (this.getNodeParameter('columns.mappingMode', 0) as string);
|
||||
|
||||
if (!items.length || dataMode === 'nothing') return [];
|
||||
|
||||
@@ -190,5 +231,9 @@ export async function execute(
|
||||
false,
|
||||
);
|
||||
|
||||
return items;
|
||||
if (nodeVersion < 4 || dataMode === 'autoMapInputData') {
|
||||
return items;
|
||||
} else {
|
||||
return this.helpers.returnJsonArray(setData);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export const description: SheetProperties = [
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['appendOrUpdate'],
|
||||
'@version': [3],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
@@ -61,6 +62,7 @@ export const description: SheetProperties = [
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['appendOrUpdate'],
|
||||
'@version': [3],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
@@ -77,6 +79,7 @@ export const description: SheetProperties = [
|
||||
resource: ['sheet'],
|
||||
operation: ['appendOrUpdate'],
|
||||
dataMode: ['defineBelow'],
|
||||
'@version': [3],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
@@ -96,6 +99,7 @@ export const description: SheetProperties = [
|
||||
resource: ['sheet'],
|
||||
operation: ['appendOrUpdate'],
|
||||
dataMode: ['defineBelow'],
|
||||
'@version': [3],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
@@ -141,6 +145,40 @@ export const description: SheetProperties = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
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',
|
||||
@@ -200,10 +238,21 @@ export async function execute(
|
||||
}
|
||||
|
||||
columnNames = sheetData[headerRow];
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
const newColumns = new Set<string>();
|
||||
|
||||
const columnToMatchOn = this.getNodeParameter('columnToMatchOn', 0) as string;
|
||||
const keyIndex = columnNames.indexOf(columnToMatchOn);
|
||||
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,
|
||||
@@ -216,12 +265,8 @@ export async function execute(
|
||||
const updateData: ISheetUpdateData[] = [];
|
||||
const appendData: IDataObject[] = [];
|
||||
|
||||
const mappedValues: IDataObject[] = [];
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const dataMode = this.getNodeParameter('dataMode', i) as
|
||||
| 'defineBelow'
|
||||
| 'autoMapInputData'
|
||||
| 'nothing';
|
||||
|
||||
if (dataMode === 'nothing') continue;
|
||||
|
||||
const data: IDataObject[] = [];
|
||||
@@ -251,33 +296,50 @@ export async function execute(
|
||||
data.push(items[i].json);
|
||||
}
|
||||
} else {
|
||||
const valueToMatchOn = this.getNodeParameter('valueToMatchOn', i) as string;
|
||||
const valueToMatchOn =
|
||||
nodeVersion < 4
|
||||
? (this.getNodeParameter('valueToMatchOn', i) as string)
|
||||
: (this.getNodeParameter(`columns.value[${columnsToMatchOn[0]}]`, i) as string);
|
||||
|
||||
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;
|
||||
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'",
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
}, {} as IDataObject);
|
||||
|
||||
fields[columnToMatchOn] = valueToMatchOn;
|
||||
|
||||
data.push(fields);
|
||||
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) {
|
||||
@@ -291,7 +353,7 @@ export async function execute(
|
||||
|
||||
const preparedData = await sheet.prepareDataForUpdateOrUpsert(
|
||||
data,
|
||||
columnToMatchOn,
|
||||
columnsToMatchOn[0],
|
||||
range,
|
||||
headerRow,
|
||||
firstDataRow,
|
||||
@@ -321,5 +383,10 @@ export async function execute(
|
||||
lastRow,
|
||||
);
|
||||
}
|
||||
return items;
|
||||
|
||||
if (nodeVersion < 4 || dataMode === 'autoMapInputData') {
|
||||
return items;
|
||||
} else {
|
||||
return this.helpers.returnJsonArray(mappedValues);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ export const description: SheetProperties = [
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['update'],
|
||||
'@version': [3],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
@@ -61,6 +62,7 @@ export const description: SheetProperties = [
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['update'],
|
||||
'@version': [3],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
@@ -77,6 +79,7 @@ export const description: SheetProperties = [
|
||||
resource: ['sheet'],
|
||||
operation: ['update'],
|
||||
dataMode: ['defineBelow'],
|
||||
'@version': [3],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
@@ -96,6 +99,7 @@ export const description: SheetProperties = [
|
||||
resource: ['sheet'],
|
||||
operation: ['update'],
|
||||
dataMode: ['defineBelow'],
|
||||
'@version': [3],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
@@ -141,6 +145,40 @@ export const description: SheetProperties = [
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'Columns',
|
||||
name: 'columns',
|
||||
type: 'resourceMapper',
|
||||
noDataExpression: true,
|
||||
default: {
|
||||
mappingMode: 'defineBelow',
|
||||
value: null,
|
||||
},
|
||||
required: true,
|
||||
typeOptions: {
|
||||
loadOptionsDependsOn: ['sheetName.value'],
|
||||
resourceMapper: {
|
||||
resourceMapperMethod: 'getMappingColumns',
|
||||
mode: 'update',
|
||||
fieldWords: {
|
||||
singular: 'column',
|
||||
plural: 'columns',
|
||||
},
|
||||
addAllFields: true,
|
||||
multiKeyMatch: false,
|
||||
},
|
||||
},
|
||||
displayOptions: {
|
||||
show: {
|
||||
resource: ['sheet'],
|
||||
operation: ['update'],
|
||||
'@version': [4],
|
||||
},
|
||||
hide: {
|
||||
...untilSheetSelected,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
@@ -175,6 +213,8 @@ export async function execute(
|
||||
|
||||
const locationDefineOptions = (options.locationDefine as IDataObject)?.values as IDataObject;
|
||||
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
|
||||
let headerRow = 0;
|
||||
let firstDataRow = 1;
|
||||
|
||||
@@ -201,8 +241,18 @@ export async function execute(
|
||||
columnNames = sheetData[headerRow];
|
||||
const newColumns = new Set<string>();
|
||||
|
||||
const columnToMatchOn = this.getNodeParameter('columnToMatchOn', 0) as string;
|
||||
const keyIndex = columnNames.indexOf(columnToMatchOn);
|
||||
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,
|
||||
@@ -214,12 +264,9 @@ export async function execute(
|
||||
|
||||
const updateData: ISheetUpdateData[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
const dataMode = this.getNodeParameter('dataMode', i) as
|
||||
| 'defineBelow'
|
||||
| 'autoMapInputData'
|
||||
| 'nothing';
|
||||
const mappedValues: IDataObject[] = [];
|
||||
|
||||
for (let i = 0; i < items.length; i++) {
|
||||
if (dataMode === 'nothing') continue;
|
||||
|
||||
const data: IDataObject[] = [];
|
||||
@@ -249,33 +296,54 @@ export async function execute(
|
||||
data.push(items[i].json);
|
||||
}
|
||||
} else {
|
||||
const valueToMatchOn = this.getNodeParameter('valueToMatchOn', i) as string;
|
||||
const valueToMatchOn =
|
||||
nodeVersion < 4
|
||||
? (this.getNodeParameter('valueToMatchOn', i) as string)
|
||||
: (this.getNodeParameter(`columns.value[${columnsToMatchOn[0]}]`, i) as string);
|
||||
|
||||
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;
|
||||
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'",
|
||||
);
|
||||
}
|
||||
return acc;
|
||||
}, {} as IDataObject);
|
||||
const fields = valuesToSend.reduce((acc, entry) => {
|
||||
if (entry.column === 'newColumn') {
|
||||
const columnName = entry.columnName as string;
|
||||
|
||||
fields[columnToMatchOn] = valueToMatchOn;
|
||||
if (!columnNames.includes(columnName)) {
|
||||
newColumns.add(columnName);
|
||||
}
|
||||
|
||||
data.push(fields);
|
||||
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) {
|
||||
@@ -289,7 +357,7 @@ export async function execute(
|
||||
|
||||
const preparedData = await sheet.prepareDataForUpdateOrUpsert(
|
||||
data,
|
||||
columnToMatchOn,
|
||||
columnsToMatchOn[0],
|
||||
range,
|
||||
headerRow,
|
||||
firstDataRow,
|
||||
@@ -306,5 +374,9 @@ export async function execute(
|
||||
await sheet.batchUpdate(updateData, valueInputMode);
|
||||
}
|
||||
|
||||
return items;
|
||||
if (nodeVersion < 4 || dataMode === 'autoMapInputData') {
|
||||
return items;
|
||||
} else {
|
||||
return this.helpers.returnJsonArray(mappedValues);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ export const versionDescription: INodeTypeDescription = {
|
||||
name: 'googleSheets',
|
||||
icon: 'file:googleSheets.svg',
|
||||
group: ['input', 'output'],
|
||||
version: 3,
|
||||
version: [3, 4],
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Read, update and write data to Google Sheets',
|
||||
defaults: {
|
||||
|
||||
@@ -195,12 +195,24 @@ export function mapFields(this: IExecuteFunctions, inputSize: number) {
|
||||
const returnData: IDataObject[] = [];
|
||||
|
||||
for (let i = 0; i < inputSize; i++) {
|
||||
const fields = this.getNodeParameter('fieldsUi.fieldValues', i, []) as IDataObject[];
|
||||
let dataToSend: IDataObject = {};
|
||||
for (const field of fields) {
|
||||
dataToSend = { ...dataToSend, [field.fieldId as string]: field.fieldValue };
|
||||
const nodeVersion = this.getNode().typeVersion;
|
||||
if (nodeVersion < 4) {
|
||||
const fields = this.getNodeParameter('fieldsUi.fieldValues', i, []) as IDataObject[];
|
||||
let dataToSend: IDataObject = {};
|
||||
for (const field of fields) {
|
||||
dataToSend = { ...dataToSend, [field.fieldId as string]: field.fieldValue };
|
||||
}
|
||||
returnData.push(dataToSend);
|
||||
} 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'",
|
||||
);
|
||||
}
|
||||
returnData.push(mappingValues);
|
||||
}
|
||||
returnData.push(dataToSend);
|
||||
}
|
||||
|
||||
return returnData;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
export * as loadOptions from './loadOptions';
|
||||
export * as listSearch from './listSearch';
|
||||
export * as credentialTest from './credentialTest';
|
||||
export * as resourceMapping from './resourceMapping';
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import type { ILoadOptionsFunctions } from 'n8n-core';
|
||||
import type { IDataObject, ResourceMapperFields } from 'n8n-workflow';
|
||||
import { GoogleSheet } from '../helpers/GoogleSheet';
|
||||
import type { ResourceLocator } from '../helpers/GoogleSheets.types';
|
||||
import { getSpreadsheetId } from '../helpers/GoogleSheets.utils';
|
||||
|
||||
export async function getMappingColumns(
|
||||
this: ILoadOptionsFunctions,
|
||||
): Promise<ResourceMapperFields> {
|
||||
const { mode, value } = this.getNodeParameter('documentId', 0) as IDataObject;
|
||||
const spreadsheetId = getSpreadsheetId(mode as ResourceLocator, value as string);
|
||||
|
||||
const sheet = new GoogleSheet(spreadsheetId, this);
|
||||
let sheetWithinDocument = this.getNodeParameter('sheetName', undefined, {
|
||||
extractValue: true,
|
||||
}) as string;
|
||||
|
||||
if (sheetWithinDocument === 'gid=0') {
|
||||
sheetWithinDocument = '0';
|
||||
}
|
||||
|
||||
const sheetName = await sheet.spreadsheetGetSheetNameById(sheetWithinDocument);
|
||||
const sheetData = await sheet.getData(`${sheetName}!1:1`, 'FORMATTED_VALUE');
|
||||
|
||||
const columns = sheet.testFilter(sheetData || [], 0, 0).filter((col) => col !== '');
|
||||
const columnData: ResourceMapperFields = {
|
||||
fields: columns.map((col) => ({
|
||||
id: col,
|
||||
displayName: col,
|
||||
required: false,
|
||||
defaultMatch: col === 'id',
|
||||
display: true,
|
||||
type: 'string',
|
||||
canBeUsedToMatch: true,
|
||||
})),
|
||||
};
|
||||
|
||||
return columnData;
|
||||
}
|
||||
Reference in New Issue
Block a user