feat: Add Onedrive Trigger Node (#8742)

Co-authored-by: Giulio Andreini <andreini@netseven.it>
Co-authored-by: Marcus <marcus@n8n.io>
This commit is contained in:
Bram Kn 2024-03-19 15:52:45 +01:00 committed by GitHub
parent 4f0b52c45d
commit ff8dd4e604
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 571 additions and 3 deletions

View file

@ -5,11 +5,13 @@ import type {
JsonObject, JsonObject,
IHttpRequestMethods, IHttpRequestMethods,
IRequestOptions, IRequestOptions,
IPollFunctions,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow'; import { NodeApiError } from 'n8n-workflow';
import { DateTime } from 'luxon';
export async function microsoftApiRequest( export async function microsoftApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions, this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
method: IHttpRequestMethods, method: IHttpRequestMethods,
resource: string, resource: string,
@ -47,7 +49,7 @@ export async function microsoftApiRequest(
} }
export async function microsoftApiRequestAllItems( export async function microsoftApiRequestAllItems(
this: IExecuteFunctions | ILoadOptionsFunctions, this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
propertyName: string, propertyName: string,
method: IHttpRequestMethods, method: IHttpRequestMethods,
endpoint: string, endpoint: string,
@ -74,7 +76,7 @@ export async function microsoftApiRequestAllItems(
} }
export async function microsoftApiRequestAllItemsSkip( export async function microsoftApiRequestAllItemsSkip(
this: IExecuteFunctions | ILoadOptionsFunctions, this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
propertyName: string, propertyName: string,
method: IHttpRequestMethods, method: IHttpRequestMethods,
endpoint: string, endpoint: string,
@ -96,3 +98,77 @@ export async function microsoftApiRequestAllItemsSkip(
return returnData; return returnData;
} }
export async function microsoftApiRequestAllItemsDelta(
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
link: string,
lastDate: DateTime,
eventType: string,
): Promise<any> {
const returnData: IDataObject[] = [];
let responseData;
let deltaLink: string = '';
let uri: string = link;
do {
responseData = (await microsoftApiRequest.call(this, 'GET', '', {}, {}, uri)) as IDataObject;
uri = responseData['@odata.nextLink'] as string;
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
for (const value of responseData.value as IDataObject[]) {
if (value.fileSystemInfo as IDataObject) {
const updatedTimeStamp = (value.fileSystemInfo as IDataObject)
?.lastModifiedDateTime as string;
const createdTimeStamp = (value.fileSystemInfo as IDataObject)?.createdDateTime as string;
if (eventType === 'created') {
if (DateTime.fromISO(createdTimeStamp) >= lastDate) {
returnData.push(value);
}
}
if (eventType === 'updated') {
if (
DateTime.fromISO(updatedTimeStamp) >= lastDate &&
DateTime.fromISO(createdTimeStamp) < lastDate
) {
returnData.push(value);
}
}
}
}
//returnData.push.apply(returnData, responseData.value as IDataObject[]);
deltaLink = (responseData['@odata.deltaLink'] as string) || '';
} while (responseData['@odata.nextLink'] !== undefined);
return { deltaLink, returnData };
}
export async function getPath(
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
itemId: string,
): Promise<string> {
const responseData = (await microsoftApiRequest.call(
this,
'GET',
'',
{},
{},
`https://graph.microsoft.com/v1.0/me/drive/items/${itemId}`,
)) as IDataObject;
if (responseData.folder) {
return (responseData?.parentReference as IDataObject)?.path + `/${responseData?.name}`;
} else {
const workflow = this.getWorkflow();
const node = this.getNode();
this.logger.error(
`There was a problem in '${node.name}' node in workflow '${workflow.id}': 'Item to watch is not a folder'`,
{
node: node.name,
workflowId: workflow.id,
error: 'Item to watch is not a folder',
},
);
throw new NodeApiError(this.getNode(), {
error: 'Item to watch is not a folder',
} as JsonObject);
}
}

View file

@ -0,0 +1,25 @@
{
"node": "n8n-nodes-base.microsoftOneDriveTrigger",
"nodeVersion": "1.0",
"codexVersion": "1.0",
"categories": ["Data & Storage"],
"resources": {
"credentialDocumentation": [
{
"url": "https://docs.n8n.io/credentials/microsoft"
}
],
"primaryDocumentation": [
{
"url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.microsoftonedrivetrigger/"
}
],
"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/"
}
]
}
}

View file

@ -0,0 +1,173 @@
import type {
IPollFunctions,
IDataObject,
INodeExecutionData,
INodeType,
INodeTypeDescription,
} from 'n8n-workflow';
import { DateTime } from 'luxon';
import { triggerDescription } from './TriggerDescription';
import { getPath, microsoftApiRequest, microsoftApiRequestAllItemsDelta } from './GenericFunctions';
export class MicrosoftOneDriveTrigger implements INodeType {
description: INodeTypeDescription = {
displayName: 'Microsoft OneDrive Trigger',
name: 'microsoftOneDriveTrigger',
icon: 'file:oneDrive.svg',
group: ['trigger'],
version: 1,
description: 'Trigger for Microsoft OneDrive API.',
subtitle: '={{($parameter["event"])}}',
defaults: {
name: 'Microsoft OneDrive Trigger',
},
credentials: [
{
name: 'microsoftOneDriveOAuth2Api',
required: true,
},
],
polling: true,
inputs: [],
outputs: ['main'],
properties: [...triggerDescription],
};
methods = {
loadOptions: {},
};
async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
const workflowData = this.getWorkflowStaticData('node');
let responseData: IDataObject[];
const lastLink: string =
(workflowData.LastLink as string) ||
'https://graph.microsoft.com/v1.0/me/drive/root/delta?token=latest';
const now = DateTime.now().toUTC();
const start = DateTime.fromISO(workflowData.lastTimeChecked as string) || now;
const end = now;
const event = this.getNodeParameter('event', 'fileCreated') as string;
const watch = this.getNodeParameter('watch', 'anyFile') as string;
const watchFolder = (this.getNodeParameter('watchFolder', false) as boolean) || false;
const folderChild = (this.getNodeParameter('options.folderChild', false) as boolean) || false;
let eventType = 'created';
let eventResource = 'file';
if (event.includes('Updated')) {
eventType = 'updated';
}
if (event.includes('folder')) {
eventResource = 'folder';
}
try {
if (this.getMode() === 'manual') {
responseData = (
await microsoftApiRequest.call(
this,
'GET',
'',
{},
{},
'https://graph.microsoft.com/v1.0/me/drive/root/delta',
)
).value as IDataObject[];
} else {
const response: IDataObject = (await microsoftApiRequestAllItemsDelta.call(
this,
lastLink,
start,
eventType,
)) as IDataObject;
responseData = response.returnData as IDataObject[];
workflowData.LastLink = response.deltaLink;
}
workflowData.lastTimeChecked = end.toISO();
if (watch === 'selectedFile') {
const fileId = (
this.getNodeParameter('fileId', '', {
extractValue: true,
}) as string
).replace('%21', '!');
if (fileId) {
responseData = responseData.filter((item: IDataObject) => item.id === fileId);
}
}
if (
!folderChild &&
(watch === 'oneSelectedFolder' || watch === 'selectedFolder' || watchFolder)
) {
const folderId = (
this.getNodeParameter('folderId', '', {
extractValue: true,
}) as string
).replace('%21', '!');
if (folderId) {
if (watch === 'oneSelectedFolder') {
responseData = responseData.filter((item: IDataObject) => item.id === folderId);
} else {
responseData = responseData.filter(
(item: IDataObject) => (item.parentReference as IDataObject).id === folderId,
);
}
}
}
if (folderChild && (watch === 'selectedFolder' || watchFolder)) {
const folderId = (
this.getNodeParameter('folderId', '', {
extractValue: true,
}) as string
).replace('%21', '!');
const folderPath = await getPath.call(this, folderId);
responseData = responseData.filter((item: IDataObject) =>
((item.parentReference as IDataObject).path as string).startsWith(folderPath),
);
}
responseData = responseData.filter((item: IDataObject) => item[eventResource]);
if (!responseData?.length) {
return null;
}
const simplify = this.getNodeParameter('simple') as boolean;
if (simplify) {
responseData = responseData.map((x) => ({
id: x.id,
createdDateTime: (x.fileSystemInfo as IDataObject)?.createdDateTime,
lastModifiedDateTime: (x.fileSystemInfo as IDataObject)?.lastModifiedDateTime,
name: x.name,
webUrl: x.webUrl,
size: x.size,
path: (x.parentReference as IDataObject)?.path || '',
mimeType: (x.file as IDataObject)?.mimeType || '',
}));
}
if (this.getMode() === 'manual') {
return [this.helpers.returnJsonArray(responseData[0])];
} else {
return [this.helpers.returnJsonArray(responseData)];
}
} catch (error) {
if (this.getMode() === 'manual' || !workflowData.lastTimeChecked) {
throw error;
}
const workflow = this.getWorkflow();
const node = this.getNode();
this.logger.error(
`There was a problem in '${node.name}' node in workflow '${workflow.id}': '${error.description}'`,
{
node: node.name,
workflowId: workflow.id,
error,
},
);
throw error;
}
return null;
}
}

View file

@ -0,0 +1,288 @@
import type { INodeProperties } from 'n8n-workflow';
import { MICROSOFT_DRIVE_FILE_URL_REGEX, MICROSOFT_DRIVE_FOLDER_URL_REGEX } from './constants';
export const fileRLC: INodeProperties = {
displayName: 'File',
name: 'fileId',
type: 'resourceLocator',
default: { mode: 'id', value: '' },
required: true,
modes: [
{
displayName: 'Link',
name: 'url',
type: 'string',
placeholder:
'e.g. https://onedrive.live.com/edit.aspx?resid=170B5C65E30736A3!257&cid=170b5c65e30736a3&CT=1708697995542&OR=ItemsView',
extractValue: {
type: 'regex',
regex: MICROSOFT_DRIVE_FILE_URL_REGEX,
},
validation: [
{
type: 'regex',
properties: {
regex: MICROSOFT_DRIVE_FILE_URL_REGEX,
errorMessage: 'Not a valid Microsoft Drive File URL',
},
},
],
},
{
displayName: 'ID',
name: 'id',
type: 'string',
placeholder: 'e.g. 170B5C65E30736A3!257',
validation: [
{
type: 'regex',
properties: {
regex: '[a-zA-Z0-9\\!%21]{5,}',
errorMessage: 'Not a valid Microsoft Drive File ID',
},
},
],
url: '=https://onedrive.live.com/?id={{$value}}',
},
],
description:
"The file to operate on. The 'By URL' option only accepts URLs that start with 'https://onedrive.live.com'.",
};
export const folderRLC: INodeProperties = {
displayName: 'Folder',
name: 'folderId',
type: 'resourceLocator',
default: { mode: 'id', value: '', cachedResultName: '' },
required: true,
modes: [
{
displayName: 'Link',
name: 'url',
type: 'string',
placeholder: 'e.g. https://onedrive.live.com/?id=170B5C65E30736A3%21103&cid=170B5C65E30736A3',
extractValue: {
type: 'regex',
regex: MICROSOFT_DRIVE_FOLDER_URL_REGEX,
},
validation: [
{
type: 'regex',
properties: {
regex: MICROSOFT_DRIVE_FOLDER_URL_REGEX,
errorMessage: 'Not a valid Microsoft Drive Folder URL',
},
},
],
},
{
displayName: 'ID',
name: 'id',
type: 'string',
placeholder: 'e.g. 170B5C65E30736A3%21136',
validation: [
{
type: 'regex',
properties: {
regex: '[a-zA-Z0-9\\!%21]{5,}',
errorMessage: 'Not a valid Microsoft Drive Folder ID',
},
},
],
url: '=https://onedrive.live.com/?id={{$value}}',
},
],
description:
"The folder to operate on. The 'By URL' option only accepts URLs that start with 'https://onedrive.live.com'.",
};
export const triggerDescription: INodeProperties[] = [
{
displayName: 'Trigger On',
name: 'event',
type: 'options',
default: 'fileCreated',
options: [
{
name: 'File Created',
value: 'fileCreated',
description: 'When a new file is created',
},
{
name: 'File Updated',
value: 'fileUpdated',
description: 'When an existing file is modified',
},
{
name: 'Folder Created',
value: 'folderCreated',
description: 'When a new folder is created',
},
{
name: 'Folder Updated',
value: 'folderUpdated',
description: 'When an existing folder is modified',
},
],
},
{
displayName: 'Simplify',
name: 'simple',
description: 'Whether to return a simplified version of the response instead of the raw data',
type: 'boolean',
default: true,
},
{
displayName: 'Watch Folder',
name: 'watchFolder',
description:
'Whether to watch for the created file in a given folder, rather than the entire OneDrive',
type: 'boolean',
default: false,
displayOptions: {
show: {
event: ['fileCreated'],
},
},
},
{
displayName: 'Watch',
name: 'watch',
description: 'How to select which file to watch',
type: 'options',
default: 'anyFile',
displayOptions: {
show: {
event: ['fileUpdated'],
},
},
options: [
{
name: 'Any File',
value: 'anyFile',
description: 'Watch for updated files in the entire OneDrive',
},
{
name: 'Inside a Folder',
value: 'selectedFolder',
description: 'Watch for updated files inside a selected folder',
},
{
name: 'A Selected File',
value: 'selectedFile',
description: 'Watch a specific file for updates',
},
],
},
{
displayName: 'Watch Folder',
name: 'watchFolder',
description:
'Whether to watch for the created folder in a given folder, rather than the entire OneDrive',
type: 'boolean',
default: false,
displayOptions: {
show: {
event: ['folderCreated'],
},
},
},
{
displayName: 'Watch',
name: 'watch',
description: 'How to select which folder to watch',
type: 'options',
default: 'anyFolder',
displayOptions: {
show: {
event: ['folderUpdated'],
},
},
options: [
{
name: 'Any Folder',
value: 'anyFolder',
description: 'Watch for updated folders in the entire OneDrive',
},
{
name: 'Inside a Folder',
value: 'selectedFolder',
description: 'Watch for updated folders inside a selected folder',
},
{
name: 'A Selected Folder',
value: 'oneSelectedFolder',
description: 'Watch a specific folder for updates',
},
],
},
{
...fileRLC,
displayOptions: {
show: {
event: ['fileUpdated'],
watch: ['selectedFile'],
},
},
},
{
...folderRLC,
displayOptions: {
show: {
watch: ['selectedFolder', 'oneSelectedFolder'],
},
},
},
{
...folderRLC,
displayOptions: {
show: {
watchFolder: [true],
},
},
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
watch: ['selectedFolder'],
},
},
options: [
{
displayName: 'Watch Nested Folders',
name: 'folderChild',
type: 'boolean',
default: false,
description:
'Whether to look for modified files/folders in all nested folders, rather than only direct descendants',
},
],
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
displayOptions: {
show: {
watchFolder: [true],
},
},
options: [
{
displayName: 'Watch Nested Folders',
name: 'folderChild',
type: 'boolean',
default: false,
description:
'Whether to look for modified files/folders in all nested folders, rather than only direct descendants',
},
],
},
];

