Merge remote-tracking branch 'origin/master' into release/1.0.1

This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™
2023-07-05 20:01:24 +02:00
193 changed files with 16701 additions and 4702 deletions

View File

@@ -4,7 +4,7 @@ import type {
INodeProperties,
JsonObject,
} from 'n8n-workflow';
import { SendInBlueNode } from './GenericFunctions';
import { BrevoNode } from './GenericFunctions';
export const attributeOperations: INodeProperties[] = [
{
@@ -43,7 +43,7 @@ export const attributeOperations: INodeProperties[] = [
requestOptions: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const selectedCategory = this.getNodeParameter('attributeCategory') as string;
const override = SendInBlueNode.INTERCEPTORS.get(selectedCategory);
const override = BrevoNode.INTERCEPTORS.get(selectedCategory);
if (override) {
override.call(this, requestOptions.body! as JsonObject);
}

View File

@@ -1,18 +1,19 @@
{
"node": "n8n-nodes-base.sendInBlue",
"node": "n8n-nodes-base.brevo",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Marketing & Content", "Communication"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/credentials/sendInBlue"
"url": "https://docs.n8n.io/credentials/brevo"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.sendinblue/"
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.brevo/"
}
]
}
},
"alias": ["sendinblue"]
}

View File

@@ -5,17 +5,18 @@ import { contactFields, contactOperations } from './ContactDescription';
import { emailFields, emailOperations } from './EmailDescription';
import { senderFields, senderOperations } from './SenderDescrition';
export class SendInBlue implements INodeType {
export class Brevo implements INodeType {
description: INodeTypeDescription = {
displayName: 'SendInBlue',
displayName: 'Brevo',
// keep sendinblue name for backward compatibility
name: 'sendInBlue',
icon: 'file:sendinblue.svg',
icon: 'file:brevo.svg',
group: ['transform'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Sendinblue API',
description: 'Consume Brevo API',
defaults: {
name: 'SendInBlue',
name: 'Brevo',
},
inputs: ['main'],
outputs: ['main'],
@@ -26,7 +27,7 @@ export class SendInBlue implements INodeType {
},
],
requestDefaults: {
baseURL: 'https://api.sendinblue.com',
baseURL: 'https://api.brevo.com',
},
properties: [
{

View File

@@ -1,19 +1,20 @@
{
"node": "n8n-nodes-base.sendInBlueTrigger",
"node": "n8n-nodes-base.brevoTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Marketing & Content", "Communication"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/credentials/sendInBlue"
"url": "https://docs.n8n.io/credentials/brevo"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.sendinbluetrigger/"
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.brevotrigger/"
}
],
"generic": []
}
},
"alias": ["sendinblue"]
}

View File

@@ -6,9 +6,9 @@ import type {
IWebhookFunctions,
IWebhookResponseData,
} from 'n8n-workflow';
import { SendInBlueWebhookApi } from './GenericFunctions';
import { BrevoWebhookApi } from './GenericFunctions';
export class SendInBlueTrigger implements INodeType {
export class BrevoTrigger implements INodeType {
description: INodeTypeDescription = {
credentials: [
{
@@ -16,14 +16,15 @@ export class SendInBlueTrigger implements INodeType {
required: true,
},
],
displayName: 'SendInBlue Trigger',
displayName: 'Brevo Trigger',
defaults: {
name: 'SendInBlue Trigger',
name: 'Brevo Trigger',
},
description: 'Starts the workflow when SendInBlue events occur',
description: 'Starts the workflow when Brevo events occur',
group: ['trigger'],
icon: 'file:sendinblue.svg',
icon: 'file:brevo.svg',
inputs: [],
// keep sendinblue name for backward compatibility
name: 'sendInBlueTrigger',
outputs: ['main'],
version: 1,
@@ -213,7 +214,7 @@ export class SendInBlueTrigger implements INodeType {
const events = this.getNodeParameter('events') as string[];
try {
const { webhooks } = await SendInBlueWebhookApi.fetchWebhooks(this, type);
const { webhooks } = await BrevoWebhookApi.fetchWebhooks(this, type);
for (const webhook of webhooks) {
if (
@@ -240,12 +241,7 @@ export class SendInBlueTrigger implements INodeType {
const events = this.getNodeParameter('events') as string[];
const responseData = await SendInBlueWebhookApi.createWebHook(
this,
type,
events,
webhookUrl,
);
const responseData = await BrevoWebhookApi.createWebHook(this, type, events, webhookUrl);
if (responseData === undefined || responseData.id === undefined) {
// Required data is missing so was not successful
@@ -261,7 +257,7 @@ export class SendInBlueTrigger implements INodeType {
if (webhookData.webhookId !== undefined) {
try {
await SendInBlueWebhookApi.deleteWebhook(this, webhookData.webhookId as string);
await BrevoWebhookApi.deleteWebhook(this, webhookData.webhookId as string);
} catch (error) {
return false;
}

View File

@@ -1,5 +1,5 @@
import type { INodeProperties } from 'n8n-workflow';
import { SendInBlueNode } from './GenericFunctions';
import { BrevoNode } from './GenericFunctions';
export const emailOperations: INodeProperties[] = [
{
@@ -120,7 +120,7 @@ const sendHtmlEmailFields: INodeProperties[] = [
required: true,
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileSenderEmail],
preSend: [BrevoNode.Validators.validateAndCompileSenderEmail],
},
},
},
@@ -138,7 +138,7 @@ const sendHtmlEmailFields: INodeProperties[] = [
required: true,
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileReceipientEmails],
preSend: [BrevoNode.Validators.validateAndCompileReceipientEmails],
},
},
},
@@ -180,7 +180,7 @@ const sendHtmlEmailFields: INodeProperties[] = [
],
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileAttachmentsData],
preSend: [BrevoNode.Validators.validateAndCompileAttachmentsData],
},
},
},
@@ -206,7 +206,7 @@ const sendHtmlEmailFields: INodeProperties[] = [
],
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileBCCEmails],
preSend: [BrevoNode.Validators.validateAndCompileBCCEmails],
},
},
},
@@ -232,7 +232,7 @@ const sendHtmlEmailFields: INodeProperties[] = [
],
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileCCEmails],
preSend: [BrevoNode.Validators.validateAndCompileCCEmails],
},
},
},
@@ -259,7 +259,7 @@ const sendHtmlEmailFields: INodeProperties[] = [
],
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileTags],
preSend: [BrevoNode.Validators.validateAndCompileTags],
},
},
},
@@ -339,7 +339,7 @@ const sendHtmlTemplateEmailFields: INodeProperties[] = [
required: true,
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileReceipientEmails],
preSend: [BrevoNode.Validators.validateAndCompileReceipientEmails],
},
},
},
@@ -381,7 +381,7 @@ const sendHtmlTemplateEmailFields: INodeProperties[] = [
],
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileAttachmentsData],
preSend: [BrevoNode.Validators.validateAndCompileAttachmentsData],
},
},
},
@@ -408,7 +408,7 @@ const sendHtmlTemplateEmailFields: INodeProperties[] = [
],
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileTags],
preSend: [BrevoNode.Validators.validateAndCompileTags],
},
},
},
@@ -437,7 +437,7 @@ const sendHtmlTemplateEmailFields: INodeProperties[] = [
],
routing: {
send: {
preSend: [SendInBlueNode.Validators.validateAndCompileTemplateParameters],
preSend: [BrevoNode.Validators.validateAndCompileTemplateParameters],
},
},
},

View File

@@ -8,7 +8,7 @@ import type {
import { jsonParse, NodeOperationError } from 'n8n-workflow';
import type { OptionsWithUri } from 'request';
import MailComposer from 'nodemailer/lib/mail-composer';
export namespace SendInBlueNode {
export namespace BrevoNode {
type ValidEmailFields = { to: string } | { sender: string } | { cc: string } | { bcc: string };
type Address = { address: string; name?: string };
type Email = { email: string; name?: string };
@@ -277,7 +277,7 @@ export namespace SendInBlueNode {
}
}
export namespace SendInBlueWebhookApi {
export namespace BrevoWebhookApi {
interface WebhookDetails {
url: string;
id: number;
@@ -297,7 +297,7 @@ export namespace SendInBlueWebhookApi {
}
const credentialsName = 'sendInBlueApi';
const baseURL = 'https://api.sendinblue.com/v3';
const baseURL = 'https://api.brevo.com/v3';
export const supportedAuthMap = new Map<string, (ref: IWebhookFunctions) => Promise<string>>([
[
'apiKey',

View File

@@ -0,0 +1,4 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="24" cy="24" r="24" fill="#0B996E"/>
<path d="M31.5036 21.8092C32.9875 20.3562 33.6829 18.677 33.6829 16.6345C33.6829 12.4146 30.5772 9.59998 25.8963 9.59998H14.4V39.6H23.6705C30.717 39.6 36 35.2887 36 29.5702C36 26.438 34.3782 23.6253 31.5036 21.8092ZM18.572 13.5024H25.4321C27.7492 13.5024 29.2797 14.8184 29.2797 16.8152C29.2797 19.084 27.287 20.8089 23.2082 22.1249C20.4269 22.9863 19.176 23.7128 18.7118 24.5762L18.572 24.5775V13.5024ZM23.2995 35.6976H18.572V31.0688C18.572 29.0263 20.3336 27.0295 22.7906 26.2573C24.9698 25.5309 26.7761 24.8044 28.3067 24.0342C30.346 25.2152 31.5969 27.2558 31.5969 29.3895C31.5969 33.0199 28.0736 35.6976 23.2995 35.6976Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 800 B

View File

@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.crowdDev",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Productivity"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/credentials/crowdDev"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.crowdDev/"
}
]
}
}

View File

@@ -0,0 +1,32 @@
import type { INodeType, INodeTypeDescription } from 'n8n-workflow';
import { allProperties } from './descriptions';
export class CrowdDev implements INodeType {
description: INodeTypeDescription = {
displayName: 'crowd.dev',
name: 'crowdDev',
icon: 'file:crowdDev.svg',
group: ['transform'],
version: 1,
subtitle: '={{ $parameter["operation"] + ": " + $parameter["resource"] }}',
description:
'crowd.dev is an open-source suite of community and data tools built to unlock community-led growth for your organization.',
defaults: {
name: 'crowd.dev',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'crowdDevApi',
required: true,
},
],
requestDefaults: {
baseURL: '={{$credentials.url}}/api/tenant/{{$credentials.tenantId}}',
json: true,
skipSslCertificateValidation: '={{ $credentials.allowUnauthorizedCerts }}',
},
properties: allProperties,
};
}

View File

@@ -0,0 +1,18 @@
{
"node": "n8n-nodes-base.crowdDevTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Productivity"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/credentials/crowdDev"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.crowddevtrigger/"
}
]
}
}

View File

@@ -0,0 +1,185 @@
import type {
IHookFunctions,
IWebhookFunctions,
INodeType,
INodeTypeDescription,
IWebhookResponseData,
IHttpRequestOptions,
} from 'n8n-workflow';
interface ICrowdDevCreds {
url: string;
tenantId: string;
token: string;
allowUnauthorizedCerts: boolean;
}
const credsName = 'crowdDevApi';
const getCreds = async (hookFns: IHookFunctions) =>
hookFns.getCredentials(credsName) as unknown as ICrowdDevCreds;
const createRequest = (
creds: ICrowdDevCreds,
opts: Partial<IHttpRequestOptions>,
): IHttpRequestOptions => {
const defaults: IHttpRequestOptions = {
baseURL: `${creds.url}/api/tenant/${creds.tenantId}`,
url: '',
json: true,
skipSslCertificateValidation: creds.allowUnauthorizedCerts,
};
return Object.assign(defaults, opts);
};
export class CrowdDevTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'crowd.dev Trigger',
name: 'crowdDevTrigger',
icon: 'file:crowdDev.svg',
group: ['trigger'],
version: 1,
description: 'Starts the workflow when crowd.dev events occur.',
defaults: {
name: 'crowd.dev Trigger',
},
inputs: [],
outputs: ['main'],
credentials: [
{
name: 'crowdDevApi',
required: true,
},
],
webhooks: [
{
name: 'default',
httpMethod: 'POST',
responseMode: 'onReceived',
path: 'webhook',
},
],
properties: [
{
displayName: 'Trigger',
name: 'trigger',
description: 'What will trigger an automation',
type: 'options',
required: true,
default: 'new_activity',
options: [
{
name: 'New Activity',
value: 'new_activity',
},
{
name: 'New Member',
value: 'new_member',
},
],
},
],
};
webhookMethods = {
default: {
async checkExists(this: IHookFunctions): Promise<boolean> {
const creds = await getCreds(this);
const webhookData = this.getWorkflowStaticData('node');
const webhookUrl = this.getNodeWebhookUrl('default') as string;
if (webhookData.webhookId !== undefined) {
try {
const options = createRequest(creds, {
url: `/automation/${webhookData.webhookId}`,
method: 'GET',
});
const data = await this.helpers.httpRequestWithAuthentication.call(
this,
credsName,
options,
);
if (data.settings.url === webhookUrl) {
return true;
}
} catch (error) {
return false;
}
}
// If it did not error then the webhook exists
return false;
},
async create(this: IHookFunctions): Promise<boolean> {
const creds = await getCreds(this);
const webhookData = this.getWorkflowStaticData('node');
const webhookUrl = this.getNodeWebhookUrl('default') as string;
const params = {
trigger: this.getNodeParameter('trigger') as string,
};
const options = createRequest(creds, {
url: '/automation',
method: 'POST',
body: {
data: {
settings: {
url: webhookUrl,
},
type: 'webhook',
trigger: params.trigger,
},
},
});
const responseData = await this.helpers.httpRequestWithAuthentication.call(
this,
'crowdDevApi',
options,
);
if (responseData === undefined || responseData.id === undefined) {
// Required data is missing so was not successful
return false;
}
webhookData.webhookId = responseData.id as string;
return true;
},
async delete(this: IHookFunctions): Promise<boolean> {
const creds = await getCreds(this);
const webhookData = this.getWorkflowStaticData('node');
if (webhookData.webhookId !== undefined) {
try {
const options = createRequest(creds, {
url: `/automation/${webhookData.webhookId}`,
method: 'DELETE',
});
await this.helpers.httpRequestWithAuthentication.call(this, credsName, options);
} catch (error) {
return false;
}
// Remove from the static workflow data so that it is clear
// that no webhooks are registered anymore
delete webhookData.webhookId;
delete webhookData.webhookEvents;
delete webhookData.hookSecret;
}
return true;
},
},
};
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
const bodyData = this.getBodyData();
return {
workflowData: [this.helpers.returnJsonArray(bodyData)],
};
}
}

View File

@@ -0,0 +1,221 @@
import type { IExecuteSingleFunctions, IHttpRequestOptions } from 'n8n-workflow';
const addOptName = 'additionalOptions';
const getAllParams = (execFns: IExecuteSingleFunctions): Record<string, unknown> => {
const params = execFns.getNode().parameters;
const keys = Object.keys(params);
const paramsWithValues = keys
.filter((i) => i !== addOptName)
.map((name) => [name, execFns.getNodeParameter(name)]);
const paramsWithValuesObj = Object.fromEntries(paramsWithValues);
if (keys.includes(addOptName)) {
const additionalOptions = execFns.getNodeParameter(addOptName);
return Object.assign(paramsWithValuesObj, additionalOptions);
}
return paramsWithValuesObj;
};
const formatParams = (
obj: Record<string, unknown>,
filters?: { [paramName: string]: (value: any) => boolean },
mappers?: { [paramName: string]: (value: any) => any },
) => {
return Object.fromEntries(
Object.entries(obj)
.filter(([name, value]) => !filters || (name in filters ? filters[name](value) : false))
.map(([name, value]) =>
!mappers || !(name in mappers) ? [name, value] : [name, mappers[name](value)],
),
);
};
const objectFromProps = (src: any, props: string[]) => {
const result = props.filter((p) => src.hasOwnProperty(p)).map((p) => [p, src[p]]);
return Object.fromEntries(result);
};
const idFn = (i: any) => i;
const keyValueToObj = (arr: any[]) => {
const obj: any = {};
arr.forEach((item) => {
obj[item.key] = item.value;
});
return obj;
};
const transformSingleProp = (prop: string) => (values: any) =>
(values.itemChoice || []).map((i: any) => i[prop]);
export async function activityPresend(
this: IExecuteSingleFunctions,
opts: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const params = getAllParams(this);
const isCreateWithMember = params.operation === 'createWithMember';
const isCreateForMember = params.operation === 'createForMember';
if (isCreateWithMember) {
// Move following props into "member" subproperty
const memberProps = ['displayName', 'emails', 'joinedAt', 'username'];
params.member = objectFromProps(params, memberProps);
memberProps.forEach((p) => delete params[p]);
}
opts.body = formatParams(
params,
{
member: (v) => (isCreateWithMember || isCreateForMember) && v,
type: idFn,
timestamp: idFn,
platform: idFn,
title: idFn,
body: idFn,
channel: idFn,
sourceId: idFn,
sourceParentId: idFn,
},
{
member: (v) =>
typeof v === 'object'
? formatParams(
v as Record<string, unknown>,
{
username: (un) => un.itemChoice,
displayName: idFn,
emails: idFn,
joinedAt: idFn,
},
{
username: (un) => keyValueToObj(un.itemChoice as any[]),
emails: transformSingleProp('email'),
},
)
: v,
},
);
return opts;
}
export async function automationPresend(
this: IExecuteSingleFunctions,
opts: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const params = getAllParams(this);
opts.body = {
data: {
settings: {
url: params.url,
},
type: 'webhook',
trigger: params.trigger,
},
};
return opts;
}
export async function memberPresend(
this: IExecuteSingleFunctions,
opts: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const params = getAllParams(this);
opts.body = formatParams(
params,
{
platform: idFn,
username: idFn,
displayName: idFn,
emails: (i) => i.itemChoice,
joinedAt: idFn,
organizations: (i) => i.itemChoice,
tags: (i) => i.itemChoice,
tasks: (i) => i.itemChoice,
notes: (i) => i.itemChoice,
activities: (i) => i.itemChoice,
},
{
emails: transformSingleProp('email'),
organizations: (i) =>
i.itemChoice.map((org: any) =>
formatParams(
org as Record<string, unknown>,
{
name: idFn,
url: idFn,
description: idFn,
logo: idFn,
employees: idFn,
members: (j) => j.itemChoice,
},
{
members: transformSingleProp('member'),
},
),
),
tags: transformSingleProp('tag'),
tasks: transformSingleProp('task'),
notes: transformSingleProp('note'),
activities: transformSingleProp('activity'),
},
);
return opts;
}
export async function notePresend(
this: IExecuteSingleFunctions,
opts: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const params = getAllParams(this);
opts.body = {
body: params.body,
};
return opts;
}
export async function organizationPresend(
this: IExecuteSingleFunctions,
opts: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const params = getAllParams(this);
opts.body = formatParams(
params,
{
name: idFn,
url: idFn,
description: idFn,
logo: idFn,
employees: idFn,
members: (j) => j.itemChoice,
},
{
members: transformSingleProp('member'),
},
);
return opts;
}
export async function taskPresend(
this: IExecuteSingleFunctions,
opts: IHttpRequestOptions,
): Promise<IHttpRequestOptions> {
const params = getAllParams(this);
opts.body = formatParams(
params,
{
name: idFn,
body: idFn,
status: idFn,
members: (i) => i.itemChoice,
activities: (i) => i.itemChoice,
assigneess: idFn,
},
{
members: transformSingleProp('member'),
activities: transformSingleProp('activity'),
},
);
return opts;
}

View File

