first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled

This commit is contained in:
2026-03-17 16:22:57 +03:30
commit 3d5eaf9445
15349 changed files with 2847338 additions and 0 deletions
@@ -0,0 +1,88 @@
import { createPrivateKey } from 'crypto';
import pick from 'lodash/pick';
import type snowflake from 'snowflake-sdk';
import { formatPrivateKey } from '@utils/utilities';
const commonConnectionFields = [
'account',
'database',
'schema',
'warehouse',
'role',
'clientSessionKeepAlive',
] as const;
export type SnowflakeCredential = Pick<
snowflake.ConnectionOptions,
(typeof commonConnectionFields)[number]
> &
(
| {
authentication: 'password';
username?: string;
password?: string;
}
| {
authentication: 'keyPair';
username: string;
privateKey: string;
passphrase?: string;
}
);
const extractPrivateKey = (credential: { privateKey: string; passphrase?: string }) => {
const key = formatPrivateKey(credential.privateKey);
if (!credential.passphrase) return key;
const privateKeyObject = createPrivateKey({
key,
format: 'pem',
passphrase: credential.passphrase,
});
return privateKeyObject.export({
format: 'pem',
type: 'pkcs8',
}) as string;
};
export const getConnectionOptions = (credential: SnowflakeCredential) => {
const connectionOptions: snowflake.ConnectionOptions = pick(credential, commonConnectionFields);
if (credential.authentication === 'keyPair') {
connectionOptions.authenticator = 'SNOWFLAKE_JWT';
connectionOptions.username = credential.username;
connectionOptions.privateKey = extractPrivateKey(credential);
} else {
connectionOptions.username = credential.username;
connectionOptions.password = credential.password;
}
return connectionOptions;
};
export async function connect(conn: snowflake.Connection) {
return await new Promise<void>((resolve, reject) => {
conn.connect((error) => (error ? reject(error) : resolve()));
});
}
export async function destroy(conn: snowflake.Connection) {
return await new Promise<void>((resolve, reject) => {
conn.destroy((error) => (error ? reject(error) : resolve()));
});
}
export async function execute(
conn: snowflake.Connection,
sqlText: string,
binds: snowflake.InsertBinds,
) {
return await new Promise<any[] | undefined>((resolve, reject) => {
conn.execute({
sqlText,
binds,
complete: (error, _, rows) => (error ? reject(error) : resolve(rows)),
});
});
}
@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.snowflake",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Data & Storage"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/credentials/snowflake/"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.snowflake/"
}
]
}
}
@@ -0,0 +1,267 @@
import type {
IExecuteFunctions,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';
import snowflake from 'snowflake-sdk';
import { getResolvables } from '@utils/utilities';
import {
connect,
destroy,
execute,
getConnectionOptions,
type SnowflakeCredential,
} from './GenericFunctions';
export class Snowflake implements INodeType {
description: INodeTypeDescription = {
displayName: 'Snowflake',
name: 'snowflake',
icon: 'file:snowflake.svg',
group: ['input'],
version: 1,
description: 'Get, add and update data in Snowflake',
defaults: {
name: 'Snowflake',
},
usableAsTool: true,
inputs: [NodeConnectionTypes.Main],
outputs: [NodeConnectionTypes.Main],
parameterPane: 'wide',
credentials: [
{
name: 'snowflake',
required: true,
},
],
properties: [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Execute Query',
value: 'executeQuery',
description: 'Execute an SQL query',
action: 'Execute a SQL query',
},
{
name: 'Insert',
value: 'insert',
description: 'Insert rows in database',
action: 'Insert rows in database',
},
{
name: 'Update',
value: 'update',
description: 'Update rows in database',
action: 'Update rows in database',
},
],
default: 'insert',
},
// ----------------------------------
// executeQuery
// ----------------------------------
{
displayName: 'Query',
name: 'query',
type: 'string',
noDataExpression: true,
typeOptions: {
editor: 'sqlEditor',
},
displayOptions: {
show: {
operation: ['executeQuery'],
},
},
default: '',
placeholder: 'SELECT id, name FROM product WHERE id < 40',
required: true,
description: 'The SQL query to execute',
},
// ----------------------------------
// insert
// ----------------------------------
{
displayName: 'Table',
name: 'table',
type: 'string',
displayOptions: {
show: {
operation: ['insert'],
},
},
default: '',
required: true,
description: 'Name of the table in which to insert data to',
},
{
displayName: 'Columns',
name: 'columns',
type: 'string',
displayOptions: {
show: {
operation: ['insert'],
},
},
default: '',
placeholder: 'id,name,description',
description:
'Comma-separated list of the properties which should used as columns for the new rows',
},
// ----------------------------------
// update
// ----------------------------------
{
displayName: 'Table',
name: 'table',
type: 'string',
displayOptions: {
show: {
operation: ['update'],
},
},
default: '',
required: true,
description: 'Name of the table in which to update data in',
},
{
displayName: 'Update Key',
name: 'updateKey',
type: 'string',
displayOptions: {
show: {
operation: ['update'],
},
},
default: 'id',
required: true,
// eslint-disable-next-line n8n-nodes-base/node-param-description-miscased-id
description:
'Name of the property which decides which rows in the database should be updated. Normally that would be "id".',
},
{
displayName: 'Columns',
name: 'columns',
type: 'string',
displayOptions: {
show: {
operation: ['update'],
},
},
default: '',
placeholder: 'name,description',
description:
'Comma-separated list of the properties which should used as columns for rows to update',
},
],
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const credentials = await this.getCredentials<SnowflakeCredential>('snowflake');
// Disable logging - https://docs.snowflake.com/en/developer-guide/node-js/nodejs-driver-logs#configure-the-default-logging-behavior
snowflake.configure({
logFilePath: 'STDOUT',
logLevel: 'OFF',
});
const connectionOptions = getConnectionOptions(credentials);
const connection = snowflake.createConnection(connectionOptions);
await connect(connection);
const returnData: INodeExecutionData[] = [];
const items = this.getInputData();
const operation = this.getNodeParameter('operation', 0);
if (operation === 'executeQuery') {
// ----------------------------------
// executeQuery
// ----------------------------------
for (let i = 0; i < items.length; i++) {
let query = this.getNodeParameter('query', i) as string;
for (const resolvable of getResolvables(query)) {
query = query.replace(resolvable, this.evaluateExpression(resolvable, i) as string);
}
const responseData = await execute(connection, query, []);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
}
}
if (operation === 'insert') {
// ----------------------------------
// insert
// ----------------------------------
const table = this.getNodeParameter('table', 0) as string;
const columnString = this.getNodeParameter('columns', 0) as string;
const columns = columnString.split(',').map((column) => column.trim());
const query = `INSERT INTO ${table}(${columns.join(',')}) VALUES (${columns
.map((_column) => '?')
.join(',')})`;
const data = this.helpers.copyInputItems(items, columns);
const binds = data.map((element) => Object.values(element));
await execute(connection, query, binds as unknown as snowflake.InsertBinds);
data.forEach((d, i) => {
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(d),
{ itemData: { item: i } },
);
returnData.push(...executionData);
});
}
if (operation === 'update') {
// ----------------------------------
// update
// ----------------------------------
const table = this.getNodeParameter('table', 0) as string;
const updateKey = this.getNodeParameter('updateKey', 0) as string;
const columnString = this.getNodeParameter('columns', 0) as string;
const columns = columnString.split(',').map((column) => column.trim());
if (!columns.includes(updateKey)) {
columns.unshift(updateKey);
}
const query = `UPDATE ${table} SET ${columns
.map((column) => `${column} = ?`)
.join(',')} WHERE ${updateKey} = ?;`;
const data = this.helpers.copyInputItems(items, columns);
const binds = data.map((element) => Object.values(element).concat(element[updateKey]));
for (let i = 0; i < binds.length; i++) {
await execute(connection, query, binds[i] as unknown as snowflake.InsertBinds);
}
data.forEach((d, i) => {
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(d),
{ itemData: { item: i } },
);
returnData.push(...executionData);
});
}
await destroy(connection);
return [returnData];
}
}
@@ -0,0 +1,78 @@
import crypto from 'crypto';
import { getConnectionOptions } from '../GenericFunctions';
jest.mock('crypto');
describe('getConnectionOptions', () => {
const commonOptions = {
account: 'test-account',
database: 'test-database',
schema: 'test-schema',
warehouse: 'test-warehouse',
role: 'test-role',
clientSessionKeepAlive: true,
};
describe('should return connection options', () => {
it('with username and password for password authentication', () => {
const result = getConnectionOptions({
...commonOptions,
authentication: 'password',
username: 'test-username',
password: 'test-password',
});
expect(result).toEqual({
...commonOptions,
username: 'test-username',
password: 'test-password',
});
});
it('with private key for keyPair authentication', () => {
const result = getConnectionOptions({
...commonOptions,
username: 'test-username',
authentication: 'keyPair',
privateKey: 'test-private-key',
});
expect(result).toEqual({
...commonOptions,
username: 'test-username',
authenticator: 'SNOWFLAKE_JWT',
privateKey: 'test-private-key',
});
});
it('with private key for keyPair authentication and passphrase', () => {
const createPrivateKeySpy = jest.spyOn(crypto, 'createPrivateKey').mockImplementation(
() =>
({
export: () => 'test-private-key',
}) as unknown as crypto.KeyObject,
);
const result = getConnectionOptions({
...commonOptions,
username: 'test-username',
authentication: 'keyPair',
privateKey: 'encrypted-private-key',
passphrase: 'test-passphrase',
});
expect(createPrivateKeySpy).toHaveBeenCalledWith({
key: 'encrypted-private-key',
format: 'pem',
passphrase: 'test-passphrase',
});
expect(result).toEqual({
...commonOptions,
username: 'test-username',
authenticator: 'SNOWFLAKE_JWT',
privateKey: 'test-private-key',
});
});
});
});
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 63 66"><use xlink:href="#a" x=".5" y=".5"/><symbol id="a" overflow="visible"><path fill="#29b5e8" fill-rule="nonzero" stroke="none" d="m8.645 15.304 13.008 7.8a3.72 3.72 0 0 0 4.589-.601 4 4 0 0 0 1.227-2.908V3.962a3.81 3.81 0 0 0-1.861-3.42 3.81 3.81 0 0 0-3.893 0 3.81 3.81 0 0 0-1.861 3.42v8.896l-7.387-4.43a3.8 3.8 0 0 0-2.922-.4 3.7 3.7 0 0 0-2.3 1.844 4.05 4.05 0 0 0 1.4 5.422m31.27 7.8 13.008-7.8a4.056 4.056 0 0 0 1.4-5.424 3.75 3.75 0 0 0-5.22-1.452l-7.3 4.37v-8.84A3.81 3.81 0 0 0 39.94.534a3.81 3.81 0 0 0-3.889 0 3.81 3.81 0 0 0-1.863 3.414v15.323a4.1 4.1 0 0 0 .494 2.367 3.75 3.75 0 0 0 2.3 1.844 3.7 3.7 0 0 0 2.922-.4m-11.567 8.894c.013-.25.108-.5.272-.68l1.52-1.58a1.06 1.06 0 0 1 .658-.282h.057a1.05 1.05 0 0 1 .656.282l1.52 1.58a1.12 1.12 0 0 1 .272.681v.06a1.13 1.13 0 0 1-.272.683L31.5 34.3a1.04 1.04 0 0 1-.656.284h-.057a1.04 1.04 0 0 1-.658-.284l-1.52-1.58a1.13 1.13 0 0 1-.272-.683zm-4.604-.65v1.364a1.54 1.54 0 0 0 .372.93l5.16 5.357a1.42 1.42 0 0 0 .895.386h1.312a1.42 1.42 0 0 0 .895-.386l5.16-5.357a1.54 1.54 0 0 0 .372-.93v-1.364a1.54 1.54 0 0 0-.372-.93l-5.16-5.357a1.42 1.42 0 0 0-.895-.386H30.16a1.42 1.42 0 0 0-.895.386l-5.16 5.367a1.55 1.55 0 0 0-.372.93m-21.818-3.71 7.365 4.417-7.365 4.413A4.06 4.06 0 0 0 .516 41.88a3.75 3.75 0 0 0 2.3 1.844 3.75 3.75 0 0 0 2.922-.392l13.008-7.8a4.11 4.11 0 0 0 1.9-3.492 4.16 4.16 0 0 0-1.9-3.492l-13.008-7.8a3.8 3.8 0 0 0-2.922-.4 3.7 3.7 0 0 0-2.3 1.844 4.053 4.053 0 0 0 1.4 5.422m38.995 4.442a4 4 0 0 0 1.91 3.477l13 7.8a3.745 3.745 0 0 0 5.22-1.451 4.05 4.05 0 0 0-1.4-5.424l-7.356-4.414 7.365-4.417a4.054 4.054 0 0 0 1.4-5.422 3.74 3.74 0 0 0-2.3-1.844 3.7 3.7 0 0 0-2.92.4l-13 7.8a4 4 0 0 0-1.91 3.507m-16.655 8.446a3.7 3.7 0 0 0-2.611.464l-13.008 7.8a4.055 4.055 0 0 0-1.4 5.422 3.74 3.74 0 0 0 2.3 1.843 3.75 3.75 0 0 0 2.922-.392l7.387-4.43v8.83a3.81 3.81 0 0 0 5.755 3.425 3.81 3.81 0 0 0 1.858-3.425V44.406a3.91 3.91 0 0 0-3.205-3.903m28.66 8.276-13.008-7.8a3.75 3.75 0 0 0-2.922-.392 3.74 3.74 0 0 0-2.3 1.843 4.1 4.1 0 0 0-.494 2.37v15.25a3.81 3.81 0 0 0 5.755 3.425 3.81 3.81 0 0 0 1.859-3.425v-8.764l7.287 4.37a3.8 3.8 0 0 0 2.922.4 3.7 3.7 0 0 0 2.3-1.844c1.057-1.9.44-4.28-1.4-5.422"/></symbol></svg>

After

Width:  |  Height:  |  Size: 2.3 KiB