Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

✨ feat(MicrosoftOutlook) Service root configurable for Outlook and OneDrive nodes #13451

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,5 +1,23 @@
import type { ICredentialType, INodeProperties } from 'n8n-workflow';

const enum EndpointNames {
Global = 'Microsoft Graph global service',
GovCloud = 'Microsoft Graph for US Government L4',
DoDCloud = 'Microsoft Graph for US Government L5 (DOD)',
China = 'Microsoft Graph China operated by 21Vianet',
}

/**
* The service endpoints are defined by Microsoft:
* https://learn.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints
*/
const endpoints: Record<EndpointNames, string> = {
[EndpointNames.Global]: 'https://graph.microsoft.com',
[EndpointNames.GovCloud]: 'https://graph.microsoft.us',
[EndpointNames.DoDCloud]: 'https://dod-graph.microsoft.us',
[EndpointNames.China]: 'https://microsoftgraph.chinacloudapi.cn',
};

export class MicrosoftOneDriveOAuth2Api implements ICredentialType {
name = 'microsoftOneDriveOAuth2Api';

Expand All @@ -17,5 +35,16 @@ export class MicrosoftOneDriveOAuth2Api implements ICredentialType {
type: 'hidden',
default: 'openid offline_access Files.ReadWrite.All',
},
{
displayName: 'Endpoint',
description: 'The service root endpoint to use when connecting to the Outlook API.',
name: 'graphEndpoint',
type: 'options',
default: endpoints[EndpointNames.Global],
options: Object.keys(endpoints).map((name) => ({
name,
value: endpoints[name as EndpointNames],
})),
},
];
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,24 @@ const scopes = [
'MailboxSettings.Read',
];

const enum EndpointNames {
Global = 'Microsoft Graph global service',
GovCloud = 'Microsoft Graph for US Government L4',
DoDCloud = 'Microsoft Graph for US Government L5 (DOD)',
China = 'Microsoft Graph China operated by 21Vianet',
}

/**
* The service endpoints are defined by Microsoft:
* https://learn.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints
*/
const endpoints: Record<EndpointNames, string> = {
[EndpointNames.Global]: 'https://graph.microsoft.com',
[EndpointNames.GovCloud]: 'https://graph.microsoft.us',
[EndpointNames.DoDCloud]: 'https://dod-graph.microsoft.us',
[EndpointNames.China]: 'https://microsoftgraph.chinacloudapi.cn',
};

export class MicrosoftOutlookOAuth2Api implements ICredentialType {
name = 'microsoftOutlookOAuth2Api';

Expand Down Expand Up @@ -50,5 +68,16 @@ export class MicrosoftOutlookOAuth2Api implements ICredentialType {
},
},
},
{
displayName: 'Endpoint',
description: 'The service root endpoint to use when connecting to the Outlook API.',
name: 'graphEndpoint',
type: 'options',
default: endpoints[EndpointNames.Global],
options: Object.keys(endpoints).map((name) => ({
name,
value: endpoints[name as EndpointNames],
})),
},
];
}
14 changes: 12 additions & 2 deletions packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ import type {
} from 'n8n-workflow';
import { NodeApiError } from 'n8n-workflow';

export async function getGraphEndpoint(
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
): Promise<string> {
const credentials = await this.getCredentials('microsoftOneDriveOAuth2Api');
return (credentials.graphEndpoint as string) || 'https://graph.microsoft.com';
}

