Skip to content

Commit

Permalink
Microsoft OneDrive service root endpoint
Browse files Browse the repository at this point in the history
This change makes the service root configurable for the Microsoft OneDrive nodes.

The service root endpoint is configurable in the OneDrive credential.

The motivation behind this change is to allow the Microsoft OneDrive N8N node to connect to the other tenant types such as the Microsoft GovCloud instances.
  • Loading branch information
kyle-blacklist committed Feb 24, 2025
1 parent 1bb8e1b commit a2bcd75
Show file tree
Hide file tree
Showing 4 changed files with 121 additions and 6 deletions.
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],
})),
},
];
}
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,
});
});
});

0 comments on commit a2bcd75

Please sign in to comment.