diff --git a/packages/nodes-base/credentials/MicrosoftOneDriveOAuth2Api.credentials.ts b/packages/nodes-base/credentials/MicrosoftOneDriveOAuth2Api.credentials.ts index 835712ac53fb0..adc86f74ce9e6 100644 --- a/packages/nodes-base/credentials/MicrosoftOneDriveOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/MicrosoftOneDriveOAuth2Api.credentials.ts @@ -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.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'; @@ -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], + })), + }, ]; } diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts index 8949db933ce93..84ae9b822c5c7 100644 --- a/packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/GenericFunctions.ts @@ -10,6 +10,13 @@ import type { } from 'n8n-workflow'; import { NodeApiError } from 'n8n-workflow'; +export async function getGraphEndpoint( + this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, +): Promise { + 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, @@ -21,6 +28,7 @@ export async function microsoftApiRequest( headers: IDataObject = {}, option: IDataObject = { json: true }, ): Promise { + const graphEndpoint = await getGraphEndpoint.call(this); const options: IRequestOptions = { headers: { 'Content-Type': 'application/json', @@ -28,7 +36,7 @@ export async function microsoftApiRequest( 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); @@ -145,13 +153,15 @@ export async function getPath( this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, itemId: string, ): Promise { + 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}`; diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDriveTrigger.node.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDriveTrigger.node.ts index 49bf57c686f4b..636486e29bd16 100644 --- a/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDriveTrigger.node.ts +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/MicrosoftOneDriveTrigger.node.ts @@ -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 { @@ -41,11 +46,12 @@ export class MicrosoftOneDriveTrigger implements INodeType { async poll(this: IPollFunctions): Promise { 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; @@ -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 { diff --git a/packages/nodes-base/nodes/Microsoft/OneDrive/test/GenericFunctions.test.ts b/packages/nodes-base/nodes/Microsoft/OneDrive/test/GenericFunctions.test.ts new file mode 100644 index 0000000000000..775d16ca7687d --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/OneDrive/test/GenericFunctions.test.ts @@ -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 { + 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, + }); + }); +});