diff --git a/packages/cli/BREAKING-CHANGES.md b/packages/cli/BREAKING-CHANGES.md
index aeb4f7e275..5435714e8e 100644
--- a/packages/cli/BREAKING-CHANGES.md
+++ b/packages/cli/BREAKING-CHANGES.md
@@ -2,6 +2,26 @@
This list shows all the versions which include breaking changes and how to upgrade.
+## 0.117.0
+
+### What changed?
+
+Changed the behavior for nodes that use Postgres Wire Protocol: Postgres, QuestDB, CrateDB and TimescaleDB.
+
+All nodes have been standardized and now follow the same patterns. Behavior will be the same for most cases, but new added functionality can now be explored.
+
+You can now also inform how you would like n8n to execute queries. Default mode is `Multiple queries` which translates to previous behavior, but you can now run them `Independently` or `Transaction`. Also, `Continue on Fail` now plays a major role for the new modes.
+
+The node output for `insert` operations now rely on the new parameter `Return fields`, just like `update` operations did previously.
+
+### When is action necessary?
+
+If you rely on the output returned by `insert` operations for any of the mentioned nodes, we recommend you review your workflows.
+
+By default, all `insert` operations will have `Return fields: *` as the default, setting, returning all information inserted.
+
+Previously, the node would return all information it received, without taking into account what actually happened in the database.
+
## 0.113.0
### What changed?
diff --git a/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts b/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts
index a2577282b8..da74abdd6c 100644
--- a/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts
+++ b/packages/nodes-base/nodes/CrateDb/CrateDb.node.ts
@@ -8,9 +8,12 @@ import {
} from 'n8n-workflow';
import {
+ generateReturning,
getItemCopy,
+ getItemsCopy,
pgInsert,
pgQuery,
+ pgUpdate,
} from '../Postgres/Postgres.node.functions';
import * as pgPromise from 'pg-promise';
@@ -125,22 +128,23 @@ export class CrateDb implements INodeType {
description:
'Comma separated list of the properties which should used as columns for the new rows.',
},
- {
- displayName: 'Return Fields',
- name: 'returnFields',
- type: 'string',
- displayOptions: {
- show: {
- operation: ['insert'],
- },
- },
- default: '*',
- description: 'Comma separated list of the fields that the operation will return',
- },
// ----------------------------------
// update
// ----------------------------------
+ {
+ displayName: 'Schema',
+ name: 'schema',
+ type: 'string',
+ displayOptions: {
+ show: {
+ operation: ['update'],
+ },
+ },
+ default: 'doc',
+ required: true,
+ description: 'Name of the schema the table belongs to',
+ },
{
displayName: 'Table',
name: 'table',
@@ -166,7 +170,7 @@ export class CrateDb implements INodeType {
default: 'id',
required: true,
description:
- 'Name of the property which decides which rows in the database should be updated. Normally that would be "id".',
+ 'Comma separated list of the properties which decides which rows in the database should be updated. Normally that would be "id".',
},
{
displayName: 'Columns',
@@ -182,6 +186,57 @@ export class CrateDb implements INodeType {
description:
'Comma separated list of the properties which should used as columns for rows to update.',
},
+
+ // ----------------------------------
+ // insert,update
+ // ----------------------------------
+ {
+ displayName: 'Return Fields',
+ name: 'returnFields',
+ type: 'string',
+ displayOptions: {
+ show: {
+ operation: ['insert', 'update'],
+ },
+ },
+ default: '*',
+ description: 'Comma separated list of the fields that the operation will return',
+ },
+ // ----------------------------------
+ // additional fields
+ // ----------------------------------
+ {
+ displayName: 'Additional Fields',
+ name: 'additionalFields',
+ type: 'collection',
+ placeholder: 'Add Field',
+ default: {},
+ options: [
+ {
+ displayName: 'Mode',
+ name: 'mode',
+ type: 'options',
+ options: [
+ {
+ name: 'Independently',
+ value: 'independently',
+ description: 'Execute each query independently',
+ },
+ {
+ name: 'Multiple queries',
+ value: 'multiple',
+ description: 'Default. Sends multiple queries at once to database.',
+ },
+ ],
+ default: 'multiple',
+ description: [
+ 'The way queries should be sent to database.',
+ 'Can be used in conjunction with Continue on Fail.',
+ 'See the docs for more examples',
+ ].join(' '),
+ },
+ ],
+ },
],
};
@@ -206,7 +261,7 @@ export class CrateDb implements INodeType {
const db = pgp(config);
- let returnItems = [];
+ let returnItems: INodeExecutionData[] = [];
const items = this.getInputData();
const operation = this.getNodeParameter('operation', 0) as string;
@@ -216,66 +271,68 @@ export class CrateDb implements INodeType {
// executeQuery
// ----------------------------------
- const queryResult = await pgQuery(this.getNodeParameter, pgp, db, items);
+ const queryResult = await pgQuery(this.getNodeParameter, pgp, db, items, this.continueOnFail());
- returnItems = this.helpers.returnJsonArray(queryResult as IDataObject[]);
+ returnItems = this.helpers.returnJsonArray(queryResult);
} else if (operation === 'insert') {
// ----------------------------------
// insert
// ----------------------------------
- const [insertData, insertItems] = await pgInsert(this.getNodeParameter, pgp, db, items);
+ const insertData = await pgInsert(this.getNodeParameter, pgp, db, items, this.continueOnFail());
- // Add the id to the data
for (let i = 0; i < insertData.length; i++) {
returnItems.push({
- json: {
- ...insertData[i],
- ...insertItems[i],
- },
+ json: insertData[i],
});
}
} else if (operation === 'update') {
// ----------------------------------
// update
// ----------------------------------
- const tableName = this.getNodeParameter('table', 0) as string;
- const updateKey = this.getNodeParameter('updateKey', 0) as string;
- const queries : string[] = [];
- const updatedKeys : string[] = [];
- let updateKeyValue : string | number;
- let columns : string[] = [];
+ const additionalFields = this.getNodeParameter('additionalFields', 0) as IDataObject;
+ const mode = additionalFields.mode ?? 'multiple' as string;
- items.map(item => {
- const setOperations : string[] = [];
- columns = Object.keys(item.json);
- columns.map((col : string) => {
- if (col !== updateKey) {
- if (typeof item.json[col] === 'string') {
- setOperations.push(`${col} = \'${item.json[col]}\'`);
- } else {
- setOperations.push(`${col} = ${item.json[col]}`);
- }
+ if(mode === 'independently') {
+ const updateItems = await pgUpdate(this.getNodeParameter, pgp, db, items, this.continueOnFail());
+
+ returnItems = this.helpers.returnJsonArray(updateItems);
+ } else if(mode === 'multiple') {
+ // Crate db does not support multiple-update queries
+ // Therefore we cannot invoke `pgUpdate` using multiple mode
+ // so we have to call multiple updates manually here
+
+ const table = this.getNodeParameter('table', 0) as string;
+ const schema = this.getNodeParameter('schema', 0) as string;
+ const updateKeys = (this.getNodeParameter('updateKey', 0) as string).split(',').map(column => column.trim());
+ const columns = (this.getNodeParameter('columns', 0) as string).split(',').map(column => column.trim());
+ const queryColumns = columns.slice();
+
+ updateKeys.forEach(updateKey => {
+ if (!queryColumns.includes(updateKey)) {
+ columns.unshift(updateKey);
+ queryColumns.unshift('?' + updateKey);
}
});
- updateKeyValue = item.json[updateKey] as string | number;
+ const cs = new pgp.helpers.ColumnSet(queryColumns, { table: { table, schema } });
- if (updateKeyValue === undefined) {
- throw new NodeOperationError(this.getNode(), 'No value found for update key!');
+ const where = ' WHERE ' + updateKeys.map(updateKey => pgp.as.name(updateKey) + ' = ${' + updateKey + '}').join(' AND ');
+ // updateKeyValue = item.json[updateKey] as string | number;
+ // if (updateKeyValue === undefined) {
+ // throw new NodeOperationError(this.getNode(), 'No value found for update key!');
+ // }
+
+ const returning = generateReturning(pgp, this.getNodeParameter('returnFields', 0) as string);
+ const queries:string[] = [];
+ for (let i = 0; i < items.length; i++) {
+ const itemCopy = getItemCopy(items[i], columns);
+ queries.push(pgp.helpers.update(itemCopy, cs) + pgp.as.format(where, itemCopy) + returning);
}
-
- updatedKeys.push(updateKeyValue as string);
-
- const query = `UPDATE "${tableName}" SET ${setOperations.join(',')} WHERE ${updateKey} = ${updateKeyValue};`;
- queries.push(query);
- });
-
-
- await db.any(pgp.helpers.concat(queries));
-
- returnItems = this.helpers.returnJsonArray(getItemCopy(items, columns) as IDataObject[]);
+ const updateItems = await db.multi(pgp.helpers.concat(queries));
+ returnItems = this.helpers.returnJsonArray(getItemsCopy(items, columns) as IDataObject[]);
+ }
} else {
await pgp.end();
throw new NodeOperationError(this.getNode(), `The operation "${operation}" is not supported!`);
diff --git a/packages/nodes-base/nodes/Postgres/Postgres.node.functions.ts b/packages/nodes-base/nodes/Postgres/Postgres.node.functions.ts
index df1f10cbbc..30cf358579 100644
--- a/packages/nodes-base/nodes/Postgres/Postgres.node.functions.ts
+++ b/packages/nodes-base/nodes/Postgres/Postgres.node.functions.ts
@@ -3,29 +3,50 @@ import pgPromise = require('pg-promise');
import pg = require('pg-promise/typescript/pg-subset');
/**
- * Returns of copy of the items which only contains the json data and
+ * Returns of a shallow copy of the items which only contains the json data and
* of that only the define properties
*
* @param {INodeExecutionData[]} items The items to copy
* @param {string[]} properties The properties it should include
* @returns
*/
-export function getItemCopy(items: INodeExecutionData[], properties: string[]): IDataObject[] {
- // Prepare the data to insert and copy it to be returned
+export function getItemsCopy(items: INodeExecutionData[], properties: string[]): IDataObject[] {
let newItem: IDataObject;
return items.map(item => {
newItem = {};
for (const property of properties) {
- if (item.json[property] === undefined) {
- newItem[property] = null;
- } else {
- newItem[property] = JSON.parse(JSON.stringify(item.json[property]));
- }
+ newItem[property] = item.json[property];
}
return newItem;
});
}
+/**
+ * Returns of a shallow copy of the item which only contains the json data and
+ * of that only the define properties
+ *
+ * @param {INodeExecutionData} item The item to copy
+ * @param {string[]} properties The properties it should include
+ * @returns
+ */
+export function getItemCopy(item: INodeExecutionData, properties: string[]): IDataObject {
+ const newItem: IDataObject = {};
+ for (const property of properties) {
+ newItem[property] = item.json[property];
+ }
+ return newItem;
+}
+
+/**
+ * Returns a returning clause from a comma separated string
+ * @param {pgPromise.IMain<{}, pg.IClient>} pgp The pgPromise instance
+ * @param string returning The comma separated string
+ * @returns string
+ */
+export function generateReturning(pgp: pgPromise.IMain<{}, pg.IClient>, returning: string): string {
+ return ' RETURNING ' + returning.split(',').map(returnedField => pgp.as.name(returnedField.trim())).join(', ');
+}
+
/**
* Executes the given SQL query on the database.
*
@@ -33,20 +54,53 @@ export function getItemCopy(items: INodeExecutionData[], properties: string[]):
* @param {pgPromise.IMain<{}, pg.IClient>} pgp The pgPromise instance
* @param {pgPromise.IDatabase<{}, pg.IClient>} db The pgPromise database connection
* @param {input[]} input The Node's input data
- * @returns Promise>
+ * @returns Promise>
*/
-export function pgQuery(
+export async function pgQuery(
getNodeParam: Function,
pgp: pgPromise.IMain<{}, pg.IClient>,
db: pgPromise.IDatabase<{}, pg.IClient>,
input: INodeExecutionData[],
-): Promise