@@ -0,0 +1,15 @@
<svg width="131" height="24" viewBox="0 0 131 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M57.3097 1.04274C57.4165 1.06739 57.4993 1.15158 57.5223 1.25872L58.1022 3.9642C58.1357 4.12016 58.0334 4.27285 57.8764 4.30128L52.2156 5.32696C52.028 5.36095 51.8614 5.20312 51.8851 5.01395L52.4824 0.247384C52.5032 0.0813045 52.6631 -0.0303341 52.8262 0.00732829L57.3097 1.04274Z" fill="#E94F2E"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M88.9753 23.6416C90.9399 23.6416 92.6615 22.9127 93.7118 21.6226C93.8883 21.4059 94.2929 21.4955 94.3218 21.7735L94.4476 22.9845C94.4626 23.1284 94.5838 23.2378 94.7285 23.2378H97.6708C97.8268 23.2378 97.9532 23.1113 97.9532 22.9553V0.407414C97.9532 0.251441 97.8268 0.125 97.6708 0.125H94.4767C94.3208 0.125 94.1943 0.25144 94.1943 0.407413V8.87388C94.1943 9.15681 93.7865 9.28936 93.5874 9.0883C92.5408 8.03107 90.9503 7.4254 89.1928 7.4254C88.3496 7.4254 87.5673 7.53878 86.8516 7.75317V11.9657C87.577 11.236 88.5844 10.8115 89.8141 10.8115C92.4547 10.8115 94.1633 12.7376 94.1633 15.5024C94.1633 18.2673 92.4547 20.1623 89.8141 20.1623C88.5844 20.1623 87.577 19.7445 86.8516 19.0221V23.365C87.5069 23.5465 88.2167 23.6416 88.9753 23.6416ZM107.836 23.6416C111.709 23.6416 114.45 21.7562 115.219 18.6021C115.261 18.4296 115.127 18.2673 114.95 18.2673H111.992C111.867 18.2673 111.759 18.3501 111.715 18.4671C111.218 19.8009 109.883 20.5351 107.899 20.5351C105.481 20.5351 104.067 19.2729 103.712 16.7761C103.688 16.6106 103.819 16.4651 103.986 16.4646L114.948 16.4352C115.104 16.4347 115.23 16.3084 115.23 16.1527V15.285C115.23 10.5009 112.31 7.4254 107.712 7.4254C103.208 7.4254 100.07 10.7494 100.07 15.5646C100.07 20.3176 103.27 23.6416 107.836 23.6416ZM107.743 10.532C109.908 10.532 111.337 11.8061 111.462 13.7596C111.472 13.9153 111.345 14.0424 111.189 14.0424H104.099C103.925 14.0424 103.792 13.8857 103.828 13.7155C104.274 11.6288 105.615 10.532 107.743 10.532ZM124.813 23.064C124.77 23.1692 124.667 23.2378 124.553 23.2378H121.205C121.089 23.2378 120.985 23.1671 120.942 23.0595L115.078 8.27795C115.004 8.09255 115.141 7.89138 115.34 7.89138H118.768C118.885 7.89138 118.99 7.96398 119.032 8.07374L121.634 14.9433C122.045 16.0969 122.406 17.1521 122.664 17.9849C122.754 18.273 123.25 18.2731 123.341 17.9856C123.618 17.1075 124.004 16.0386 124.43 14.9433L127.155 8.06971C127.198 7.96207 127.302 7.89138 127.418 7.89138H130.717C130.918 7.89138 131.055 8.09646 130.977 8.28249L124.813 23.064Z" fill="url(#paint0_linear_470_15680)"/>
<path d="M0 15.5335C0 20.4108 3.04442 23.6416 7.70425 23.6416C11.5916 23.6416 14.5144 21.3323 15.1114 17.9329C15.1408 17.765 15.0088 17.6149 14.8383 17.6149H11.5614C11.4309 17.6149 11.3184 17.705 11.2808 17.83C10.8176 19.3719 9.51665 20.2244 7.70425 20.2244C5.28114 20.2244 3.78999 18.4226 3.78999 15.5335C3.78999 12.6444 5.4054 10.8115 7.82851 10.8115C9.55529 10.8115 10.797 11.639 11.2803 13.2392C11.3177 13.3632 11.4298 13.4521 11.5594 13.4521H14.814C14.9821 13.4521 15.1134 13.3058 15.0884 13.1395C14.5611 9.62446 11.7476 7.4254 7.67318 7.4254C3.13762 7.4254 0 10.7805 0 15.5335Z" fill="#140505"/>
<path d="M26.6792 8.04975C26.6792 7.92016 26.5911 7.80665 26.4646 7.77846C25.9342 7.66022 25.4679 7.61179 25.0017 7.61179C23.4123 7.61179 22.1776 8.22781 21.3989 9.21386C21.2178 9.44318 20.7606 9.35331 20.7323 9.06247L20.6462 8.17753C20.6322 8.03283 20.5105 7.92245 20.3652 7.92245H17.3313C17.1753 7.92245 17.0489 8.04889 17.0489 8.20486V22.9553C17.0489 23.1113 17.1753 23.2378 17.3313 23.2378H20.5565C20.7125 23.2378 20.8389 23.1113 20.8389 22.9553V15.782C20.8389 12.7997 22.5475 11.3397 25.2813 11.3397H26.3968C26.5528 11.3397 26.6792 11.2132 26.6792 11.0572V8.04975Z" fill="#140505"/>
<path d="M26.8959 15.5335C26.8959 20.3176 30.3442 23.6105 35.0972 23.6105C39.8503 23.6105 43.2985 20.3176 43.2985 15.5335C43.2985 10.7494 39.8503 7.45646 35.0972 7.45646C30.3442 7.45646 26.8959 10.7494 26.8959 15.5335ZM30.6859 15.5335C30.6859 12.7376 32.4877 10.8426 35.0972 10.8426C37.7068 10.8426 39.5085 12.7376 39.5085 15.5335C39.5085 18.3294 37.7068 20.2244 35.0972 20.2244C32.4877 20.2244 30.6859 18.3294 30.6859 15.5335Z" fill="#140505"/>
<path d="M73.9835 23.6416C75.9481 23.6416 77.6697 22.9127 78.72 21.6226C78.8964 21.4059 79.3011 21.4955 79.33 21.7735L79.4558 22.9845C79.4707 23.1284 79.592 23.2378 79.7367 23.2378H82.679C82.835 23.2378 82.9614 23.1113 82.9614 22.9553V0.407414C82.9614 0.251441 82.835 0.125 82.679 0.125H79.4849C79.329 0.125 79.2025 0.251441 79.2025 0.407414V8.87388C79.2025 9.15681 78.7947 9.28936 78.5957 9.0883C77.549 8.03107 75.9585 7.4254 74.201 7.4254C69.5722 7.4254 66.7763 10.8426 66.7763 15.6267C66.7763 20.3797 69.5411 23.6416 73.9835 23.6416ZM74.8223 20.1623C72.1817 20.1623 70.5663 18.2362 70.5663 15.5024C70.5663 12.7687 72.1817 10.8115 74.8223 10.8115C77.4629 10.8115 79.1714 12.7376 79.1714 15.5024C79.1714 18.2673 77.4629 20.1623 74.8223 20.1623Z" fill="#140505"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M44.468 11.2986C44.2998 11.3291 44.197 11.5008 44.2497 11.6635L47.9351 23.0424C47.9729 23.1588 48.0814 23.2378 48.2038 23.2378H51.4892C51.612 23.2378 51.7206 23.1585 51.7581 23.0416L53.8077 16.6519C54.202 15.3742 54.4682 14.465 54.655 13.7859C54.7379 13.4849 55.2539 13.4953 55.3278 13.7986C55.5105 14.5481 55.7667 15.4853 56.1065 16.5897L58.1564 23.0409C58.1937 23.1581 58.3025 23.2378 58.4256 23.2378H61.5593C61.6797 23.2378 61.7868 23.1615 61.8262 23.0477L66.9429 8.26618C67.0064 8.08272 66.8702 7.89138 66.676 7.89138H63.2731C63.1494 7.89138 63.0402 7.9718 63.0034 8.08985L61.0149 14.4773C60.8324 15.1057 60.5574 16.1045 60.3192 17.042C60.2432 17.3415 59.7535 17.3377 59.6796 17.0377C59.4283 16.0181 59.1181 14.8855 58.9956 14.4773L57.0071 8.08985C56.9703 7.9718 56.8611 7.89138 56.7374 7.89138H53.2992C53.1764 7.89138 53.0677 7.97066 53.0302 8.08754L50.9807 14.4773C50.6607 15.4573 50.4308 16.2056 50.233 17.0122C50.1589 17.3145 49.6915 17.3143 49.6243 17.0104C49.427 16.1185 49.2164 15.2712 48.9925 14.4773L47.9046 10.929C47.8627 10.7922 47.725 10.7084 47.5843 10.7339L44.468 11.2986Z" fill="#140505"/>
<defs>
<linearGradient id="paint0_linear_470_15680" x1="93.4995" y1="15.0132" x2="84.4995" y2="16.0133" gradientUnits="userSpaceOnUse">
<stop stop-color="#E94F2E"/>
<stop offset="1" stop-color="#CA2400"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

@@ -0,0 +1,186 @@
import type { INodeProperties } from 'n8n-workflow';
import { activityPresend } from '../GenericFunctions';
import { emailsField } from './shared';
import { getAdditionalOptions, mapWith, showFor } from './utils';
const displayOpts = showFor(['activity']);
const displayFor = {
resource: displayOpts(),
createWithMember: displayOpts(['createWithMember']),
createForMember: displayOpts(['createForMember']),
};
const activityOperations: INodeProperties = {
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: displayFor.resource.displayOptions,
noDataExpression: true,
default: 'createWithMember',
options: [
{
name: 'Create or Update with a Member',
value: 'createWithMember',
description: 'Create or update an activity with a member',
action: 'Create or update an activity with a member',
routing: {
send: { preSend: [activityPresend] },
request: {
method: 'POST',
url: '/activity/with-member',
},
},
},
{
name: 'Create',
value: 'createForMember',
description: 'Create an activity for a member',
action: 'Create an activity for a member',
routing: {
send: { preSend: [activityPresend] },
request: {
method: 'POST',
url: '/activity',
},
},
},
],
};
const createWithMemberFields: INodeProperties[] = [
{
displayName: 'Username',
name: 'username',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
required: true,
default: {},
options: [
{
displayName: 'Item Choice',
name: 'itemChoice',
values: [
{
displayName: 'Platform',
description: 'Platform name (e.g twitter, github, etc)',
name: 'key',
type: 'string',
required: true,
default: '',
},
{
displayName: 'Username',
description: 'Username at the specified Platform',
name: 'value',
type: 'string',
required: true,
default: '',
},
],
},
],
},
{
displayName: 'displayName',
name: 'displayName',
description: 'UI friendly name of the member',
type: 'string',
default: '',
},
emailsField,
{
displayName: 'Joined At',
name: 'joinedAt',
description: 'Date of joining the community',
type: 'dateTime',
default: '',
},
];
const memberIdField: INodeProperties = {
displayName: 'Member',
name: 'member',
description: 'The ID of the member that performed the activity',
type: 'string',
required: true,
default: '',
};
const createCommonFields: INodeProperties[] = [
{
displayName: 'Type',
name: 'type',
description: 'Type of activity',
type: 'string',
required: true,
default: '',
},
{
displayName: 'Timestamp',
name: 'timestamp',
description: 'Date and time when the activity took place',
type: 'dateTime',
required: true,
default: '',
},
{
displayName: 'Platform',
name: 'platform',
description: 'Platform on which the activity took place',
type: 'string',
required: true,
default: '',
},
{
displayName: 'Source ID',
name: 'sourceId',
description: 'The ID of the activity in the platform (e.g. the ID of the message in Discord)',
type: 'string',
required: true,
default: '',
},
];
const additionalOptions: INodeProperties[] = [
{
displayName: 'Title',
name: 'title',
description: 'Title of the activity',
type: 'string',
default: '',
},
{
displayName: 'Body',
name: 'body',
description: 'Body of the activity',
type: 'string',
default: '',
},
{
displayName: 'Channel',
name: 'channel',
description: 'Channel of the activity',
type: 'string',
default: '',
},
{
displayName: 'Source Parent ID',
name: 'sourceParentId',
description:
'The ID of the parent activity in the platform (e.g. the ID of the parent message in Discord)',
type: 'string',
default: '',
},
];
const activityFields: INodeProperties[] = [
...createWithMemberFields.map(mapWith(displayFor.createWithMember)),
Object.assign({}, memberIdField, displayFor.createForMember),
...createCommonFields.map(mapWith(displayFor.resource)),
Object.assign({}, getAdditionalOptions(additionalOptions), displayFor.resource),
];
export { activityOperations, activityFields };

View File

@@ -0,0 +1,129 @@
import type { INodeProperties } from 'n8n-workflow';
import { automationPresend } from '../GenericFunctions';
import { mapWith, showFor } from './utils';
const displayOpts = showFor(['automation']);
const displayFor = {
resource: displayOpts(),
createOrUpdate: displayOpts(['create', 'update']),
id: displayOpts(['destroy', 'find', 'update']),
};
const automationOperations: INodeProperties = {
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: displayFor.resource.displayOptions,
noDataExpression: true,
default: 'list',
options: [
{
name: 'Create',
value: 'create',
description: 'Create a new automation for the tenant',
action: 'Create a new automation for the tenant',
routing: {
send: { preSend: [automationPresend] },
request: {
method: 'POST',
url: '/automation',
},
},
},
{
name: 'Destroy',
value: 'destroy',
description: 'Destroy an existing automation for the tenant',
action: 'Destroy an existing automation for the tenant',
routing: {
request: {
method: 'DELETE',
url: '=/automation/{{$parameter["id"]}}',
},
},
},
{
name: 'Find',
value: 'find',
description: 'Get an existing automation data for the tenant',
action: 'Get an existing automation data for the tenant',
routing: {
request: {
method: 'GET',
url: '=/automation/{{$parameter["id"]}}',
},
},
},
{
name: 'List',
value: 'list',
description: 'Get all existing automation data for tenant',
action: 'Get all existing automation data for tenant',
routing: {
request: {
method: 'GET',
url: '/automation',
},
},
},
{
name: 'Update',
value: 'update',
description: 'Updates an existing automation for the tenant',
action: 'Updates an existing automation for the tenant',
routing: {
send: { preSend: [automationPresend] },
request: {
method: 'PUT',
url: '=/automation/{{$parameter["id"]}}',
},
},
},
],
};
const idField: INodeProperties = {
displayName: 'ID',
name: 'id',
description: 'The ID of the automation',
type: 'string',
required: true,
default: '',
};
const commonFields: INodeProperties[] = [
{
displayName: 'Trigger',
name: 'trigger',
description: 'What will trigger an automation',
type: 'options',
required: true,
default: 'new_activity',
options: [
{
name: 'New Activity',
value: 'new_activity',
},
{
name: 'New Member',
value: 'new_member',
},
],
},
{
displayName: 'URL',
name: 'url',
description: 'URL to POST webhook data to',
type: 'string',
required: true,
default: '',
},
];
const automationFields: INodeProperties[] = [
Object.assign({}, idField, displayFor.id),
...commonFields.map(mapWith(displayFor.createOrUpdate)),
];
export { automationOperations, automationFields };

View File

@@ -0,0 +1,24 @@
import { resources } from './resources';
import { activityOperations, activityFields } from './activityFields';
import { memberFields, memberOperations } from './memberFields';
import { noteFields, noteOperations } from './noteFields';
import { organizationFields, organizationOperations } from './organizationFields';
import { taskFields, taskOperations } from './taskFields';
import type { INodeProperties } from 'n8n-workflow';
import { automationFields, automationOperations } from './automationFields';
export const allProperties: INodeProperties[] = [
resources,
activityOperations,
memberOperations,
noteOperations,
organizationOperations,
taskOperations,
automationOperations,
...activityFields,
...memberFields,
...noteFields,
...organizationFields,
...taskFields,
...automationFields,
];

View File

@@ -0,0 +1,275 @@
import type { INodeProperties } from 'n8n-workflow';
import { getAdditionalOptions, getId, mapWith, showFor } from './utils';
import * as shared from './shared';
import { memberPresend } from '../GenericFunctions';
const displayOpts = showFor(['member']);
const displayFor = {
resource: displayOpts(),
createOrUpdate: displayOpts(['createOrUpdate', 'update']),
id: displayOpts(['delete', 'find', 'update']),
};
const memberOperations: INodeProperties = {
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: displayFor.resource.displayOptions,
noDataExpression: true,
default: 'find',
options: [
{
name: 'Create or Update',
value: 'createOrUpdate',
description: 'Create or update a member',
action: 'Create or update a member',
routing: {
send: { preSend: [memberPresend] },
request: {
method: 'POST',
url: '/member',
},
},
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a member',
action: 'Delete a member',
routing: {
request: {
method: 'DELETE',
url: '=/member',
},
},
},
{
name: 'Find',
value: 'find',
description: 'Find a member',
action: 'Find a member',
routing: {
request: {
method: 'GET',
url: '=/member/{{$parameter["id"]}}',
},
},
},
{
name: 'Update',
value: 'update',
description: 'Update a member',
action: 'Update a member',
routing: {
send: { preSend: [memberPresend] },
request: {
method: 'PUT',
url: '=/member/{{$parameter["id"]}}',
},
},
},
],
};
const commonFields: INodeProperties[] = [
{
displayName: 'Platform',
name: 'platform',
description: 'Platform for which to check member existence',
type: 'string',
required: true,
default: '',
},
{
displayName: 'Username',
name: 'username',
description: 'Username of the member in platform',
type: 'string',
required: true,
default: '',
},
];
const additionalOptions: INodeProperties[] = [
{
displayName: 'Display Name',
name: 'displayName',
description: 'UI friendly name of the member',
type: 'string',
default: '',
},
shared.emailsField,
{
displayName: 'Joined At',
name: 'joinedAt',
description: 'Date of joining the community',
type: 'dateTime',
default: '',
},
{
displayName: 'Organizations',
name: 'organizations',
description:
'Organizations associated with the member. Each element in the array is the name of the organization, or an organization object. If the organization does not exist, it will be created.',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
displayName: 'Item Choice',
name: 'itemChoice',
values: [
{
displayName: 'Name',
name: 'name',
description: 'The name of the organization',
type: 'string',
required: true,
default: '',
},
{
displayName: 'Url',
name: 'url',
description: 'The URL of the organization',
type: 'string',
default: '',
},
{
displayName: 'Description',
name: 'description',
description: 'A short description of the organization',
type: 'string',
typeOptions: {
rows: 3,
},
default: '',
},
{
displayName: 'Logo',
name: 'logo',
description: 'A URL for logo of the organization',
type: 'string',
default: '',
},
{
displayName: 'Employees',
name: 'employees',
description: 'The number of employees of the organization',
type: 'number',
default: '',
},
],
},
],
},
{
displayName: 'Tags',
name: 'tags',
description: 'Tags associated with the member. Each element in the array is the ID of the tag.',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
displayName: 'Item Choice',
name: 'itemChoice',
values: [
{
displayName: 'Tag',
name: 'tag',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Tasks',
name: 'tasks',
description:
'Tasks associated with the member. Each element in the array is the ID of the task.',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
displayName: 'Item Choice',
name: 'itemChoice',
values: [
{
displayName: 'Task',
name: 'task',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Notes',
name: 'notes',
description:
'Notes associated with the member. Each element in the array is the ID of the note.',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
displayName: 'Item Choice',
name: 'itemChoice',
values: [
{
displayName: 'Note',
name: 'note',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Activities',
name: 'activities',
description:
'Activities associated with the member. Each element in the array is the ID of the activity.',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
displayName: 'Item Choice',
name: 'itemChoice',
values: [
{
displayName: 'Activity',
name: 'activity',
type: 'string',
default: '',
},
],
},
],
},
];
const memberFields: INodeProperties[] = [
Object.assign(getId(), { description: 'The ID of the member' }, displayFor.id),
...commonFields.map(mapWith(displayFor.createOrUpdate)),
Object.assign({}, getAdditionalOptions(additionalOptions), displayFor.createOrUpdate),
];
export { memberOperations, memberFields };

View File

@@ -0,0 +1,92 @@
import type { INodeProperties } from 'n8n-workflow';
import { notePresend } from '../GenericFunctions';
import { getId, mapWith, showFor } from './utils';
const displayOpts = showFor(['note']);
const displayFor = {
resource: displayOpts(),
createOrUpdate: displayOpts(['create', 'update']),
id: displayOpts(['delete', 'find', 'update']),
};
const noteOperations: INodeProperties = {
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: displayFor.resource.displayOptions,
noDataExpression: true,
default: 'find',
options: [
{
name: 'Create',
value: 'create',
description: 'Create a note',
action: 'Create a note',
routing: {
send: { preSend: [notePresend] },
request: {
method: 'POST',
url: '/note',
},
},
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a note',
action: 'Delete a note',
routing: {
request: {
method: 'DELETE',
url: '=/note',
},
},
},
{
name: 'Find',
value: 'find',
description: 'Find a note',
action: 'Find a note',
routing: {
request: {
method: 'GET',
url: '=/note/{{$parameter["id"]}}',
},
},
},
{
name: 'Update',
value: 'update',
description: 'Update a note',
action: 'Update a note',
routing: {
send: { preSend: [notePresend] },
request: {
method: 'PUT',
url: '=/note/{{$parameter["id"]}}',
},
},
},
],
};
const commonFields: INodeProperties[] = [
{
displayName: 'Body',
name: 'body',
description: 'The body of the note',
type: 'string',
typeOptions: {
rows: 4,
},
default: '',
},
];
const noteFields: INodeProperties[] = [
Object.assign(getId(), { description: 'The ID of the note' }, displayFor.id),
...commonFields.map(mapWith(displayFor.createOrUpdate)),
];
export { noteOperations, noteFields };

View File

@@ -0,0 +1,150 @@
import type { INodeProperties } from 'n8n-workflow';
import { organizationPresend } from '../GenericFunctions';
import { getAdditionalOptions, getId, mapWith, showFor } from './utils';
const displayOpts = showFor(['organization']);
const displayFor = {
resource: displayOpts(),
createOrUpdate: displayOpts(['create', 'update']),
id: displayOpts(['delete', 'find', 'update']),
};
const organizationOperations: INodeProperties = {
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: displayFor.resource.displayOptions,
noDataExpression: true,
default: 'find',
options: [
{
name: 'Create',
value: 'create',
description: 'Create an organization',
action: 'Create an organization',
routing: {
send: { preSend: [organizationPresend] },
request: {
method: 'POST',
url: '/organization',
},
},
},
{
name: 'Delete',
value: 'delete',
description: 'Delete an organization',
action: 'Delete an organization',
routing: {
request: {
method: 'DELETE',
url: '=/organization',
},
},
},
{
name: 'Find',
value: 'find',
description: 'Find an organization',
action: 'Find an organization',
routing: {
request: {
method: 'GET',
url: '=/organization/{{$parameter["id"]}}',
},
},
},
{
name: 'Update',
value: 'update',
description: 'Update an organization',
action: 'Update an organization',
routing: {
send: { preSend: [organizationPresend] },
request: {
method: 'PUT',
url: '=/organization/{{$parameter["id"]}}',
},
},
},
],
};
const commonFields: INodeProperties[] = [
{
displayName: 'Name',
name: 'name',
description: 'The name of the organization',
type: 'string',
required: true,
default: '',
},
];
const additionalOptions: INodeProperties[] = [
{
displayName: 'Url',
name: 'url',
description: 'The URL of the organization',
type: 'string',
default: '',
},
{
displayName: 'Description',
name: 'description',
description: 'A short description of the organization',
type: 'string',
typeOptions: {
rows: 3,
},
default: '',
},
{
displayName: 'Logo',
name: 'logo',
description: 'A URL for logo of the organization',
type: 'string',
default: '',
},
{
displayName: 'Employees',
name: 'employees',
description: 'The number of employees of the organization',
type: 'number',
default: '',
},
{
displayName: 'Members',
name: 'members',
description:
'Members associated with the organization. Each element in the array is the ID of the member.',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
displayName: 'Item Choice',
name: 'itemChoice',
values: [
{
displayName: 'Member',
name: 'member',
type: 'string',
default: '',
},
],
},
],
},
];
const organizationFields: INodeProperties[] = [
Object.assign(getId(), { description: 'The ID of the organization' }, displayFor.id),
...commonFields.map(mapWith(displayFor.createOrUpdate)),
Object.assign({}, getAdditionalOptions(additionalOptions), displayFor.createOrUpdate),
];
export { organizationOperations, organizationFields };

View File

@@ -0,0 +1,36 @@
import type { INodeProperties } from 'n8n-workflow';
export const resources: INodeProperties = {
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
default: 'activity',
placeholder: 'Resourcee',
options: [
{
name: 'Activity',
value: 'activity',
},
{
name: 'Automation',
value: 'automation',
},
{
name: 'Member',
value: 'member',
},
{
name: 'Note',
value: 'note',
},
{
name: 'Organization',
value: 'organization',
},
{
name: 'Task',
value: 'task',
},
],
};

View File

@@ -0,0 +1,27 @@
import type { INodeProperties } from 'n8n-workflow';
export const emailsField: INodeProperties = {
displayName: 'Emails',
name: 'emails',
description: 'Email addresses of the member',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
displayName: 'Item Choice',
name: 'itemChoice',
values: [
{
displayName: 'Email',
name: 'email',
type: 'string',
placeholder: 'name@email.com',
default: '',
},
],
},
],
};

View File

@@ -0,0 +1,163 @@
import type { INodeProperties } from 'n8n-workflow';
import { taskPresend } from '../GenericFunctions';
import { getAdditionalOptions, getId, showFor } from './utils';
const displayOpts = showFor(['task']);
const displayFor = {
resource: displayOpts(),
createOrUpdate: displayOpts(['create', 'update']),
id: displayOpts(['delete', 'find', 'update']),
};
const taskOperations: INodeProperties = {
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: displayFor.resource.displayOptions,
noDataExpression: true,
default: 'find',
options: [
{
name: 'Create',
value: 'create',
description: 'Create a task',
action: 'Create a task',
routing: {
send: { preSend: [taskPresend] },
request: {
method: 'POST',
url: '/task',
},
},
},
{
name: 'Delete',
value: 'delete',
description: 'Delete a task',
action: 'Delete a task',
routing: {
request: {
method: 'DELETE',
url: '=/task',
},
},
},
{
name: 'Find',
value: 'find',
description: 'Find a task',
action: 'Find a task',
routing: {
request: {
method: 'GET',
url: '=/task/{{$parameter["id"]}}',
},
},
},
{
name: 'Update',
value: 'update',
description: 'Update a task',
action: 'Update a task',
routing: {
send: { preSend: [taskPresend] },
request: {
method: 'PUT',
url: '=/task/{{$parameter["id"]}}',
},
},
},
],
};
const additionalOptions: INodeProperties[] = [
{
displayName: 'Name',
name: 'name',
description: 'The name of the task',
type: 'string',
default: '',
},
{
displayName: 'Body',
name: 'body',
description: 'The body of the task',
type: 'string',
typeOptions: {
rows: 4,
},
default: '',
},
{
displayName: 'Status',
name: 'status',
description: 'The status of the task',
type: 'string',
default: '',
},
{
displayName: 'Members',
name: 'members',
description:
'Members associated with the task. Each element in the array is the ID of the member.',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
displayName: 'Item Choice',
name: 'itemChoice',
values: [
{
displayName: 'Member',
name: 'member',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Activities',
name: 'activities',
description:
'Activities associated with the task. Each element in the array is the ID of the activity.',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
default: {},
options: [
{
displayName: 'Item Choice',
name: 'itemChoice',
values: [
{
displayName: 'Activity',
name: 'activity',
type: 'string',
default: '',
},
],
},
],
},
{
displayName: 'Assigneess',
name: 'assigneess',
description: 'Users assigned with the task. Each element in the array is the ID of the user.',
type: 'string',
default: '',
},
];
const taskFields: INodeProperties[] = [
Object.assign(getId(), { description: 'The ID of the task' }, displayFor.id),
Object.assign({}, getAdditionalOptions(additionalOptions), displayFor.createOrUpdate),
];
export { taskOperations, taskFields };

View File

@@ -0,0 +1,57 @@
import type { INodeProperties } from 'n8n-workflow';
export const showFor =
(resources: string[]) =>
(operations?: string[]): Partial<INodeProperties> => {
return operations !== undefined
? {
displayOptions: {
show: {
resource: resources,
operation: operations,
},
},
}
: {
displayOptions: {
show: {
resource: resources,
},
},
};
};
export const mapWith =
<T>(...objects: Array<Partial<T>>) =>
(item: Partial<T>) =>
Object.assign({}, item, ...objects);
export const getId = (): INodeProperties => ({
displayName: 'ID',
name: 'id',
type: 'string',
required: true,
default: '',
routing: {
send: {
type: 'query',
property: 'ids[]',
},
},
});
export const getAdditionalOptions = (fields: INodeProperties[]): INodeProperties => {
return {
displayName: 'Additional Options',
name: 'additionalOptions',
type: 'collection',
displayOptions: {
show: {
operation: ['getAll'],
},
},
default: {},
placeholder: 'Add Option',
options: fields,
};
};

File diff suppressed because it is too large Load Diff

View File

@@ -9,10 +9,10 @@ import type {
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import { extractId, googleApiRequest, googleApiRequestAllItems } from './GenericFunctions';
import { extractId, googleApiRequest, googleApiRequestAllItems } from './v1/GenericFunctions';
import moment from 'moment';
import { fileSearch, folderSearch } from './SearchFunctions';
import { fileSearch, folderSearch } from './v1/SearchFunctions';
export class GoogleDriveTrigger implements INodeType {
description: INodeTypeDescription = {

View File

@@ -0,0 +1,78 @@
import nock from 'nock';
import * as create from '../../../../v2/actions/drive/create.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
// eslint-disable-next-line @typescript-eslint/no-unused-vars, unused-imports/no-unused-imports
import * as uuid from 'uuid';
jest.mock('uuid', () => {
const originalModule = jest.requireActual('uuid');
return {
...originalModule,
v4: jest.fn(function () {
return '430c0ca1-2498-472c-9d43-da0163839823';
}),
};
});
describe('test GoogleDriveV2: drive create', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
resource: 'drive',
name: 'newDrive',
options: {
capabilities: {
canComment: true,
canRename: true,
canTrashChildren: true,
},
colorRgb: '#451AD3',
hidden: false,
restrictions: {
driveMembersOnly: true,
},
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await create.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'POST',
'/drive/v3/drives',
{
capabilities: { canComment: true, canRename: true, canTrashChildren: true },
colorRgb: '#451AD3',
hidden: false,
name: 'newDrive',
restrictions: { driveMembersOnly: true },
},
{ requestId: '430c0ca1-2498-472c-9d43-da0163839823' },
);
});
});

View File

@@ -0,0 +1,50 @@
import nock from 'nock';
import * as deleteDrive from '../../../../v2/actions/drive/deleteDrive.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: drive deleteDrive', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
resource: 'drive',
operation: 'deleteDrive',
driveId: {
__rl: true,
value: 'driveIDxxxxxx',
mode: 'id',
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await deleteDrive.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'DELETE',
'/drive/v3/drives/driveIDxxxxxx',
);
});
});

View File

@@ -0,0 +1,55 @@
import nock from 'nock';
import * as get from '../../../../v2/actions/drive/get.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: drive get', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
resource: 'drive',
operation: 'get',
driveId: {
__rl: true,
value: 'driveIDxxxxxx',
mode: 'id',
},
options: {
useDomainAdminAccess: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await get.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'GET',
'/drive/v3/drives/driveIDxxxxxx',
{},
{ useDomainAdminAccess: true },
);
});
});

View File

@@ -0,0 +1,78 @@
import nock from 'nock';
import * as list from '../../../../v2/actions/drive/list.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function (method: string) {
if (method === 'GET') {
return {};
}
}),
googleApiRequestAllItems: jest.fn(async function (method: string) {
if (method === 'GET') {
return {};
}
}),
};
});
describe('test GoogleDriveV2: drive list', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with limit', async () => {
const nodeParameters = {
resource: 'drive',
operation: 'list',
limit: 20,
options: {},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await list.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'GET',
'/drive/v3/drives',
{},
{ pageSize: 20 },
);
});
it('shuold be called with returnAll true', async () => {
const nodeParameters = {
resource: 'drive',
operation: 'list',
returnAll: true,
options: {},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await list.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequestAllItems).toBeCalledTimes(1);
expect(transport.googleApiRequestAllItems).toHaveBeenCalledWith(
'GET',
'drives',
'/drive/v3/drives',
{},
{},
);
});
});

View File

@@ -0,0 +1,58 @@
import nock from 'nock';
import * as update from '../../../../v2/actions/drive/update.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: drive update', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
resource: 'drive',
operation: 'update',
driveId: {
__rl: true,
value: 'sharedDriveIDxxxxx',
mode: 'id',
},
options: {
colorRgb: '#F4BEBE',
name: 'newName',
restrictions: {
driveMembersOnly: true,
},
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await update.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'PATCH',
'/drive/v3/drives/sharedDriveIDxxxxx',
{ colorRgb: '#F4BEBE', name: 'newName', restrictions: { driveMembersOnly: true } },
);
});
});

View File

@@ -0,0 +1,76 @@
import nock from 'nock';
import * as copy from '../../../../v2/actions/file/copy.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: file copy', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
operation: 'copy',
fileId: {
__rl: true,
value: 'fileIDxxxxxx',
mode: 'list',
cachedResultName: 'test01.png',
cachedResultUrl: 'https://drive.google.com/file/d/fileIDxxxxxx/view?usp=drivesdk',
},
name: 'copyImage.png',
sameFolder: false,
folderId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder 3',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
options: {
copyRequiresWriterPermission: true,
description: 'image copy',
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await copy.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toBeCalledWith(
'POST',
'/drive/v3/files/fileIDxxxxxx/copy',
{
copyRequiresWriterPermission: true,
description: 'image copy',
name: 'copyImage.png',
parents: ['folderIDxxxxxx'],
},
{
supportsAllDrives: true,
corpora: 'allDrives',
includeItemsFromAllDrives: true,
spaces: 'appDataFolder, drive',
},
);
});
});