export async function microsoftApiRequest(
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
method: IHttpRequestMethods,
Expand All @@ -21,14 +28,15 @@ export async function microsoftApiRequest(
headers: IDataObject = {},
option: IDataObject = { json: true },
): Promise<any> {
const graphEndpoint = await getGraphEndpoint.call(this);
const options: IRequestOptions = {
headers: {
'Content-Type': 'application/json',
},
method,
body,
qs,
uri: uri || `https://graph.microsoft.com/v1.0/me${resource}`,
uri: uri || `${graphEndpoint}/v1.0/me${resource}`,
};
try {
Object.assign(options, option);
Expand Down Expand Up @@ -145,13 +153,15 @@ export async function getPath(
this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions,
itemId: string,
): Promise<string> {
const graphEndpoint = await getGraphEndpoint.call(this);

const responseData = (await microsoftApiRequest.call(
this,
'GET',
'',
{},
{},
`https://graph.microsoft.com/v1.0/me/drive/items/${itemId}`,
`${graphEndpoint}/v1.0/me/drive/items/${itemId}`,
)) as IDataObject;
if (responseData.folder) {
return (responseData?.parentReference as IDataObject)?.path + `/${responseData?.name}`;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import {
NodeConnectionType,
} from 'n8n-workflow';

import { getPath, microsoftApiRequest, microsoftApiRequestAllItemsDelta } from './GenericFunctions';
import {
getGraphEndpoint,
getPath,
microsoftApiRequest,
microsoftApiRequestAllItemsDelta,
} from './GenericFunctions';
import { triggerDescription } from './TriggerDescription';

export class MicrosoftOneDriveTrigger implements INodeType {
Expand Down Expand Up @@ -41,11 +46,12 @@ export class MicrosoftOneDriveTrigger implements INodeType {

async poll(this: IPollFunctions): Promise<INodeExecutionData[][] | null> {
const workflowData = this.getWorkflowStaticData('node');
const graphEndpoint = await getGraphEndpoint.call(this);

let responseData: IDataObject[];

const lastLink: string =
(workflowData.LastLink as string) ||
'https://graph.microsoft.com/v1.0/me/drive/root/delta?token=latest';
(workflowData.LastLink as string) || `${graphEndpoint}/v1.0/me/drive/root/delta?token=latest`;

const now = DateTime.now().toUTC();
const start = DateTime.fromISO(workflowData.lastTimeChecked as string) || now;
Expand All @@ -72,7 +78,7 @@ export class MicrosoftOneDriveTrigger implements INodeType {
'',
{},
{},
'https://graph.microsoft.com/v1.0/me/drive/root/delta',
`${graphEndpoint}/v1.0/me/drive/root/delta`,
)
).value as IDataObject[];
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import type { ICredentialDataDecryptedObject, IExecuteFunctions } from 'n8n-workflow';

import * as GenericFunctions from '../GenericFunctions';

const fakeExecute = (credentials: ICredentialDataDecryptedObject, result: unknown) => {
const fakeExecuteFunction = {
async getCredentials(): Promise<ICredentialDataDecryptedObject> {
return credentials;
},
getNode: jest.fn(),

helpers: {
requestOAuth2: jest.fn().mockResolvedValue(result),
},
} as unknown as IExecuteFunctions;
return fakeExecuteFunction;
};

describe('Test MicrosoftOneDrive, GenericFunctions => microsoftApiRequest', () => {
it('should call microsoftApiRequest using the defined service root', async () => {
const execute = fakeExecute(
{
graphEndpoint: 'https://foo.bar',
useShared: false,
userPrincipalName: 'test-principal',
},
'foo',
);

const result: string = (await GenericFunctions.microsoftApiRequest.call(
execute,
'GET',
'/foo',
)) as string;

expect(result).toEqual('foo');
expect(execute.helpers.requestOAuth2).toHaveBeenCalledTimes(1);
expect(execute.helpers.requestOAuth2).toHaveBeenCalledWith('microsoftOneDriveOAuth2Api', {
headers: { 'Content-Type': 'application/json' },
method: 'GET',
uri: 'https://foo.bar/v1.0/me/foo',
json: true,
});
});

it('should call microsoftApiRequest using the service root if no root is provided', async () => {
const execute = fakeExecute(
{
useShared: false,
userPrincipalName: 'test-principal',
},
'foo',
);

const result: string = (await GenericFunctions.microsoftApiRequest.call(
execute,
'GET',
'/foo',
)) as string;

expect(result).toEqual('foo');
expect(execute.helpers.requestOAuth2).toHaveBeenCalledTimes(1);
expect(execute.helpers.requestOAuth2).toHaveBeenCalledWith('microsoftOneDriveOAuth2Api', {
headers: { 'Content-Type': 'application/json' },
method: 'GET',
uri: 'https://graph.microsoft.com/v1.0/me/foo',
json: true,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import type { ICredentialDataDecryptedObject, IExecuteFunctions } from 'n8n-workflow';

import * as transport from '../../../v2/transport';

const fakeExecute = (credentials: ICredentialDataDecryptedObject, result: unknown) => {
const fakeExecuteFunction = {
async getCredentials(): Promise<ICredentialDataDecryptedObject> {
return credentials;
},

helpers: {
requestWithAuthentication: jest.fn().mockResolvedValue(result),
},
} as unknown as IExecuteFunctions;
return fakeExecuteFunction;
};

describe('Test MicrosoftOutlookV2, transport => microsoftApiRequest', () => {
it('should call microsoftApiRequest using the defined service root', async () => {
const execute = fakeExecute(
{
graphEndpoint: 'https://foo.bar',
useShared: false,
userPrincipalName: 'test-principal',
},
'foo',
);

const result: string = (await transport.microsoftApiRequest.call(
execute,
'GET',
'/foo',
)) as string;

expect(result).toEqual('foo');
expect(execute.helpers.requestWithAuthentication).toHaveBeenCalledTimes(1);
expect(execute.helpers.requestWithAuthentication).toHaveBeenCalledWith(
'microsoftOutlookOAuth2Api',
{
headers: { 'Content-Type': 'application/json' },
method: 'GET',
qs: {},
uri: 'https://foo.bar/v1.0/me/foo',
json: true,
},
);
});

it('should call microsoftApiRequest using the service root if no root is provided', async () => {
const execute = fakeExecute(
{
useShared: false,
userPrincipalName: 'test-principal',
},
'foo',
);

const result: string = (await transport.microsoftApiRequest.call(
execute,
'GET',
'/foo',
)) as string;

expect(result).toEqual('foo');
expect(execute.helpers.requestWithAuthentication).toHaveBeenCalledTimes(1);
expect(execute.helpers.requestWithAuthentication).toHaveBeenCalledWith(
'microsoftOutlookOAuth2Api',
{
headers: { 'Content-Type': 'application/json' },
method: 'GET',
qs: {},
uri: 'https://graph.microsoft.com/v1.0/me/foo',
json: true,
},
);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ export async function microsoftApiRequest(
option: IDataObject = { json: true },
) {
const credentials = await this.getCredentials('microsoftOutlookOAuth2Api');
const graphEndpoint = (credentials.graphEndpoint as string) || 'https://graph.microsoft.com';

let apiUrl = `https://graph.microsoft.com/v1.0/me${resource}`;
let apiUrl = `${graphEndpoint}/v1.0/me${resource}`;
// If accessing shared mailbox
if (credentials.useShared && credentials.userPrincipalName) {
apiUrl = `https://graph.microsoft.com/v1.0/users/${credentials.userPrincipalName}${resource}`;
apiUrl = `${graphEndpoint}/v1.0/users/${credentials.userPrincipalName}${resource}`;
}

const options: IRequestOptions = {
Expand Down