feat(Microsoft Outlook Node): Node overhaul (#4449)

[N8N-4995](https://linear.app/n8n/issue/N8N-4995)

---------

Co-authored-by: Giulio Andreini <g.andreini@gmail.com>
This commit is contained in:
Michael Kret
2023-09-15 12:52:18 +03:00
committed by GitHub
parent bb215bd12a
commit 556a6132ba
98 changed files with 11215 additions and 1202 deletions

View File

@@ -0,0 +1,157 @@
import type {
IBinaryKeyData,
IDataObject,
IExecuteFunctions,
INodeExecutionData,
INodeProperties,
JsonObject,
} from 'n8n-workflow';
import { NodeApiError, NodeOperationError } from 'n8n-workflow';
import { microsoftApiRequest } from '../../transport';
import { updateDisplayOptions } from '@utils/utilities';
import { messageRLC } from '../../descriptions';
export const properties: INodeProperties[] = [
messageRLC,
{
displayName: 'Input Data Field Name',
name: 'binaryPropertyName',
hint: 'The name of the input field containing the binary file data to be attached',
type: 'string',
required: true,
default: 'data',
placeholder: 'e.g. data',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'File Name',
name: 'fileName',
description:
'Filename of the attachment. If not set will the file-name of the binary property be used, if it exists.',
type: 'string',
default: '',
},
],
},
];
const displayOptions = {
show: {
resource: ['messageAttachment'],
operation: ['add'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number, items: INodeExecutionData[]) {
let responseData;
const messageId = this.getNodeParameter('messageId', index, undefined, {
extractValue: true,
}) as string;
const binaryPropertyName = this.getNodeParameter('binaryPropertyName', 0);
const options = this.getNodeParameter('options', index);
if (items[index].binary === undefined) {
throw new NodeOperationError(this.getNode(), 'No binary data exists on item!');
}
if (
items[index].binary &&
(items[index].binary as IDataObject)[binaryPropertyName] === undefined
) {
throw new NodeOperationError(
this.getNode(),
`No binary data property "${binaryPropertyName}" does not exists on item!`,
{ itemIndex: index },
);
}
const binaryData = (items[index].binary as IBinaryKeyData)[binaryPropertyName];
const dataBuffer = await this.helpers.getBinaryDataBuffer(index, binaryPropertyName);
const fileName = options.fileName === undefined ? binaryData.fileName : options.fileName;
if (!fileName) {
throw new NodeOperationError(
this.getNode(),
'File name is not set. It has either to be set via "Additional Fields" or has to be set on the binary property!',
{ itemIndex: index },
);
}
// Check if the file is over 3MB big
if (dataBuffer.length > 3e6) {
// Maximum chunk size is 4MB
const chunkSize = 4e6;
const body: IDataObject = {
AttachmentItem: {
attachmentType: 'file',
name: fileName,
size: dataBuffer.length,
},
};
// Create upload session
responseData = await microsoftApiRequest.call(
this,
'POST',
`/messages/${messageId}/attachments/createUploadSession`,
body,
);
const uploadUrl = responseData.uploadUrl;
if (uploadUrl === undefined) {
throw new NodeApiError(this.getNode(), responseData as JsonObject, {
message: 'Failed to get upload session',
});
}
for (let bytesUploaded = 0; bytesUploaded < dataBuffer.length; bytesUploaded += chunkSize) {
// Upload the file chunk by chunk
const nextChunk = Math.min(bytesUploaded + chunkSize, dataBuffer.length);
const contentRange = `bytes ${bytesUploaded}-${nextChunk - 1}/${dataBuffer.length}`;
const data = dataBuffer.subarray(bytesUploaded, nextChunk);
responseData = await this.helpers.request(uploadUrl, {
method: 'PUT',
headers: {
'Content-Type': 'application/octet-stream',
'Content-Length': data.length,
'Content-Range': contentRange,
},
body: data,
});
}
} else {
const body: IDataObject = {
'@odata.type': '#microsoft.graph.fileAttachment',
name: fileName,
contentBytes: binaryData.data,
};
responseData = await microsoftApiRequest.call(
this,
'POST',
`/messages/${messageId}/attachments`,
body,
{},
);
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray({ success: true }),
{ itemData: { item: index } },
);
return executionData;
}

View File

@@ -0,0 +1,86 @@
import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow';
import { microsoftApiRequest } from '../../transport';
import { updateDisplayOptions } from '@utils/utilities';
import { attachmentRLC, messageRLC } from '../../descriptions';
export const properties: INodeProperties[] = [
messageRLC,
attachmentRLC,
{
displayName: 'Put Output in Field',
name: 'binaryPropertyName',
hint: 'The name of the output field to put the binary file data in',
type: 'string',
required: true,
default: 'data',
},
];
const displayOptions = {
show: {
resource: ['messageAttachment'],
operation: ['download'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number, items: INodeExecutionData[]) {
const messageId = this.getNodeParameter('messageId', index, undefined, {
extractValue: true,
}) as string;
const attachmentId = this.getNodeParameter('attachmentId', index, undefined, {
extractValue: true,
}) as string;
const dataPropertyNameDownload = this.getNodeParameter('binaryPropertyName', index);
// Get attachment details first
const attachmentDetails = await microsoftApiRequest.call(
this,
'GET',
`/messages/${messageId}/attachments/${attachmentId}`,
undefined,
{ $select: 'id,name,contentType' },
);
let mimeType: string | undefined;
if (attachmentDetails.contentType) {
mimeType = attachmentDetails.contentType;
}
const fileName = attachmentDetails.name;
const response = await microsoftApiRequest.call(
this,
'GET',
`/messages/${messageId}/attachments/${attachmentId}/$value`,
undefined,
{},
undefined,
{},
{ encoding: null, resolveWithFullResponse: true },
);
const newItem: INodeExecutionData = {
json: items[index].json,
binary: {},
};
if (items[index].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!, items[index].binary);
}
items[index] = newItem;
const data = Buffer.from(response.body as string, 'utf8');
items[index].binary![dataPropertyNameDownload] = await this.helpers.prepareBinaryData(
data as unknown as Buffer,
fileName as string,
mimeType,
);
return items;
}

View File

@@ -0,0 +1,94 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { microsoftApiRequest } from '../../transport';
import { updateDisplayOptions } from '@utils/utilities';
import { attachmentRLC, messageRLC } from '../../descriptions';
export const properties: INodeProperties[] = [
messageRLC,
attachmentRLC,
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Field',
default: {},
options: [
{
displayName: 'Fields',
name: 'fields',
type: 'multiOptions',
description: 'The fields to add to the output',
default: [],
options: [
{
name: 'contentType',
value: 'contentType',
},
{
name: 'isInline',
value: 'isInline',
},
{
name: 'lastModifiedDateTime',
value: 'lastModifiedDateTime',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'name',
value: 'name',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'size',
value: 'size',
},
],
},
],
},
];
const displayOptions = {
show: {
resource: ['messageAttachment'],
operation: ['get'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
const qs: IDataObject = {};
const messageId = this.getNodeParameter('messageId', index, undefined, {
extractValue: true,
}) as string;
const attachmentId = this.getNodeParameter('attachmentId', index, undefined, {
extractValue: true,
}) as string;
const options = this.getNodeParameter('options', index);
// Have sane defaults so we don't fetch attachment data in this operation
qs.$select = 'id,lastModifiedDateTime,name,contentType,size,isInline';
if (options.fields && (options.fields as string[]).length) {
qs.$select = (options.fields as string[]).map((field) => field.trim()).join(',');
}
const responseData = await microsoftApiRequest.call(
this,
'GET',
`/messages/${messageId}/attachments/${attachmentId}`,
undefined,
qs,
);
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{ itemData: { item: index } },
);
return executionData;
}

View File

@@ -0,0 +1,100 @@
import type { IDataObject, IExecuteFunctions, INodeProperties } from 'n8n-workflow';
import { microsoftApiRequest, microsoftApiRequestAllItems } from '../../transport';
import { updateDisplayOptions } from '@utils/utilities';
import { messageRLC, returnAllOrLimit } from '../../descriptions';
export const properties: INodeProperties[] = [
messageRLC,
...returnAllOrLimit,
{
displayName: 'Options',
name: 'options',
type: 'collection',
placeholder: 'Add Option',
default: {},
options: [
{
displayName: 'Fields',
name: 'fields',
type: 'multiOptions',
description: 'The fields to add to the output',
default: [],
options: [
{
name: 'contentType',
value: 'contentType',
},
{
name: 'isInline',
value: 'isInline',
},
{
name: 'lastModifiedDateTime',
value: 'lastModifiedDateTime',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'name',
value: 'name',
},
{
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
name: 'size',
value: 'size',
},
],
},
],
},
];
const displayOptions = {
show: {
resource: ['messageAttachment'],
operation: ['getAll'],
},
};
export const description = updateDisplayOptions(displayOptions, properties);
export async function execute(this: IExecuteFunctions, index: number) {
let responseData;
const qs = {} as IDataObject;
const messageId = this.getNodeParameter('messageId', index, undefined, {
extractValue: true,
}) as string;
const returnAll = this.getNodeParameter('returnAll', index);
const options = this.getNodeParameter('options', index);
// Have sane defaults so we don't fetch attachment data in this operation
qs.$select = 'id,lastModifiedDateTime,name,contentType,size,isInline';
if (options.fields && (options.fields as string[]).length) {
qs.$select = (options.fields as string[]).map((field) => field.trim()).join(',');
}
const endpoint = `/messages/${messageId}/attachments`;
if (returnAll) {
responseData = await microsoftApiRequestAllItems.call(
this,
'value',
'GET',
endpoint,
undefined,
qs,
);
} else {
qs.$top = this.getNodeParameter('limit', index);
responseData = await microsoftApiRequest.call(this, 'GET', endpoint, undefined, qs);
responseData = responseData.value;
}
const executionData = this.helpers.constructExecutionMetaData(
this.helpers.returnJsonArray(responseData as IDataObject),
{ itemData: { item: index } },
);
return executionData;
}

View File

@@ -0,0 +1,52 @@
import type { INodeProperties } from 'n8n-workflow';
import * as add from './add.operation';
import * as download from './download.operation';
import * as get from './get.operation';
import * as getAll from './getAll.operation';
export { add, download, get, getAll };
export const description: INodeProperties[] = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
noDataExpression: true,
displayOptions: {
show: {
resource: ['messageAttachment'],
},
},
options: [
{
name: 'Add',
value: 'add',
description: 'Add an attachment to a message',
action: 'Add an attachment',
},
{
name: 'Download',
value: 'download',
description: 'Download an attachment from a message',
action: 'Download an attachment',
},
{
name: 'Get',
value: 'get',
description: 'Retrieve information about an attachment of a message',
action: 'Get an attachment',
},
{
name: 'Get Many',
value: 'getAll',
description: 'Retrieve information about the attachments of a message',
action: 'Get many attachments',
},
],
default: 'add',
},
...add.description,
...download.description,
...get.description,
...getAll.description,
];