View File

@@ -0,0 +1,91 @@
import nock from 'nock';
import * as createFromText from '../../../../v2/actions/file/createFromText.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: file createFromText', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
operation: 'createFromText',
content: 'hello drive!',
name: 'helloDrive.txt',
folderId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder 3',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
options: {
appPropertiesUi: {
appPropertyValues: [
{
key: 'appKey1',
value: 'appValue1',
},
],
},
propertiesUi: {
propertyValues: [
{
key: 'prop1',
value: 'value1',
},
{
key: 'prop2',
value: 'value2',
},
],
},
keepRevisionForever: true,
ocrLanguage: 'en',
useContentAsIndexableText: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await createFromText.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'POST',
'/upload/drive/v3/files',
'\n\t\t\n--XXXXXX\t\t\nContent-Type: application/json; charset=UTF-8\t\t\n\n{"name":"helloDrive.txt","parents":["folderIDxxxxxx"],"mimeType":"text/plain","properties":{"prop1":"value1","prop2":"value2"},"appProperties":{"appKey1":"appValue1"}}\t\t\n--XXXXXX\t\t\nContent-Type: text/plain\t\t\nContent-Transfer-Encoding: base64\t\t\n\nhello drive!\t\t\n--XXXXXX--',
{
corpora: 'allDrives',
includeItemsFromAllDrives: true,
keepRevisionForever: true,
ocrLanguage: 'en',
spaces: 'appDataFolder, drive',
supportsAllDrives: true,
uploadType: 'multipart',
useContentAsIndexableText: true,
},
undefined,
{ headers: { 'Content-Length': 12, 'Content-Type': 'multipart/related; boundary=XXXXXX' } },
);
});
});

View File

@@ -0,0 +1,56 @@
import nock from 'nock';
import * as deleteFile from '../../../../v2/actions/file/deleteFile.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: file deleteFile', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
operation: 'deleteFile',
fileId: {
__rl: true,
value: 'fileIDxxxxxx',
mode: 'list',
cachedResultName: 'test.txt',
cachedResultUrl: 'https://drive.google.com/file/d/fileIDxxxxxx/view?usp=drivesdk',
},
options: {
deletePermanently: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await deleteFile.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'DELETE',
'/drive/v3/files/fileIDxxxxxx',
undefined,
{ supportsAllDrives: true },
);
});
});

View File

@@ -0,0 +1,64 @@
import nock from 'nock';
import * as download from '../../../../v2/actions/file/download.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: file download', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
operation: 'deleteFile',
fileId: {
__rl: true,
value: 'fileIDxxxxxx',
mode: 'list',
cachedResultName: 'test.txt',
cachedResultUrl: 'https://drive.google.com/file/d/fileIDxxxxxx/view?usp=drivesdk',
},
options: {
deletePermanently: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await download.execute.call(fakeExecuteFunction, 0, { json: {} });
expect(transport.googleApiRequest).toBeCalledTimes(2);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'GET',
'/drive/v3/files/fileIDxxxxxx',
{},
{ fields: 'mimeType,name', supportsTeamDrives: true },
);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'GET',
'/drive/v3/files/fileIDxxxxxx',
{},
{ alt: 'media' },
undefined,
{ encoding: null, json: false, resolveWithFullResponse: true, useStream: true },
);
});
});

View File

@@ -0,0 +1,84 @@
import nock from 'nock';
import * as move from '../../../../v2/actions/file/move.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function (method: string) {
if (method === 'GET') {
return {
parents: ['parentFolderIDxxxxxx'],
};
}
return {};
}),
};
});
describe('test GoogleDriveV2: file move', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
operation: 'move',
fileId: {
__rl: true,
value: 'fileIDxxxxxx',
mode: 'list',
cachedResultName: 'test.txt',
cachedResultUrl: 'https://drive.google.com/file/d/fileIDxxxxxx/view?usp=drivesdk',
},
folderId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder1',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await move.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(2);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'GET',
'/drive/v3/files/fileIDxxxxxx',
undefined,
{
corpora: 'allDrives',
fields: 'parents',
includeItemsFromAllDrives: true,
spaces: 'appDataFolder, drive',
supportsAllDrives: true,
},
);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'PATCH',
'/drive/v3/files/fileIDxxxxxx',
undefined,
{
addParents: 'folderIDxxxxxx',
removeParents: 'parentFolderIDxxxxxx',
corpora: 'allDrives',
includeItemsFromAllDrives: true,
spaces: 'appDataFolder, drive',
supportsAllDrives: true,
},
);
});
});

View File

@@ -0,0 +1,74 @@
import nock from 'nock';
import * as share from '../../../../v2/actions/file/share.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: file share', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
operation: 'share',
fileId: {
__rl: true,
value: 'fileIDxxxxxx',
mode: 'list',
cachedResultName: 'test.txt',
cachedResultUrl: 'https://drive.google.com/file/d/fileIDxxxxxx/view?usp=drivesdk',
},
permissionsUi: {
permissionsValues: {
role: 'owner',
type: 'user',
emailAddress: 'user@gmail.com',
},
},
options: {
emailMessage: 'some message',
moveToNewOwnersRoot: true,
sendNotificationEmail: true,
transferOwnership: true,
useDomainAdminAccess: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await share.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'POST',
'/drive/v3/files/fileIDxxxxxx/permissions',
{ emailAddress: 'user@gmail.com', role: 'owner', type: 'user' },
{
emailMessage: 'some message',
moveToNewOwnersRoot: true,
sendNotificationEmail: true,
supportsAllDrives: true,
transferOwnership: true,
useDomainAdminAccess: true,
},
);
});
});

View File

@@ -0,0 +1,66 @@
import nock from 'nock';
import * as update from '../../../../v2/actions/file/update.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: file update', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
operation: 'update',
fileId: {
__rl: true,
value: 'fileIDxxxxxx',
mode: 'list',
cachedResultName: 'test.txt',
cachedResultUrl: 'https://drive.google.com/file/d/fileIDxxxxxx/view?usp=drivesdk',
},
newUpdatedFileName: 'test2.txt',
options: {
keepRevisionForever: true,
ocrLanguage: 'en',
useContentAsIndexableText: true,
fields: ['hasThumbnail', 'starred'],
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await update.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'PATCH',
'/drive/v3/files/fileIDxxxxxx',
{ name: 'test2.txt' },
{
fields: 'hasThumbnail, starred',
keepRevisionForever: true,
ocrLanguage: 'en',
supportsAllDrives: true,
useContentAsIndexableText: true,
},
);
});
});

View File

@@ -0,0 +1,96 @@
import nock from 'nock';
import * as upload from '../../../../v2/actions/file/upload.operation';
import * as transport from '../../../../v2/transport';
import * as utils from '../../../../v2/helpers/utils';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function (method: string) {
if (method === 'POST') {
return {
headers: { location: 'someLocation' },
};
}
return {};
}),
};
});
jest.mock('../../../../v2/helpers/utils', () => {
const originalModule = jest.requireActual('../../../../v2/helpers/utils');
return {
...originalModule,
getItemBinaryData: jest.fn(async function () {
return {
contentLength: '123',
fileContent: 'Hello Drive!',
originalFilename: 'original.txt',
mimeType: 'text/plain',
};
}),
};
});
describe('test GoogleDriveV2: file upload', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
jest.unmock('../../../../v2/helpers/utils');
});
it('shuold be called with', async () => {
const nodeParameters = {
name: 'newFile.txt',
folderId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder 3',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
options: {
simplifyOutput: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await upload.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(2);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'POST',
'/upload/drive/v3/files',
undefined,
{ uploadType: 'resumable' },
undefined,
{ resolveWithFullResponse: true },
);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'PATCH',
'/drive/v3/files/undefined',
{ mimeType: 'text/plain', name: 'newFile.txt', originalFilename: 'original.txt' },
{
addParents: 'folderIDxxxxxx',
supportsAllDrives: true,
corpora: 'allDrives',
includeItemsFromAllDrives: true,
spaces: 'appDataFolder, drive',
},
);
expect(utils.getItemBinaryData).toBeCalledTimes(1);
expect(utils.getItemBinaryData).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,119 @@
import nock from 'nock';
import * as search from '../../../../v2/actions/fileFolder/search.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function (method: string) {
if (method === 'GET') {
return {};
}
}),
googleApiRequestAllItems: jest.fn(async function (method: string) {
if (method === 'GET') {
return {};
}
}),
};
});
describe('test GoogleDriveV2: fileFolder search', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('returnAll = false', async () => {
const nodeParameters = {
searchMethod: 'name',
resource: 'fileFolder',
queryString: 'test',
returnAll: false,
limit: 2,
filter: {
whatToSearch: 'files',
fileTypes: ['application/vnd.google-apps.document'],
},
options: {
fields: ['id', 'name', 'starred', 'version'],
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await search.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toBeCalledWith('GET', '/drive/v3/files', undefined, {
corpora: 'allDrives',
fields: 'nextPageToken, files(id, name, starred, version)',
includeItemsFromAllDrives: true,
pageSize: 2,
q: "name contains 'test' and mimeType != 'application/vnd.google-apps.folder' and trashed = false and (mimeType = 'application/vnd.google-apps.document')",
spaces: 'appDataFolder, drive',
supportsAllDrives: true,
});
});
it('returnAll = true', async () => {
const nodeParameters = {
resource: 'fileFolder',
searchMethod: 'query',
queryString: 'test',
returnAll: true,
filter: {
driveId: {
__rl: true,
value: 'driveID000000123',
mode: 'list',
cachedResultName: 'sharedDrive',
cachedResultUrl: 'https://drive.google.com/drive/folders/driveID000000123',
},
folderId: {
__rl: true,
value: 'folderID000000123',
mode: 'list',
cachedResultName: 'testFolder 3',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderID000000123',
},
whatToSearch: 'all',
fileTypes: ['*'],
includeTrashed: true,
},
options: {
fields: ['permissions', 'mimeType'],
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await search.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequestAllItems).toBeCalledTimes(1);
expect(transport.googleApiRequestAllItems).toBeCalledWith(
'GET',
'files',
'/drive/v3/files',
{},
{
corpora: 'drive',
driveId: 'driveID000000123',
fields: 'nextPageToken, files(permissions, mimeType)',
includeItemsFromAllDrives: true,
q: "test and 'folderID000000123' in parents",
spaces: 'appDataFolder, drive',
supportsAllDrives: true,
},
);
});
});

View File

@@ -0,0 +1,68 @@
import nock from 'nock';
import * as create from '../../../../v2/actions/folder/create.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: folder create', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
resource: 'folder',
name: 'testFolder 2',
folderId: {
__rl: true,
value: 'root',
mode: 'list',
cachedResultName: 'root',
cachedResultUrl: 'https://drive.google.com/drive',
},
options: {
folderColorRgb: '#167D08',
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await create.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'POST',
'/drive/v3/files',
{
folderColorRgb: '#167D08',
mimeType: 'application/vnd.google-apps.folder',
name: 'testFolder 2',
parents: ['root'],
},
{
fields: undefined,
includeItemsFromAllDrives: true,
supportsAllDrives: true,
spaces: 'appDataFolder, drive',
corpora: 'allDrives',
},
);
});
});

View File

@@ -0,0 +1,80 @@
import nock from 'nock';
import * as deleteFolder from '../../../../v2/actions/folder/deleteFolder.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: folder deleteFolder', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with PATCH', async () => {
const nodeParameters = {
resource: 'folder',
operation: 'deleteFolder',
folderNoRootId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder 2',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
options: {},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await deleteFolder.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'PATCH',
'/drive/v3/files/folderIDxxxxxx',
{ trashed: true },
{ supportsAllDrives: true },
);
});
it('shuold be called with DELETE', async () => {
const nodeParameters = {
resource: 'folder',
operation: 'deleteFolder',
folderNoRootId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder 2',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
options: { deletePermanently: true },
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await deleteFolder.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'DELETE',
'/drive/v3/files/folderIDxxxxxx',
undefined,
{ supportsAllDrives: true },
);
});
});

View File

@@ -0,0 +1,64 @@
import nock from 'nock';
import * as share from '../../../../v2/actions/folder/share.operation';
import * as transport from '../../../../v2/transport';
import { createMockExecuteFunction, driveNode } from '../helpers';
jest.mock('../../../../v2/transport', () => {
const originalModule = jest.requireActual('../../../../v2/transport');
return {
...originalModule,
googleApiRequest: jest.fn(async function () {
return {};
}),
};
});
describe('test GoogleDriveV2: folder share', () => {
beforeAll(() => {
nock.disableNetConnect();
});
afterAll(() => {
nock.restore();
jest.unmock('../../../../v2/transport');
});
it('shuold be called with', async () => {
const nodeParameters = {
resource: 'folder',
operation: 'share',
folderNoRootId: {
__rl: true,
value: 'folderIDxxxxxx',
mode: 'list',
cachedResultName: 'testFolder 2',
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
},
permissionsUi: {
permissionsValues: {
role: 'reader',
type: 'anyone',
allowFileDiscovery: true,
},
},
options: {
moveToNewOwnersRoot: true,
},
};
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
await share.execute.call(fakeExecuteFunction, 0);
expect(transport.googleApiRequest).toBeCalledTimes(1);
expect(transport.googleApiRequest).toHaveBeenCalledWith(
'POST',
'/drive/v3/files/folderIDxxxxxx/permissions',
{ allowFileDiscovery: true, role: 'reader', type: 'anyone' },
{ moveToNewOwnersRoot: true, supportsAllDrives: true },
);
});
});

View File

@@ -0,0 +1,42 @@
import type { IDataObject, IExecuteFunctions, IGetNodeParameterOptions, INode } from 'n8n-workflow';
import { get } from 'lodash';
import { constructExecutionMetaData, returnJsonArray } from 'n8n-core';
export const driveNode: INode = {
id: '11',
name: 'Google Drive node',
typeVersion: 3,
type: 'n8n-nodes-base.googleDrive',
position: [42, 42],
parameters: {},
};
export const createMockExecuteFunction = (
nodeParameters: IDataObject,
node: INode,
continueOnFail = false,
) => {
const fakeExecuteFunction = {
getNodeParameter(
parameterName: string,
_itemIndex: number,
fallbackValue?: IDataObject | undefined,
options?: IGetNodeParameterOptions | undefined,
) {
const parameter = options?.extractValue ? `${parameterName}.value` : parameterName;
return get(nodeParameters, parameter, fallbackValue);
},
getNode() {
return node;
},
helpers: {
constructExecutionMetaData,
returnJsonArray,
prepareBinaryData: () => {},
httpRequest: () => {},
},
continueOnFail: () => continueOnFail,
} as unknown as IExecuteFunctions;
return fakeExecuteFunction;
};

View File

@@ -0,0 +1,125 @@
import {
prepareQueryString,
setFileProperties,
setUpdateCommonParams,
} from '../../v2/helpers/utils';
describe('test GoogleDriveV2, prepareQueryString', () => {
it('should return id, name', () => {
const fields = undefined;
const result = prepareQueryString(fields);
expect(result).toEqual('id, name');
});
it('should return *', () => {
const fields = ['*'];
const result = prepareQueryString(fields);
expect(result).toEqual('*');
});
it('should return string joined by ,', () => {
const fields = ['id', 'name', 'mimeType'];
const result = prepareQueryString(fields);
expect(result).toEqual('id, name, mimeType');
});
});
describe('test GoogleDriveV2, setFileProperties', () => {
it('should return empty object', () => {
const body = {};
const options = {};
const result = setFileProperties(body, options);
expect(result).toEqual({});
});
it('should return object with properties', () => {
const body = {};
const options = {
propertiesUi: {
propertyValues: [
{
key: 'propertyKey1',
value: 'propertyValue1',
},
{
key: 'propertyKey2',
value: 'propertyValue2',
},
],
},
};
const result = setFileProperties(body, options);
expect(result).toEqual({
properties: {
propertyKey1: 'propertyValue1',
propertyKey2: 'propertyValue2',
},
});
});
it('should return object with appProperties', () => {
const body = {};
const options = {
appPropertiesUi: {
appPropertyValues: [
{
key: 'appPropertyKey1',
value: 'appPropertyValue1',
},
{
key: 'appPropertyKey2',
value: 'appPropertyValue2',
},
],
},
};
const result = setFileProperties(body, options);
expect(result).toEqual({
appProperties: {
appPropertyKey1: 'appPropertyValue1',
appPropertyKey2: 'appPropertyValue2',
},
});
});
});
describe('test GoogleDriveV2, setUpdateCommonParams', () => {
it('should return empty object', () => {
const qs = {};
const options = {};
const result = setUpdateCommonParams(qs, options);
expect(result).toEqual({});
});
it('should return qs with params', () => {
const options = {
useContentAsIndexableText: true,
keepRevisionForever: true,
ocrLanguage: 'en',
trashed: true,
includePermissionsForView: 'published',
};
const qs = setUpdateCommonParams({}, options);
expect(qs).toEqual({
useContentAsIndexableText: true,
keepRevisionForever: true,
ocrLanguage: 'en',
});
});
});

View File

@@ -10,7 +10,7 @@ import type {
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import { getGoogleAccessToken } from '../GenericFunctions';
import { getGoogleAccessToken } from '../../GenericFunctions';
export async function googleApiRequest(
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions,

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,28 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import type {
IExecuteFunctions,
INodeType,
INodeTypeBaseDescription,
INodeTypeDescription,
} from 'n8n-workflow';
import { versionDescription } from './actions/versionDescription';
import { listSearch } from './methods';
import { router } from './actions/router';
export class GoogleDriveV2 implements INodeType {
description: INodeTypeDescription;
constructor(baseDescription: INodeTypeBaseDescription) {
this.description = {
...baseDescription,
...versionDescription,
};
}
methods = { listSearch };
async execute(this: IExecuteFunctions) {
return router.call(this);
}
}

View File

@@ -0,0 +1,623 @@
import type { INodeProperties } from 'n8n-workflow';
import { DRIVE, RLC_DRIVE_DEFAULT } from '../helpers/interfaces';
export const fileRLC: INodeProperties = {
displayName: 'File',
name: 'fileId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'File',
name: 'list',
type: 'list',
placeholder: 'Select a file...',
typeOptions: {
searchListMethod: 'fileSearch',
searchable: true,
},
},
{
displayName: 'Link',
name: 'url',
type: 'string',
placeholder:
'e.g. https://drive.google.com/file/d/1anGBg0b5re2VtF2bKu201_a-Vnz5BHq9Y4r-yBDAj5A/edit',
extractValue: {
type: 'regex',
regex:
'https:\\/\\/(?:drive|docs)\\.google\\.com\\/\\w+\\/d\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
},
validation: [
{
type: 'regex',
properties: {
regex:
'https:\\/\\/(?:drive|docs)\\.google.com\\/\\w+\\/d\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
errorMessage: 'Not a valid Google Drive File URL',
},
},
],
},
{
displayName: 'ID',
name: 'id',
type: 'string',
placeholder: 'e.g. 1anGBg0b5re2VtF2bKu201_a-Vnz5BHq9Y4r-yBDAj5A',
validation: [
{
type: 'regex',
properties: {
regex: '[a-zA-Z0-9\\-_]{2,}',
errorMessage: 'Not a valid Google Drive File ID',
},
},
],
url: '=https://drive.google.com/file/d/{{$value}}/view',
},
],
description: 'The file to operate on',
};
export const folderNoRootRLC: INodeProperties = {
displayName: 'Folder',
name: 'folderNoRootId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'Folder',
name: 'list',
type: 'list',
placeholder: 'Select a folder...',
typeOptions: {
searchListMethod: 'folderSearch',
searchable: true,
},
},
{
displayName: 'Link',
name: 'url',
type: 'string',
placeholder: 'e.g. https://drive.google.com/drive/folders/1Tx9WHbA3wBpPB4C_HcoZDH9WZFWYxAMU',
extractValue: {
type: 'regex',
regex:
'https:\\/\\/drive\\.google\\.com(?:\\/.*|)\\/folders\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
},
validation: [
{
type: 'regex',
properties: {
regex:
'https:\\/\\/drive\\.google\\.com(?:\\/.*|)\\/folders\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
errorMessage: 'Not a valid Google Drive Folder URL',
},
},
],
},
{
displayName: 'ID',
name: 'id',
type: 'string',
placeholder: 'e.g. 1anGBg0b5re2VtF2bKu201_a-Vnz5BHq9Y4r-yBDAj5A',
validation: [
{
type: 'regex',
properties: {
regex: '[a-zA-Z0-9\\-_]{2,}',
errorMessage: 'Not a valid Google Drive Folder ID',
},
},
],
url: '=https://drive.google.com/drive/folders/{{$value}}',
},
],
description: 'The folder to operate on',
};
export const folderRLC: INodeProperties = {
displayName: 'Folder',
name: 'folderId',
type: 'resourceLocator',
default: { mode: 'list', value: 'root', cachedResultName: '/ (Root folder)' },
required: true,
modes: [
{
displayName: 'Folder',
name: 'list',
type: 'list',
placeholder: 'Select a folder...',
typeOptions: {
searchListMethod: 'folderSearchWithDefault',
searchable: true,
},
},
{
displayName: 'Link',
name: 'url',
type: 'string',
placeholder: 'e.g. https://drive.google.com/drive/folders/1Tx9WHbA3wBpPB4C_HcoZDH9WZFWYxAMU',
extractValue: {
type: 'regex',
regex:
'https:\\/\\/drive\\.google\\.com(?:\\/.*|)\\/folders\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
},
validation: [
{
type: 'regex',
properties: {
regex:
'https:\\/\\/drive\\.google\\.com(?:\\/.*|)\\/folders\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
errorMessage: 'Not a valid Google Drive Folder URL',
},
},
],
},
{
displayName: 'ID',
name: 'id',
type: 'string',
placeholder: 'e.g. 1anGBg0b5re2VtF2bKu201_a-Vnz5BHq9Y4r-yBDAj5A',
validation: [
{
type: 'regex',
properties: {
regex: '[a-zA-Z0-9\\-_]{2,}',
errorMessage: 'Not a valid Google Drive Folder ID',
},
},
],
url: '=https://drive.google.com/drive/folders/{{$value}}',
},
],
description: 'The folder to operate on',
};
export const driveRLC: INodeProperties = {
displayName: 'Drive',
name: 'driveId',
type: 'resourceLocator',
default: { mode: 'list', value: RLC_DRIVE_DEFAULT },
required: true,
modes: [
{
displayName: 'Drive',
name: 'list',
type: 'list',
placeholder: 'Select a drive...',
typeOptions: {
searchListMethod: 'driveSearchWithDefault',
searchable: true,
},
},
{
displayName: 'Link',
name: 'url',
type: 'string',
placeholder: 'https://drive.google.com/drive/folders/0AaaaaAAAAAAAaa',
extractValue: {
type: 'regex',
regex:
'https:\\/\\/drive\\.google\\.com(?:\\/.*|)\\/folders\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
},
validation: [
{
type: 'regex',
properties: {
regex:
'https:\\/\\/drive\\.google\\.com(?:\\/.*|)\\/folders\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
errorMessage: 'Not a valid Google Drive Drive URL',
},
},
],
},
{
displayName: 'ID',
name: 'id',
type: 'string',
hint: 'The ID of the shared drive',
validation: [
{
type: 'regex',
properties: {
regex: '[a-zA-Z0-9\\-_]{2,}',
errorMessage: 'Not a valid Google Drive Drive ID',
},
},
],
url: '=https://drive.google.com/drive/folders/{{$value}}',
},
],
description: 'The ID of the drive',
};
export const sharedDriveRLC: INodeProperties = {
displayName: 'Shared Drive',
name: 'driveId',
type: 'resourceLocator',
default: { mode: 'list', value: '' },
required: true,
modes: [
{
displayName: 'Drive',
name: 'list',
type: 'list',
placeholder: 'Select a shared drive...',
typeOptions: {
searchListMethod: 'driveSearch',
searchable: true,
},
},
{
displayName: 'Link',
name: 'url',
type: 'string',
placeholder: 'e.g. https://drive.google.com/drive/u/1/folders/0AIjtcbwnjtcbwn9PVA',
extractValue: {
type: 'regex',
regex:
'https:\\/\\/drive\\.google\\.com(?:\\/.*|)\\/folders\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
},
validation: [
{
type: 'regex',
properties: {
regex:
'https:\\/\\/drive\\.google\\.com(?:\\/.*|)\\/folders\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)',
errorMessage: 'Not a valid Google Drive Drive URL',
},
},
],
},
{
displayName: 'ID',
name: 'id',
type: 'string',
// hint: 'The ID of the shared drive',
placeholder: 'e.g. 0AMXTKI5ZSiM7Uk9PVA',
validation: [
{
type: 'regex',
properties: {
regex: '[a-zA-Z0-9\\-_]{2,}',
errorMessage: 'Not a valid Google Drive Drive ID',
},
},
],
url: '=https://drive.google.com/drive/folders/{{$value}}',
},
],
description: 'The shared drive to operate on',
};
export const shareOptions: INodeProperties = {
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Email Message',
name: 'emailMessage',
type: 'string',
default: '',
description: 'A plain text custom message to include in the notification email',
typeOptions: {
rows: 2,
},
},
{
displayName: 'Move To New Owners Root',
name: 'moveToNewOwnersRoot',
type: 'boolean',
default: false,
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
description:
"<p>This parameter only takes effect if the item is not in a shared drive and the request is attempting to transfer the ownership of the item.</p><p>When set to true, the item is moved to the new owner's My Drive root folder and all prior parents removed.</p>",
},
{
displayName: 'Send Notification Email',
name: 'sendNotificationEmail',
type: 'boolean',
default: false,
description: 'Whether to send a notification email when sharing to users or groups',
},
{
displayName: 'Transfer Ownership',
name: 'transferOwnership',
type: 'boolean',
default: false,
description:
'Whether to transfer ownership to the specified user and downgrade the current owner to a writer',
},
{
displayName: 'Use Domain Admin Access',
name: 'useDomainAdminAccess',
type: 'boolean',
default: false,
description:
'Whether to perform the operation as domain administrator, i.e. if you are an administrator of the domain to which the shared drive belongs, you will be granted access automatically.',
},
],
};
export const permissionsOptions: INodeProperties = {
displayName: 'Permissions',
name: 'permissionsUi',
placeholder: 'Add Permission',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: false,
},
options: [
{
displayName: 'Permission',
name: 'permissionsValues',
values: [
{
displayName: 'Role',
name: 'role',
type: 'options',
description: 'Defines what users can do with the file or folder',
options: [
{
name: 'Commenter',
value: 'commenter',
},
{
name: 'File Organizer',
value: 'fileOrganizer',
},
{
name: 'Organizer',
value: 'organizer',
},
{
name: 'Owner',
value: 'owner',
},
{
name: 'Reader',
value: 'reader',
},
{
name: 'Writer',
value: 'writer',
},
],
default: '',
},
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'User',
value: 'user',
},
{
name: 'Group',
value: 'group',
},
{
name: 'Domain',
value: 'domain',
},
{
name: 'Anyone',
value: 'anyone',
},
],
default: '',
description:
'The scope of the permission. A permission with type=user applies to a specific user whereas a permission with type=domain applies to everyone in a specific domain.',
},
{
displayName: 'Email Address',
name: 'emailAddress',
type: 'string',
displayOptions: {
show: {
type: ['user', 'group'],
},
},
placeholder: '“e.g. name@mail.com',
default: '',
description: 'The email address of the user or group to which this permission refers',
},
{
displayName: 'Domain',
name: 'domain',
type: 'string',
displayOptions: {
show: {
type: ['domain'],
},
},
placeholder: 'e.g. mycompany.com',
default: '',
description: 'The domain to which this permission refers',
},
{
displayName: 'Allow File Discovery',
name: 'allowFileDiscovery',
type: 'boolean',
displayOptions: {
show: {
type: ['domain', 'anyone'],
},
},
default: false,
description: 'Whether to allow the file to be discovered through search',
},
],
},
],
};
export const updateCommonOptions: INodeProperties[] = [
{
displayName: 'APP Properties',
name: 'appPropertiesUi',
placeholder: 'Add Property',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: true,
},
description:
'A collection of arbitrary key-value pairs which are private to the requesting app',
options: [
{
name: 'appPropertyValues',
displayName: 'APP Property',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: 'Name of the key to add',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value to set for the key',
},
],
},
],
},
{
displayName: 'Properties',
name: 'propertiesUi',
placeholder: 'Add Property',
type: 'fixedCollection',
default: {},
typeOptions: {
multipleValues: true,
},
description: 'A collection of arbitrary key-value pairs which are visible to all apps',
options: [
{
name: 'propertyValues',
displayName: 'Property',
values: [
{
displayName: 'Key',
name: 'key',
type: 'string',
default: '',
description: 'Name of the key to add',
},
{
displayName: 'Value',
name: 'value',
type: 'string',
default: '',
description: 'Value to set for the key',
},
],
},
],
},
{
displayName: 'Keep Revision Forever',
name: 'keepRevisionForever',
type: 'boolean',
default: false,
description:
"Whether to set the 'keepForever' field in the new head revision. This is only applicable to files with binary content in Google Drive. Only 200 revisions for the file can be kept forever. If the limit is reached, try deleting pinned revisions.",
},
{
displayName: 'OCR Language',
name: 'ocrLanguage',
type: 'string',
default: '',
placeholder: 'e.g. en',
description: 'A language hint for OCR processing during image import (ISO 639-1 code)',
},
{
displayName: 'Use Content As Indexable Text',
name: 'useContentAsIndexableText',
type: 'boolean',
default: false,
description: 'Whether to use the uploaded content as indexable text',
},
];
export const fileTypesOptions = [
{
name: 'All',
value: '*',
description: 'Return all file types',
},
{
name: '3rd Party Shortcut',
value: DRIVE.SDK,
},
{
name: 'Audio',
value: DRIVE.AUDIO,
},
{
name: 'Folder',
value: DRIVE.FOLDER,
},
{
name: 'Google Apps Scripts',
value: DRIVE.APP_SCRIPTS,
},
{
name: 'Google Docs',
value: DRIVE.DOCUMENT,
},
{
name: 'Google Drawing',
value: DRIVE.DRAWING,
},
{
name: 'Google Forms',
value: DRIVE.FORM,
},
{
name: 'Google Fusion Tables',
value: DRIVE.FUSIONTABLE,
},
{
name: 'Google My Maps',
value: DRIVE.MAP,
},
{
name: 'Google Sheets',
value: DRIVE.SPREADSHEET,
},
{
name: 'Google Sites',
value: DRIVE.SITES,
},
{
name: 'Google Slides',
value: DRIVE.PRESENTATION,
},
{
name: 'Photo',
value: DRIVE.PHOTO,
},
{
name: 'Unknown',
value: DRIVE.UNKNOWN,
},
{
name: 'Video',
value: DRIVE.VIDEO,
},
];