View file

@ -0,0 +1,5 @@
export const MICROSOFT_DRIVE_FILE_URL_REGEX =
'https:\\/\\/onedrive.live.com(?:\\/.*?(?:\\&|\\?)(?:id=|resid=))(.+?)(?:\\&.*)';
export const MICROSOFT_DRIVE_FOLDER_URL_REGEX =
'https:\\/\\/onedrive.live.com(?:\\/.*id=)(.+)(?:\\&cid.*)';

View file

@ -616,6 +616,7 @@
"dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js", "dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js",
"dist/nodes/Microsoft/GraphSecurity/MicrosoftGraphSecurity.node.js", "dist/nodes/Microsoft/GraphSecurity/MicrosoftGraphSecurity.node.js",
"dist/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.js", "dist/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.js",
"dist/nodes/Microsoft/OneDrive/MicrosoftOneDriveTrigger.node.js",
"dist/nodes/Microsoft/Outlook/MicrosoftOutlook.node.js", "dist/nodes/Microsoft/Outlook/MicrosoftOutlook.node.js",
"dist/nodes/Microsoft/Outlook/MicrosoftOutlookTrigger.node.js", "dist/nodes/Microsoft/Outlook/MicrosoftOutlookTrigger.node.js",
"dist/nodes/Microsoft/Sql/MicrosoftSql.node.js", "dist/nodes/Microsoft/Sql/MicrosoftSql.node.js",