first commit
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
Security: Sync from Public / sync-from-public (push) Has been cancelled
Test: Benchmark Nightly / build (push) Has been cancelled
Test: Benchmark Nightly / Notify Cats on failure (push) Has been cancelled
CI: Python / Checks (push) Has been cancelled
Test: Evals Python / Workflow Comparison Python (push) Has been cancelled
Util: Check Docs URLs / check-docs-urls (push) Has been cancelled
Test: Visual Storybook / Cloudflare Pages (push) Has been cancelled
Test: E2E Performance / build-and-test-performance (push) Has been cancelled
Test: Workflows Nightly / Run Workflow Tests (push) Has been cancelled
Util: Cleanup CI Docker Images / Delete stale CI images (push) Has been cancelled
Test: Benchmark Destroy Env / build (push) Has been cancelled
Util: Update Node Popularity / update-popularity (push) Has been cancelled
Test: E2E Coverage Weekly / Coverage Tests (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.googleDrive",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Data & Storage"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/google/oauth-single-service/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.googledrive/"
|
||||
}
|
||||
],
|
||||
"generic": [
|
||||
{
|
||||
"label": "Hey founders! Your business doesn't need you to operate",
|
||||
"icon": " 🖥️",
|
||||
"url": "https://n8n.io/blog/your-business-doesnt-need-you-to-operate/"
|
||||
},
|
||||
{
|
||||
"label": "Why this Product Manager loves workflow automation with n8n",
|
||||
"icon": "🧠",
|
||||
"url": "https://n8n.io/blog/why-this-product-manager-loves-workflow-automation-with-n8n/"
|
||||
},
|
||||
{
|
||||
"label": "7 no-code workflow automations for Amazon Web Services",
|
||||
"url": "https://n8n.io/blog/aws-workflow-automation/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
|
||||
import { VersionedNodeType } from 'n8n-workflow';
|
||||
|
||||
import { GoogleDriveV1 } from './v1/GoogleDriveV1.node';
|
||||
import { GoogleDriveV2 } from './v2/GoogleDriveV2.node';
|
||||
|
||||
export class GoogleDrive extends VersionedNodeType {
|
||||
constructor() {
|
||||
const baseDescription: INodeTypeBaseDescription = {
|
||||
displayName: 'Google Drive',
|
||||
name: 'googleDrive',
|
||||
icon: 'file:googleDrive.svg',
|
||||
group: ['input'],
|
||||
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
||||
description: 'Access data on Google Drive',
|
||||
defaultVersion: 3,
|
||||
schemaPath: 'Google/Drive',
|
||||
};
|
||||
|
||||
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||
1: new GoogleDriveV1(baseDescription),
|
||||
2: new GoogleDriveV1(baseDescription),
|
||||
3: new GoogleDriveV2(baseDescription),
|
||||
};
|
||||
|
||||
super(nodeVersions, baseDescription);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"node": "n8n-nodes-base.googleDriveTrigger",
|
||||
"nodeVersion": "1.0",
|
||||
"codexVersion": "1.0",
|
||||
"categories": ["Data & Storage"],
|
||||
"resources": {
|
||||
"credentialDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/credentials/google/oauth-single-service/"
|
||||
}
|
||||
],
|
||||
"primaryDocumentation": [
|
||||
{
|
||||
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.googledrivetrigger/"
|
||||
}
|
||||
],
|
||||
"generic": [
|
||||
{
|
||||
"label": "7 no-code workflow automations for Amazon Web Services",
|
||||
"url": "https://n8n.io/blog/aws-workflow-automation/"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,522 @@
|
||||
import moment from 'moment-timezone';
|
||||
import type {
|
||||
IPollFunctions,
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeExecutionData,
|
||||
INodePropertyOptions,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeConnectionTypes, NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { GOOGLE_DRIVE_FILE_URL_REGEX, GOOGLE_DRIVE_FOLDER_URL_REGEX } from '../constants';
|
||||
import { extractId, googleApiRequest, googleApiRequestAllItems } from './v1/GenericFunctions';
|
||||
import { fileSearch, folderSearch } from './v2/methods/listSearch';
|
||||
|
||||
export class GoogleDriveTrigger implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Google Drive Trigger',
|
||||
name: 'googleDriveTrigger',
|
||||
icon: 'file:googleDrive.svg',
|
||||
group: ['trigger'],
|
||||
version: 1,
|
||||
description: 'Starts the workflow when Google Drive events occur',
|
||||
subtitle: '={{$parameter["event"]}}',
|
||||
defaults: {
|
||||
name: 'Google Drive Trigger',
|
||||
},
|
||||
credentials: [
|
||||
{
|
||||
name: 'googleApi',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['serviceAccount'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'googleDriveOAuth2Api',
|
||||
required: true,
|
||||
displayOptions: {
|
||||
show: {
|
||||
authentication: ['oAuth2'],
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
polling: true,
|
||||
inputs: [],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Credential Type',
|
||||
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: 'Trigger On',
|
||||
name: 'triggerOn',
|
||||
type: 'options',
|
||||
required: true,
|
||||
default: '',
|
||||
options: [
|
||||
{
|
||||
name: 'Changes to a Specific File',
|
||||
value: 'specificFile',
|
||||
},
|
||||
{
|
||||
name: 'Changes Involving a Specific Folder',
|
||||
value: 'specificFolder',
|
||||
},
|
||||
// {
|
||||
// name: 'Changes To Any File/Folder',
|
||||
// value: 'anyFileFolder',
|
||||
// },
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'File',
|
||||
name: 'fileToWatch',
|
||||
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: 'https://drive.google.com/file/d/1wroCSfK-hupQIYf_xzeoUEzOhvfTFH2P/edit',
|
||||
extractValue: {
|
||||
type: 'regex',
|
||||
regex: GOOGLE_DRIVE_FILE_URL_REGEX,
|
||||
},
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: GOOGLE_DRIVE_FILE_URL_REGEX,
|
||||
errorMessage: 'Not a valid Google Drive File URL',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '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',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
triggerOn: ['specificFile'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Watch For',
|
||||
name: 'event',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
triggerOn: ['specificFile'],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
default: 'fileUpdated',
|
||||
options: [
|
||||
{
|
||||
name: 'File Updated',
|
||||
value: 'fileUpdated',
|
||||
},
|
||||
],
|
||||
description: 'When to trigger this node',
|
||||
},
|
||||
{
|
||||
displayName: 'Folder',
|
||||
name: 'folderToWatch',
|
||||
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: 'https://drive.google.com/drive/folders/1Tx9WHbA3wBpPB4C_HcoZDH9WZFWYxAMU',
|
||||
extractValue: {
|
||||
type: 'regex',
|
||||
regex: GOOGLE_DRIVE_FOLDER_URL_REGEX,
|
||||
},
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: GOOGLE_DRIVE_FOLDER_URL_REGEX,
|
||||
errorMessage: 'Not a valid Google Drive Folder URL',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: 'ID',
|
||||
name: 'id',
|
||||
type: 'string',
|
||||
placeholder: '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}}',
|
||||
},
|
||||
],
|
||||
displayOptions: {
|
||||
show: {
|
||||
triggerOn: ['specificFolder'],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Watch For',
|
||||
name: 'event',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
triggerOn: ['specificFolder'],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
default: '',
|
||||
options: [
|
||||
{
|
||||
name: 'File Created',
|
||||
value: 'fileCreated',
|
||||
description: 'When a file is created in the watched folder',
|
||||
},
|
||||
{
|
||||
name: 'File Updated',
|
||||
value: 'fileUpdated',
|
||||
description: 'When a file is updated in the watched folder',
|
||||
},
|
||||
{
|
||||
name: 'Folder Created',
|
||||
value: 'folderCreated',
|
||||
description: 'When a folder is created in the watched folder',
|
||||
},
|
||||
{
|
||||
name: 'Folder Updated',
|
||||
value: 'folderUpdated',
|
||||
description: 'When a folder is updated in the watched folder',
|
||||
},
|
||||
{
|
||||
name: 'Watch Folder Updated',
|
||||
value: 'watchFolderUpdated',
|
||||
description: 'When the watched folder itself is modified',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
displayName: "Changes within subfolders won't trigger this node",
|
||||
name: 'asas',
|
||||
type: 'notice',
|
||||
displayOptions: {
|
||||
show: {
|
||||
triggerOn: ['specificFolder'],
|
||||
},
|
||||
hide: {
|
||||
event: ['watchFolderUpdated'],
|
||||
},
|
||||
},
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||
displayName: 'Drive To Watch',
|
||||
name: 'driveToWatch',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
triggerOn: ['anyFileFolder'],
|
||||
},
|
||||
},
|
||||
typeOptions: {
|
||||
loadOptionsMethod: 'getDrives',
|
||||
},
|
||||
default: 'root',
|
||||
required: true,
|
||||
description:
|
||||
'The drive to monitor. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
||||
},
|
||||
{
|
||||
displayName: 'Watch For',
|
||||
name: 'event',
|
||||
type: 'options',
|
||||
displayOptions: {
|
||||
show: {
|
||||
triggerOn: ['anyFileFolder'],
|
||||
},
|
||||
},
|
||||
required: true,
|
||||
default: 'fileCreated',
|
||||
options: [
|
||||
{
|
||||
name: 'File Created',
|
||||
value: 'fileCreated',
|
||||
description: 'When a file is created in the watched drive',
|
||||
},
|
||||
{
|
||||
name: 'File Updated',
|
||||
value: 'fileUpdated',
|
||||
description: 'When a file is updated in the watched drive',
|
||||
},
|
||||
{
|
||||
name: 'Folder Created',
|
||||
value: 'folderCreated',
|
||||
description: 'When a folder is created in the watched drive',
|
||||
},
|
||||
{
|
||||
name: 'Folder Updated',
|
||||
value: 'folderUpdated',
|
||||
description: 'When a folder is updated in the watched drive',
|
||||
},
|
||||
],
|
||||
description: 'When to trigger this node',
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
displayOptions: {
|
||||
show: {
|
||||
event: ['fileCreated', 'fileUpdated'],
|
||||
},
|
||||
hide: {
|
||||
triggerOn: ['specificFile'],
|
||||
},
|
||||
},
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
displayName: 'File Type',
|
||||
name: 'fileType',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: '[All]',
|
||||
value: 'all',
|
||||
},
|
||||
{
|
||||
name: 'Audio',
|
||||
value: 'application/vnd.google-apps.audio',
|
||||
},
|
||||
{
|
||||
name: 'Google Docs',
|
||||
value: 'application/vnd.google-apps.document',
|
||||
},
|
||||
{
|
||||
name: 'Google Drawings',
|
||||
value: 'application/vnd.google-apps.drawing',
|
||||
},
|
||||
{
|
||||
name: 'Google Slides',
|
||||
value: 'application/vnd.google-apps.presentation',
|
||||
},
|
||||
{
|
||||
name: 'Google Spreadsheets',
|
||||
value: 'application/vnd.google-apps.spreadsheet',
|
||||
},
|
||||
{
|
||||
name: 'Photos and Images',
|
||||
value: 'application/vnd.google-apps.photo',
|
||||
},
|
||||
{
|
||||
name: 'Videos',
|
||||
value: 'application/vnd.google-apps.video',
|
||||
},
|
||||
],
|
||||
default: 'all',
|
||||
description: 'Triggers only when the file is this type',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
methods = {
|
||||
listSearch: {
|
||||
fileSearch,
|
||||
folderSearch,
|
||||
},
|
||||
loadOptions: {
|
||||
// Get all the calendars to display them to user so that they can
|
||||
// select them easily
|
||||
async getDrives(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||
const returnData: INodePropertyOptions[] = [];
|
||||
const drives = await googleApiRequestAllItems.call(
|
||||
this,
|
||||
'drives',
|
||||
'GET',
|
||||
'/drive/v3/drives',
|
||||
);
|
||||
returnData.push({
|
||||
name: 'Root',
|
||||
value: 'root',
|
||||
});
|
||||
for (const drive of drives) {
|
||||
returnData.push({
|
||||
name: drive.name,
|
||||
value: drive.id,
|
||||
});
|
||||
}
|
||||
return returnData;
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
|
||||
const triggerOn = this.getNodeParameter('triggerOn') as string;
|
||||
const event = this.getNodeParameter('event') as string;
|
||||
const webhookData = this.getWorkflowStaticData('node');
|
||||
const options = this.getNodeParameter('options', {}) as IDataObject;
|
||||
const qs: IDataObject = {
|
||||
includeItemsFromAllDrives: true,
|
||||
supportsAllDrives: true,
|
||||
spaces: 'appDataFolder, drive',
|
||||
corpora: 'allDrives',
|
||||
};
|
||||
|
||||
const now = moment().utc().format();
|
||||
|
||||
const startDate = (webhookData.lastTimeChecked as string) || now;
|
||||
|
||||
const endDate = now;
|
||||
|
||||
const query = ['trashed = false'];
|
||||
|
||||
if (triggerOn === 'specificFolder' && event !== 'watchFolderUpdated') {
|
||||
const folderToWatch = extractId(
|
||||
this.getNodeParameter('folderToWatch', '', { extractValue: true }) as string,
|
||||
);
|
||||
query.push(`'${folderToWatch}' in parents`);
|
||||
}
|
||||
|
||||
// if (triggerOn === 'anyFileFolder') {
|
||||
// const driveToWatch = this.getNodeParameter('driveToWatch');
|
||||
// query.push(`'${driveToWatch}' in parents`);
|
||||
// }
|
||||
|
||||
if (event.startsWith('file')) {
|
||||
query.push("mimeType != 'application/vnd.google-apps.folder'");
|
||||
} else {
|
||||
query.push("mimeType = 'application/vnd.google-apps.folder'");
|
||||
}
|
||||
|
||||
if (options.fileType && options.fileType !== 'all') {
|
||||
query.push(`mimeType = '${options.fileType}'`);
|
||||
}
|
||||
|
||||
if (this.getMode() !== 'manual') {
|
||||
if (event.includes('Created')) {
|
||||
query.push(`createdTime > '${startDate}'`);
|
||||
} else {
|
||||
query.push(`modifiedTime > '${startDate}'`);
|
||||
}
|
||||
}
|
||||
|
||||
qs.q = query.join(' AND ');
|
||||
|
||||
qs.fields = 'nextPageToken, files(*)';
|
||||
|
||||
let files;
|
||||
|
||||
if (this.getMode() === 'manual') {
|
||||
qs.pageSize = 1;
|
||||
files = await googleApiRequest.call(this, 'GET', '/drive/v3/files', {}, qs);
|
||||
files = files.files;
|
||||
} else {
|
||||
files = await googleApiRequestAllItems.call(this, 'files', 'GET', '/drive/v3/files', {}, qs);
|
||||
}
|
||||
|
||||
if (triggerOn === 'specificFile' && this.getMode() !== 'manual') {
|
||||
const fileToWatch = extractId(
|
||||
this.getNodeParameter('fileToWatch', '', { extractValue: true }) as string,
|
||||
);
|
||||
files = files.filter((file: { id: string }) => file.id === fileToWatch);
|
||||
}
|
||||
|
||||
if (
|
||||
triggerOn === 'specificFolder' &&
|
||||
event === 'watchFolderUpdated' &&
|
||||
this.getMode() !== 'manual'
|
||||
) {
|
||||
const folderToWatch = extractId(
|
||||
this.getNodeParameter('folderToWatch', '', { extractValue: true }) as string,
|
||||
);
|
||||
files = files.filter((file: { id: string }) => file.id === folderToWatch);
|
||||
}
|
||||
|
||||
webhookData.lastTimeChecked = endDate;
|
||||
|
||||
if (Array.isArray(files) && files.length) {
|
||||
return [this.helpers.returnJsonArray(files)];
|
||||
}
|
||||
|
||||
if (this.getMode() === 'manual') {
|
||||
throw new NodeApiError(this.getNode(), {
|
||||
message: 'No data with the current filter could be found',
|
||||
});
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"mimeType": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 2
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"mimeType": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allowFileDiscovery": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"mimeType": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,366 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"capabilities": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"canAcceptOwnership": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canAddChildren": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canAddMyDriveParent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canChangeCopyRequiresWriterPermission": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canChangeItemDownloadRestriction": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canChangeSecurityUpdateEnabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canChangeViewersCanCopyContent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canComment": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canCopy": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canDelete": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canDisableInheritedPermissions": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canDownload": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canEdit": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canEnableInheritedPermissions": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canListChildren": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canModifyContent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canModifyContentRestriction": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canModifyEditorContentRestriction": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canModifyLabels": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canModifyOwnerContentRestriction": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canMoveChildrenWithinDrive": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canMoveItemIntoTeamDrive": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canMoveItemOutOfDrive": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canMoveItemWithinDrive": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canReadLabels": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canReadRevisions": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canRemoveChildren": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canRemoveContentRestriction": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canRemoveMyDriveParent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canRename": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canShare": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canTrash": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"canUntrash": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"copyRequiresWriterPermission": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"createdTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"downloadRestrictions": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"effectiveDownloadRestrictionWithContext": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"restrictedForReaders": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"restrictedForWriters": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"itemDownloadRestriction": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"restrictedForReaders": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"restrictedForWriters": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"explicitlyTrashed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"fileExtension": {
|
||||
"type": "string"
|
||||
},
|
||||
"fullFileExtension": {
|
||||
"type": "string"
|
||||
},
|
||||
"hasThumbnail": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"headRevisionId": {
|
||||
"type": "string"
|
||||
},
|
||||
"iconLink": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"imageMediaMetadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"height": {
|
||||
"type": "integer"
|
||||
},
|
||||
"rotation": {
|
||||
"type": "integer"
|
||||
},
|
||||
"width": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
},
|
||||
"inheritedPermissionsDisabled": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"isAppAuthorized": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"lastModifyingUser": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"emailAddress": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"me": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"permissionId": {
|
||||
"type": "string"
|
||||
},
|
||||
"photoLink": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"linkShareMetadata": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"securityUpdateEligible": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"securityUpdateEnabled": {
|
||||
"type": "boolean"
|
||||
}
|
||||
}
|
||||
},
|
||||
"md5Checksum": {
|
||||
"type": "string"
|
||||
},
|
||||
"mimeType": {
|
||||
"type": "string"
|
||||
},
|
||||
"modifiedByMe": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"modifiedByMeTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"modifiedTime": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
},
|
||||
"originalFilename": {
|
||||
"type": "string"
|
||||
},
|
||||
"ownedByMe": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"owners": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"emailAddress": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"me": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"permissionId": {
|
||||
"type": "string"
|
||||
},
|
||||
"photoLink": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"parents": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"permissionIds": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"permissions": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"deleted": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"displayName": {
|
||||
"type": "string"
|
||||
},
|
||||
"emailAddress": {
|
||||
"type": "string"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"pendingOwner": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"quotaBytesUsed": {
|
||||
"type": "string"
|
||||
},
|
||||
"sha1Checksum": {
|
||||
"type": "string"
|
||||
},
|
||||
"sha256Checksum": {
|
||||
"type": "string"
|
||||
},
|
||||
"shared": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"size": {
|
||||
"type": "string"
|
||||
},
|
||||
"spaces": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"starred": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"thumbnailLink": {
|
||||
"type": "string"
|
||||
},
|
||||
"thumbnailVersion": {
|
||||
"type": "string"
|
||||
},
|
||||
"trashed": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"version": {
|
||||
"type": "string"
|
||||
},
|
||||
"viewedByMe": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"viewersCanCopyContent": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"webContentLink": {
|
||||
"type": "string"
|
||||
},
|
||||
"webViewLink": {
|
||||
"type": "string"
|
||||
},
|
||||
"writersCanShare": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"version": 6
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"mimeType": {
|
||||
"type": "string"
|
||||
},
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"fileId": {
|
||||
"type": "string"
|
||||
},
|
||||
"success": {
|
||||
"type": "boolean"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"allowFileDiscovery": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"id": {
|
||||
"type": "string"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string"
|
||||
},
|
||||
"role": {
|
||||
"type": "string"
|
||||
},
|
||||
"type": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"version": 1
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" fill="#fff" fill-rule="evenodd" stroke="#000" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 81 73"><use xlink:href="#a" x=".5" y=".5"/><symbol id="a" overflow="visible"><g fill-rule="nonzero" stroke="none"><path fill="#0066da" d="m6.048 61.26 3.528 6.094c.733 1.283 1.787 2.291 3.024 3.024l12.6-21.81H0a8.3 8.3 0 0 0 1.1 4.124z"/><path fill="#00ac47" d="M40 22.91 27.4 1.1c-1.237.733-2.291 1.741-3.024 3.024L1.1 44.445A8.3 8.3 0 0 0 0 48.568h25.2z"/><path fill="#ea4335" d="M67.4 70.378c1.237-.733 2.291-1.741 3.024-3.024l1.466-2.52 7.01-12.142a8.3 8.3 0 0 0 1.1-4.124H54.798l5.363 10.538z"/><path fill="#00832d" d="M40 22.91 52.6 1.1C51.363.367 49.943 0 48.477 0H31.524c-1.466 0-2.887.412-4.124 1.1z"/><path fill="#2684fc" d="M54.799 48.568H25.2l-12.6 21.81c1.237.733 2.657 1.1 4.124 1.1h46.552c1.466 0 2.887-.412 4.124-1.1z"/><path fill="#ffba00" d="M67.262 24.284 55.624 4.124C54.891 2.841 53.837 1.833 52.6 1.1L40 22.91l14.8 25.659h25.155a8.3 8.3 0 0 0-1.1-4.124z"/></g></symbol></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,610 @@
|
||||
import { mockDeep } from 'jest-mock-extended';
|
||||
import moment from 'moment-timezone';
|
||||
import type { IPollFunctions, INode, ILoadOptionsFunctions, IDataObject } from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { GoogleDriveTrigger } from '../GoogleDriveTrigger.node';
|
||||
import * as GenericFunctions from '../v1/GenericFunctions';
|
||||
import * as listSearch from '../v2/methods/listSearch';
|
||||
|
||||
jest.mock('../v1/GenericFunctions', () => ({
|
||||
extractId: jest.fn(),
|
||||
googleApiRequest: jest.fn(),
|
||||
googleApiRequestAllItems: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../v2/methods/listSearch', () => ({
|
||||
fileSearch: jest.fn(),
|
||||
folderSearch: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('GoogleDriveTrigger', () => {
|
||||
let trigger: GoogleDriveTrigger;
|
||||
let mockPollFunctions: jest.Mocked<IPollFunctions>;
|
||||
let mockNode: INode;
|
||||
|
||||
const extractIdSpy = jest.spyOn(GenericFunctions, 'extractId');
|
||||
const googleApiRequestSpy = jest.spyOn(GenericFunctions, 'googleApiRequest');
|
||||
const googleApiRequestAllItemsSpy = jest.spyOn(GenericFunctions, 'googleApiRequestAllItems');
|
||||
|
||||
beforeEach(() => {
|
||||
trigger = new GoogleDriveTrigger();
|
||||
mockPollFunctions = mockDeep<IPollFunctions>();
|
||||
mockNode = {
|
||||
id: 'test-node-id',
|
||||
name: 'Google Drive Trigger Test',
|
||||
type: 'n8n-nodes-base.googleDriveTrigger',
|
||||
typeVersion: 1,
|
||||
position: [0, 0],
|
||||
parameters: {},
|
||||
};
|
||||
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockPollFunctions.getNode.mockReturnValue(mockNode);
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue({});
|
||||
mockPollFunctions.getMode.mockReturnValue('trigger');
|
||||
(mockPollFunctions.helpers.returnJsonArray as jest.Mock).mockImplementation((data: unknown[]) =>
|
||||
data.map((item: unknown, index: number) => ({ json: item, pairedItem: { item: index } })),
|
||||
);
|
||||
extractIdSpy.mockImplementation((id) => id);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe('Methods', () => {
|
||||
it('should have correct list search methods', () => {
|
||||
expect(trigger.methods?.listSearch?.fileSearch).toBe(listSearch.fileSearch);
|
||||
expect(trigger.methods?.listSearch?.folderSearch).toBe(listSearch.folderSearch);
|
||||
});
|
||||
|
||||
it('should have correct load options methods', () => {
|
||||
expect(trigger.methods?.loadOptions?.getDrives).toBeDefined();
|
||||
});
|
||||
|
||||
describe('getDrives', () => {
|
||||
it('should return drives with root option', async () => {
|
||||
const mockDrives = [
|
||||
{ id: 'drive1', name: 'My Drive 1' },
|
||||
{ id: 'drive2', name: 'My Drive 2' },
|
||||
];
|
||||
|
||||
const mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue(mockDrives);
|
||||
|
||||
const result = await trigger.methods.loadOptions.getDrives.call(mockLoadOptionsFunctions);
|
||||
|
||||
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'drives',
|
||||
'GET',
|
||||
'/drive/v3/drives',
|
||||
);
|
||||
|
||||
expect(result).toEqual([
|
||||
{ name: 'Root', value: 'root' },
|
||||
{ name: 'My Drive 1', value: 'drive1' },
|
||||
{ name: 'My Drive 2', value: 'drive2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should handle empty drives list', async () => {
|
||||
const mockLoadOptionsFunctions = mockDeep<ILoadOptionsFunctions>();
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
|
||||
const result = await trigger.methods.loadOptions.getDrives.call(mockLoadOptionsFunctions);
|
||||
|
||||
expect(result).toEqual([{ name: 'Root', value: 'root' }]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll Function - Parameter Setup', () => {
|
||||
beforeEach(() => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFile',
|
||||
event: 'fileUpdated',
|
||||
fileToWatch: 'test-file-id',
|
||||
folderToWatch: 'test-folder-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle specific file trigger', async () => {
|
||||
const now = moment().utc();
|
||||
const webhookData = { lastTimeChecked: now.clone().subtract(1, 'hour').format() };
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(webhookData);
|
||||
|
||||
const mockFiles = [
|
||||
{
|
||||
id: 'test-file-id',
|
||||
name: 'Test File',
|
||||
modifiedTime: now.format(),
|
||||
},
|
||||
];
|
||||
|
||||
extractIdSpy.mockReturnValue('test-file-id');
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue(mockFiles);
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'files',
|
||||
'GET',
|
||||
'/drive/v3/files',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
includeItemsFromAllDrives: true,
|
||||
supportsAllDrives: true,
|
||||
spaces: 'appDataFolder, drive',
|
||||
corpora: 'allDrives',
|
||||
q: expect.stringContaining('trashed = false'),
|
||||
fields: 'nextPageToken, files(*)',
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result![0]).toHaveLength(1);
|
||||
expect(result![0][0].json.id).toBe('test-file-id');
|
||||
});
|
||||
|
||||
it('should handle specific folder trigger for file created', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFolder',
|
||||
event: 'fileCreated',
|
||||
folderToWatch: 'test-folder-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
const now = moment().utc();
|
||||
const webhookData = { lastTimeChecked: now.clone().subtract(1, 'hour').format() };
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(webhookData);
|
||||
|
||||
const mockFiles = [
|
||||
{
|
||||
id: 'new-file-id',
|
||||
name: 'New File',
|
||||
createdTime: now.format(),
|
||||
},
|
||||
];
|
||||
|
||||
extractIdSpy.mockReturnValue('test-folder-id');
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue(mockFiles);
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'files',
|
||||
'GET',
|
||||
'/drive/v3/files',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
q: expect.stringContaining("'test-folder-id' in parents"),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result![0]).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should filter by file type when specified', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFolder',
|
||||
event: 'fileCreated',
|
||||
folderToWatch: 'test-folder-id',
|
||||
options: { fileType: 'application/vnd.google-apps.document' },
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'files',
|
||||
'GET',
|
||||
'/drive/v3/files',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
q: expect.stringContaining("mimeType = 'application/vnd.google-apps.document'"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle folder events with folder mime type filter', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFolder',
|
||||
event: 'folderCreated',
|
||||
folderToWatch: 'test-folder-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'files',
|
||||
'GET',
|
||||
'/drive/v3/files',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
q: expect.stringContaining("mimeType = 'application/vnd.google-apps.folder'"),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle watch folder updated event', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFolder',
|
||||
event: 'watchFolderUpdated',
|
||||
folderToWatch: 'test-folder-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
const mockFiles = [
|
||||
{
|
||||
id: 'test-folder-id',
|
||||
name: 'Test Folder',
|
||||
modifiedTime: moment().utc().format(),
|
||||
},
|
||||
];
|
||||
|
||||
extractIdSpy.mockReturnValue('test-folder-id');
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue(mockFiles);
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'files',
|
||||
'GET',
|
||||
'/drive/v3/files',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
q: expect.not.stringContaining('in parents'),
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
});
|
||||
|
||||
it('should use createdTime for Created events and modifiedTime for Updated events', async () => {
|
||||
const now = moment().utc();
|
||||
const webhookData: IDataObject = {
|
||||
lastTimeChecked: now.clone().subtract(1, 'hour').format(),
|
||||
};
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(webhookData);
|
||||
|
||||
// Test fileCreated event uses createdTime
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, unknown> = {
|
||||
triggerOn: 'specificFolder',
|
||||
event: 'fileCreated',
|
||||
folderToWatch: 'test-folder-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'files',
|
||||
'GET',
|
||||
'/drive/v3/files',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
q: expect.stringContaining('createdTime >'),
|
||||
}),
|
||||
);
|
||||
|
||||
// Reset mock
|
||||
googleApiRequestAllItemsSpy.mockClear();
|
||||
|
||||
// Test fileUpdated event uses modifiedTime
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, unknown> = {
|
||||
triggerOn: 'specificFolder',
|
||||
event: 'fileUpdated',
|
||||
folderToWatch: 'test-folder-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(googleApiRequestAllItemsSpy).toHaveBeenCalledWith(
|
||||
'files',
|
||||
'GET',
|
||||
'/drive/v3/files',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
q: expect.stringContaining('modifiedTime >'),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll Function - Manual Mode', () => {
|
||||
beforeEach(() => {
|
||||
mockPollFunctions.getMode.mockReturnValue('manual');
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFile',
|
||||
event: 'fileUpdated',
|
||||
fileToWatch: 'test-file-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch single file in manual mode', async () => {
|
||||
const mockResponse = {
|
||||
files: [
|
||||
{
|
||||
id: 'test-file-id',
|
||||
name: 'Test File',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
googleApiRequestSpy.mockResolvedValue(mockResponse);
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(googleApiRequestSpy).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'/drive/v3/files',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
pageSize: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result![0]).toHaveLength(1);
|
||||
expect(result![0][0].json.id).toBe('test-file-id');
|
||||
});
|
||||
|
||||
it('should throw NodeApiError when no data found in manual mode', async () => {
|
||||
googleApiRequestSpy.mockResolvedValue({ files: [] });
|
||||
|
||||
await expect(trigger.poll.call(mockPollFunctions)).rejects.toThrow(NodeApiError);
|
||||
await expect(trigger.poll.call(mockPollFunctions)).rejects.toThrow(
|
||||
'No data with the current filter could be found',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll Function - State Management', () => {
|
||||
beforeEach(() => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFile',
|
||||
event: 'fileUpdated',
|
||||
fileToWatch: 'test-file-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
});
|
||||
|
||||
it('should update lastTimeChecked in webhook data', async () => {
|
||||
const mockWebhookData = { lastTimeChecked: moment().subtract(1, 'day').format() };
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWebhookData);
|
||||
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(mockWebhookData.lastTimeChecked).toBeDefined();
|
||||
expect(moment(mockWebhookData.lastTimeChecked).isValid()).toBe(true);
|
||||
});
|
||||
|
||||
it('should use current time as startDate when no lastTimeChecked exists', async () => {
|
||||
const mockWebhookData: IDataObject = {};
|
||||
mockPollFunctions.getWorkflowStaticData.mockReturnValue(mockWebhookData);
|
||||
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(mockWebhookData.lastTimeChecked).toBeDefined();
|
||||
expect(moment(mockWebhookData.lastTimeChecked as string).isValid()).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll Function - Error Handling', () => {
|
||||
beforeEach(() => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFile',
|
||||
event: 'fileUpdated',
|
||||
fileToWatch: 'test-file-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle API request errors', async () => {
|
||||
const apiError = new Error('API Error');
|
||||
googleApiRequestAllItemsSpy.mockRejectedValue(apiError);
|
||||
|
||||
await expect(trigger.poll.call(mockPollFunctions)).rejects.toThrow('API Error');
|
||||
});
|
||||
|
||||
it('should handle invalid extractId results', async () => {
|
||||
extractIdSpy.mockImplementation(() => {
|
||||
throw new Error('Invalid ID');
|
||||
});
|
||||
|
||||
await expect(trigger.poll.call(mockPollFunctions)).rejects.toThrow('Invalid ID');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll Function - Edge Cases', () => {
|
||||
it('should return null when no files found in trigger mode', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFile',
|
||||
event: 'fileUpdated',
|
||||
fileToWatch: 'test-file-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle empty files array', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFolder',
|
||||
event: 'fileCreated',
|
||||
folderToWatch: 'test-folder-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue([]);
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle files without required fields gracefully', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFile',
|
||||
event: 'fileUpdated',
|
||||
fileToWatch: 'test-file-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
const mockFiles = [
|
||||
{
|
||||
id: 'test-file-id',
|
||||
// Missing name and other fields
|
||||
},
|
||||
];
|
||||
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue(mockFiles);
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result![0]).toHaveLength(1);
|
||||
expect(result![0][0].json.id).toBe('test-file-id');
|
||||
});
|
||||
|
||||
it('should skip time filtering in manual mode', async () => {
|
||||
mockPollFunctions.getMode.mockReturnValue('manual');
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFolder',
|
||||
event: 'fileCreated',
|
||||
folderToWatch: 'test-folder-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
const mockResponse = { files: [{ id: 'test-file', name: 'Test' }] };
|
||||
googleApiRequestSpy.mockResolvedValue(mockResponse);
|
||||
|
||||
await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(googleApiRequestSpy).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'/drive/v3/files',
|
||||
{},
|
||||
expect.objectContaining({
|
||||
q: expect.not.stringMatching(/createdTime|modifiedTime/),
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Poll Function - File Filtering', () => {
|
||||
it('should filter specific file results correctly', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFile',
|
||||
event: 'fileUpdated',
|
||||
fileToWatch: 'target-file-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
const mockFiles = [
|
||||
{ id: 'target-file-id', name: 'Target File' },
|
||||
{ id: 'other-file-id', name: 'Other File' },
|
||||
];
|
||||
|
||||
extractIdSpy.mockReturnValue('target-file-id');
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue(mockFiles);
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result![0]).toHaveLength(1);
|
||||
expect(result![0][0].json.id).toBe('target-file-id');
|
||||
});
|
||||
|
||||
it('should filter specific folder results correctly for watchFolderUpdated', async () => {
|
||||
mockPollFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||
const params: Record<string, any> = {
|
||||
triggerOn: 'specificFolder',
|
||||
event: 'watchFolderUpdated',
|
||||
folderToWatch: 'target-folder-id',
|
||||
options: {},
|
||||
};
|
||||
return params[paramName] ?? '';
|
||||
});
|
||||
|
||||
const mockFiles = [
|
||||
{ id: 'target-folder-id', name: 'Target Folder' },
|
||||
{ id: 'other-folder-id', name: 'Other Folder' },
|
||||
];
|
||||
|
||||
extractIdSpy.mockReturnValue('target-folder-id');
|
||||
googleApiRequestAllItemsSpy.mockResolvedValue(mockFiles);
|
||||
|
||||
const result = await trigger.poll.call(mockPollFunctions);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result![0]).toHaveLength(1);
|
||||
expect(result![0][0].json.id).toBe('target-folder-id');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,62 @@
|
||||
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 {};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
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', () => {
|
||||
it('should 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' },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
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', () => {
|
||||
it('should 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',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
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', () => {
|
||||
it('should 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 },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { IHttpRequestMethods } from 'n8n-workflow';
|
||||
|
||||
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: IHttpRequestMethods) {
|
||||
if (method === 'GET') {
|
||||
return {};
|
||||
}
|
||||
}),
|
||||
googleApiRequestAllItems: jest.fn(async function (method: IHttpRequestMethods) {
|
||||
if (method === 'GET') {
|
||||
return {};
|
||||
}
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('test GoogleDriveV2: drive list', () => {
|
||||
it('should 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('should 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',
|
||||
{},
|
||||
{},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
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', () => {
|
||||
it('should 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 } },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,63 @@
|
||||
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', () => {
|
||||
it('should 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',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,94 @@
|
||||
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 { id: 42 };
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('test GoogleDriveV2: file createFromText', () => {
|
||||
it('should 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(2);
|
||||
expect(transport.googleApiRequest).toHaveBeenCalledWith(
|
||||
'POST',
|
||||
'/upload/drive/v3/files',
|
||||
expect.anything(), // Buffer of content goes here
|
||||
{ uploadType: 'multipart', supportsAllDrives: true },
|
||||
undefined,
|
||||
{
|
||||
headers: {
|
||||
'Content-Length': 503,
|
||||
'Content-Type': expect.stringMatching(/^multipart\/related; boundary=(\\S)*/),
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(transport.googleApiRequest).toHaveBeenCalledWith(
|
||||
'PATCH',
|
||||
'/drive/v3/files/42',
|
||||
{
|
||||
appProperties: { appKey1: 'appValue1' },
|
||||
mimeType: 'text/plain',
|
||||
name: 'helloDrive.txt',
|
||||
properties: { prop1: 'value1', prop2: 'value2' },
|
||||
},
|
||||
{
|
||||
addParents: 'folderIDxxxxxx',
|
||||
corpora: 'allDrives',
|
||||
includeItemsFromAllDrives: true,
|
||||
keepRevisionForever: true,
|
||||
ocrLanguage: 'en',
|
||||
spaces: 'appDataFolder, drive',
|
||||
supportsAllDrives: true,
|
||||
useContentAsIndexableText: true,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,43 @@
|
||||
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', () => {
|
||||
it('should 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 },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
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', () => {
|
||||
it('should 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, supportsAllDrives: true },
|
||||
);
|
||||
expect(transport.googleApiRequest).toHaveBeenCalledWith(
|
||||
'GET',
|
||||
'/drive/v3/files/fileIDxxxxxx',
|
||||
{},
|
||||
{ alt: 'media', supportsAllDrives: true },
|
||||
undefined,
|
||||
{ encoding: 'arraybuffer', json: false, returnFullResponse: true, useStream: true },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import type { IHttpRequestMethods } from 'n8n-workflow';
|
||||
|
||||
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: IHttpRequestMethods) {
|
||||
if (method === 'GET') {
|
||||
return {
|
||||
parents: ['parentFolderIDxxxxxx'],
|
||||
};
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('test GoogleDriveV2: file move', () => {
|
||||
it('should 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,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
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', () => {
|
||||
it('should 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,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
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', () => {
|
||||
it('should 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,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,160 @@
|
||||
import type { IHttpRequestMethods } from 'n8n-workflow';
|
||||
|
||||
import * as upload from '../../../../v2/actions/file/upload.operation';
|
||||
import * as utils from '../../../../v2/helpers/utils';
|
||||
import * as transport from '../../../../v2/transport';
|
||||
import { createMockExecuteFunction, createTestStream, driveNode } from '../helpers';
|
||||
|
||||
const fileContent = Buffer.from('Hello Drive!');
|
||||
const originalFilename = 'original.txt';
|
||||
const contentLength = 123;
|
||||
const mimeType = 'text/plain';
|
||||
|
||||
jest.mock('../../../../v2/transport', () => {
|
||||
const originalModule = jest.requireActual('../../../../v2/transport');
|
||||
return {
|
||||
...originalModule,
|
||||
googleApiRequest: jest.fn(async function (method: IHttpRequestMethods) {
|
||||
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,
|
||||
fileContent,
|
||||
originalFilename,
|
||||
mimeType,
|
||||
};
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('test GoogleDriveV2: file upload', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should upload buffers', async () => {
|
||||
const name = 'newFile.txt';
|
||||
const parent = 'folderIDxxxxxx';
|
||||
const nodeParameters = {
|
||||
name,
|
||||
folderId: {
|
||||
__rl: true,
|
||||
value: parent,
|
||||
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',
|
||||
expect.any(Buffer),
|
||||
{ uploadType: 'multipart', supportsAllDrives: true },
|
||||
undefined,
|
||||
{
|
||||
headers: {
|
||||
'Content-Length': 498,
|
||||
'Content-Type': expect.stringMatching(/^multipart\/related; boundary=(\\S)*/),
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(transport.googleApiRequest).toHaveBeenCalledWith(
|
||||
'PATCH',
|
||||
'/drive/v3/files/undefined',
|
||||
{ mimeType, name, originalFilename },
|
||||
{
|
||||
addParents: parent,
|
||||
supportsAllDrives: true,
|
||||
corpora: 'allDrives',
|
||||
includeItemsFromAllDrives: true,
|
||||
spaces: 'appDataFolder, drive',
|
||||
},
|
||||
);
|
||||
|
||||
expect(utils.getItemBinaryData).toBeCalledTimes(1);
|
||||
expect(utils.getItemBinaryData).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should stream large files in 2MB chunks', async () => {
|
||||
const name = 'newFile.jpg';
|
||||
const parent = 'folderIDxxxxxx';
|
||||
const originalFilename = 'test.jpg';
|
||||
const mimeType = 'image/jpg';
|
||||
const nodeParameters = {
|
||||
name,
|
||||
folderId: {
|
||||
__rl: true,
|
||||
value: parent,
|
||||
mode: 'list',
|
||||
cachedResultName: 'testFolder 3',
|
||||
cachedResultUrl: 'https://drive.google.com/drive/folders/folderIDxxxxxx',
|
||||
},
|
||||
options: {
|
||||
simplifyOutput: true,
|
||||
},
|
||||
};
|
||||
|
||||
const fakeExecuteFunction = createMockExecuteFunction(nodeParameters, driveNode);
|
||||
const httpRequestSpy = jest.spyOn(fakeExecuteFunction.helpers, 'httpRequest');
|
||||
|
||||
const fileSize = 7 * 1024 * 1024; // 7MB
|
||||
jest.mocked(utils.getItemBinaryData).mockResolvedValue({
|
||||
mimeType,
|
||||
originalFilename,
|
||||
contentLength: fileSize,
|
||||
fileContent: createTestStream(fileSize),
|
||||
});
|
||||
|
||||
await upload.execute.call(fakeExecuteFunction, 0);
|
||||
|
||||
// 4 chunks: 7MB = 3x2MB + 1x1MB
|
||||
expect(httpRequestSpy).toHaveBeenCalledTimes(4);
|
||||
expect(httpRequestSpy).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ body: expect.any(Buffer) }),
|
||||
);
|
||||
expect(transport.googleApiRequest).toBeCalledTimes(2);
|
||||
expect(transport.googleApiRequest).toHaveBeenCalledWith(
|
||||
'POST',
|
||||
'/upload/drive/v3/files',
|
||||
{ name, parents: [parent] },
|
||||
{ uploadType: 'resumable', supportsAllDrives: true },
|
||||
undefined,
|
||||
{ returnFullResponse: true, headers: { 'X-Upload-Content-Type': 'image/jpg' } },
|
||||
);
|
||||
expect(transport.googleApiRequest).toHaveBeenCalledWith(
|
||||
'PATCH',
|
||||
'/drive/v3/files/undefined',
|
||||
{ mimeType, name, originalFilename },
|
||||
{
|
||||
addParents: parent,
|
||||
supportsAllDrives: true,
|
||||
corpora: 'allDrives',
|
||||
includeItemsFromAllDrives: true,
|
||||
spaces: 'appDataFolder, drive',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
import type { IHttpRequestMethods } from 'n8n-workflow';
|
||||
|
||||
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: IHttpRequestMethods) {
|
||||
if (method === 'GET') {
|
||||
return {};
|
||||
}
|
||||
}),
|
||||
googleApiRequestAllItems: jest.fn(async function (method: IHttpRequestMethods) {
|
||||
if (method === 'GET') {
|
||||
return {};
|
||||
}
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
describe('test GoogleDriveV2: fileFolder search', () => {
|
||||
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,
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
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', () => {
|
||||
it('should 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',
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
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', () => {
|
||||
it('should 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('should 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 },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
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', () => {
|
||||
it('should 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 },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import get from 'lodash/get';
|
||||
import { constructExecutionMetaData } from 'n8n-core';
|
||||
import type { IDataObject, IExecuteFunctions, IGetNodeParameterOptions, INode } from 'n8n-workflow';
|
||||
import { Readable } from 'stream';
|
||||
|
||||
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,
|
||||
options?: IGetNodeParameterOptions,
|
||||
) {
|
||||
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;
|
||||
};
|
||||
|
||||
export function createTestStream(byteSize: number) {
|
||||
let bytesSent = 0;
|
||||
const CHUNK_SIZE = 64 * 1024; // 64kB chunks (default NodeJS highWaterMark)
|
||||
|
||||
return new Readable({
|
||||
read() {
|
||||
const remainingBytes = byteSize - bytesSent;
|
||||
|
||||
if (remainingBytes <= 0) {
|
||||
this.push(null);
|
||||
return;
|
||||
}
|
||||
|
||||
const chunkSize = Math.min(CHUNK_SIZE, remainingBytes);
|
||||
const chunk = Buffer.alloc(chunkSize, 'A'); // Test data just a string of "A"
|
||||
|
||||
bytesSent += chunkSize;
|
||||
this.push(chunk);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,101 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
IDataObject,
|
||||
IPollFunctions,
|
||||
JsonObject,
|
||||
IHttpRequestMethods,
|
||||
IRequestOptions,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { getGoogleAccessToken } from '../../GenericFunctions';
|
||||
|
||||
export async function googleApiRequest(
|
||||
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
|
||||
method: IHttpRequestMethods,
|
||||
resource: string,
|
||||
body: any = {},
|
||||
qs: IDataObject = {},
|
||||
uri?: string,
|
||||
option: IDataObject = {},
|
||||
): Promise<any> {
|
||||
const authenticationMethod = this.getNodeParameter(
|
||||
'authentication',
|
||||
0,
|
||||
'serviceAccount',
|
||||
) as string;
|
||||
|
||||
let options: IRequestOptions = {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
method,
|
||||
body,
|
||||
qs,
|
||||
uri: uri || `https://www.googleapis.com${resource}`,
|
||||
json: true,
|
||||
};
|
||||
|
||||
options = Object.assign({}, options, option);
|
||||
|
||||
try {
|
||||
if (Object.keys(body as IDataObject).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.request(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';
|
||||
}
|
||||
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||
}
|
||||
}
|
||||
|
||||
export async function googleApiRequestAllItems(
|
||||
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
|
||||
propertyName: string,
|
||||
method: IHttpRequestMethods,
|
||||
endpoint: string,
|
||||
|
||||
body: any = {},
|
||||
query: IDataObject = {},
|
||||
): Promise<any> {
|
||||
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[]);
|
||||
} while (responseData.nextPageToken !== undefined && responseData.nextPageToken !== '');
|
||||
|
||||
return returnData;
|
||||
}
|
||||
|
||||
export function extractId(url: string): string {
|
||||
if (url.includes('/d/')) {
|
||||
//https://docs.google.com/document/d/1TUJGUf5HUv9e6MJBzcOsPruxXDeGMnGYTBWfkMagcg4/edit
|
||||
const data = url.match(/[-\w]{25,}/);
|
||||
if (Array.isArray(data)) {
|
||||
return data[0];
|
||||
}
|
||||
} else if (url.includes('/folders/')) {
|
||||
//https://drive.google.com/drive/u/0/folders/19MqnruIXju5sAWYD3J71im1d2CBJkZzy
|
||||
return url.split('/folders/')[1];
|
||||
}
|
||||
return url;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,85 @@
|
||||
import type { ILoadOptionsFunctions, INodeListSearchResult } from 'n8n-workflow';
|
||||
|
||||
import { googleApiRequest } from './GenericFunctions';
|
||||
|
||||
interface GoogleDriveFilesItem {
|
||||
id: string;
|
||||
name: string;
|
||||
mimeType: string;
|
||||
webViewLink: string;
|
||||
}
|
||||
|
||||
interface GoogleDriveDriveItem {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
export async function fileSearch(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const query: string[] = [];
|
||||
if (filter) {
|
||||
query.push(`name contains '${filter.replace("'", "\\'")}'`);
|
||||
}
|
||||
query.push("mimeType != 'application/vnd.google-apps.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',
|
||||
});
|
||||
return {
|
||||
results: res.files.map((i: GoogleDriveFilesItem) => ({
|
||||
name: i.name,
|
||||
value: i.id,
|
||||
url: i.webViewLink,
|
||||
})),
|
||||
paginationToken: res.nextPageToken,
|
||||
};
|
||||
}
|
||||
|
||||
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 = 'application/vnd.google-apps.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',
|
||||
});
|
||||
return {
|
||||
results: res.files.map((i: GoogleDriveFilesItem) => ({
|
||||
name: i.name,
|
||||
value: i.id,
|
||||
url: i.webViewLink,
|
||||
})),
|
||||
paginationToken: res.nextPageToken,
|
||||
};
|
||||
}
|
||||
|
||||
export async function driveSearch(
|
||||
this: ILoadOptionsFunctions,
|
||||
filter?: string,
|
||||
paginationToken?: string,
|
||||
): Promise<INodeListSearchResult> {
|
||||
const res = await googleApiRequest.call(this, 'GET', '/drive/v3/drives', undefined, {
|
||||
q: filter ? `name contains '${filter.replace("'", "\\'")}'` : undefined,
|
||||
pageToken: paginationToken,
|
||||
});
|
||||
return {
|
||||
results: res.drives.map((i: GoogleDriveDriveItem) => ({
|
||||
name: i.name,
|
||||
value: i.id,
|
||||
})),
|
||||
paginationToken: res.nextPageToken,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeType,
|
||||
INodeTypeBaseDescription,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { router } from './actions/router';
|
||||
import { versionDescription } from './actions/versionDescription';
|
||||
import { listSearch } from './methods';
|
||||
|
||||
export class GoogleDriveV2 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
...versionDescription,
|
||||
};
|
||||
}
|
||||
|
||||
methods = { listSearch };
|
||||
|
||||
async execute(this: IExecuteFunctions) {
|
||||
return await router.call(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,615 @@
|
||||
import type { INodeProperties } from 'n8n-workflow';
|
||||
|
||||
import { GOOGLE_DRIVE_FILE_URL_REGEX, GOOGLE_DRIVE_FOLDER_URL_REGEX } from '../../../constants';
|
||||
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: GOOGLE_DRIVE_FILE_URL_REGEX,
|
||||
},
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: GOOGLE_DRIVE_FILE_URL_REGEX,
|
||||
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: GOOGLE_DRIVE_FOLDER_URL_REGEX,
|
||||
},
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: GOOGLE_DRIVE_FOLDER_URL_REGEX,
|
||||
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: GOOGLE_DRIVE_FOLDER_URL_REGEX,
|
||||
},
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: GOOGLE_DRIVE_FOLDER_URL_REGEX,
|
||||
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: GOOGLE_DRIVE_FOLDER_URL_REGEX,
|
||||
},
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: GOOGLE_DRIVE_FOLDER_URL_REGEX,
|
||||
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: GOOGLE_DRIVE_FOLDER_URL_REGEX,
|
||||
},
|
||||
validation: [
|
||||
{
|
||||
type: 'regex',
|
||||
properties: {
|
||||
regex: GOOGLE_DRIVE_FOLDER_URL_REGEX,
|
||||
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,
|
||||
},
|
||||
];
|
||||
@@ -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,
|
||||
];
|
||||
@@ -0,0 +1,267 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
import { v4 as uuid } from 'uuid';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { googleApiRequest } from '../../transport';
|
||||
|
||||
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 share files or folders in 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;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { IExecuteFunctions, 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;
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
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;
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
@@ -0,0 +1,137 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeParameterResourceLocator,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { setParentFolder } from '../../helpers/utils';
|
||||
import { googleApiRequest } from '../../transport';
|
||||
import { driveRLC, fileRLC, folderRLC } from '../common.descriptions';
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { DRIVE } from '../../helpers/interfaces';
|
||||
import { setFileProperties, setParentFolder, setUpdateCommonParams } from '../../helpers/utils';
|
||||
import { googleApiRequest } from '../../transport';
|
||||
import { driveRLC, folderRLC, updateCommonOptions } from '../common.descriptions';
|
||||
|
||||
import FormData from 'form-data';
|
||||
|
||||
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 metadata = {
|
||||
name,
|
||||
parents: [setParentFolder(folderId, driveId)],
|
||||
mimeType,
|
||||
};
|
||||
|
||||
const bodyParameters = setFileProperties(metadata, options);
|
||||
|
||||
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 multiPartBody = new FormData();
|
||||
multiPartBody.append('metadata', JSON.stringify(metadata), {
|
||||
contentType: 'application/json',
|
||||
});
|
||||
multiPartBody.append('data', content, {
|
||||
contentType: mimeType,
|
||||
knownLength: contentLength,
|
||||
});
|
||||
|
||||
const uploadData = await googleApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
'/upload/drive/v3/files',
|
||||
multiPartBody.getBuffer(),
|
||||
{
|
||||
uploadType: 'multipart',
|
||||
supportsAllDrives: true,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': `multipart/related; boundary=${multiPartBody.getBoundary()}`,
|
||||
'Content-Length': multiPartBody.getLengthSync(),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const uploadId = uploadData.id;
|
||||
|
||||
qs.addParents = setParentFolder(folderId, driveId);
|
||||
delete bodyParameters.parents;
|
||||
|
||||
const responseData = await googleApiRequest.call(
|
||||
this,
|
||||
'PATCH',
|
||||
`/drive/v3/files/${uploadId}`,
|
||||
bodyParameters,
|
||||
qs,
|
||||
);
|
||||
|
||||
response = { id: responseData.id };
|
||||
}
|
||||
|
||||
const executionData = this.helpers.constructExecutionMetaData(
|
||||
this.helpers.returnJsonArray(response as IDataObject),
|
||||
{ itemData: { item: i } },
|
||||
);
|
||||
|
||||
return executionData;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import type { IExecuteFunctions, 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;
|
||||
}
|
||||
@@ -0,0 +1,285 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
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: 'Put Output File in Field',
|
||||
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 binary field to put the file 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',
|
||||
|
||||
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,
|
||||
returnFullResponse: true,
|
||||
encoding: 'arraybuffer',
|
||||
json: false,
|
||||
};
|
||||
|
||||
const file = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/drive/v3/files/${fileId}`,
|
||||
{},
|
||||
{ fields: 'mimeType,name', supportsTeamDrives: true, supportsAllDrives: 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, supportsAllDrives: true },
|
||||
undefined,
|
||||
requestOptions,
|
||||
);
|
||||
} else {
|
||||
response = await googleApiRequest.call(
|
||||
this,
|
||||
'GET',
|
||||
`/drive/v3/files/${fileId}`,
|
||||
{},
|
||||
{ alt: 'media', supportsAllDrives: true },
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { setParentFolder } from '../../helpers/utils';
|
||||
import { googleApiRequest } from '../../transport';
|
||||
import { driveRLC, fileRLC, folderRLC } from '../common.descriptions';
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,280 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
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',
|
||||
supportsAllDrives: true,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': mimeType,
|
||||
'Content-Length': contentLength,
|
||||
},
|
||||
},
|
||||
);
|
||||
} else {
|
||||
const resumableUpload = await googleApiRequest.call(
|
||||
this,
|
||||
'PATCH',
|
||||
`/upload/drive/v3/files/${fileId}`,
|
||||
undefined,
|
||||
{ uploadType: 'resumable', supportsAllDrives: true },
|
||||
undefined,
|
||||
{
|
||||
returnFullResponse: 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;
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
import FormData from 'form-data';
|
||||
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import {
|
||||
getItemBinaryData,
|
||||
setFileProperties,
|
||||
setUpdateCommonParams,
|
||||
setParentFolder,
|
||||
processInChunks,
|
||||
} from '../../helpers/utils';
|
||||
import { googleApiRequest } from '../../transport';
|
||||
import { driveRLC, folderRLC, updateCommonOptions } from '../common.descriptions';
|
||||
|
||||
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;
|
||||
const metadata = {
|
||||
name,
|
||||
parents: [setParentFolder(folderId, driveId)],
|
||||
};
|
||||
if (Buffer.isBuffer(fileContent)) {
|
||||
const multiPartBody = new FormData();
|
||||
multiPartBody.append('metadata', JSON.stringify(metadata), {
|
||||
contentType: 'application/json',
|
||||
});
|
||||
multiPartBody.append('data', fileContent, {
|
||||
contentType: mimeType,
|
||||
knownLength: contentLength,
|
||||
});
|
||||
|
||||
const response = await googleApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
'/upload/drive/v3/files',
|
||||
multiPartBody.getBuffer(),
|
||||
{
|
||||
uploadType: 'multipart',
|
||||
supportsAllDrives: true,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
headers: {
|
||||
'Content-Type': `multipart/related; boundary=${multiPartBody.getBoundary()}`,
|
||||
'Content-Length': multiPartBody.getLengthSync(),
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
uploadId = response.id;
|
||||
} else {
|
||||
const resumableUpload = await googleApiRequest.call(
|
||||
this,
|
||||
'POST',
|
||||
'/upload/drive/v3/files',
|
||||
metadata,
|
||||
{
|
||||
uploadType: 'resumable',
|
||||
supportsAllDrives: true,
|
||||
},
|
||||
undefined,
|
||||
{
|
||||
returnFullResponse: true,
|
||||
headers: {
|
||||
'X-Upload-Content-Type': mimeType,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
const uploadUrl = resumableUpload.headers.location;
|
||||
|
||||
// 2MB chunks, needs to be a multiple of 256kB for Google Drive API
|
||||
const chunkSizeBytes = 2048 * 1024;
|
||||
|
||||
await processInChunks(fileContent, chunkSizeBytes, async (chunk, offset) => {
|
||||
try {
|
||||
const response = await this.helpers.httpRequest({
|
||||
method: 'PUT',
|
||||
url: uploadUrl,
|
||||
headers: {
|
||||
'Content-Length': chunk.length,
|
||||
'Content-Range': `bytes ${offset}-${offset + chunk.byteLength - 1}/${contentLength}`,
|
||||
},
|
||||
body: chunk,
|
||||
});
|
||||
uploadId = response?.id;
|
||||
} catch (error) {
|
||||
if (error.response?.status !== 308) throw error;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
@@ -0,0 +1,364 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import type { SearchFilter } from '../../helpers/interfaces';
|
||||
import { DRIVE, RLC_FOLDER_DEFAULT } from '../../helpers/interfaces';
|
||||
import { prepareQueryString, updateDriveScopes } from '../../helpers/utils';
|
||||
import { googleApiRequest, googleApiRequestAllItems } from '../../transport';
|
||||
import { driveRLC, fileTypesOptions, folderRLC } from '../common.descriptions';
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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,
|
||||
];
|
||||
@@ -0,0 +1,114 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeProperties,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { updateDisplayOptions } from '@utils/utilities';
|
||||
|
||||
import { DRIVE } from '../../helpers/interfaces';
|
||||
import { setParentFolder } from '../../helpers/utils';
|
||||
import { googleApiRequest } from '../../transport';
|
||||
import { driveRLC, folderRLC } from '../common.descriptions';
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
import type { IExecuteFunctions, 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;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
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;
|
||||
}
|
||||
@@ -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>;
|
||||
@@ -0,0 +1,54 @@
|
||||
import type { IExecuteFunctions, INodeExecutionData } from 'n8n-workflow';
|
||||
import { NodeOperationError } 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';
|
||||
import type { GoogleDriveType } from './node.type';
|
||||
|
||||
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 [returnData];
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
/* eslint-disable n8n-nodes-base/node-filename-against-convention */
|
||||
import { NodeConnectionTypes, 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: [NodeConnectionTypes.Main],
|
||||
outputs: [NodeConnectionTypes.Main],
|
||||
usableAsTool: true,
|
||||
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,
|
||||
],
|
||||
};
|
||||
@@ -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 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',
|
||||
} as const;
|
||||
@@ -0,0 +1,159 @@
|
||||
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 = await 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';
|
||||
}
|
||||
}
|
||||
|
||||
export async function processInChunks(
|
||||
stream: Readable,
|
||||
chunkSize: number,
|
||||
process: (chunk: Buffer, offset: number) => void | Promise<void>,
|
||||
) {
|
||||
let buffer = Buffer.alloc(0);
|
||||
let offset = 0;
|
||||
|
||||
for await (const chunk of stream) {
|
||||
buffer = Buffer.concat([buffer, chunk]);
|
||||
|
||||
while (buffer.length >= chunkSize) {
|
||||
const chunkToProcess = buffer.subarray(0, chunkSize);
|
||||
await process(chunkToProcess, offset);
|
||||
|
||||
buffer = buffer.subarray(chunkSize);
|
||||
offset += chunkSize;
|
||||
}
|
||||
}
|
||||
|
||||
// Process last chunk, could be smaller than chunkSize
|
||||
if (buffer.length > 0) {
|
||||
await process(buffer, offset);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export * as listSearch from './listSearch';
|
||||
@@ -0,0 +1,199 @@
|
||||
import type {
|
||||
IDataObject,
|
||||
ILoadOptionsFunctions,
|
||||
INodeListSearchItems,
|
||||
INodeListSearchResult,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import type { SearchFilter } from '../helpers/interfaces';
|
||||
import { DRIVE, RLC_DRIVE_DEFAULT, RLC_FOLDER_DEFAULT } from '../helpers/interfaces';
|
||||
import { updateDriveScopes } from '../helpers/utils';
|
||||
import { googleApiRequest } from '../transport';
|
||||
|
||||
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 = [
|
||||
{
|
||||
name: rootDefaultDisplayName,
|
||||
value: RLC_FOLDER_DEFAULT,
|
||||
url: 'https://drive.google.com/drive',
|
||||
},
|
||||
...folders.results,
|
||||
];
|
||||
}
|
||||
|
||||
return {
|
||||
results,
|
||||
paginationToken: folders.paginationToken,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,266 @@
|
||||
import { NodeTestHarness } from '@nodes-testing/node-test-harness';
|
||||
import nock from 'nock';
|
||||
|
||||
describe('Google Drive V2', () => {
|
||||
const credentials = {
|
||||
googleDriveOAuth2Api: {
|
||||
scope: 'https://www.googleapis.com/auth/drive',
|
||||
oauthTokenData: {
|
||||
access_token: 'test-access-token',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
describe('Folder Create Operation', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://www.googleapis.com');
|
||||
|
||||
// Mock folder creation
|
||||
mock
|
||||
.post('/drive/v3/files')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
|
||||
name: 'Test Folder',
|
||||
mimeType: 'application/vnd.google-apps.folder',
|
||||
createdTime: '2024-01-01T00:00:00.000Z',
|
||||
modifiedTime: '2024-01-01T00:00:00.000Z',
|
||||
webViewLink:
|
||||
'https://drive.google.com/drive/folders/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
|
||||
})
|
||||
.persist();
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['folder-create-basic.workflow.json'],
|
||||
});
|
||||
});
|
||||
|
||||
// Note: File Upload operations are skipped from workflow tests due to binary data complexity
|
||||
// The upload functionality is thoroughly tested in the dedicated unit tests
|
||||
|
||||
describe('File Operations', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://www.googleapis.com');
|
||||
|
||||
// Mock file copy - correct endpoint
|
||||
mock
|
||||
.post('/drive/v3/files/123/copy')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: 'copy123',
|
||||
name: 'Copy of Test File',
|
||||
mimeType: 'text/plain',
|
||||
})
|
||||
.persist();
|
||||
|
||||
// Mock file createFromText - multipart upload
|
||||
mock
|
||||
.post('/upload/drive/v3/files')
|
||||
.query({ uploadType: 'multipart', supportsAllDrives: true })
|
||||
.reply(200, {
|
||||
id: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
|
||||
name: 'Test Text File',
|
||||
mimeType: 'text/plain',
|
||||
})
|
||||
.persist();
|
||||
|
||||
// Mock metadata update after createFromText
|
||||
mock
|
||||
.patch('/drive/v3/files/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
|
||||
name: 'Test Text File',
|
||||
mimeType: 'text/plain',
|
||||
size: '48',
|
||||
createdTime: '2024-01-01T00:00:00.000Z',
|
||||
modifiedTime: '2024-01-01T00:00:00.000Z',
|
||||
})
|
||||
.persist();
|
||||
|
||||
// Mock file download - metadata fetch first
|
||||
mock
|
||||
.get('/drive/v3/files/123')
|
||||
.query({ fields: 'mimeType,name', supportsTeamDrives: true, supportsAllDrives: true })
|
||||
.reply(200, {
|
||||
id: '123',
|
||||
name: 'Test File',
|
||||
mimeType: 'text/plain',
|
||||
})
|
||||
.persist();
|
||||
|
||||
// Mock file download - content download
|
||||
mock
|
||||
.get('/drive/v3/files/123')
|
||||
.query({ alt: 'media', supportsAllDrives: true })
|
||||
.reply(200, 'Hello World', {
|
||||
'Content-Type': 'text/plain',
|
||||
})
|
||||
.persist();
|
||||
|
||||
mock.delete('/drive/v3/files/123').query(true).reply(200).persist();
|
||||
|
||||
mock
|
||||
.get('/drive/v3/files/123')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: '123',
|
||||
name: 'Test File',
|
||||
mimeType: 'text/plain',
|
||||
size: '1024',
|
||||
})
|
||||
.persist();
|
||||
|
||||
mock
|
||||
.patch('/drive/v3/files/123')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: '123',
|
||||
name: 'Updated Test File',
|
||||
mimeType: 'text/plain',
|
||||
})
|
||||
.persist();
|
||||
|
||||
mock
|
||||
.post('/drive/v3/files/123/permissions')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: 'permission123',
|
||||
role: 'reader',
|
||||
type: 'user',
|
||||
})
|
||||
.persist();
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: [
|
||||
'file-copy.workflow.json',
|
||||
'file-createFromText.workflow.json',
|
||||
'file-delete.workflow.json',
|
||||
'file-download.workflow.json',
|
||||
'file-move.workflow.json',
|
||||
'file-share.workflow.json',
|
||||
'file-update.workflow.json',
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
describe('Folder Operations', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://www.googleapis.com');
|
||||
|
||||
// Mock folder delete
|
||||
mock.delete('/drive/v3/files/folder123').query(true).reply(200).persist();
|
||||
|
||||
// Mock folder share
|
||||
mock
|
||||
.post('/drive/v3/files/folder123/permissions')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: 'permission123',
|
||||
role: 'reader',
|
||||
type: 'user',
|
||||
})
|
||||
.persist();
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['folder-delete.workflow.json', 'folder-share.workflow.json'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('File/Folder Search Operations', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://www.googleapis.com');
|
||||
|
||||
// Mock search
|
||||
mock
|
||||
.get('/drive/v3/files')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
files: [
|
||||
{
|
||||
id: '123',
|
||||
name: 'Test File',
|
||||
mimeType: 'text/plain',
|
||||
},
|
||||
{
|
||||
id: 'folder123',
|
||||
name: 'Test Folder',
|
||||
mimeType: 'application/vnd.google-apps.folder',
|
||||
},
|
||||
],
|
||||
})
|
||||
.persist();
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: ['filefolder-search.workflow.json'],
|
||||
});
|
||||
});
|
||||
|
||||
describe('Shared Drive Operations', () => {
|
||||
beforeAll(() => {
|
||||
const mock = nock('https://www.googleapis.com');
|
||||
|
||||
// Mock drive operations
|
||||
mock
|
||||
.post('/drive/v3/drives')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: 'drive123',
|
||||
name: 'Test Drive',
|
||||
})
|
||||
.persist();
|
||||
|
||||
mock.delete('/drive/v3/drives/drive123').query(true).reply(200).persist();
|
||||
|
||||
mock
|
||||
.get('/drive/v3/drives/drive123')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: 'drive123',
|
||||
name: 'Test Drive',
|
||||
})
|
||||
.persist();
|
||||
|
||||
mock
|
||||
.get('/drive/v3/drives')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
drives: [
|
||||
{
|
||||
id: 'drive123',
|
||||
name: 'Test Drive',
|
||||
},
|
||||
],
|
||||
})
|
||||
.persist();
|
||||
|
||||
mock
|
||||
.patch('/drive/v3/drives/drive123')
|
||||
.query(true)
|
||||
.reply(200, {
|
||||
id: 'drive123',
|
||||
name: 'Updated Test Drive',
|
||||
})
|
||||
.persist();
|
||||
});
|
||||
|
||||
new NodeTestHarness().setupTests({
|
||||
credentials,
|
||||
workflowFiles: [
|
||||
'drive-create.workflow.json',
|
||||
'drive-delete.workflow.json',
|
||||
'drive-get.workflow.json',
|
||||
'drive-list.workflow.json',
|
||||
'drive-update.workflow.json',
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "Google Drive V2 Drive Create Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "drive",
|
||||
"operation": "create",
|
||||
"name": "Test Drive"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "drive-create",
|
||||
"name": "Create Drive",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Create Drive": [
|
||||
{
|
||||
"json": {
|
||||
"id": "drive123",
|
||||
"name": "Test Drive"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create Drive",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
{
|
||||
"name": "Google Drive V2 Drive Delete Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "drive",
|
||||
"operation": "deleteDrive",
|
||||
"driveId": {
|
||||
"__rl": true,
|
||||
"value": "drive123",
|
||||
"mode": "id"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "drive-delete",
|
||||
"name": "Delete Drive",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Delete Drive": [
|
||||
{
|
||||
"json": {
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Delete Drive",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "Google Drive V2 Drive Get Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "drive",
|
||||
"operation": "get",
|
||||
"driveId": {
|
||||
"__rl": true,
|
||||
"value": "drive123",
|
||||
"mode": "id"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "drive-get",
|
||||
"name": "Get Drive",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Get Drive": [
|
||||
{
|
||||
"json": {
|
||||
"id": "drive123",
|
||||
"name": "Test Drive"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Get Drive",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
{
|
||||
"name": "Google Drive V2 Drive List Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "drive",
|
||||
"operation": "list"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "drive-list",
|
||||
"name": "List Drives",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"List Drives": [
|
||||
{
|
||||
"json": {
|
||||
"id": "drive123",
|
||||
"name": "Test Drive"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "List Drives",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "Google Drive V2 Drive Update Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "drive",
|
||||
"operation": "update",
|
||||
"driveId": {
|
||||
"__rl": true,
|
||||
"value": "drive123",
|
||||
"mode": "id"
|
||||
},
|
||||
"options": {
|
||||
"name": "Updated Test Drive"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "drive-update",
|
||||
"name": "Update Drive",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Update Drive": [
|
||||
{
|
||||
"json": {
|
||||
"id": "drive123",
|
||||
"name": "Updated Test Drive"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Update Drive",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "Google Drive V2 File Copy Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "file",
|
||||
"operation": "copy",
|
||||
"fileId": {
|
||||
"__rl": true,
|
||||
"value": "123",
|
||||
"mode": "id"
|
||||
},
|
||||
"name": "Copy of Test File"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "file-copy",
|
||||
"name": "Copy File",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Copy File": [
|
||||
{
|
||||
"json": {
|
||||
"id": "copy123",
|
||||
"name": "Copy of Test File",
|
||||
"mimeType": "text/plain"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Copy File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "Google Drive V2 File Create From Text Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "file",
|
||||
"operation": "createFromText",
|
||||
"name": "Test Text File",
|
||||
"content": "Hello World! This is a test file created from text."
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "file-createFromText",
|
||||
"name": "Create File From Text",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Create File From Text": [
|
||||
{
|
||||
"json": {
|
||||
"id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create File From Text",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "Google Drive V2 File Delete Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "file",
|
||||
"operation": "deleteFile",
|
||||
"fileId": {
|
||||
"__rl": true,
|
||||
"value": "123",
|
||||
"mode": "id"
|
||||
},
|
||||
"options": {
|
||||
"deletePermanently": true
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "file-delete",
|
||||
"name": "Delete File",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Delete File": [
|
||||
{
|
||||
"json": {
|
||||
"id": "123",
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Delete File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"name": "Google Drive V2 File Download Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "file",
|
||||
"operation": "download",
|
||||
"fileId": {
|
||||
"__rl": true,
|
||||
"value": "123",
|
||||
"mode": "id"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "file-download",
|
||||
"name": "Download File",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Download File": [
|
||||
{
|
||||
"json": {}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Download File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "Google Drive V2 File Move Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "file",
|
||||
"operation": "move",
|
||||
"fileId": {
|
||||
"__rl": true,
|
||||
"value": "123",
|
||||
"mode": "id"
|
||||
},
|
||||
"folderId": {
|
||||
"__rl": true,
|
||||
"value": "folder456",
|
||||
"mode": "id"
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "file-move",
|
||||
"name": "Move File",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Move File": [
|
||||
{
|
||||
"json": {
|
||||
"id": "123",
|
||||
"mimeType": "text/plain",
|
||||
"name": "Updated Test File"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Move File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "Google Drive V2 File Share Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "file",
|
||||
"operation": "share",
|
||||
"fileId": {
|
||||
"__rl": true,
|
||||
"value": "123",
|
||||
"mode": "id"
|
||||
},
|
||||
"permissionsUi": {
|
||||
"permissionsValues": {
|
||||
"role": "reader",
|
||||
"type": "user",
|
||||
"emailAddress": "test@example.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "file-share",
|
||||
"name": "Share File",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Share File": [
|
||||
{
|
||||
"json": {
|
||||
"id": "permission123",
|
||||
"role": "reader",
|
||||
"type": "user"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Share File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
{
|
||||
"name": "Google Drive V2 File Update Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "file",
|
||||
"operation": "update",
|
||||
"fileId": {
|
||||
"__rl": true,
|
||||
"value": "123",
|
||||
"mode": "id"
|
||||
},
|
||||
"newUpdatedFileName": "Updated Test File"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "file-update",
|
||||
"name": "Update File",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Update File": [
|
||||
{
|
||||
"json": {
|
||||
"id": "123",
|
||||
"name": "Updated Test File",
|
||||
"mimeType": "text/plain"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Update File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
{
|
||||
"_comment": "This workflow tests basic file upload functionality with small embedded binary data. It uses the regular upload path (not resumable), as resumable uploads require binary data with an 'id' field which is not easily testable in workflow tests. Large file upload functionality is covered by dedicated unit tests.",
|
||||
"name": "Google Drive V2 File Upload Basic Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "file",
|
||||
"operation": "upload",
|
||||
"inputDataFieldName": "data",
|
||||
"name": "small-file.pdf"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "file-upload-basic",
|
||||
"name": "Upload Small File",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"When clicking 'Execute Workflow'": [
|
||||
{
|
||||
"json": {},
|
||||
"binary": {
|
||||
"data": {
|
||||
"data": "JVBERi0xLjQKJcfsj6IKNSAwIG9iago8PAovTGVuZ3RoIDYgMCBSCi9GaWx0ZXIgL0ZsYXRlRGVjb2RlCj4+CnN0cmVhbQp4nEWQwQrCMBBE",
|
||||
"mimeType": "application/pdf",
|
||||
"fileName": "small-file.pdf"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"Upload Small File": [
|
||||
{
|
||||
"json": {
|
||||
"id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms",
|
||||
"name": "small-file.pdf",
|
||||
"mimeType": "application/pdf",
|
||||
"size": "81",
|
||||
"createdTime": "2024-01-01T00:00:00.000Z",
|
||||
"modifiedTime": "2024-01-01T00:00:00.000Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Upload Small File",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"name": "Google Drive V2 File/Folder Search Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "fileFolder",
|
||||
"operation": "search",
|
||||
"searchMethod": "query",
|
||||
"queryString": "name contains 'test'"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "filefolder-search",
|
||||
"name": "Search Files and Folders",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Search Files and Folders": [
|
||||
{
|
||||
"json": {
|
||||
"id": "123",
|
||||
"name": "Test File",
|
||||
"mimeType": "text/plain"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": "folder123",
|
||||
"name": "Test Folder",
|
||||
"mimeType": "application/vnd.google-apps.folder"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Search Files and Folders",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
{
|
||||
"name": "Google Drive V2 Folder Create Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "folder",
|
||||
"operation": "create",
|
||||
"name": "Test Folder"
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "folder-create-basic",
|
||||
"name": "Create Folder",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Create Folder": [
|
||||
{
|
||||
"json": {
|
||||
"id": "1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms",
|
||||
"name": "Test Folder",
|
||||
"mimeType": "application/vnd.google-apps.folder",
|
||||
"createdTime": "2024-01-01T00:00:00.000Z",
|
||||
"modifiedTime": "2024-01-01T00:00:00.000Z",
|
||||
"webViewLink": "https://drive.google.com/drive/folders/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Create Folder",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"name": "Google Drive V2 Folder Delete Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "folder",
|
||||
"operation": "deleteFolder",
|
||||
"folderNoRootId": {
|
||||
"__rl": true,
|
||||
"value": "folder123",
|
||||
"mode": "id"
|
||||
},
|
||||
"options": {
|
||||
"deletePermanently": true
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "folder-delete",
|
||||
"name": "Delete Folder",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Delete Folder": [
|
||||
{
|
||||
"json": {
|
||||
"fileId": "folder123",
|
||||
"success": true
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Delete Folder",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
{
|
||||
"name": "Google Drive V2 Folder Share Test",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [0, 0],
|
||||
"id": "trigger-id",
|
||||
"name": "When clicking 'Execute Workflow'"
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"resource": "folder",
|
||||
"operation": "share",
|
||||
"folderNoRootId": {
|
||||
"__rl": true,
|
||||
"value": "folder123",
|
||||
"mode": "id"
|
||||
},
|
||||
"permissionsUi": {
|
||||
"permissionsValues": {
|
||||
"role": "reader",
|
||||
"type": "user",
|
||||
"emailAddress": "test@example.com"
|
||||
}
|
||||
}
|
||||
},
|
||||
"type": "n8n-nodes-base.googleDrive",
|
||||
"typeVersion": 3,
|
||||
"position": [200, 0],
|
||||
"id": "folder-share",
|
||||
"name": "Share Folder",
|
||||
"credentials": {
|
||||
"googleDriveOAuth2Api": {
|
||||
"id": "test-credential-id",
|
||||
"name": "Test Google Drive OAuth2"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Share Folder": [
|
||||
{
|
||||
"json": {
|
||||
"id": "permission123",
|
||||
"role": "reader",
|
||||
"type": "user"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"When clicking 'Execute Workflow'": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Share Folder",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
ILoadOptionsFunctions,
|
||||
IDataObject,
|
||||
IPollFunctions,
|
||||
JsonObject,
|
||||
IHttpRequestOptions,
|
||||
IHttpRequestMethods,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeApiError } from 'n8n-workflow';
|
||||
|
||||
import { getGoogleAccessToken } from '../../../GenericFunctions';
|
||||
|
||||
export async function googleApiRequest(
|
||||
this: IExecuteFunctions | 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.httpRequestWithAuthentication.call(
|
||||
this,
|
||||
'googleDriveOAuth2Api',
|
||||
options,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.code === 'ERR_OSSL_PEM_NO_START_LINE') {
|
||||
error.statusCode = '401';
|
||||
}
|
||||
|
||||
throw new NodeApiError(this.getNode(), error as JsonObject);
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user