View File

@@ -0,0 +1,61 @@
import type { INodeProperties } from 'n8n-workflow';
import * as create from './create.operation';
import * as deleteDrive from './deleteDrive.operation';
import * as get from './get.operation';
import * as list from './list.operation';
import * as update from './update.operation';
export { create, deleteDrive, get, list, update };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
options: [
{
name: 'Create',
value: 'create',
description: 'Create a shared drive',
action: 'Create shared drive',
},
{
name: 'Delete',
value: 'deleteDrive',
description: 'Permanently delete a shared drive',
action: 'Delete shared drive',
},
{
name: 'Get',
value: 'get',
description: 'Get a shared drive',
action: 'Get shared drive',
},
{
name: 'Get Many',
value: 'list',
description: 'Get the list of shared drives',
action: 'Get many shared drives',
},
{
name: 'Update',
value: 'update',
description: 'Update a shared drive',
action: 'Update shared drive',
},
],
default: 'create',
displayOptions: {
show: {
resource: ['drive'],
},
},
},
...create.description,
...deleteDrive.description,
...get.description,
...list.description,
...update.description,
];

View File

@@ -0,0 +1,263 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { googleApiRequest } from '../../transport';
import { v4 as uuid } from 'uuid';
const properties: INodeProperties[] = [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
placeholder: 'e.g. New Shared Drive',
description: 'The name of the shared drive to create',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Capabilities',
name: 'capabilities',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'Can Add Children',
name: 'canAddChildren',
type: 'boolean',
default: false,
description:
'Whether the current user can add children to folders in this shared drive',
},
{
displayName: 'Can Change Copy Requires Writer Permission Restriction',
name: 'canChangeCopyRequiresWriterPermissionRestriction',
type: 'boolean',
default: false,
description:
'Whether the current user can change the copyRequiresWriterPermission restriction of this shared drive',
},
{
displayName: 'Can Change Domain Users Only Restriction',
name: 'canChangeDomainUsersOnlyRestriction',
type: 'boolean',
default: false,
description:
'Whether the current user can change the domainUsersOnly restriction of this shared drive',
},
{
displayName: 'Can Change Drive Background',
name: 'canChangeDriveBackground',
type: 'boolean',
default: false,
description: 'Whether the current user can change the background of this shared drive',
},
{
displayName: 'Can Change Drive Members Only Restriction',
name: 'canChangeDriveMembersOnlyRestriction',
type: 'boolean',
default: false,
description:
'Whether the current user can change the driveMembersOnly restriction of this shared drive',
},
{
displayName: 'Can Comment',
name: 'canComment',
type: 'boolean',
default: false,
description: 'Whether the current user can comment on files in this shared drive',
},
{
displayName: 'Can Copy',
name: 'canCopy',
type: 'boolean',
default: false,
description: 'Whether the current user can copy files in this shared drive',
},
{
displayName: 'Can Delete Children',
name: 'canDeleteChildren',
type: 'boolean',
default: false,
description:
'Whether the current user can delete children from folders in this shared drive',
},
{
displayName: 'Can Delete Drive',
name: 'canDeleteDrive',
type: 'boolean',
default: false,
description:
'Whether the current user can delete this shared drive. Attempting to delete the shared drive may still fail if there are untrashed items inside the shared drive.',
},
{
displayName: 'Can Download',
name: 'canDownload',
type: 'boolean',
default: false,
description: 'Whether the current user can download files in this shared drive',
},
{
displayName: 'Can Edit',
name: 'canEdit',
type: 'boolean',
default: false,
description: 'Whether the current user can edit files in this shared drive',
},
{
displayName: 'Can List Children',
name: 'canListChildren',
type: 'boolean',
default: false,
description:
'Whether the current user can list the children of folders in this shared drive',
},
{
displayName: 'Can Manage Members',
name: 'canManageMembers',
type: 'boolean',
default: false,
description:
'Whether the current user can add members to this shared drive or remove them or change their role',
},
{
displayName: 'Can Read Revisions',
name: 'canReadRevisions',
type: 'boolean',
default: false,
description:
'Whether the current user can read the revisions resource of files in this shared drive',
},
{
displayName: 'Can Rename',
name: 'canRename',
type: 'boolean',
default: false,
description:
'Whether the current user can rename files or folders in this shared drive',
},
{
displayName: 'Can Rename Drive',
name: 'canRenameDrive',
type: 'boolean',
default: false,
description: 'Whether the current user can rename this shared drive',
},
{
displayName: 'Can Share',
name: 'canShare',
type: 'boolean',
default: false,
description: 'Whether the current user can rename this shared drive',
},
{
displayName: 'Can Trash Children',
name: 'canTrashChildren',
type: 'boolean',
default: false,
description:
'Whether the current user can trash children from folders in this shared drive',
},
],
},
{
displayName: 'Color RGB',
name: 'colorRgb',
type: 'color',
default: '',
description: 'The color of this shared drive as an RGB hex string',
},
{
displayName: 'Hidden',
name: 'hidden',
type: 'boolean',
default: false,
description: 'Whether the shared drive is hidden from default view',
},
{
displayName: 'Restrictions',
name: 'restrictions',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'Admin Managed Restrictions',
name: 'adminManagedRestrictions',
type: 'boolean',
default: false,
description:
'Whether the options to copy, print, or download files inside this shared drive, should be disabled for readers and commenters. When this restriction is set to true, it will override the similarly named field to true for any file inside this shared drive.',
},
{
displayName: 'Copy Requires Writer Permission',
name: 'copyRequiresWriterPermission',
type: 'boolean',
default: false,
description:
'Whether the options to copy, print, or download files inside this shared drive, should be disabled for readers and commenters. When this restriction is set to true, it will override the similarly named field to true for any file inside this shared drive.',
},
{
displayName: 'Domain Users Only',
name: 'domainUsersOnly',
type: 'boolean',
default: false,
description:
'Whether access to this shared drive and items inside this shared drive is restricted to users of the domain to which this shared drive belongs. This restriction may be overridden by other sharing policies controlled outside of this shared drive.',
},
{
displayName: 'Drive Members Only',
name: 'driveMembersOnly',
type: 'boolean',
default: false,
description:
'Whether access to items inside this shared drive is restricted to its members',
},
],
},
],
},
];
const displayOptions = {
show: {
resource: ['drive'],
operation: ['create'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const options = this.getNodeParameter('options', i);
const name = this.getNodeParameter('name', i) as string;
const body: IDataObject = {
name,
};
Object.assign(body, options);
const response = await googleApiRequest.call(this, 'POST', '/drive/v3/drives', body, {
requestId: uuid(),
});
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
return returnData;
}

View File

@@ -0,0 +1,41 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { googleApiRequest } from '../../transport';
import { sharedDriveRLC } from '../common.descriptions';
const properties: INodeProperties[] = [
{
...sharedDriveRLC,
description: 'The shared drive to delete',
},
];
const displayOptions = {
show: {
resource: ['drive'],
operation: ['deleteDrive'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const driveId = this.getNodeParameter('driveId', i, undefined, {
extractValue: true,
}) as string;
await googleApiRequest.call(this, 'DELETE', `/drive/v3/drives/${driveId}`);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: i } },
);
returnData.push(...executionData);
return returnData;
}

View File

@@ -0,0 +1,63 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { googleApiRequest } from '../../transport';
import { sharedDriveRLC } from '../common.descriptions';
const properties: INodeProperties[] = [
{
...sharedDriveRLC,
description: 'The shared drive to get',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Use Domain Admin Access',
name: 'useDomainAdminAccess',
type: 'boolean',
default: false,
description:
'Whether to issue the request as a domain administrator; if set to true, then the requester will be granted access if they are an administrator of the domain to which the shared drive belongs',
},
],
},
];
const displayOptions = {
show: {
resource: ['drive'],
operation: ['get'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const options = this.getNodeParameter('options', i);
const driveId = this.getNodeParameter('driveId', i, undefined, {
extractValue: true,
}) as string;
const qs: IDataObject = {};
Object.assign(qs, options);
const response = await googleApiRequest.call(this, 'GET', `/drive/v3/drives/${driveId}`, {}, qs);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
return returnData;
}

View File

@@ -0,0 +1,103 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { googleApiRequest, googleApiRequestAllItems } from '../../transport';
const properties: INodeProperties[] = [
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
returnAll: [false],
},
},
typeOptions: {
minValue: 1,
maxValue: 200,
},
default: 100,
description: 'Max number of results to return',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Query',
name: 'q',
type: 'string',
default: '',
description:
'Query string for searching shared drives. See the <a href="https://developers.google.com/drive/api/v3/search-shareddrives">"Search for shared drives"</a> guide for supported syntax.',
},
{
displayName: 'Use Domain Admin Access',
name: 'useDomainAdminAccess',
type: 'boolean',
default: false,
description:
'Whether to issue the request as a domain administrator; if set to true, then the requester will be granted access if they are an administrator of the domain to which the shared drive belongs',
},
],
},
];
const displayOptions = {
show: {
resource: ['drive'],
operation: ['list'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const options = this.getNodeParameter('options', i);
const returnAll = this.getNodeParameter('returnAll', i);
const qs: IDataObject = {};
let response: IDataObject[] = [];
Object.assign(qs, options);
if (returnAll) {
response = await googleApiRequestAllItems.call(
this,
'GET',
'drives',
'/drive/v3/drives',
{},
qs,
);
} else {
qs.pageSize = this.getNodeParameter('limit', i);
const data = await googleApiRequest.call(this, 'GET', '/drive/v3/drives', {}, qs);
response = data.drives as IDataObject[];
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response),
{ itemData: { item: i } },
);
returnData.push(...executionData);
return returnData;
}

View File

@@ -0,0 +1,116 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { googleApiRequest } from '../../transport';
import { sharedDriveRLC } from '../common.descriptions';
const properties: INodeProperties[] = [
{
...sharedDriveRLC,
description: 'The shared drive to update',
},
{
displayName: 'Update Fields',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
operation: ['update'],
resource: ['drive'],
},
},
options: [
{
displayName: 'Color RGB',
name: 'colorRgb',
type: 'color',
default: '',
description: 'The color of this shared drive as an RGB hex string',
},
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
description: 'The updated name of the shared drive',
},
{
displayName: 'Restrictions',
name: 'restrictions',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'Admin Managed Restrictions',
name: 'adminManagedRestrictions',
type: 'boolean',
default: false,
description:
'Whether the options to copy, print, or download files inside this shared drive, should be disabled for readers and commenters. When this restriction is set to true, it will override the similarly named field to true for any file inside this shared drive.',
},
{
displayName: 'Copy Requires Writer Permission',
name: 'copyRequiresWriterPermission',
type: 'boolean',
default: false,
description:
'Whether the options to copy, print, or download files inside this shared drive, should be disabled for readers and commenters. When this restriction is set to true, it will override the similarly named field to true for any file inside this shared drive.',
},
{
displayName: 'Domain Users Only',
name: 'domainUsersOnly',
type: 'boolean',
default: false,
description:
'Whether access to this shared drive and items inside this shared drive is restricted to users of the domain to which this shared drive belongs. This restriction may be overridden by other sharing policies controlled outside of this shared drive.',
},
{
displayName: 'Drive Members Only',
name: 'driveMembersOnly',
type: 'boolean',
default: false,
description:
'Whether access to items inside this shared drive is restricted to its members',
},
],
},
],
},
];
const displayOptions = {
show: {
resource: ['drive'],
operation: ['update'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const options = this.getNodeParameter('options', i, {});
const driveId = this.getNodeParameter('driveId', i, undefined, {
extractValue: true,
}) as string;
const body: IDataObject = {};
Object.assign(body, options);
const response = await googleApiRequest.call(this, 'PATCH', `/drive/v3/drives/${driveId}`, body);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
return returnData;
}

View File

@@ -0,0 +1,85 @@
import type { INodeProperties } from 'n8n-workflow';
import * as copy from './copy.operation';
import * as createFromText from './createFromText.operation';
import * as deleteFile from './deleteFile.operation';
import * as download from './download.operation';
import * as move from './move.operation';
import * as share from './share.operation';
import * as update from './update.operation';
import * as upload from './upload.operation';
export { copy, createFromText, deleteFile, download, move, share, update, upload };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['file'],
},
},
options: [
{
name: 'Copy',
value: 'copy',
description: 'Create a copy of an existing file',
action: 'Copy file',
},
{
name: 'Create From Text',
value: 'createFromText',
description: 'Create a file from a provided text',
action: 'Create file from text',
},
{
name: 'Delete',
value: 'deleteFile',
description: 'Permanently delete a file',
action: 'Delete a file',
},
{
name: 'Download',
value: 'download',
description: 'Download a file',
action: 'Download file',
},
{
name: 'Move',
value: 'move',
description: 'Move a file to another folder',
action: 'Move file',
},
{
name: 'Share',
value: 'share',
description: 'Add sharing permissions to a file',
action: 'Share file',
},
{
name: 'Update',
value: 'update',
description: 'Update a file',
action: 'Update file',
},
{
name: 'Upload',
value: 'upload',
description: 'Upload an existing file to Google Drive',
action: 'Upload file',
},
],
default: 'upload',
},
...copy.description,
...deleteFile.description,
...createFromText.description,
...download.description,
...move.description,
...share.description,
...update.description,
...upload.description,
];

View File

@@ -0,0 +1,136 @@
import type { IExecuteFunctions } from 'n8n-core';
import type {
IDataObject,
INodeExecutionData,
INodeParameterResourceLocator,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { googleApiRequest } from '../../transport';
import { driveRLC, fileRLC, folderRLC } from '../common.descriptions';
import { setParentFolder } from '../../helpers/utils';
const properties: INodeProperties[] = [
{
...fileRLC,
description: 'The file to copy',
},
{
displayName: 'File Name',
name: 'name',
type: 'string',
default: '',
placeholder: 'e.g. My File',
description:
'The name of the new file. If not set, “Copy of {original file name}” will be used.',
},
{
displayName: 'Copy In The Same Folder',
name: 'sameFolder',
type: 'boolean',
default: true,
description: 'Whether to copy the file in the same folder as the original file',
},
{
...driveRLC,
displayName: 'Parent Drive',
description: 'The drive where to save the copied file',
displayOptions: { show: { sameFolder: [false] } },
},
{
...folderRLC,
displayName: 'Parent Folder',
description: 'The folder where to save the copied file',
displayOptions: { show: { sameFolder: [false] } },
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Copy Requires Writer Permission',
name: 'copyRequiresWriterPermission',
type: 'boolean',
default: false,
description:
'Whether the options to copy, print, or download this file, should be disabled for readers and commenters',
},
{
displayName: 'Description',
name: 'description',
type: 'string',
default: '',
description: 'A short description of the file',
},
],
},
];
const displayOptions = {
show: {
resource: ['file'],
operation: ['copy'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const file = this.getNodeParameter('fileId', i) as INodeParameterResourceLocator;
const fileId = file.value;
const options = this.getNodeParameter('options', i, {});
let name = this.getNodeParameter('name', i) as string;
name = name ? name : `Copy of ${file.cachedResultName}`;
const copyRequiresWriterPermission = options.copyRequiresWriterPermission || false;
const qs = {
includeItemsFromAllDrives: true,
supportsAllDrives: true,
spaces: 'appDataFolder, drive',
corpora: 'allDrives',
};
const parents: string[] = [];
const sameFolder = this.getNodeParameter('sameFolder', i) as boolean;
if (!sameFolder) {
const driveId = this.getNodeParameter('driveId', i, undefined, {
extractValue: true,
}) as string;
const folderId = this.getNodeParameter('folderId', i, undefined, {
extractValue: true,
}) as string;
parents.push(setParentFolder(folderId, driveId));
}
const body: IDataObject = { copyRequiresWriterPermission, parents, name };
if (options.description) {
body.description = options.description;
}
const response = await googleApiRequest.call(
this,
'POST',
`/drive/v3/files/${fileId}/copy`,
body,
qs,
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response as IDataObject[]),
{ itemData: { item: i } },
);
return executionData;
}

View File

@@ -0,0 +1,183 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { driveRLC, folderRLC, updateCommonOptions } from '../common.descriptions';
import { googleApiRequest } from '../../transport';
import { DRIVE } from '../../helpers/interfaces';
import { setFileProperties, setParentFolder, setUpdateCommonParams } from '../../helpers/utils';
const properties: INodeProperties[] = [
{
displayName: 'File Content',
name: 'content',
type: 'string',
default: '',
typeOptions: {
rows: 2,
},
description: 'The text to create the file with',
},
{
displayName: 'File Name',
name: 'name',
type: 'string',
default: '',
placeholder: 'e.g. My New File',
description:
"The name of the file you want to create. If not specified, 'Untitled' will be used.",
},
{
...driveRLC,
displayName: 'Parent Drive',
required: false,
description: 'The drive where to create the new file',
},
{
...folderRLC,
displayName: 'Parent Folder',
required: false,
description: 'The folder where to create the new file',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
...updateCommonOptions,
{
displayName: 'Convert to Google Document',
name: 'convertToGoogleDocument',
type: 'boolean',
default: false,
description: 'Whether to create a Google Document (instead of the .txt default format)',
hint: 'Google Docs API has to be enabled in the <a href="https://console.developers.google.com/apis/library/docs.googleapis.com" target="_blank">Google API Console</a>.',
},
],
},
];
const displayOptions = {
show: {
resource: ['file'],
operation: ['createFromText'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const name = (this.getNodeParameter('name', i) as string) || 'Untitled';
const options = this.getNodeParameter('options', i, {});
const convertToGoogleDocument = (options.convertToGoogleDocument as boolean) || false;
const mimeType = convertToGoogleDocument ? DRIVE.DOCUMENT : 'text/plain';
const driveId = this.getNodeParameter('driveId', i, undefined, {
extractValue: true,
}) as string;
const folderId = this.getNodeParameter('folderId', i, undefined, {
extractValue: true,
}) as string;
const bodyParameters = setFileProperties(
{
name,
parents: [setParentFolder(folderId, driveId)],
mimeType,
},
options,
);
const boundary = 'XXXXXX';
const qs = setUpdateCommonParams(
{
includeItemsFromAllDrives: true,
supportsAllDrives: true,
spaces: 'appDataFolder, drive',
corpora: 'allDrives',
},
options,
);
let response;
if (convertToGoogleDocument) {
const document = await googleApiRequest.call(
this,
'POST',
'/drive/v3/files',
bodyParameters,
qs,
);
const text = this.getNodeParameter('content', i, '') as string;
const body = {
requests: [
{
insertText: {
text,
endOfSegmentLocation: {
segmentId: '', //empty segment ID signifies the document's body
},
},
},
],
};
const updateResponse = await googleApiRequest.call(
this,
'POST',
'',
body,
undefined,
`https://docs.googleapis.com/v1/documents/${document.id}:batchUpdate`,
);
response = { id: updateResponse.documentId };
} else {
const content = Buffer.from(this.getNodeParameter('content', i, '') as string, 'utf8');
const contentLength = content.byteLength;
const body = `
\n--${boundary}\
\nContent-Type: application/json; charset=UTF-8\
\n\n${JSON.stringify(bodyParameters)}\
\n--${boundary}\
\nContent-Type: text/plain\
\nContent-Transfer-Encoding: base64\
\n\n${content}\
\n--${boundary}--`;
const responseData = await googleApiRequest.call(
this,
'POST',
'/upload/drive/v3/files',
body,
{
uploadType: 'multipart',
...qs,
},
undefined,
{
headers: {
'Content-Type': `multipart/related; boundary=${boundary}`,
'Content-Length': contentLength,
},
},
);
response = { id: responseData.id };
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response as IDataObject),
{ itemData: { item: i } },
);
return executionData;
}

View File

@@ -0,0 +1,67 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { googleApiRequest } from '../../transport';
import { fileRLC } from '../common.descriptions';
const properties: INodeProperties[] = [
{
...fileRLC,
description: 'The file to delete',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Delete Permanently',
name: 'deletePermanently',
type: 'boolean',
default: false,
description:
'Whether to delete the file immediately. If false, the file will be moved to the trash.',
},
],
},
];
const displayOptions = {
show: {
resource: ['file'],
operation: ['deleteFile'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const fileId = this.getNodeParameter('fileId', i, undefined, {
extractValue: true,
}) as string;
const deletePermanently = this.getNodeParameter('options.deletePermanently', i, false) as boolean;
const qs = {
supportsAllDrives: true,
};
if (deletePermanently) {
await googleApiRequest.call(this, 'DELETE', `/drive/v3/files/${fileId}`, undefined, qs);
} else {
await googleApiRequest.call(this, 'PATCH', `/drive/v3/files/${fileId}`, { trashed: true }, qs);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({
id: fileId,
success: true,
}),
{ itemData: { item: i } },
);
return executionData;
}

View File

@@ -0,0 +1,284 @@
import type { IExecuteFunctions } from 'n8n-core';
import type {
IBinaryKeyData,
IDataObject,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { googleApiRequest } from '../../transport';
import { fileRLC } from '../common.descriptions';
const properties: INodeProperties[] = [
{
...fileRLC,
description: 'The file to download',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Binary Property',
name: 'binaryPropertyName',
type: 'string',
placeholder: 'e.g. data',
default: 'data',
description: 'Use this field name in the following nodes, to use the binary file data',
hint: 'The name of the output field to put the binary file data in',
},
{
displayName: 'Google File Conversion',
name: 'googleFileConversion',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
default: {},
placeholder: 'Add Conversion',
options: [
{
displayName: 'Conversion',
name: 'conversion',
values: [
{
displayName: 'Google Docs',
name: 'docsToFormat',
type: 'options',
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
options: [
{
name: 'HTML',
value: 'text/html',
},
{
name: 'MS Word Document',
value:
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
},
{
name: 'Open Office Document',
value: 'application/vnd.oasis.opendocument.text',
},
{
name: 'PDF',
value: 'application/pdf',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'Rich Text (rtf)',
value: 'application/rtf',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'Text (txt)',
value: 'text/plain',
},
],
default: 'text/html',
description: 'Format used to export when downloading Google Docs files',
},
{
displayName: 'Google Drawings',
name: 'drawingsToFormat',
type: 'options',
options: [
{
name: 'JPEG',
value: 'image/jpeg',
},
{
name: 'PDF',
value: 'application/pdf',
},
{
name: 'PNG',
value: 'image/png',
},
{
name: 'SVG',
value: 'image/svg+xml',
},
],
default: 'image/jpeg',
description: 'Format used to export when downloading Google Drawings files',
},
{
displayName: 'Google Slides',
name: 'slidesToFormat',
type: 'options',
options: [
{
name: 'MS PowerPoint',
value:
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
},
{
name: 'OpenOffice Presentation',
value: 'application/vnd.oasis.opendocument.presentation',
},
{
name: 'PDF',
value: 'application/pdf',
},
],
default:
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
description: 'Format used to export when downloading Google Slides files',
},
{
displayName: 'Google Sheets',
name: 'sheetsToFormat',
type: 'options',
options: [
{
name: 'CSV',
value: 'text/csv',
},
{
name: 'MS Excel',
value: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
},
{
name: 'Open Office Sheet',
value: 'application/vnd.oasis.opendocument.spreadsheet',
},
{
name: 'PDF',
value: 'application/pdf',
},
],
default: 'text/csv',
description: 'Format used to export when downloading Google Sheets files',
},
],
},
],
},
{
displayName: 'File Name',
name: 'fileName',
type: 'string',
default: '',
description: 'File name. Ex: data.pdf.',
},
],
},
];
const displayOptions = {
show: {
resource: ['file'],
operation: ['download'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
i: number,
item: INodeExecutionData,
): Promise<INodeExecutionData[]> {
const fileId = this.getNodeParameter('fileId', i, undefined, {
extractValue: true,
}) as string;
const downloadOptions = this.getNodeParameter('options', i);
const requestOptions = {
useStream: true,
resolveWithFullResponse: true,
encoding: null,
json: false,
};
const file = await googleApiRequest.call(
this,
'GET',
`/drive/v3/files/${fileId}`,
{},
{ fields: 'mimeType,name', supportsTeamDrives: true },
);
let response;
if (file.mimeType?.includes('vnd.google-apps')) {
const parameterKey = 'options.googleFileConversion.conversion';
const type = file.mimeType.split('.')[2];
let mime;
if (type === 'document') {
mime = this.getNodeParameter(
`${parameterKey}.docsToFormat`,
i,
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
) as string;
} else if (type === 'presentation') {
mime = this.getNodeParameter(
`${parameterKey}.slidesToFormat`,
i,
'application/vnd.openxmlformats-officedocument.presentationml.presentation',
) as string;
} else if (type === 'spreadsheet') {
mime = this.getNodeParameter(
`${parameterKey}.sheetsToFormat`,
i,
'application/x-vnd.oasis.opendocument.spreadsheet',
) as string;
} else {
mime = this.getNodeParameter(`${parameterKey}.drawingsToFormat`, i, 'image/jpeg') as string;
}
response = await googleApiRequest.call(
this,
'GET',
`/drive/v3/files/${fileId}/export`,
{},
{ mimeType: mime },
undefined,
requestOptions,
);
} else {
response = await googleApiRequest.call(
this,
'GET',
`/drive/v3/files/${fileId}`,
{},
{ alt: 'media' },
undefined,
requestOptions,
);
}
const mimeType =
(response.headers as IDataObject)?.['content-type'] ?? file.mimeType ?? undefined;
const fileName = downloadOptions.fileName ?? file.name ?? undefined;
const newItem: INodeExecutionData = {
json: item.json,
binary: {},
};
if (item.binary !== undefined) {
// Create a shallow copy of the binary data so that the old
// data references which do not get changed still stay behind
// but the incoming data does not get changed.
Object.assign(newItem.binary as IBinaryKeyData, item.binary);
}
item = newItem;
const dataPropertyNameDownload = (downloadOptions.binaryPropertyName as string) || 'data';
item.binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(
response.body as Buffer,
fileName as string,
mimeType as string,
);
const executionData = this.helpers.constructExecutionMetaData([item], { itemData: { item: i } });
return executionData;
}

View File

@@ -0,0 +1,84 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { driveRLC, fileRLC, folderRLC } from '../common.descriptions';
import { googleApiRequest } from '../../transport';
import { setParentFolder } from '../../helpers/utils';
const properties: INodeProperties[] = [
{
...fileRLC,
description: 'The file to move',
},
{
...driveRLC,
displayName: 'Parent Drive',
description: 'The drive where to move the file',
},
{
...folderRLC,
displayName: 'Parent Folder',
description: 'The folder where to move the file',
},
];
const displayOptions = {
show: {
resource: ['file'],
operation: ['move'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const fileId = this.getNodeParameter('fileId', i, undefined, {
extractValue: true,
});
const driveId = this.getNodeParameter('driveId', i, undefined, {
extractValue: true,
}) as string;
const folderId = this.getNodeParameter('folderId', i, undefined, {
extractValue: true,
}) as string;
const qs = {
includeItemsFromAllDrives: true,
supportsAllDrives: true,
spaces: 'appDataFolder, drive',
corpora: 'allDrives',
};
const { parents } = await googleApiRequest.call(
this,
'GET',
`/drive/v3/files/${fileId}`,
undefined,
{
...qs,
fields: 'parents',
},
);
const response = await googleApiRequest.call(
this,
'PATCH',
`/drive/v3/files/${fileId}`,
undefined,
{
...qs,
addParents: setParentFolder(folderId, driveId),
removeParents: ((parents as string[]) || []).join(','),
},
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response as IDataObject[]),
{ itemData: { item: i } },
);
return executionData;
}

View File

@@ -0,0 +1,64 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { googleApiRequest } from '../../transport';
import { fileRLC, permissionsOptions, shareOptions } from '../common.descriptions';
const properties: INodeProperties[] = [
{
...fileRLC,
description: 'The file to share',
},
permissionsOptions,
shareOptions,
];
const displayOptions = {
show: {
resource: ['file'],
operation: ['share'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const fileId = this.getNodeParameter('fileId', i, undefined, {
extractValue: true,
}) as string;
const permissions = this.getNodeParameter('permissionsUi', i) as IDataObject;
const shareOption = this.getNodeParameter('options', i);
const body: IDataObject = {};
const qs: IDataObject = {
supportsAllDrives: true,
};
if (permissions.permissionsValues) {
Object.assign(body, permissions.permissionsValues);
}
Object.assign(qs, shareOption);
const response = await googleApiRequest.call(
this,
'POST',
`/drive/v3/files/${fileId}/permissions`,
body,
qs,
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
return returnData;
}

View File

@@ -0,0 +1,274 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import {
getItemBinaryData,
prepareQueryString,
setFileProperties,
setUpdateCommonParams,
} from '../../helpers/utils';
import { googleApiRequest } from '../../transport';
import { fileRLC, updateCommonOptions } from '../common.descriptions';
const properties: INodeProperties[] = [
{
...fileRLC,
displayName: 'File to Update',
description: 'The file to update',
},
{
displayName: 'Change File Content',
name: 'changeFileContent',
type: 'boolean',
default: false,
description: 'Whether to send a new binary data to update the file',
},
{
displayName: 'Input Data Field Name',
name: 'inputDataFieldName',
type: 'string',
placeholder: 'e.g. data',
default: 'data',
hint: 'The name of the input field containing the binary file data to update the file',
description:
'Find the name of input field containing the binary data to update the file in the Input panel on the left, in the Binary tab',
displayOptions: {
show: {
changeFileContent: [true],
},
},
},
{
displayName: 'New Updated File Name',
name: 'newUpdatedFileName',
type: 'string',
default: '',
placeholder: 'e.g. My New File',
description: 'If not specified, the file name will not be changed',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
...updateCommonOptions,
{
displayName: 'Move to Trash',
name: 'trashed',
type: 'boolean',
default: false,
description: 'Whether to move a file to the trash. Only the owner may trash a file.',
},
{
displayName: 'Return Fields',
name: 'fields',
type: 'multiOptions',
options: [
{
name: '[All]',
value: '*',
description: 'All fields',
},
{
name: 'explicitlyTrashed',
value: 'explicitlyTrashed',
},
{
name: 'exportLinks',
value: 'exportLinks',
},
{
name: 'hasThumbnail',
value: 'hasThumbnail',
},
{
name: 'iconLink',
value: 'iconLink',
},
{
name: 'ID',
value: 'id',
},
{
name: 'Kind',
value: 'kind',
},
{
name: 'mimeType',
value: 'mimeType',
},
{
name: 'Name',
value: 'name',
},
{
name: 'Permissions',
value: 'permissions',
},
{
name: 'Shared',
value: 'shared',
},
{
name: 'Spaces',
value: 'spaces',
},
{
name: 'Starred',
value: 'starred',
},
{
name: 'thumbnailLink',
value: 'thumbnailLink',
},
{
name: 'Trashed',
value: 'trashed',
},
{
name: 'Version',
value: 'version',
},
{
name: 'webViewLink',
value: 'webViewLink',
},
],
default: [],
description: 'The fields to return',
},
],
},
];
const displayOptions = {
show: {
resource: ['file'],
operation: ['update'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const fileId = this.getNodeParameter('fileId', i, undefined, {
extractValue: true,
}) as string;
const changeFileContent = this.getNodeParameter('changeFileContent', i, false) as boolean;
let mimeType;
// update file binary data
if (changeFileContent) {
const inputDataFieldName = this.getNodeParameter('inputDataFieldName', i) as string;
const binaryData = await getItemBinaryData.call(this, inputDataFieldName, i);
const { contentLength, fileContent } = binaryData;
mimeType = binaryData.mimeType;
if (Buffer.isBuffer(fileContent)) {
await googleApiRequest.call(
this,
'PATCH',
`/upload/drive/v3/files/${fileId}`,
fileContent,
{
uploadType: 'media',
},
undefined,
{
headers: {
'Content-Type': mimeType,
'Content-Length': contentLength,
},
},
);
} else {
const resumableUpload = await googleApiRequest.call(
this,
'PATCH',
`/upload/drive/v3/files/${fileId}`,
undefined,
{ uploadType: 'resumable' },
undefined,
{
resolveWithFullResponse: true,
},
);
const uploadUrl = resumableUpload.headers.location;
let offset = 0;
for await (const chunk of fileContent) {
const nextOffset = offset + Number(chunk.length);
try {
await this.helpers.httpRequest({
method: 'PUT',
url: uploadUrl,
headers: {
'Content-Length': chunk.length,
'Content-Range': `bytes ${offset}-${nextOffset - 1}/${contentLength}`,
},
body: chunk,
});
} catch (error) {
if (error.response?.status !== 308) {
throw new NodeOperationError(this.getNode(), error as Error, { itemIndex: i });
}
}
offset = nextOffset;
}
}
}
const options = this.getNodeParameter('options', i, {});
const qs: IDataObject = setUpdateCommonParams(
{
supportsAllDrives: true,
},
options,
);
if (options.fields) {
const queryFields = prepareQueryString(options.fields as string[]);
qs.fields = queryFields;
}
if (options.trashed) {
qs.trashed = options.trashed;
}
const body: IDataObject = setFileProperties({}, options);
const newUpdatedFileName = this.getNodeParameter('newUpdatedFileName', i, '') as string;
if (newUpdatedFileName) {
body.name = newUpdatedFileName;
}
if (mimeType) {
body.mimeType = mimeType;
}
// update file metadata
const responseData = await googleApiRequest.call(
this,
'PATCH',
`/drive/v3/files/${fileId}`,
body,
qs,
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject[]),
{ itemData: { item: i } },
);
return executionData;
}

View File

@@ -0,0 +1,189 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { googleApiRequest } from '../../transport';
import { driveRLC, folderRLC, updateCommonOptions } from '../common.descriptions';
import {
getItemBinaryData,
setFileProperties,
setUpdateCommonParams,
setParentFolder,
} from '../../helpers/utils';
const properties: INodeProperties[] = [
{
displayName: 'Input Data Field Name',
name: 'inputDataFieldName',
type: 'string',
placeholder: '“e.g. data',
default: 'data',
required: true,
hint: 'The name of the input field containing the binary file data to update the file',
description:
'Find the name of input field containing the binary data to update the file in the Input panel on the left, in the Binary tab',
},
{
displayName: 'File Name',
name: 'name',
type: 'string',
default: '',
placeholder: 'e.g. My New File',
description: 'If not specified, the original file name will be used',
},
{
...driveRLC,
displayName: 'Parent Drive',
description: 'The drive where to upload the file',
},
{
...folderRLC,
displayName: 'Parent Folder',
description: 'The folder where to upload the file',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
...updateCommonOptions,
{
displayName: 'Simplify Output',
name: 'simplifyOutput',
type: 'boolean',
default: true,
description: 'Whether to return a simplified version of the response instead of all fields',
},
],
},
];
const displayOptions = {
show: {
resource: ['file'],
operation: ['upload'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const inputDataFieldName = this.getNodeParameter('inputDataFieldName', i) as string;
const { contentLength, fileContent, originalFilename, mimeType } = await getItemBinaryData.call(
this,
inputDataFieldName,
i,
);
const name = (this.getNodeParameter('name', i) as string) || originalFilename;
const driveId = this.getNodeParameter('driveId', i, undefined, {
extractValue: true,
}) as string;
const folderId = this.getNodeParameter('folderId', i, undefined, {
extractValue: true,
}) as string;
let uploadId;
if (Buffer.isBuffer(fileContent)) {
const response = await googleApiRequest.call(
this,
'POST',
'/upload/drive/v3/files',
fileContent,
{
uploadType: 'media',
},
undefined,
{
headers: {
'Content-Type': mimeType,
'Content-Length': contentLength,
},
},
);
uploadId = response.id;
} else {
const resumableUpload = await googleApiRequest.call(
this,
'POST',
'/upload/drive/v3/files',
undefined,
{ uploadType: 'resumable' },
undefined,
{
resolveWithFullResponse: true,
},
);
const uploadUrl = resumableUpload.headers.location;
let offset = 0;
for await (const chunk of fileContent) {
const nextOffset = offset + Number(chunk.length);
try {
const response = await this.helpers.httpRequest({
method: 'PUT',
url: uploadUrl,
headers: {
'Content-Length': chunk.length,
'Content-Range': `bytes ${offset}-${nextOffset - 1}/${contentLength}`,
},
body: chunk,
});
uploadId = response?.id;
} catch (error) {
if (error.response?.status !== 308) throw error;
}
offset = nextOffset;
}
}
const options = this.getNodeParameter('options', i, {});
const qs = setUpdateCommonParams(
{
addParents: setParentFolder(folderId, driveId),
includeItemsFromAllDrives: true,
supportsAllDrives: true,
spaces: 'appDataFolder, drive',
corpora: 'allDrives',
},
options,
);
if (!options.simplifyOutput) {
qs.fields = '*';
}
const body = setFileProperties(
{
mimeType,
name,
originalFilename,
},
options,
);
const response = await googleApiRequest.call(
this,
'PATCH',
`/drive/v3/files/${uploadId}`,
body,
qs,
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
return returnData;
}

View File

@@ -0,0 +1,29 @@
import type { INodeProperties } from 'n8n-workflow';
import * as search from './search.operation';
export { search };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['fileFolder'],
},
},
options: [
{
name: 'Search',
value: 'search',
description: 'Search or list files and folders',
action: 'Search files and folders',
},
],
default: 'search',
},
...search.description,
];

View File

@@ -0,0 +1,359 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { driveRLC, fileTypesOptions, folderRLC } from '../common.descriptions';
import { googleApiRequest, googleApiRequestAllItems } from '../../transport';
import { prepareQueryString, updateDriveScopes } from '../../helpers/utils';
import type { SearchFilter } from '../../helpers/interfaces';
import { DRIVE, RLC_FOLDER_DEFAULT } from '../../helpers/interfaces';
const properties: INodeProperties[] = [
{
displayName: 'Search Method',
name: 'searchMethod',
type: 'options',
options: [
{
name: 'Search File/Folder Name',
value: 'name',
},
{
name: 'Advanced Search',
value: 'query',
},
],
default: 'name',
description: 'Whether to search for the file/folder name or use a query string',
},
{
displayName: 'Search Query',
name: 'queryString',
type: 'string',
default: '',
displayOptions: {
show: {
searchMethod: ['name'],
},
},
placeholder: 'e.g. My File / My Folder',
description:
'The name of the file or folder to search for. Returns also files and folders whose names partially match this search term.',
},
{
displayName: 'Query String',
name: 'queryString',
type: 'string',
default: '',
displayOptions: {
show: {
searchMethod: ['query'],
},
},
placeholder: "e.g. not name contains 'hello'",
description:
'Use the Google query strings syntax to search for a specific set of files or folders. <a href="https://developers.google.com/drive/api/v3/search-files" target="_blank">Learn more</a>.',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
default: 50,
description: 'Max number of results to return',
typeOptions: {
minValue: 1,
},
displayOptions: {
show: {
returnAll: [false],
},
},
},
{
displayName: 'Filter',
name: 'filter',
type: 'collection',
placeholder: 'Add Filter',
default: {},
options: [
{
...driveRLC,
description:
'The drive you want to search in. By default, the personal "My Drive" is used.',
required: false,
},
{
...folderRLC,
description:
'The folder you want to search in. By default, the root folder of the drive is used. If you select a folder other than the root folder, only the direct children will be included.',
required: false,
},
{
displayName: 'What to Search',
name: 'whatToSearch',
type: 'options',
default: 'all',
options: [
{
name: 'Files and Folders',
value: 'all',
},
{
name: 'Files',
value: 'files',
},
{
name: 'Folders',
value: 'folders',
},
],
},
{
displayName: 'File Types',
name: 'fileTypes',
type: 'multiOptions',
default: [],
description: 'Return only items corresponding to the selected MIME types',
options: fileTypesOptions,
displayOptions: {
show: {
whatToSearch: ['all'],
},
},
},
{
displayName: 'File Types',
name: 'fileTypes',
type: 'multiOptions',
default: [],
description: 'Return only items corresponding to the selected MIME types',
options: fileTypesOptions.filter((option) => option.name !== 'Folder'),
displayOptions: {
show: {
whatToSearch: ['files'],
},
},
},
{
displayName: 'Include Trashed Items',
name: 'includeTrashed',
type: 'boolean',
default: false,
description: "Whether to return also items in the Drive's bin",
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Fields',
name: 'fields',
type: 'multiOptions',
options: [
{
name: '*',
value: '*',
description: 'All fields',
},
{
name: 'explicitlyTrashed',
value: 'explicitlyTrashed',
},
{
name: 'exportLinks',
value: 'exportLinks',
},
{
name: 'hasThumbnail',
value: 'hasThumbnail',
},
{
name: 'iconLink',
value: 'iconLink',
},
{
name: 'ID',
value: 'id',
},
{
name: 'Kind',
value: 'kind',
},
{
name: 'mimeType',
value: 'mimeType',
},
{
name: 'Name',
value: 'name',
},
{
name: 'Permissions',
value: 'permissions',
},
{
name: 'Shared',
value: 'shared',
},
{
name: 'Spaces',
value: 'spaces',
},
{
name: 'Starred',
value: 'starred',
},
{
name: 'thumbnailLink',
value: 'thumbnailLink',
},
{
name: 'Trashed',
value: 'trashed',
},
{
name: 'Version',
value: 'version',
},
{
name: 'webViewLink',
value: 'webViewLink',
},
],
default: [],
description: 'The fields to return',
},
],
},
];
const displayOptions = {
show: {
resource: ['fileFolder'],
operation: ['search'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const searchMethod = this.getNodeParameter('searchMethod', i) as string;
const options = this.getNodeParameter('options', i, {});
const query = [];
const queryString = this.getNodeParameter('queryString', i) as string;
if (searchMethod === 'name') {
query.push(`name contains '${queryString}'`);
} else {
query.push(queryString);
}
const filter = this.getNodeParameter('filter', i, {}) as SearchFilter;
let driveId = '';
let folderId = '';
const returnedTypes: string[] = [];
if (Object.keys(filter)?.length) {
if (filter.folderId) {
if (filter.folderId.mode === 'url') {
folderId = this.getNodeParameter('filter.folderId', i, undefined, {
extractValue: true,
}) as string;
} else {
folderId = filter.folderId.value;
}
}
if (folderId && folderId !== RLC_FOLDER_DEFAULT) {
query.push(`'${folderId}' in parents`);
}
if (filter.driveId) {
let value;
if (filter.driveId.mode === 'url') {
value = this.getNodeParameter('filter.driveId', i, undefined, {
extractValue: true,
}) as string;
} else {
value = filter.driveId.value;
}
driveId = value;
}
const whatToSearch = filter.whatToSearch || 'all';
if (whatToSearch === 'folders') {
query.push(`mimeType = '${DRIVE.FOLDER}'`);
} else {
if (whatToSearch === 'files') {
query.push(`mimeType != '${DRIVE.FOLDER}'`);
}
if (filter?.fileTypes?.length && !filter.fileTypes.includes('*')) {
filter.fileTypes.forEach((fileType: string) => {
returnedTypes.push(`mimeType = '${fileType}'`);
});
}
}
if (!filter.includeTrashed) {
query.push('trashed = false');
}
}
if (returnedTypes.length) {
query.push(`(${returnedTypes.join(' or ')})`);
}
const queryFields = prepareQueryString(options.fields as string[]);
const qs: IDataObject = {
fields: `nextPageToken, files(${queryFields})`,
q: query.filter((q) => q).join(' and '),
includeItemsFromAllDrives: true,
supportsAllDrives: true,
spaces: 'appDataFolder, drive',
corpora: 'allDrives',
};
updateDriveScopes(qs, driveId);
if (!driveId && folderId === RLC_FOLDER_DEFAULT) {
qs.corpora = 'user';
qs.spaces = 'drive';
qs.includeItemsFromAllDrives = false;
qs.supportsAllDrives = false;
}
const returnAll = this.getNodeParameter('returnAll', i, false);
let response;
if (returnAll) {
response = await googleApiRequestAllItems.call(this, 'GET', 'files', '/drive/v3/files', {}, qs);
} else {
qs.pageSize = this.getNodeParameter('limit', i);
response = await googleApiRequest.call(this, 'GET', '/drive/v3/files', undefined, qs);
response = response.files;
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response as IDataObject[]),
{ itemData: { item: i } },
);
return executionData;
}

View File

@@ -0,0 +1,45 @@
import type { INodeProperties } from 'n8n-workflow';
import * as create from './create.operation';
import * as deleteFolder from './deleteFolder.operation';
import * as share from './share.operation';
export { create, deleteFolder, share };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['folder'],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a folder',
action: 'Create folder',
},
{
name: 'Delete',
value: 'deleteFolder',
description: 'Permanently delete a folder',
action: 'Delete folder',
},
{
name: 'Share',
value: 'share',
description: 'Add sharing permissions to a folder',
action: 'Share folder',
},
],
default: 'create',
},
...create.description,
...deleteFolder.description,
...share.description,
];

View File

@@ -0,0 +1,109 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { googleApiRequest } from '../../transport';
import { driveRLC, folderRLC } from '../common.descriptions';
import { DRIVE } from '../../helpers/interfaces';
import { setParentFolder } from '../../helpers/utils';
const properties: INodeProperties[] = [
{
displayName: 'Folder Name',
name: 'name',
type: 'string',
default: '',
placeholder: 'e.g. New Folder',
description: "The name of the new folder. If not set, 'Untitled' will be used.",
},
{
...driveRLC,
displayName: 'Parent Drive',
description: 'The drive where to create the new folder',
},
{
...folderRLC,
displayName: 'Parent Folder',
description: 'The parent folder where to create the new folder',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Simplify Output',
name: 'simplifyOutput',
type: 'boolean',
default: true,
description: 'Whether to return a simplified version of the response instead of all fields',
},
{
displayName: 'Folder Color',
name: 'folderColorRgb',
type: 'color',
default: '',
description:
'The color of the folder as an RGB hex string. If an unsupported color is specified, the closest color in the palette will be used instead.',
},
],
},
];
const displayOptions = {
show: {
resource: ['folder'],
operation: ['create'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const name = (this.getNodeParameter('name', i) as string) || 'Untitled';
const driveId = this.getNodeParameter('driveId', i, undefined, {
extractValue: true,
}) as string;
const folderId = this.getNodeParameter('folderId', i, undefined, {
extractValue: true,
}) as string;
const body: IDataObject = {
name,
mimeType: DRIVE.FOLDER,
parents: [setParentFolder(folderId, driveId)],
};
const folderColorRgb =
(this.getNodeParameter('options.folderColorRgb', i, '') as string) || undefined;
if (folderColorRgb) {
body.folderColorRgb = folderColorRgb;
}
const simplifyOutput = this.getNodeParameter('options.simplifyOutput', i, true) as boolean;
let fields;
if (!simplifyOutput) {
fields = '*';
}
const qs = {
fields,
includeItemsFromAllDrives: true,
supportsAllDrives: true,
spaces: 'appDataFolder, drive',
corpora: 'allDrives',
};
const response = await googleApiRequest.call(this, 'POST', '/drive/v3/files', body, qs);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response as IDataObject[]),
{ itemData: { item: i } },
);
return executionData;
}

View File

@@ -0,0 +1,77 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { googleApiRequest } from '../../transport';
import { folderNoRootRLC } from '../common.descriptions';
const properties: INodeProperties[] = [
{
...folderNoRootRLC,
description: 'The folder to delete',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Delete Permanently',
name: 'deletePermanently',
type: 'boolean',
default: false,
description:
'Whether to delete the folder immediately. If false, the folder will be moved to the trash.',
},
],
},
];
const displayOptions = {
show: {
resource: ['folder'],
operation: ['deleteFolder'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const folderId = this.getNodeParameter('folderNoRootId', i, undefined, {
extractValue: true,
}) as string;
const deletePermanently = this.getNodeParameter('options.deletePermanently', i, false) as boolean;
const qs = {
supportsAllDrives: true,
};
if (deletePermanently) {
await googleApiRequest.call(this, 'DELETE', `/drive/v3/files/${folderId}`, undefined, qs);
} else {
await googleApiRequest.call(
this,
'PATCH',
`/drive/v3/files/${folderId}`,
{ trashed: true },
qs,
);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({
fileId: folderId,
success: true,
}),
{ itemData: { item: i } },
);
returnData.push(...executionData);
return returnData;
}

View File

@@ -0,0 +1,64 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '../../../../../../utils/utilities';
import { googleApiRequest } from '../../transport';
import { folderNoRootRLC, permissionsOptions, shareOptions } from '../common.descriptions';
const properties: INodeProperties[] = [
{
...folderNoRootRLC,
description: 'The folder to share',
},
permissionsOptions,
shareOptions,
];
const displayOptions = {
show: {
resource: ['folder'],
operation: ['share'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, i: number): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const folderId = this.getNodeParameter('folderNoRootId', i, undefined, {
extractValue: true,
}) as string;
const permissions = this.getNodeParameter('permissionsUi', i) as IDataObject;
const shareOption = this.getNodeParameter('options', i);
const body: IDataObject = {};
const qs: IDataObject = {
supportsAllDrives: true,
};
if (permissions.permissionsValues) {
Object.assign(body, permissions.permissionsValues);
}
Object.assign(qs, shareOption);
const response = await googleApiRequest.call(
this,
'POST',
`/drive/v3/files/${folderId}/permissions`,
body,
qs,
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(response as IDataObject[]),
{ itemData: { item: i } },
);
returnData.push(...executionData);
return returnData;
}

View File

@@ -0,0 +1,18 @@
import type { AllEntities } from 'n8n-workflow';
type NodeMap = {
drive: 'create' | 'deleteDrive' | 'get' | 'list' | 'update';
file:
| 'copy'
| 'createFromText'
| 'download'
| 'deleteFile'
| 'move'
| 'share'
| 'upload'
| 'update';
folder: 'create' | 'deleteFolder' | 'share';
fileFolder: 'search';
};
export type GoogleDriveType = AllEntities<NodeMap>;

View File

@@ -0,0 +1,55 @@
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import type { GoogleDriveType } from './node.type';
import * as drive from './drive/Drive.resource';
import * as file from './file/File.resource';
import * as fileFolder from './fileFolder/FileFolder.resource';
import * as folder from './folder/Folder.resource';
export async function router(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: INodeExecutionData[] = [];
const resource = this.getNodeParameter<GoogleDriveType>('resource', 0);
const operation = this.getNodeParameter('operation', 0);
const googleDrive = {
resource,
operation,
} as GoogleDriveType;
for (let i = 0; i < items.length; i++) {
try {
switch (googleDrive.resource) {
case 'drive':
returnData.push(...(await drive[googleDrive.operation].execute.call(this, i)));
break;
case 'file':
returnData.push(...(await file[googleDrive.operation].execute.call(this, i, items[i])));
break;
case 'fileFolder':
returnData.push(...(await fileFolder[googleDrive.operation].execute.call(this, i)));
break;
case 'folder':
returnData.push(...(await folder[googleDrive.operation].execute.call(this, i)));
break;
default:
throw new NodeOperationError(this.getNode(), `The resource "${resource}" is not known`);
}
} catch (error) {
if (this.continueOnFail()) {
if (resource === 'file' && operation === 'download') {
items[i].json = { error: error.message };
} else {
returnData.push({ json: { error: error.message } });
}
continue;
}
throw error;
}
}
return this.prepareOutputData(returnData);
}

View File

@@ -0,0 +1,90 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import type { INodeTypeDescription } from 'n8n-workflow';
import * as drive from './drive/Drive.resource';
import * as file from './file/File.resource';
import * as fileFolder from './fileFolder/FileFolder.resource';
import * as folder from './folder/Folder.resource';
export const versionDescription: INodeTypeDescription = {
displayName: 'Google Drive',
name: 'googleDrive',
icon: 'file:googleDrive.svg',
group: ['input'],
version: 3,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Access data on Google Drive',
defaults: {
name: 'Google Drive',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'googleApi',
required: true,
displayOptions: {
show: {
authentication: ['serviceAccount'],
},
},
},
{
name: 'googleDriveOAuth2Api',
required: true,
displayOptions: {
show: {
authentication: ['oAuth2'],
},
},
},
],
properties: [
{
displayName: 'Authentication',
name: 'authentication',
type: 'options',
options: [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'OAuth2 (recommended)',
value: 'oAuth2',
},
{
name: 'Service Account',
value: 'serviceAccount',
},
],
default: 'oAuth2',
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
noDataExpression: true,
options: [
{
name: 'File',
value: 'file',
},
{
name: 'File/Folder',
value: 'fileFolder',
},
{
name: 'Folder',
value: 'folder',
},
{
name: 'Shared Drive',
value: 'drive',
},
],
default: 'file',
},
...drive.description,
...file.description,
...fileFolder.description,
...folder.description,
],
};

View File

@@ -0,0 +1,37 @@
export const UPLOAD_CHUNK_SIZE = 256 * 1024;
export type SearchFilter = {
driveId?: {
value: string;
mode: string;
};
folderId?: {
value: string;
mode: string;
};
whatToSearch?: 'all' | 'files' | 'folders';
fileTypes?: string[];
includeTrashed?: boolean;
};
export const RLC_DRIVE_DEFAULT = 'My Drive';
export const RLC_FOLDER_DEFAULT = 'root';
export const enum DRIVE {
FOLDER = 'application/vnd.google-apps.folder',
AUDIO = 'application/vnd.google-apps.audio',
DOCUMENT = 'application/vnd.google-apps.document',
SDK = 'application/vnd.google-apps.drive-sdk',
DRAWING = 'application/vnd.google-apps.drawing',
FILE = 'application/vnd.google-apps.file',
FORM = 'application/vnd.google-apps.form',
FUSIONTABLE = 'application/vnd.google-apps.fusiontable',
MAP = 'application/vnd.google-apps.map',
PHOTO = 'application/vnd.google-apps.photo',
PRESENTATION = 'application/vnd.google-apps.presentation',
APP_SCRIPTS = 'application/vnd.google-apps.script',
SITES = 'application/vnd.google-apps.sites',
SPREADSHEET = 'application/vnd.google-apps.spreadsheet',
UNKNOWN = 'application/vnd.google-apps.unknown',
VIDEO = 'application/vnd.google-apps.video',
}

View File

@@ -0,0 +1,133 @@
import type { IDataObject, IExecuteFunctions } from 'n8n-workflow';
import { BINARY_ENCODING, NodeOperationError } from 'n8n-workflow';
import type { Readable } from 'stream';
import { RLC_DRIVE_DEFAULT, RLC_FOLDER_DEFAULT, UPLOAD_CHUNK_SIZE } from './interfaces';
export function prepareQueryString(fields: string[] | undefined) {
let queryFields = 'id, name';
if (fields) {
if (fields.includes('*')) {
queryFields = '*';
} else {
queryFields = fields.join(', ');
}
}
return queryFields;
}
export async function getItemBinaryData(
this: IExecuteFunctions,
inputDataFieldName: string,
i: number,
chunkSize = UPLOAD_CHUNK_SIZE,
) {
let contentLength: number;
let fileContent: Buffer | Readable;
let originalFilename: string | undefined;
let mimeType;
if (!inputDataFieldName) {
throw new NodeOperationError(
this.getNode(),
'The name of the input field containing the binary file data must be set',
{
itemIndex: i,
},
);
}
const binaryData = this.helpers.assertBinaryData(i, inputDataFieldName);
if (binaryData.id) {
// Stream data in 256KB chunks, and upload the via the resumable upload api
fileContent = this.helpers.getBinaryStream(binaryData.id, chunkSize);
const metadata = await this.helpers.getBinaryMetadata(binaryData.id);
contentLength = metadata.fileSize;
originalFilename = metadata.fileName;
if (metadata.mimeType) mimeType = binaryData.mimeType;
} else {
fileContent = Buffer.from(binaryData.data, BINARY_ENCODING);
contentLength = fileContent.length;
originalFilename = binaryData.fileName;
mimeType = binaryData.mimeType;
}
return {
contentLength,
fileContent,
originalFilename,
mimeType,
};
}
export function setFileProperties(body: IDataObject, options: IDataObject) {
if (options.propertiesUi) {
const values = ((options.propertiesUi as IDataObject).propertyValues as IDataObject[]) || [];
body.properties = values.reduce(
(acc, value) => Object.assign(acc, { [`${value.key}`]: value.value }),
{} as IDataObject,
);
}
if (options.appPropertiesUi) {
const values =
((options.appPropertiesUi as IDataObject).appPropertyValues as IDataObject[]) || [];
body.appProperties = values.reduce(
(acc, value) => Object.assign(acc, { [`${value.key}`]: value.value }),
{} as IDataObject,
);
}
return body;
}
export function setUpdateCommonParams(qs: IDataObject, options: IDataObject) {
if (options.keepRevisionForever) {
qs.keepRevisionForever = options.keepRevisionForever;
}
if (options.ocrLanguage) {
qs.ocrLanguage = options.ocrLanguage;
}
if (options.useContentAsIndexableText) {
qs.useContentAsIndexableText = options.useContentAsIndexableText;
}
return qs;
}
export function updateDriveScopes(
qs: IDataObject,
driveId: string,
defaultDrive = RLC_DRIVE_DEFAULT,
) {
if (driveId) {
if (driveId === defaultDrive) {
qs.includeItemsFromAllDrives = false;
qs.supportsAllDrives = false;
qs.spaces = 'appDataFolder, drive';
qs.corpora = 'user';
} else {
qs.driveId = driveId;
qs.corpora = 'drive';
}
}
}
export function setParentFolder(
folderId: string,
driveId: string,
folderIdDefault = RLC_FOLDER_DEFAULT,
driveIdDefault = RLC_DRIVE_DEFAULT,
) {
if (folderId !== folderIdDefault) {
return folderId;
} else if (driveId && driveId !== driveIdDefault) {
return driveId;
} else {
return 'root';
}
}

View File

@@ -0,0 +1 @@
export * as listSearch from './listSearch';

View File

@@ -0,0 +1,199 @@
import type {
IDataObject,
ILoadOptionsFunctions,
INodeListSearchItems,
INodeListSearchResult,
} from 'n8n-workflow';
import { googleApiRequest } from '../transport';
import type { SearchFilter } from '../helpers/interfaces';
import { DRIVE, RLC_DRIVE_DEFAULT, RLC_FOLDER_DEFAULT } from '../helpers/interfaces';
import { updateDriveScopes } from '../helpers/utils';
interface FilesItem {
id: string;
name: string;
mimeType: string;
webViewLink: string;
}
interface DriveItem {
id: string;
name: string;
}
export async function fileSearch(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const query: string[] = ['trashed = false'];
if (filter) {
query.push(`name contains '${filter.replace("'", "\\'")}'`);
}
query.push(`mimeType != '${DRIVE.FOLDER}'`);
const res = await googleApiRequest.call(this, 'GET', '/drive/v3/files', undefined, {
q: query.join(' and '),
pageToken: paginationToken,
fields: 'nextPageToken,files(id,name,mimeType,webViewLink)',
orderBy: 'name_natural',
includeItemsFromAllDrives: true,
supportsAllDrives: true,
spaces: 'appDataFolder, drive',
corpora: 'allDrives',
});
return {
results: res.files.map((file: FilesItem) => ({
name: file.name,
value: file.id,
url: file.webViewLink,
})),
paginationToken: res.nextPageToken,
};
}
export async function driveSearch(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
let res = { drives: [], nextPageToken: undefined };
res = await googleApiRequest.call(this, 'GET', '/drive/v3/drives', undefined, {
q: filter ? `name contains '${filter.replace("'", "\\'")}'` : undefined,
pageToken: paginationToken,
});
const results: INodeListSearchItems[] = [];
res.drives.forEach((drive: DriveItem) => {
results.push({
name: drive.name,
value: drive.id,
url: `https://drive.google.com/drive/folders/${drive.id}`,
});
});
return {
results,
paginationToken: res.nextPageToken,
};
}
export async function driveSearchWithDefault(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const drives = await driveSearch.call(this, filter, paginationToken);
let results: INodeListSearchItems[] = [];
if (filter && !RLC_DRIVE_DEFAULT.toLowerCase().includes(filter.toLowerCase())) {
results = drives.results;
} else {
results = [
{
name: RLC_DRIVE_DEFAULT,
value: RLC_DRIVE_DEFAULT,
url: 'https://drive.google.com/drive/my-drive',
},
...drives.results,
];
}
return {
results,
paginationToken: drives.paginationToken,
};
}
export async function folderSearch(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const query: string[] = [];
if (filter) {
query.push(`name contains '${filter.replace("'", "\\'")}'`);
}
query.push(`mimeType = '${DRIVE.FOLDER}'`);
const qs: IDataObject = {
q: query.join(' and '),
pageToken: paginationToken,
fields: 'nextPageToken,files(id,name,mimeType,webViewLink,parents,driveId)',
orderBy: 'name_natural',
includeItemsFromAllDrives: true,
supportsAllDrives: true,
spaces: 'appDataFolder, drive',
corpora: 'allDrives',
};
let driveId;
driveId = this.getNodeParameter('driveId', '') as IDataObject;
if (!driveId) {
const searchFilter = this.getNodeParameter('filter', {}) as SearchFilter;
if (searchFilter?.driveId?.mode === 'url') {
searchFilter.driveId.value = this.getNodeParameter('filter.folderId', undefined, {
extractValue: true,
}) as string;
}
driveId = searchFilter.driveId;
}
updateDriveScopes(qs, driveId?.value as string);
const res = await googleApiRequest.call(this, 'GET', '/drive/v3/files', undefined, qs);
const results: INodeListSearchItems[] = [];
res.files.forEach((i: FilesItem) => {
results.push({
name: i.name,
value: i.id,
url: i.webViewLink,
});
});
return {
results,
paginationToken: res.nextPageToken,
};
}
export async function folderSearchWithDefault(
this: ILoadOptionsFunctions,
filter?: string,
paginationToken?: string,
): Promise<INodeListSearchResult> {
const folders = await folderSearch.call(this, filter, paginationToken);
let results: INodeListSearchItems[] = [];
const rootDefaultDisplayName = '/ (Root folder)';
if (
filter &&
!(
RLC_FOLDER_DEFAULT.toLowerCase().includes(filter.toLowerCase()) ||
rootDefaultDisplayName.toLowerCase().includes(filter.toLowerCase())
)
) {
results = folders.results;
} else {
results = [
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: rootDefaultDisplayName,
value: RLC_FOLDER_DEFAULT,
url: 'https://drive.google.com/drive',
},
...folders.results,
];
}
return {
results,
paginationToken: folders.paginationToken,
};
}

View File

@@ -0,0 +1,111 @@
import type {
IExecuteFunctions,
IExecuteSingleFunctions,
ILoadOptionsFunctions,
IDataObject,
IPollFunctions,
JsonObject,
IHttpRequestOptions,
IHttpRequestMethods,
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';
import { getGoogleAccessToken } from '../../../GenericFunctions';
export async function googleApiRequest(
this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions,
method: IHttpRequestMethods,
resource: string,
body: IDataObject | string | Buffer = {},
qs: IDataObject = {},
uri?: string,
option: IDataObject = {},
) {
const authenticationMethod = this.getNodeParameter(
'authentication',
0,
'serviceAccount',
) as string;
let options: IHttpRequestOptions = {
headers: {
'Content-Type': 'application/json',
},
method,
body,
qs,
url: uri || `https://www.googleapis.com${resource}`,
json: true,
};
options = Object.assign({}, options, option);
try {
if (Object.keys(body).length === 0) {
delete options.body;
}
if (authenticationMethod === 'serviceAccount') {
const credentials = await this.getCredentials('googleApi');
const { access_token } = await getGoogleAccessToken.call(this, credentials, 'drive');
options.headers!.Authorization = `Bearer ${access_token}`;
return await this.helpers.httpRequest(options);
} else {
return await this.helpers.requestOAuth2.call(this, 'googleDriveOAuth2Api', options);
}
} catch (error) {
if (error.code === 'ERR_OSSL_PEM_NO_START_LINE') {
error.statusCode = '401';
}
const apiError = new NodeApiError(
this.getNode(),
{
reason: error.error,
} as JsonObject,
{ httpCode: String(error.statusCode) },
);
if (
apiError.message &&
apiError.description &&
(apiError.message.toLowerCase().includes('bad request') ||
apiError.message.toLowerCase().includes('forbidden') ||
apiError.message.toUpperCase().includes('UNKNOWN ERROR'))
) {
const message = apiError.message;
apiError.message = apiError.description;
apiError.description = message;
}
throw apiError;
}
}
export async function googleApiRequestAllItems(
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
method: IHttpRequestMethods,
propertyName: string,
endpoint: string,
body: IDataObject = {},
query: IDataObject = {},
) {
const returnData: IDataObject[] = [];
let responseData;
query.maxResults = query.maxResults || 100;
query.pageSize = query.pageSize || 100;
do {
responseData = await googleApiRequest.call(this, method, endpoint, body, query);
returnData.push.apply(returnData, responseData[propertyName] as IDataObject[]);
if (responseData.nextPageToken) {
query.pageToken = responseData.nextPageToken as string;
}
} while (responseData.nextPageToken !== undefined && responseData.nextPageToken !== '');
return returnData;
}

View File

@@ -6,8 +6,9 @@ import type {
INodeType,
INodeTypeDescription,
JsonObject,
IRequestOptionsSimplified,
} from 'n8n-workflow';
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
import { NodeApiError, NodeOperationError, jsonParse } from 'n8n-workflow';
import type { OptionsWithUri } from 'request';
import type { RequestPromiseOptions } from 'request-promise-native';
@@ -36,6 +37,15 @@ export class GraphQL implements INodeType {
},
},
},
{
name: 'httpCustomAuth',
required: true,
displayOptions: {
show: {
authentication: ['customAuth'],
},
},
},
{
name: 'httpDigestAuth',
required: true,
@@ -92,6 +102,10 @@ export class GraphQL implements INodeType {
name: 'Basic Auth',
value: 'basicAuth',
},
{
name: 'Custom Auth',
value: 'customAuth',
},
{
name: 'Digest Auth',
value: 'digestAuth',
@@ -284,6 +298,7 @@ export class GraphQL implements INodeType {
const items = this.getInputData();
let httpBasicAuth;
let httpDigestAuth;
let httpCustomAuth;
let httpHeaderAuth;
let httpQueryAuth;
let oAuth1Api;
@@ -294,6 +309,11 @@ export class GraphQL implements INodeType {
} catch (error) {
// Do nothing
}
try {
httpCustomAuth = await this.getCredentials('httpCustomAuth');
} catch (error) {
// Do nothing
}
try {
httpDigestAuth = await this.getCredentials('httpDigestAuth');
} catch (error) {
@@ -361,6 +381,21 @@ export class GraphQL implements INodeType {
pass: httpBasicAuth.password as string,
};
}
if (httpCustomAuth !== undefined) {
const customAuth = jsonParse<IRequestOptionsSimplified>(
(httpCustomAuth.json as string) || '{}',
{ errorMessage: 'Invalid Custom Auth JSON' },
);
if (customAuth.headers) {
requestOptions.headers = { ...requestOptions.headers, ...customAuth.headers };
}
if (customAuth.body) {
requestOptions.body = { ...requestOptions.body, ...customAuth.body };
}
if (customAuth.qs) {
requestOptions.qs = { ...requestOptions.qs, ...customAuth.qs };
}
}
if (httpHeaderAuth !== undefined) {
requestOptions.headers![httpHeaderAuth.name as string] = httpHeaderAuth.value;
}
@@ -387,6 +422,7 @@ export class GraphQL implements INodeType {
} else {
if (requestFormat === 'json') {
requestOptions.body = {
...requestOptions.body,
query: gqlQuery,
variables: this.getNodeParameter('variables', itemIndex, {}) as object,
operationName: this.getNodeParameter('operationName', itemIndex) as string,

View File

@@ -8,6 +8,7 @@ import type {
INodeType,
INodeTypeBaseDescription,
INodeTypeDescription,
IRequestOptionsSimplified,
JsonObject,
} from 'n8n-workflow';
@@ -969,6 +970,7 @@ export class HttpRequestV3 implements INodeType {
let httpDigestAuth;
let httpHeaderAuth;
let httpQueryAuth;
let httpCustomAuth;
let oAuth1Api;
let oAuth2Api;
let nodeCredentialType;
@@ -992,6 +994,10 @@ export class HttpRequestV3 implements INodeType {
try {
httpQueryAuth = await this.getCredentials('httpQueryAuth');
} catch {}
} else if (genericAuthType === 'httpCustomAuth') {
try {
httpCustomAuth = await this.getCredentials('httpCustomAuth');
} catch {}
} else if (genericAuthType === 'oAuth1Api') {
try {
oAuth1Api = await this.getCredentials('oAuth1Api');
@@ -1345,6 +1351,24 @@ export class HttpRequestV3 implements INodeType {
};
authDataKeys.auth = ['pass'];
}
if (httpCustomAuth !== undefined) {
const customAuth = jsonParse<IRequestOptionsSimplified>(
(httpCustomAuth.json as string) || '{}',
{ errorMessage: 'Invalid Custom Auth JSON' },
);
if (customAuth.headers) {
requestOptions.headers = { ...requestOptions.headers, ...customAuth.headers };
authDataKeys.headers = Object.keys(customAuth.headers);
}
if (customAuth.body) {
requestOptions.body = { ...requestOptions.body, ...customAuth.body };
authDataKeys.body = Object.keys(customAuth.body);
}
if (customAuth.qs) {
requestOptions.qs = { ...requestOptions.qs, ...customAuth.qs };
authDataKeys.qs = Object.keys(customAuth.qs);
}
}
if (requestOptions.headers!.accept === undefined) {
if (responseFormat === 'json') {

View File

@@ -2,8 +2,8 @@ import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow'
import { VersionedNodeType } from 'n8n-workflow';
import { ItemListsV1 } from './V1/ItemListsV1.node';
import { ItemListsV2 } from './V2/ItemListsV2.node';
import { ItemListsV3 } from './V3/ItemListsV3.node';
export class ItemLists extends VersionedNodeType {
constructor() {
@@ -14,7 +14,7 @@ export class ItemLists extends VersionedNodeType {
group: ['input'],
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Helper for working with lists of items and transforming arrays',
defaultVersion: 2.2,
defaultVersion: 3,
};
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
@@ -22,6 +22,7 @@ export class ItemLists extends VersionedNodeType {
2: new ItemListsV2(baseDescription),
2.1: new ItemListsV2(baseDescription),
2.2: new ItemListsV2(baseDescription),
3: new ItemListsV3(baseDescription),
};
super(nodeVersions, baseDescription);

View File

@@ -0,0 +1,24 @@
import type {
IExecuteFunctions,
INodeType,
INodeTypeBaseDescription,
INodeTypeDescription,
} from 'n8n-workflow';
import { versionDescription } from './actions/versionDescription';
import { router } from './actions/router';
export class ItemListsV3 implements INodeType {
description: INodeTypeDescription;
constructor(baseDescription: INodeTypeBaseDescription) {
this.description = {
...baseDescription,
...versionDescription,
};
}
async execute(this: IExecuteFunctions) {
return router.call(this);
}
}

View File

@@ -0,0 +1,10 @@
import type { INodeProperties } from 'n8n-workflow';
export const disableDotNotationBoolean: INodeProperties = {
displayName: 'Disable Dot Notation',
name: 'disableDotNotation',
type: 'boolean',
default: false,
description:
'Whether to disallow referencing child fields using `parent.child` in the field name',
};

View File

@@ -0,0 +1,349 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import get from 'lodash/get';
import isEmpty from 'lodash/isEmpty';
import set from 'lodash/set';
import { prepareFieldsArray } from '../../helpers/utils';
import { disableDotNotationBoolean } from '../common.descriptions';
const properties: INodeProperties[] = [
{
displayName: 'Aggregate',
name: 'aggregate',
type: 'options',
default: 'aggregateIndividualFields',
options: [
{
name: 'Individual Fields',
value: 'aggregateIndividualFields',
},
{
name: 'All Item Data (Into a Single List)',
value: 'aggregateAllItemData',
},
],
},
{
displayName: 'Fields To Aggregate',
name: 'fieldsToAggregate',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
placeholder: 'Add Field To Aggregate',
default: { fieldToAggregate: [{ fieldToAggregate: '', renameField: false }] },
displayOptions: {
show: {
aggregate: ['aggregateIndividualFields'],
},
},
options: [
{
displayName: '',
name: 'fieldToAggregate',
values: [
{
displayName: 'Input Field Name',
name: 'fieldToAggregate',
type: 'string',
default: '',
description: 'The name of a field in the input items to aggregate together',
// eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id
placeholder: 'e.g. id',
hint: ' Enter the field name as text',
requiresDataPath: 'single',
},
{
displayName: 'Rename Field',
name: 'renameField',
type: 'boolean',
default: false,
description: 'Whether to give the field a different name in the output',
},
{
displayName: 'Output Field Name',
name: 'outputFieldName',
displayOptions: {
show: {
renameField: [true],
},
},
type: 'string',
default: '',
description:
'The name of the field to put the aggregated data in. Leave blank to use the input field name.',
requiresDataPath: 'single',
},
],
},
],
},
{
displayName: 'Put Output in Field',
name: 'destinationFieldName',
type: 'string',
displayOptions: {
show: {
aggregate: ['aggregateAllItemData'],
},
},
default: 'data',
description: 'The name of the output field to put the data in',
},
{
displayName: 'Include',
name: 'include',
type: 'options',
default: 'allFields',
options: [
{
name: 'All Fields',
value: 'allFields',
},
{
name: 'Specified Fields',
value: 'specifiedFields',
},
{
name: 'All Fields Except',
value: 'allFieldsExcept',
},
],
displayOptions: {
show: {
aggregate: ['aggregateAllItemData'],
},
},
},
{
displayName: 'Fields To Exclude',
name: 'fieldsToExclude',
type: 'string',
placeholder: 'e.g. email, name',
default: '',
requiresDataPath: 'multiple',
displayOptions: {
show: {
aggregate: ['aggregateAllItemData'],
include: ['allFieldsExcept'],
},
},
},
{
displayName: 'Fields To Include',
name: 'fieldsToInclude',
type: 'string',
placeholder: 'e.g. email, name',
default: '',
requiresDataPath: 'multiple',
displayOptions: {
show: {
aggregate: ['aggregateAllItemData'],
include: ['specifiedFields'],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
hide: {
aggregate: ['aggregateAllItemData'],
},
},
options: [
disableDotNotationBoolean,
{
displayName: 'Merge Lists',
name: 'mergeLists',
type: 'boolean',
default: false,
description:
'Whether to merge the output into a single flat list (rather than a list of lists), if the field to aggregate is a list',
},
{
displayName: 'Keep Missing And Null Values',
name: 'keepMissing',
type: 'boolean',
default: false,
description:
'Whether to add a null entry to the aggregated list when there is a missing or null value',
},
],
},
];
const displayOptions = {
show: {
resource: ['itemList'],
operation: ['concatenateItems'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
items: INodeExecutionData[],
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
const aggregate = this.getNodeParameter('aggregate', 0, '') as string;
if (aggregate === 'aggregateIndividualFields') {
const disableDotNotation = this.getNodeParameter(
'options.disableDotNotation',
0,
false,
) as boolean;
const mergeLists = this.getNodeParameter('options.mergeLists', 0, false) as boolean;
const fieldsToAggregate = this.getNodeParameter(
'fieldsToAggregate.fieldToAggregate',
0,
[],
) as [{ fieldToAggregate: string; renameField: boolean; outputFieldName: string }];
const keepMissing = this.getNodeParameter('options.keepMissing', 0, false) as boolean;
if (!fieldsToAggregate.length) {
throw new NodeOperationError(this.getNode(), 'No fields specified', {
description: 'Please add a field to aggregate',
});
}
const newItem: INodeExecutionData = {
json: {},
pairedItem: Array.from({ length: items.length }, (_, i) => i).map((index) => {
return {
item: index,
};
}),
};
const values: { [key: string]: any } = {};
const outputFields: string[] = [];
for (const { fieldToAggregate, outputFieldName, renameField } of fieldsToAggregate) {
const field = renameField ? outputFieldName : fieldToAggregate;
if (outputFields.includes(field)) {
throw new NodeOperationError(
this.getNode(),
`The '${field}' output field is used more than once`,
{ description: 'Please make sure each output field name is unique' },
);
} else {
outputFields.push(field);
}
const getFieldToAggregate = () =>
!disableDotNotation && fieldToAggregate.includes('.')
? fieldToAggregate.split('.').pop()
: fieldToAggregate;
const _outputFieldName = outputFieldName
? outputFieldName
: (getFieldToAggregate() as string);
if (fieldToAggregate !== '') {
values[_outputFieldName] = [];
for (let i = 0; i < items.length; i++) {
if (!disableDotNotation) {
let value = get(items[i].json, fieldToAggregate);
if (!keepMissing) {
if (Array.isArray(value)) {
value = value.filter((entry) => entry !== null);
} else if (value === null || value === undefined) {
continue;
}
}
if (Array.isArray(value) && mergeLists) {
values[_outputFieldName].push(...value);
} else {
values[_outputFieldName].push(value);
}
} else {
let value = items[i].json[fieldToAggregate];
if (!keepMissing) {
if (Array.isArray(value)) {
value = value.filter((entry) => entry !== null);
} else if (value === null || value === undefined) {
continue;
}
}
if (Array.isArray(value) && mergeLists) {
values[_outputFieldName].push(...value);
} else {
values[_outputFieldName].push(value);
}
}
}
}
}
for (const key of Object.keys(values)) {
if (!disableDotNotation) {
set(newItem.json, key, values[key]);
} else {
newItem.json[key] = values[key];
}
}
returnData.push(newItem);
} else {
let newItems: IDataObject[] = items.map((item) => item.json);
const destinationFieldName = this.getNodeParameter('destinationFieldName', 0) as string;
const fieldsToExclude = prepareFieldsArray(
this.getNodeParameter('fieldsToExclude', 0, '') as string,
'Fields To Exclude',
);
const fieldsToInclude = prepareFieldsArray(
this.getNodeParameter('fieldsToInclude', 0, '') as string,
'Fields To Include',
);
if (fieldsToExclude.length || fieldsToInclude.length) {
newItems = newItems.reduce((acc, item) => {
const newItem: IDataObject = {};
let outputFields = Object.keys(item);
if (fieldsToExclude.length) {
outputFields = outputFields.filter((key) => !fieldsToExclude.includes(key));
}
if (fieldsToInclude.length) {
outputFields = outputFields.filter((key) =>
fieldsToInclude.length ? fieldsToInclude.includes(key) : true,
);
}
outputFields.forEach((key) => {
newItem[key] = item[key];
});
if (isEmpty(newItem)) {
return acc;
}
return acc.concat([newItem]);
}, [] as IDataObject[]);
}
const output: INodeExecutionData = { json: { [destinationFieldName]: newItems } };
returnData.push(output);
}
return returnData;
}

View File

@@ -0,0 +1,70 @@
import type { INodeProperties } from 'n8n-workflow';
import * as concatenateItems from './concatenateItems.operation';
import * as limit from './limit.operation';
import * as removeDuplicates from './removeDuplicates.operation';
import * as sort from './sort.operation';
import * as splitOutItems from './splitOutItems.operation';
import * as summarize from './summarize.operation';
export { concatenateItems, limit, removeDuplicates, sort, splitOutItems, summarize };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['itemList'],
},
},
options: [
{
name: 'Concatenate Items',
value: 'concatenateItems',
description: 'Combine fields into a list in a single new item',
action: 'Concatenate Items',
},
{
name: 'Limit',
value: 'limit',
description: 'Remove items if there are too many',
action: 'Limit',
},
{
name: 'Remove Duplicates',
value: 'removeDuplicates',
description: 'Remove extra items that are similar',
action: 'Remove Duplicates',
},
{
name: 'Sort',
value: 'sort',
description: 'Change the item order',
action: 'Sort',
},
{
name: 'Split Out Items',
value: 'splitOutItems',
description:
"Turn a list or values of object's properties inside item(s) into separate items",
action: 'Split Out Items',
},
{
name: 'Summarize',
value: 'summarize',
description: 'Aggregate items together (pivot table)',
action: 'Summarize',
},
],
default: 'splitOutItems',
},
...concatenateItems.description,
...limit.description,
...removeDuplicates.description,
...sort.description,
...splitOutItems.description,
...summarize.description,
];

View File

@@ -0,0 +1,62 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
const properties: INodeProperties[] = [
{
displayName: 'Max Items',
name: 'maxItems',
type: 'number',
typeOptions: {
minValue: 1,
},
default: 1,
description: 'If there are more items than this number, some are removed',
},
{
displayName: 'Keep',
name: 'keep',
type: 'options',
options: [
{
name: 'First Items',
value: 'firstItems',
},
{
name: 'Last Items',
value: 'lastItems',
},
],
default: 'firstItems',
description: 'When removing items, whether to keep the ones at the start or the ending',
},
];
const displayOptions = {
show: {
resource: ['itemList'],
operation: ['limit'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
items: INodeExecutionData[],
): Promise<INodeExecutionData[]> {
let returnData = items;
const maxItems = this.getNodeParameter('maxItems', 0) as number;
const keep = this.getNodeParameter('keep', 0) as string;
if (maxItems > items.length) {
return returnData;
}
if (keep === 'firstItems') {
returnData = items.slice(0, maxItems);
} else {
returnData = items.slice(items.length - maxItems, items.length);
}
return returnData;
}

View File

@@ -0,0 +1,246 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';
import lt from 'lodash/lt';
import pick from 'lodash/pick';
import { compareItems, flattenKeys, prepareFieldsArray } from '../../helpers/utils';
import { disableDotNotationBoolean } from '../common.descriptions';
const properties: INodeProperties[] = [
{
displayName: 'Compare',
name: 'compare',
type: 'options',
options: [
{
name: 'All Fields',
value: 'allFields',
},
{
name: 'All Fields Except',
value: 'allFieldsExcept',
},
{
name: 'Selected Fields',
value: 'selectedFields',
},
],
default: 'allFields',
description: 'The fields of the input items to compare to see if they are the same',
},
{
displayName: 'Fields To Exclude',
name: 'fieldsToExclude',
type: 'string',
placeholder: 'e.g. email, name',
requiresDataPath: 'multiple',
description: 'Fields in the input to exclude from the comparison',
default: '',
displayOptions: {
show: {
compare: ['allFieldsExcept'],
},
},
},
{
displayName: 'Fields To Compare',
name: 'fieldsToCompare',
type: 'string',
placeholder: 'e.g. email, name',
requiresDataPath: 'multiple',
description: 'Fields in the input to add to the comparison',
default: '',
displayOptions: {
show: {
compare: ['selectedFields'],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
compare: ['allFieldsExcept', 'selectedFields'],
},
},
options: [
disableDotNotationBoolean,
{
displayName: 'Remove Other Fields',
name: 'removeOtherFields',
type: 'boolean',
default: false,
description:
'Whether to remove any fields that are not being compared. If disabled, will keep the values from the first of the duplicates.',
},
],
},
];
const displayOptions = {
show: {
resource: ['itemList'],
operation: ['removeDuplicates'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
items: INodeExecutionData[],
): Promise<INodeExecutionData[]> {
const compare = this.getNodeParameter('compare', 0) as string;
const disableDotNotation = this.getNodeParameter(
'options.disableDotNotation',
0,
false,
) as boolean;
const removeOtherFields = this.getNodeParameter('options.removeOtherFields', 0, false) as boolean;
let keys = disableDotNotation
? Object.keys(items[0].json)
: Object.keys(flattenKeys(items[0].json));
for (const item of items) {
for (const key of disableDotNotation
? Object.keys(item.json)
: Object.keys(flattenKeys(item.json))) {
if (!keys.includes(key)) {
keys.push(key);
}
}
}
if (compare === 'allFieldsExcept') {
const fieldsToExclude = prepareFieldsArray(
this.getNodeParameter('fieldsToExclude', 0, '') as string,
'Fields To Exclude',
);
if (!fieldsToExclude.length) {
throw new NodeOperationError(
this.getNode(),
'No fields specified. Please add a field to exclude from comparison',
);
}
if (!disableDotNotation) {
keys = Object.keys(flattenKeys(items[0].json));
}
keys = keys.filter((key) => !fieldsToExclude.includes(key));
}
if (compare === 'selectedFields') {
const fieldsToCompare = prepareFieldsArray(
this.getNodeParameter('fieldsToCompare', 0, '') as string,
'Fields To Compare',
);
if (!fieldsToCompare.length) {
throw new NodeOperationError(
this.getNode(),
'No fields specified. Please add a field to compare on',
);
}
if (!disableDotNotation) {
keys = Object.keys(flattenKeys(items[0].json));
}
keys = fieldsToCompare.map((key) => key.trim());
}
// This solution is O(nlogn)
// add original index to the items
const newItems = items.map(
(item, index) =>
({
json: { ...item.json, __INDEX: index },
pairedItem: { item: index },
} as INodeExecutionData),
);
//sort items using the compare keys
newItems.sort((a, b) => {
let result = 0;
for (const key of keys) {
let equal;
if (!disableDotNotation) {
equal = isEqual(get(a.json, key), get(b.json, key));
} else {
equal = isEqual(a.json[key], b.json[key]);
}
if (!equal) {
let lessThan;
if (!disableDotNotation) {
lessThan = lt(get(a.json, key), get(b.json, key));
} else {
lessThan = lt(a.json[key], b.json[key]);
}
result = lessThan ? -1 : 1;
break;
}
}
return result;
});
for (const key of keys) {
let type: any = undefined;
for (const item of newItems) {
if (key === '') {
throw new NodeOperationError(this.getNode(), 'Name of field to compare is blank');
}
const value = !disableDotNotation ? get(item.json, key) : item.json[key];
if (value === undefined && disableDotNotation && key.includes('.')) {
throw new NodeOperationError(
this.getNode(),
`'${key}' field is missing from some input items`,
{
description:
"If you're trying to use a nested field, make sure you turn off 'disable dot notation' in the node options",
},
);
} else if (value === undefined) {
throw new NodeOperationError(
this.getNode(),
`'${key}' field is missing from some input items`,
);
}
if (type !== undefined && value !== undefined && type !== typeof value) {
throw new NodeOperationError(this.getNode(), `'${key}' isn't always the same type`, {
description: 'The type of this field varies between items',
});
} else {
type = typeof value;
}
}
}
// collect the original indexes of items to be removed
const removedIndexes: number[] = [];
let temp = newItems[0];
for (let index = 1; index < newItems.length; index++) {
if (compareItems(newItems[index], temp, keys, disableDotNotation, this.getNode())) {
removedIndexes.push(newItems[index].json.__INDEX as unknown as number);
} else {
temp = newItems[index];
}
}
let returnData = items.filter((_, index) => !removedIndexes.includes(index));
if (removeOtherFields) {
returnData = returnData.map((item, index) => ({
json: pick(item.json, ...keys),
pairedItem: { item: index },
}));
}
return returnData;
}

View File

@@ -0,0 +1,303 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import type { NodeVMOptions } from 'vm2';
import { NodeVM } from 'vm2';
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';
import lt from 'lodash/lt';
import { shuffleArray } from '../../helpers/utils';
import { disableDotNotationBoolean } from '../common.descriptions';
const properties: INodeProperties[] = [
{
displayName: 'Type',
name: 'type',
type: 'options',
options: [
{
name: 'Simple',
value: 'simple',
},
{
name: 'Random',
value: 'random',
},
{
name: 'Code',
value: 'code',
},
],
default: 'simple',
description: 'The fields of the input items to compare to see if they are the same',
},
{
displayName: 'Fields To Sort By',
name: 'sortFieldsUi',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
placeholder: 'Add Field To Sort By',
options: [
{
displayName: '',
name: 'sortField',
values: [
{
displayName: 'Field Name',
name: 'fieldName',
type: 'string',
required: true,
default: '',
description: 'The field to sort by',
// eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id
placeholder: 'e.g. id',
hint: ' Enter the field name as text',
requiresDataPath: 'single',
},
{
displayName: 'Order',
name: 'order',
type: 'options',
options: [
{
name: 'Ascending',
value: 'ascending',
},
{
name: 'Descending',
value: 'descending',
},
],
default: 'ascending',
description: 'The order to sort by',
},
],
},
],
default: {},
description: 'The fields of the input items to compare to see if they are the same',
displayOptions: {
show: {
type: ['simple'],
},
},
},
{
displayName: 'Code',
name: 'code',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
editor: 'code',
rows: 10,
},
default: `// The two items to compare are in the variables a and b
// Access the fields in a.json and b.json
// Return -1 if a should go before b
// Return 1 if b should go before a
// Return 0 if there's no difference
fieldName = 'myField';
if (a.json[fieldName] < b.json[fieldName]) {
return -1;
}
if (a.json[fieldName] > b.json[fieldName]) {
return 1;
}
return 0;`,
description: 'Javascript code to determine the order of any two items',
displayOptions: {
show: {
type: ['code'],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
type: ['simple'],
},
},
options: [disableDotNotationBoolean],
},
];
const displayOptions = {
show: {
resource: ['itemList'],
operation: ['sort'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
items: INodeExecutionData[],
): Promise<INodeExecutionData[]> {
let returnData = [...items];
const type = this.getNodeParameter('type', 0) as string;
const disableDotNotation = this.getNodeParameter(
'options.disableDotNotation',
0,
false,
) as boolean;
if (type === 'random') {
shuffleArray(returnData);
return returnData;
}
if (type === 'simple') {
const sortFieldsUi = this.getNodeParameter('sortFieldsUi', 0) as IDataObject;
const sortFields = sortFieldsUi.sortField as Array<{
fieldName: string;
order: 'ascending' | 'descending';
}>;
if (!sortFields?.length) {
throw new NodeOperationError(
this.getNode(),
'No sorting specified. Please add a field to sort by',
);
}
for (const { fieldName } of sortFields) {
let found = false;
for (const item of items) {
if (!disableDotNotation) {
if (get(item.json, fieldName) !== undefined) {
found = true;
}
} else if (item.json.hasOwnProperty(fieldName)) {
found = true;
}
}
if (!found && disableDotNotation && fieldName.includes('.')) {
throw new NodeOperationError(
this.getNode(),
`Couldn't find the field '${fieldName}' in the input data`,
{
description:
"If you're trying to use a nested field, make sure you turn off 'disable dot notation' in the node options",
},
);
} else if (!found) {
throw new NodeOperationError(
this.getNode(),
`Couldn't find the field '${fieldName}' in the input data`,
);
}
}
const sortFieldsWithDirection = sortFields.map((field) => ({
name: field.fieldName,
dir: field.order === 'ascending' ? 1 : -1,
}));
returnData.sort((a, b) => {
let result = 0;
for (const field of sortFieldsWithDirection) {
let equal;
if (!disableDotNotation) {
const _a =
typeof get(a.json, field.name) === 'string'
? (get(a.json, field.name) as string).toLowerCase()
: get(a.json, field.name);
const _b =
typeof get(b.json, field.name) === 'string'
? (get(b.json, field.name) as string).toLowerCase()
: get(b.json, field.name);
equal = isEqual(_a, _b);
} else {
const _a =
typeof a.json[field.name] === 'string'
? (a.json[field.name] as string).toLowerCase()
: a.json[field.name];
const _b =
typeof b.json[field.name] === 'string'
? (b.json[field.name] as string).toLowerCase()
: b.json[field.name];
equal = isEqual(_a, _b);
}
if (!equal) {
let lessThan;
if (!disableDotNotation) {
const _a =
typeof get(a.json, field.name) === 'string'
? (get(a.json, field.name) as string).toLowerCase()
: get(a.json, field.name);
const _b =
typeof get(b.json, field.name) === 'string'
? (get(b.json, field.name) as string).toLowerCase()
: get(b.json, field.name);
lessThan = lt(_a, _b);
} else {
const _a =
typeof a.json[field.name] === 'string'
? (a.json[field.name] as string).toLowerCase()
: a.json[field.name];
const _b =
typeof b.json[field.name] === 'string'
? (b.json[field.name] as string).toLowerCase()
: b.json[field.name];
lessThan = lt(_a, _b);
}
if (lessThan) {
result = -1 * field.dir;
} else {
result = 1 * field.dir;
}
break;
}
}
return result;
});
} else {
const code = this.getNodeParameter('code', 0) as string;
const regexCheck = /\breturn\b/g.exec(code);
if (regexCheck?.length) {
const sandbox = {
newItems: returnData,
};
const mode = this.getMode();
const options = {
console: mode === 'manual' ? 'redirect' : 'inherit',
sandbox,
};
const vm = new NodeVM(options as unknown as NodeVMOptions);
returnData = await vm.run(
`
module.exports = async function() {
newItems.sort( (a,b) => {
${code}
})
return newItems;
}()`,
__dirname,
);
} else {
throw new NodeOperationError(
this.getNode(),
"Sort code doesn't return. Please add a 'return' statement to your code",
);
}
}
return returnData;
}

View File

@@ -0,0 +1,218 @@
import type { IExecuteFunctions } from 'n8n-core';
import type { IDataObject, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { deepCopy, NodeOperationError } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import get from 'lodash/get';
import unset from 'lodash/unset';
import { disableDotNotationBoolean } from '../common.descriptions';
import { prepareFieldsArray } from '../../helpers/utils';
const properties: INodeProperties[] = [
{
displayName: 'Fields To Split Out',
name: 'fieldToSplitOut',
type: 'string',
default: '',
required: true,
description: 'The name of the input fields to break out into separate items',
requiresDataPath: 'multiple',
},
{
displayName: 'Include',
name: 'include',
type: 'options',
options: [
{
name: 'No Other Fields',
value: 'noOtherFields',
},
{
name: 'All Other Fields',
value: 'allOtherFields',
},
{
name: 'Selected Other Fields',
value: 'selectedOtherFields',
},
],
default: 'noOtherFields',
description: 'Whether to copy any other fields into the new items',
},
{
displayName: 'Fields To Include',
name: 'fieldsToInclude',
type: 'string',
placeholder: 'e.g. email, name',
requiresDataPath: 'multiple',
description: 'Fields in the input items to aggregate together',
default: '',
displayOptions: {
show: {
include: ['selectedOtherFields'],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
disableDotNotationBoolean,
{
displayName: 'Destination Field Name',
name: 'destinationFieldName',
type: 'string',
requiresDataPath: 'multiple',
default: '',
description: 'The field in the output under which to put the split field contents',
},
],
},
];
const displayOptions = {
show: {
resource: ['itemList'],
operation: ['splitOutItems'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(
this: IExecuteFunctions,
items: INodeExecutionData[],
): Promise<INodeExecutionData[]> {
const returnData: INodeExecutionData[] = [];
for (let i = 0; i < items.length; i++) {
const fieldsToSplitOut = (this.getNodeParameter('fieldToSplitOut', i) as string)
.split(',')
.map((field) => field.trim());
const disableDotNotation = this.getNodeParameter(
'options.disableDotNotation',
0,
false,
) as boolean;
const destinationFields = (
this.getNodeParameter('options.destinationFieldName', i, '') as string
)
.split(',')
.filter((field) => field.trim() !== '')
.map((field) => field.trim());
if (destinationFields.length && destinationFields.length !== fieldsToSplitOut.length) {
throw new NodeOperationError(
this.getNode(),
'If multiple fields to split out are given, the same number of destination fields must be given',
);
}
const include = this.getNodeParameter('include', i) as
| 'selectedOtherFields'
| 'allOtherFields'
| 'noOtherFields';
const multiSplit = fieldsToSplitOut.length > 1;
const item = { ...items[i].json };
const splited: IDataObject[] = [];
for (const [entryIndex, fieldToSplitOut] of fieldsToSplitOut.entries()) {
const destinationFieldName = destinationFields[entryIndex] || '';
let arrayToSplit;
if (!disableDotNotation) {
arrayToSplit = get(item, fieldToSplitOut);
} else {
arrayToSplit = item[fieldToSplitOut];
}
if (arrayToSplit === undefined) {
arrayToSplit = [];
}
if (typeof arrayToSplit !== 'object' || arrayToSplit === null) {
arrayToSplit = [arrayToSplit];
}
if (!Array.isArray(arrayToSplit)) {
arrayToSplit = Object.values(arrayToSplit);
}
for (const [elementIndex, element] of arrayToSplit.entries()) {
if (splited[elementIndex] === undefined) {
splited[elementIndex] = {};
}
const fieldName = destinationFieldName || fieldToSplitOut;
if (typeof element === 'object' && element !== null && include === 'noOtherFields') {
if (destinationFieldName === '' && !multiSplit) {
splited[elementIndex] = { ...splited[elementIndex], ...element };
} else {
splited[elementIndex][fieldName] = element;
}
} else {
splited[elementIndex][fieldName] = element;
}
}
}
for (const splitEntry of splited) {
let newItem: IDataObject = {};
if (include === 'noOtherFields') {
newItem = splitEntry;
}
if (include === 'allOtherFields') {
const itemCopy = deepCopy(item);
for (const fieldToSplitOut of fieldsToSplitOut) {
if (!disableDotNotation) {
unset(itemCopy, fieldToSplitOut);
} else {
delete itemCopy[fieldToSplitOut];
}
}
newItem = { ...itemCopy, ...splitEntry };
}
if (include === 'selectedOtherFields') {
const fieldsToInclude = prepareFieldsArray(
this.getNodeParameter('fieldsToInclude', i, '') as string,
'Fields To Include',
);
if (!fieldsToInclude.length) {
throw new NodeOperationError(this.getNode(), 'No fields specified', {
description: 'Please add a field to include',
});
}
for (const field of fieldsToInclude) {
if (!disableDotNotation) {
splitEntry[field] = get(item, field);
} else {
splitEntry[field] = item[field];
}
}
newItem = splitEntry;
}
returnData.push({
json: newItem,
pairedItem: {
item: i,
},
});
}
}
return returnData;
}

View File

@@ -0,0 +1,617 @@
import type {
GenericValue,
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
} from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import { updateDisplayOptions } from '@utils/utilities';
import get from 'lodash/get';
import { disableDotNotationBoolean } from '../common.descriptions';
type AggregationType =
| 'append'
| 'average'
| 'concatenate'
| 'count'
| 'countUnique'
| 'max'
| 'min'
| 'sum';
type Aggregation = {
aggregation: AggregationType;
field: string;
includeEmpty?: boolean;
separateBy?: string;
customSeparator?: string;
};
type Aggregations = Aggregation[];
// eslint-disable-next-line no-restricted-syntax
const AggregationDisplayNames = {
append: 'appended_',
average: 'average_',
concatenate: 'concatenated_',
count: 'count_',
countUnique: 'unique_count_',
max: 'max_',
min: 'min_',
sum: 'sum_',
};
const NUMERICAL_AGGREGATIONS = ['average', 'sum'];
type SummarizeOptions = {
disableDotNotation?: boolean;
outputFormat?: 'separateItems' | 'singleItem';
skipEmptySplitFields?: boolean;
};
type ValueGetterFn = (
item: IDataObject,
field: string,
) => IDataObject | IDataObject[] | GenericValue | GenericValue[];
export const properties: INodeProperties[] = [
{
displayName: 'Fields to Summarize',
name: 'fieldsToSummarize',
type: 'fixedCollection',
placeholder: 'Add Field',
default: { values: [{ aggregation: 'count', field: '' }] },
typeOptions: {
multipleValues: true,
},
options: [
{
displayName: '',
name: 'values',
values: [
{
displayName: 'Aggregation',
name: 'aggregation',
type: 'options',
options: [
{
name: 'Append',
value: 'append',
},
{
name: 'Average',
value: 'average',
},
{
name: 'Concatenate',
value: 'concatenate',
},
{
name: 'Count',
value: 'count',
},
{
name: 'Count Unique',
value: 'countUnique',
},
{
name: 'Max',
value: 'max',
},
{
name: 'Min',
value: 'min',
},
{
name: 'Sum',
value: 'sum',
},
],
default: 'count',
description: 'How to combine the values of the field you want to summarize',
},
//field repeated to have different descriptions for different aggregations --------------------------------
{
displayName: 'Field',
name: 'field',
type: 'string',
default: '',
description: 'The name of an input field that you want to summarize',
placeholder: 'e.g. cost',
hint: ' Enter the field name as text',
displayOptions: {
hide: {
aggregation: [...NUMERICAL_AGGREGATIONS, 'countUnique', 'count', 'max', 'min'],
},
},
requiresDataPath: 'single',
},
{
displayName: 'Field',
name: 'field',
type: 'string',
default: '',
description:
'The name of an input field that you want to summarize. The field should contain numerical values; null, undefined, empty strings would be ignored.',
placeholder: 'e.g. cost',
hint: ' Enter the field name as text',
displayOptions: {
show: {
aggregation: NUMERICAL_AGGREGATIONS,
},
},
requiresDataPath: 'single',
},
{
displayName: 'Field',
name: 'field',
type: 'string',
default: '',
description:
'The name of an input field that you want to summarize; null, undefined, empty strings would be ignored',
placeholder: 'e.g. cost',
hint: ' Enter the field name as text',
displayOptions: {
show: {
aggregation: ['countUnique', 'count', 'max', 'min'],
},
},
requiresDataPath: 'single',
},
// ----------------------------------------------------------------------------------------------------------
{
displayName: 'Include Empty Values',
name: 'includeEmpty',
type: 'boolean',
default: false,
displayOptions: {
show: {
aggregation: ['append', 'concatenate'],
},
},
},
{
displayName: 'Separator',
name: 'separateBy',
type: 'options',
default: ',',
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
options: [
{
name: 'Comma',
value: ',',
},
{
name: 'Comma and Space',
value: ', ',
},
{
name: 'New Line',
value: '\n',
},
{
name: 'None',
value: '',
},
{
name: 'Space',
value: ' ',
},
{
name: 'Other',
value: 'other',
},
],
hint: 'What to insert between values',
displayOptions: {
show: {
aggregation: ['concatenate'],
},
},
},
{
displayName: 'Custom Separator',
name: 'customSeparator',
type: 'string',
default: '',
displayOptions: {
show: {
aggregation: ['concatenate'],
separateBy: ['other'],
},
},
},
],
},
],
},
// fieldsToSplitBy repeated to have different displayName for singleItem and separateItems -----------------------------
{
displayName: 'Fields to Split By',
name: 'fieldsToSplitBy',
type: 'string',
placeholder: 'e.g. country, city',
default: '',
description: 'The name of the input fields that you want to split the summary by',
hint: 'Enter the name of the fields as text (separated by commas)',
displayOptions: {
hide: {
'/options.outputFormat': ['singleItem'],
},
},
requiresDataPath: 'multiple',
},
{
displayName: 'Fields to Group By',
name: 'fieldsToSplitBy',
type: 'string',
placeholder: 'e.g. country, city',
default: '',
description: 'The name of the input fields that you want to split the summary by',
hint: 'Enter the name of the fields as text (separated by commas)',
displayOptions: {
show: {
'/options.outputFormat': ['singleItem'],
},
},
requiresDataPath: 'multiple',
},
// ----------------------------------------------------------------------------------------------------------
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
disableDotNotationBoolean,
{
displayName: 'Output Format',
name: 'outputFormat',
type: 'options',
default: 'separateItems',
options: [
{
name: 'Each Split in a Separate Item',
value: 'separateItems',
},
{
name: 'All Splits in a Single Item',
value: 'singleItem',
},
],
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
displayName: 'Ignore items without valid fields to group by',
name: 'skipEmptySplitFields',
type: 'boolean',
default: false,
},
],
},
];
const displayOptions = {
show: {
resource: ['itemList'],
operation: ['summarize'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
function isEmpty<T>(value: T) {
return value === undefined || value === null || value === '';
}
function parseReturnData(returnData: IDataObject) {
const regexBrackets = /[\]\["]/g;
const regexSpaces = /[ .]/g;
for (const key of Object.keys(returnData)) {
if (key.match(regexBrackets)) {
const newKey = key.replace(regexBrackets, '');
returnData[newKey] = returnData[key];
delete returnData[key];
}
}
for (const key of Object.keys(returnData)) {
if (key.match(regexSpaces)) {
const newKey = key.replace(regexSpaces, '_');
returnData[newKey] = returnData[key];
delete returnData[key];
}
}
}
function parseFieldName(fieldName: string[]) {
const regexBrackets = /[\]\["]/g;
const regexSpaces = /[ .]/g;
fieldName = fieldName.map((field) => {
field = field.replace(regexBrackets, '');
field = field.replace(regexSpaces, '_');
return field;
});
return fieldName;
}
const fieldValueGetter = (disableDotNotation?: boolean) => {
if (disableDotNotation) {
return (item: IDataObject, field: string) => item[field];
} else {
return (item: IDataObject, field: string) => get(item, field);
}
};
function checkIfFieldExists(
this: IExecuteFunctions,
items: IDataObject[],
aggregations: Aggregations,
getValue: ValueGetterFn,
) {
for (const aggregation of aggregations) {
if (aggregation.field === '') {
continue;
}
const exist = items.some((item) => getValue(item, aggregation.field) !== undefined);
if (!exist) {
throw new NodeOperationError(
this.getNode(),
`The field '${aggregation.field}' does not exist in any items`,
);
}
}
}
function aggregate(items: IDataObject[], entry: Aggregation, getValue: ValueGetterFn) {
const { aggregation, field } = entry;
let data = [...items];
if (NUMERICAL_AGGREGATIONS.includes(aggregation)) {
data = data.filter(
(item) => typeof getValue(item, field) === 'number' && !isEmpty(getValue(item, field)),
);
}
switch (aggregation) {
//combine operations
case 'append':
if (!entry.includeEmpty) {
data = data.filter((item) => !isEmpty(getValue(item, field)));
}
return data.map((item) => getValue(item, field));
case 'concatenate':
const separateBy = entry.separateBy === 'other' ? entry.customSeparator : entry.separateBy;
if (!entry.includeEmpty) {
data = data.filter((item) => !isEmpty(getValue(item, field)));
}
return data
.map((item) => {
let value = getValue(item, field);
if (typeof value === 'object') {
value = JSON.stringify(value);
}
if (typeof value === 'undefined') {
value = 'undefined';
}
return value;
})
.join(separateBy);
//numerical operations
case 'average':
return (
data.reduce((acc, item) => {
return acc + (getValue(item, field) as number);
}, 0) / data.length
);
case 'sum':
return data.reduce((acc, item) => {
return acc + (getValue(item, field) as number);
}, 0);
//comparison operations
case 'min':
let min;
for (const item of data) {
const value = getValue(item, field);
if (value !== undefined && value !== null && value !== '') {
if (min === undefined || value < min) {
min = value;
}
}
}
return min !== undefined ? min : null;
case 'max':
let max;
for (const item of data) {
const value = getValue(item, field);
if (value !== undefined && value !== null && value !== '') {
if (max === undefined || value > max) {
max = value;
}
}
}
return max !== undefined ? max : null;
//count operations
case 'countUnique':
return new Set(data.map((item) => getValue(item, field)).filter((item) => !isEmpty(item)))
.size;
default:
//count by default
return data.filter((item) => !isEmpty(getValue(item, field))).length;
}
}
function aggregateData(
data: IDataObject[],
fieldsToSummarize: Aggregations,
options: SummarizeOptions,
getValue: ValueGetterFn,
) {
const returnData = fieldsToSummarize.reduce((acc, aggregation) => {
acc[`${AggregationDisplayNames[aggregation.aggregation]}${aggregation.field}`] = aggregate(
data,
aggregation,
getValue,
);
return acc;
}, {} as IDataObject);
parseReturnData(returnData);
if (options.outputFormat === 'singleItem') {
parseReturnData(returnData);
return returnData;
} else {
return { ...returnData, pairedItems: data.map((item) => item._itemIndex as number) };
}
}
function splitData(
splitKeys: string[],
data: IDataObject[],
fieldsToSummarize: Aggregations,
options: SummarizeOptions,
getValue: ValueGetterFn,
) {
if (!splitKeys || splitKeys.length === 0) {
return aggregateData(data, fieldsToSummarize, options, getValue);
}
const [firstSplitKey, ...restSplitKeys] = splitKeys;
const groupedData = data.reduce((acc, item) => {
let keyValuee = getValue(item, firstSplitKey) as string;
if (typeof keyValuee === 'object') {
keyValuee = JSON.stringify(keyValuee);
}
if (options.skipEmptySplitFields && typeof keyValuee !== 'number' && !keyValuee) {
return acc;
}
if (acc[keyValuee] === undefined) {
acc[keyValuee] = [item];
} else {
(acc[keyValuee] as IDataObject[]).push(item);
}
return acc;
}, {} as IDataObject);
return Object.keys(groupedData).reduce((acc, key) => {
const value = groupedData[key] as IDataObject[];
acc[key] = splitData(restSplitKeys, value, fieldsToSummarize, options, getValue);
return acc;
}, {} as IDataObject);
}
function aggregationToArray(
aggregationResult: IDataObject,
fieldsToSplitBy: string[],
previousStage: IDataObject = {},
) {
const returnData: IDataObject[] = [];
fieldsToSplitBy = parseFieldName(fieldsToSplitBy);
const splitFieldName = fieldsToSplitBy[0];
const isNext = fieldsToSplitBy[1];
if (isNext === undefined) {
for (const fieldName of Object.keys(aggregationResult)) {
returnData.push({
...previousStage,
[splitFieldName]: fieldName,
...(aggregationResult[fieldName] as IDataObject),
});
}
return returnData;
} else {
for (const key of Object.keys(aggregationResult)) {
returnData.push(
...aggregationToArray(aggregationResult[key] as IDataObject, fieldsToSplitBy.slice(1), {
...previousStage,
[splitFieldName]: key,
}),
);
}
return returnData;
}
}
export async function execute(
this: IExecuteFunctions,
items: INodeExecutionData[],
): Promise<INodeExecutionData[]> {
const newItems = items.map(({ json }, i) => ({ ...json, _itemIndex: i }));
const options = this.getNodeParameter('options', 0, {}) as SummarizeOptions;
const fieldsToSplitBy = (this.getNodeParameter('fieldsToSplitBy', 0, '') as string)
.split(',')
.map((field) => field.trim())
.filter((field) => field);
const fieldsToSummarize = this.getNodeParameter(
'fieldsToSummarize.values',
0,
[],
) as Aggregations;
if (fieldsToSummarize.filter((aggregation) => aggregation.field !== '').length === 0) {
throw new NodeOperationError(
this.getNode(),
"You need to add at least one aggregation to 'Fields to Summarize' with non empty 'Field'",
);
}
const getValue = fieldValueGetter(options.disableDotNotation);
const nodeVersion = this.getNode().typeVersion;
if (nodeVersion < 2.1) {
checkIfFieldExists.call(this, newItems, fieldsToSummarize, getValue);
}
const aggregationResult = splitData(
fieldsToSplitBy,
newItems,
fieldsToSummarize,
options,
getValue,
);
if (options.outputFormat === 'singleItem') {
const executionData: INodeExecutionData = {
json: aggregationResult,
pairedItem: newItems.map((_v, index) => ({
item: index,
})),
};
return [executionData];
} else {
if (!fieldsToSplitBy.length) {
const { pairedItems, ...json } = aggregationResult;
const executionData: INodeExecutionData = {
json,
pairedItem: ((pairedItems as number[]) || []).map((index: number) => ({
item: index,
})),
};
return [executionData];
}
const returnData = aggregationToArray(aggregationResult, fieldsToSplitBy);
const executionData = returnData.map((item) => {
const { pairedItems, ...json } = item;
return {
json,
pairedItem: ((pairedItems as number[]) || []).map((index: number) => ({
item: index,
})),
};
});
return executionData;
}
}

View File

@@ -0,0 +1,13 @@
import type { AllEntities } from 'n8n-workflow';
type NodeMap = {
itemList:
| 'concatenateItems'
| 'limit'
| 'removeDuplicates'
| 'sort'
| 'splitOutItems'
| 'summarize';
};
export type ItemListsType = AllEntities<NodeMap>;

View File

@@ -0,0 +1,35 @@
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
import { NodeOperationError } from 'n8n-workflow';
import type { ItemListsType } from './node.type';
import * as itemList from './itemList';
export async function router(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
let returnData: INodeExecutionData[] = [];
const items = this.getInputData();
const resource = this.getNodeParameter<ItemListsType>('resource', 0);
const operation = this.getNodeParameter('operation', 0);
const itemListsNodeData = {
resource,
operation,
} as ItemListsType;
try {
switch (itemListsNodeData.resource) {
case 'itemList':
returnData = await itemList[itemListsNodeData.operation].execute.call(this, items);
break;
default:
throw new NodeOperationError(
this.getNode(),
`The operation "${operation}" is not supported!`,
);
}
} catch (error) {
throw error;
}
return this.prepareOutputData(returnData);
}

View File

@@ -0,0 +1,35 @@
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
import type { INodeTypeDescription } from 'n8n-workflow';
import * as itemList from './itemList';
export const versionDescription: INodeTypeDescription = {
displayName: 'Item Lists',
name: 'itemLists',
icon: 'file:itemLists.svg',
group: ['input'],
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Helper for working with lists of items and transforming arrays',
version: 3,
defaults: {
name: 'Item Lists',
},
inputs: ['main'],
outputs: ['main'],
credentials: [],
properties: [
{
displayName: 'Resource',
name: 'resource',
type: 'hidden',
options: [
{
name: 'Item List',
value: 'itemList',
},
],
default: 'itemList',
},
...itemList.description,
],
};

View File

@@ -0,0 +1,59 @@
import type { IDataObject, INode, INodeExecutionData } from 'n8n-workflow';
import get from 'lodash/get';
import isEqual from 'lodash/isEqual';
import isObject from 'lodash/isObject';
import merge from 'lodash/merge';
import reduce from 'lodash/reduce';
export const compareItems = (
obj: INodeExecutionData,
obj2: INodeExecutionData,
keys: string[],
disableDotNotation: boolean,
_node: INode,
) => {
let result = true;
for (const key of keys) {
if (!disableDotNotation) {
if (!isEqual(get(obj.json, key), get(obj2.json, key))) {
result = false;
break;
}
} else {
if (!isEqual(obj.json[key], obj2.json[key])) {
result = false;
break;
}
}
}
return result;
};
export const flattenKeys = (obj: IDataObject, path: string[] = []): IDataObject => {
return !isObject(obj)
? { [path.join('.')]: obj }
: reduce(obj, (cum, next, key) => merge(cum, flattenKeys(next as IDataObject, [...path, key])), {}); //prettier-ignore
};
export const shuffleArray = (array: any[]) => {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
};
export const prepareFieldsArray = (fields: string | string[], fieldName = 'Fields') => {
if (typeof fields === 'string') {
return fields
.split(',')
.map((entry) => entry.trim())
.filter((entry) => entry !== '');
}
if (Array.isArray(fields)) {
return fields;
}
throw new Error(
`The \'${fieldName}\' parameter must be a string of fields separated by commas or an array of strings.`,
);
};

View File

@@ -0,0 +1,606 @@
{
"name": "itemList refactor",
"nodes": [
{
"parameters": {},
"id": "e7ecaa9c-e35d-4095-a85b-85b83f807c2a",
"name": "When clicking \"Execute Workflow\"",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [420, 400]
},
{
"parameters": {
"operation": "getAllPeople",
"returnAll": true
},
"id": "7d925077-afaa-46d5-ba2f-0c19d93afecc",
"name": "Customer Datastore (n8n training)",
"type": "n8n-nodes-base.n8nTrainingCustomerDatastore",
"typeVersion": 1,
"position": [640, 400]
},
{
"parameters": {
"operation": "concatenateItems",
"fieldsToAggregate": {
"fieldToAggregate": [
{
"fieldToAggregate": "email"
},
{
"fieldToAggregate": "notes"
}
]
},
"options": {}
},
"id": "f80182d8-54f6-4a26-82b3-27d67e4ca39b",
"name": "Item Lists1",
"type": "n8n-nodes-base.itemLists",
"typeVersion": 3,
"position": [1120, -120]
},
{
"parameters": {
"operation": "concatenateItems",
"aggregate": "aggregateAllItemData",
"destinationFieldName": "data2"
},
"id": "23eefe2c-6394-4b53-a791-852dcd671ea0",
"name": "Item Lists",
"type": "n8n-nodes-base.itemLists",
"typeVersion": 3,
"position": [1120, 40]
},
{
"parameters": {
"operation": "limit",
"maxItems": 2
},
"id": "676dc72f-9766-43c0-aab7-2f8a93ed46e5",
"name": "Item Lists2",
"type": "n8n-nodes-base.itemLists",
"typeVersion": 3,
"position": [1120, 200]
},
{
"parameters": {
"operation": "limit",
"keep": "lastItems"
},
"id": "9615299b-5acc-4459-a7d3-2cb23c3224ab",
"name": "Item Lists3",
"type": "n8n-nodes-base.itemLists",
"typeVersion": 3,
"position": [1120, 360]
},
{
"parameters": {
"operation": "sort",
"sortFieldsUi": {
"sortField": [
{
"fieldName": "country"
}
]
},
"options": {}
},
"id": "fd2f190a-b161-48ff-93e1-0f30efce051a",
"name": "Item Lists4",
"type": "n8n-nodes-base.itemLists",
"typeVersion": 3,
"position": [1120, 540]
},
{
"parameters": {
"operation": "limit",
"maxItems": 4
},
"id": "14759521-76ad-46d8-8add-1d7361788fe1",
"name": "Item Lists5",
"type": "n8n-nodes-base.itemLists",
"typeVersion": 3,
"position": [1360, 540]
},
{
"parameters": {
"operation": "removeDuplicates",
"compare": "selectedFields",
"fieldsToCompare": "country",
"options": {}
},
"id": "c0eba4d0-f975-4987-8bb2-853e8a98665e",
"name": "Item Lists6",
"type": "n8n-nodes-base.itemLists",
"typeVersion": 3,
"position": [1560, 540]
},
{
"parameters": {
"operation": "concatenateItems",
"aggregate": "aggregateAllItemData",
"include": "specifiedFields",
"fieldsToInclude": "country, notes, name, created"
},
"id": "b5962f40-a891-4b02-8fec-c2d76f85375f",
"name": "Item Lists7",
"type": "n8n-nodes-base.itemLists",
"typeVersion": 3,
"position": [1120, 740]
},
{
"parameters": {
"fieldToSplitOut": "data",
"include": "allOtherFields",
"options": {
"destinationFieldName": "newData"
}
},
"id": "15c6fc86-7e38-4d76-836a-8f8426ca05e3",
"name": "Item Lists8",
"type": "n8n-nodes-base.itemLists",
"typeVersion": 3,
"position": [1380, 740]
},
{
"parameters": {
"operation": "summarize",
"fieldsToSummarize": {
"values": [
{
"aggregation": "append",
"field": "newData.notes"
},
{
"aggregation": "max",
"field": "newData.created"
},
{
"aggregation": "min",
"field": "newData.created"
}
]
},
"options": {}
},
"id": "8c51ae57-487e-472a-b268-6e8ad347edbb",
"name": "Item Lists9",
"type": "n8n-nodes-base.itemLists",
"typeVersion": 3,
"position": [1560, 940]
},
{
"parameters": {},
"id": "e859b082-284c-4bb3-96b6-39a86152d8f6",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1760, -120]
},
{
"parameters": {},
"id": "9da56c21-739d-4f2f-adf7-953cf0550d97",
"name": "No Operation, do nothing1",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1760, 40]
},
{
"parameters": {},
"id": "f85ac031-24de-4701-bb3d-76c684924002",
"name": "No Operation, do nothing2",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1760, 200]
},
{
"parameters": {},
"id": "e7faff14-55d6-4d78-983d-027fd56bcd5a",
"name": "No Operation, do nothing3",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1760, 360]
},
{
"parameters": {},
"id": "dc8b7bbc-b1a8-4ba8-b214-d73341cb9f85",
"name": "No Operation, do nothing4",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1760, 740]
},
{
"parameters": {},
"id": "09bdeca1-6a9e-4668-b9d1-14ed6139e047",
"name": "No Operation, do nothing5",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1760, 540]
},
{
"parameters": {},
"id": "7d508889-d94e-4818-abc8-b669a0fe64ea",
"name": "No Operation, do nothing6",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [1760, 940]
}
],
"pinData": {
"No Operation, do nothing": [
{
"json": {
"email": [
"gatsby@west-egg.com",
"jab@macondo.co",
"info@in-and-out-of-weeks.org",
"captain@heartofgold.com",
"edmund@narnia.gov"
],
"notes": [
"Keeps asking about a green light??",
"Lots of people named after him. Very confusing",
"Keeps rolling his terrible eyes",
"Felt like I was talking to more than one person",
"Passionate sailor"
]
}
}
],
"No Operation, do nothing1": [
{
"json": {
"data2": [
{
"id": "23423532",
"name": "Jay Gatsby",
"email": "gatsby@west-egg.com",
"notes": "Keeps asking about a green light??",
"country": "US",
"created": "1925-04-10"
},
{
"id": "23423533",
"name": "José Arcadio Buendía",
"email": "jab@macondo.co",
"notes": "Lots of people named after him. Very confusing",
"country": "CO",
"created": "1967-05-05"
},
{
"id": "23423534",
"name": "Max Sendak",
"email": "info@in-and-out-of-weeks.org",
"notes": "Keeps rolling his terrible eyes",
"country": "US",
"created": "1963-04-09"
},
{
"id": "23423535",
"name": "Zaphod Beeblebrox",
"email": "captain@heartofgold.com",
"notes": "Felt like I was talking to more than one person",
"country": null,
"created": "1979-10-12"
},
{
"id": "23423536",
"name": "Edmund Pevensie",
"email": "edmund@narnia.gov",
"notes": "Passionate sailor",
"country": "UK",
"created": "1950-10-16"
}
]
}
}
],
"No Operation, do nothing2": [
{
"json": {
"id": "23423532",
"name": "Jay Gatsby",
"email": "gatsby@west-egg.com",
"notes": "Keeps asking about a green light??",
"country": "US",
"created": "1925-04-10"
}
},
{
"json": {
"id": "23423533",
"name": "José Arcadio Buendía",
"email": "jab@macondo.co",
"notes": "Lots of people named after him. Very confusing",
"country": "CO",
"created": "1967-05-05"
}
}
],
"No Operation, do nothing3": [
{
"json": {
"id": "23423536",
"name": "Edmund Pevensie",
"email": "edmund@narnia.gov",
"notes": "Passionate sailor",
"country": "UK",
"created": "1950-10-16"
}
}
],
"No Operation, do nothing5": [
{
"json": {
"id": "23423533",
"name": "José Arcadio Buendía",
"email": "jab@macondo.co",
"notes": "Lots of people named after him. Very confusing",
"country": "CO",
"created": "1967-05-05"
}
},
{
"json": {
"id": "23423536",
"name": "Edmund Pevensie",
"email": "edmund@narnia.gov",
"notes": "Passionate sailor",
"country": "UK",
"created": "1950-10-16"
}
},
{
"json": {
"id": "23423532",
"name": "Jay Gatsby",
"email": "gatsby@west-egg.com",
"notes": "Keeps asking about a green light??",
"country": "US",
"created": "1925-04-10"
}
}
],
"No Operation, do nothing4": [
{
"json": {
"newData": {
"name": "Jay Gatsby",
"notes": "Keeps asking about a green light??",
"country": "US",
"created": "1925-04-10"
}
}
},
{
"json": {
"newData": {
"name": "José Arcadio Buendía",
"notes": "Lots of people named after him. Very confusing",
"country": "CO",
"created": "1967-05-05"
}
}
},
{
"json": {
"newData": {
"name": "Max Sendak",
"notes": "Keeps rolling his terrible eyes",
"country": "US",
"created": "1963-04-09"
}
}
},
{
"json": {
"newData": {
"name": "Zaphod Beeblebrox",
"notes": "Felt like I was talking to more than one person",
"country": null,
"created": "1979-10-12"
}
}
},
{
"json": {
"newData": {
"name": "Edmund Pevensie",
"notes": "Passionate sailor",
"country": "UK",
"created": "1950-10-16"
}
}
}
],
"No Operation, do nothing6": [
{
"json": {
"appended_newData_notes": [
"Keeps asking about a green light??",
"Lots of people named after him. Very confusing",
"Keeps rolling his terrible eyes",
"Felt like I was talking to more than one person",
"Passionate sailor"
],
"max_newData_created": "1979-10-12",
"min_newData_created": "1925-04-10"
}
}
]
},
"connections": {
"When clicking \"Execute Workflow\"": {
"main": [
[
{
"node": "Customer Datastore (n8n training)",
"type": "main",
"index": 0
}
]
]
},
"Customer Datastore (n8n training)": {
"main": [
[
{
"node": "Item Lists1",
"type": "main",
"index": 0
},
{
"node": "Item Lists",
"type": "main",
"index": 0
},
{
"node": "Item Lists2",
"type": "main",
"index": 0
},
{
"node": "Item Lists3",
"type": "main",
"index": 0
},
{
"node": "Item Lists4",
"type": "main",
"index": 0
},
{
"node": "Item Lists7",
"type": "main",
"index": 0
}
]
]
},
"Item Lists1": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
]
]
},
"Item Lists4": {
"main": [
[
{
"node": "Item Lists5",
"type": "main",
"index": 0
}
]
]
},
"Item Lists5": {
"main": [
[
{
"node": "Item Lists6",
"type": "main",
"index": 0
}
]
]
},
"Item Lists7": {
"main": [
[
{
"node": "Item Lists8",
"type": "main",
"index": 0
}
]
]
},
"Item Lists8": {
"main": [
[
{
"node": "Item Lists9",
"type": "main",
"index": 0
},
{
"node": "No Operation, do nothing4",
"type": "main",
"index": 0
}
]
]
},
"Item Lists": {
"main": [
[
{
"node": "No Operation, do nothing1",
"type": "main",
"index": 0
}
]
]
},
"Item Lists2": {
"main": [
[
{
"node": "No Operation, do nothing2",
"type": "main",
"index": 0
}
]
]
},
"Item Lists3": {
"main": [
[
{
"node": "No Operation, do nothing3",
"type": "main",
"index": 0
}
]
]
},
"Item Lists6": {
"main": [
[
{
"node": "No Operation, do nothing5",
"type": "main",
"index": 0
}
]
]
},
"Item Lists9": {
"main": [
[
{
"node": "No Operation, do nothing6",
"type": "main",
"index": 0
}
]
]
}
},
"active": false,
"settings": {},
"versionId": "ce3e0124-aa56-497c-a2e1-24158837c7f9",
"id": "m7QDuxo599dkZ0Ex",
"meta": {
"instanceId": "e34acda144ba98351e38adb4db781751ca8cd64a8248aef8b65608fc9a49008c"
},
"tags": []
}

View File

@@ -189,6 +189,7 @@ export async function handleMatrixCall(
const roomId = this.getNodeParameter('roomId', index) as string;
const mediaType = this.getNodeParameter('mediaType', index) as string;
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', index);
const additionalFields = this.getNodeParameter('additionalFields', index);
let body;
const qs: IDataObject = {};
@@ -197,7 +198,12 @@ export async function handleMatrixCall(
const { fileName, mimeType } = this.helpers.assertBinaryData(index, binaryPropertyName);
body = await this.helpers.getBinaryDataBuffer(index, binaryPropertyName);
qs.filename = fileName;
if (additionalFields.fileName) {
qs.filename = additionalFields.fileName as string;
} else {
qs.filename = fileName;
}
headers['Content-Type'] = mimeType;
headers.accept = 'application/json,text/*;q=0.99';
@@ -216,7 +222,7 @@ export async function handleMatrixCall(
body = {
msgtype: `m.${mediaType}`,
body: fileName,
body: qs.filename,
url: uploadRequestResult.content_uri,
};
const messageId = uuid();

View File

@@ -81,8 +81,30 @@ export const mediaFields: INodeProperties[] = [
description: 'Image media type',
},
],
description: 'Name of the uploaded file',
description: 'Type of file being uploaded',
placeholder: 'mxc://matrix.org/uploaded-media-uri',
required: true,
},
{
displayName: 'Additional Fields',
name: 'additionalFields',
type: 'collection',
placeholder: 'Add Field',
default: {},
displayOptions: {
show: {
resource: ['media'],
operation: ['upload'],
},
},
options: [
{
displayName: 'File Name',
name: 'fileName',
type: 'string',
default: '',
description: 'Name of the file being uploaded',
},
],
},
];

Some files were not shown because too many files have changed in this diff Show More