From 3006ccf41bb911ba72f087a1479889fbf308c17d Mon Sep 17 00:00:00 2001 From: feelgood-interface <78543720+feelgood-interface@users.noreply.github.com> Date: Wed, 8 Jan 2025 11:49:26 +0100 Subject: [PATCH 1/4] feat(Microsoft Entra ID Node): New node (#11779) Co-authored-by: Giulio Andreini --- .../MicrosoftEntraOAuth2Api.credentials.ts | 7 +- .../nodes/Microsoft/Entra/GenericFunctions.ts | 412 ++++ .../Microsoft/Entra/MicrosoftEntra.node.json | 18 + .../Microsoft/Entra/MicrosoftEntra.node.ts | 115 + .../Entra/descriptions/GroupDescription.ts | 1179 +++++++++ .../Entra/descriptions/UserDescription.ts | 2109 +++++++++++++++++ .../Microsoft/Entra/descriptions/index.ts | 2 + .../Microsoft/Entra/microsoftEntra.dark.svg | 4 + .../nodes/Microsoft/Entra/microsoftEntra.svg | 8 + .../Entra/test/GroupDescription.test.ts | 775 ++++++ .../Entra/test/MicrosoftEntra.node.test.ts | 249 ++ .../Entra/test/UserDescription.test.ts | 1161 +++++++++ .../nodes/Microsoft/Entra/test/mocks.ts | 1270 ++++++++++ packages/nodes-base/package.json | 1 + .../test/nodes/FakeCredentialsMap.ts | 24 + packages/workflow/src/Interfaces.ts | 2 +- 16 files changed, 7331 insertions(+), 5 deletions(-) create mode 100644 packages/nodes-base/nodes/Microsoft/Entra/GenericFunctions.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Entra/MicrosoftEntra.node.json create mode 100644 packages/nodes-base/nodes/Microsoft/Entra/MicrosoftEntra.node.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Entra/descriptions/GroupDescription.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Entra/descriptions/UserDescription.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Entra/descriptions/index.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Entra/microsoftEntra.dark.svg create mode 100644 packages/nodes-base/nodes/Microsoft/Entra/microsoftEntra.svg create mode 100644 packages/nodes-base/nodes/Microsoft/Entra/test/GroupDescription.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Entra/test/MicrosoftEntra.node.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Entra/test/UserDescription.test.ts create mode 100644 packages/nodes-base/nodes/Microsoft/Entra/test/mocks.ts diff --git a/packages/nodes-base/credentials/MicrosoftEntraOAuth2Api.credentials.ts b/packages/nodes-base/credentials/MicrosoftEntraOAuth2Api.credentials.ts index c1c77edb41717..be1e04de199d1 100644 --- a/packages/nodes-base/credentials/MicrosoftEntraOAuth2Api.credentials.ts +++ b/packages/nodes-base/credentials/MicrosoftEntraOAuth2Api.credentials.ts @@ -1,4 +1,4 @@ -import type { ICredentialType, INodeProperties, Icon } from 'n8n-workflow'; +import type { ICredentialType, INodeProperties } from 'n8n-workflow'; export class MicrosoftEntraOAuth2Api implements ICredentialType { name = 'microsoftEntraOAuth2Api'; @@ -7,8 +7,6 @@ export class MicrosoftEntraOAuth2Api implements ICredentialType { extends = ['microsoftOAuth2Api']; - icon: Icon = 'file:icons/Azure.svg'; - documentationUrl = 'microsoftentra'; properties: INodeProperties[] = [ @@ -16,8 +14,9 @@ export class MicrosoftEntraOAuth2Api implements ICredentialType { displayName: 'Scope', name: 'scope', type: 'hidden', + // Sites.FullControl.All required to update user specific properties https://github.com/microsoftgraph/msgraph-sdk-dotnet/issues/1316 default: - 'openid offline_access AccessReview.ReadWrite.All Directory.ReadWrite.All NetworkAccessPolicy.ReadWrite.All DelegatedAdminRelationship.ReadWrite.All EntitlementManagement.ReadWrite.All', + 'openid offline_access AccessReview.ReadWrite.All Directory.ReadWrite.All NetworkAccessPolicy.ReadWrite.All DelegatedAdminRelationship.ReadWrite.All EntitlementManagement.ReadWrite.All User.ReadWrite.All Directory.AccessAsUser.All Sites.FullControl.All GroupMember.ReadWrite.All', }, ]; } diff --git a/packages/nodes-base/nodes/Microsoft/Entra/GenericFunctions.ts b/packages/nodes-base/nodes/Microsoft/Entra/GenericFunctions.ts new file mode 100644 index 0000000000000..e7b2e940b77f6 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Entra/GenericFunctions.ts @@ -0,0 +1,412 @@ +import type { + JsonObject, + IDataObject, + IExecuteFunctions, + IExecuteSingleFunctions, + IHttpRequestMethods, + IHttpRequestOptions, + ILoadOptionsFunctions, + IRequestOptions, + INodeExecutionData, + IN8nHttpFullResponse, + INodePropertyOptions, + INodeListSearchResult, + INodeListSearchItems, +} from 'n8n-workflow'; +import { NodeApiError } from 'n8n-workflow'; +import { parseStringPromise } from 'xml2js'; + +export async function microsoftApiRequest( + this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + method: IHttpRequestMethods, + endpoint: string, + body: IDataObject = {}, + qs?: IDataObject, + headers?: IDataObject, + url?: string, +): Promise { + const options: IHttpRequestOptions = { + method, + url: url ?? `https://graph.microsoft.com/v1.0${endpoint}`, + json: true, + headers, + body, + qs, + }; + + return await this.helpers.requestWithAuthentication.call( + this, + 'microsoftEntraOAuth2Api', + options, + ); +} + +export async function microsoftApiPaginateRequest( + this: IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions, + method: IHttpRequestMethods, + endpoint: string, + body: IDataObject = {}, + qs?: IDataObject, + headers?: IDataObject, + url?: string, + itemIndex: number = 0, +): Promise { + // Todo: IHttpRequestOptions doesn't have uri property which is required for requestWithAuthenticationPaginated + const options: IRequestOptions = { + method, + uri: url ?? `https://graph.microsoft.com/v1.0${endpoint}`, + json: true, + headers, + body, + qs, + }; + + const pages = await this.helpers.requestWithAuthenticationPaginated.call( + this, + options, + itemIndex, + { + continue: '={{ !!$response.body?.["@odata.nextLink"] }}', + request: { + url: '={{ $response.body?.["@odata.nextLink"] ?? $request.url }}', + }, + requestInterval: 0, + }, + 'microsoftEntraOAuth2Api', + ); + + let results: IDataObject[] = []; + for (const page of pages) { + const items = page.body.value as IDataObject[]; + if (items) { + results = results.concat(items); + } + } + + return results; +} + +export async function handleErrorPostReceive( + this: IExecuteSingleFunctions, + data: INodeExecutionData[], + response: IN8nHttpFullResponse, +): Promise { + if (String(response.statusCode).startsWith('4') || String(response.statusCode).startsWith('5')) { + const resource = this.getNodeParameter('resource') as string; + const operation = this.getNodeParameter('operation') as string; + const { + code: errorCode, + message: errorMessage, + details: errorDetails, + } = (response.body as IDataObject)?.error as { + code: string; + message: string; + innerError?: { + code: string; + 'request-id'?: string; + date?: string; + }; + details?: Array<{ + code: string; + message: string; + }>; + }; + + // Operation specific errors + if (resource === 'group') { + if (operation === 'create') { + } else if (operation === 'delete') { + if (errorCode === 'Request_ResourceNotFound') { + throw new NodeApiError(this.getNode(), response as unknown as JsonObject, { + message: "The required group doesn't match any existing one", + description: "Double-check the value in the parameter 'Group to Delete' and try again", + }); + } + } else if (operation === 'get') { + if (errorCode === 'Request_ResourceNotFound') { + throw new NodeApiError(this.getNode(), response as unknown as JsonObject, { + message: "The required group doesn't match any existing one", + description: "Double-check the value in the parameter 'Group to Get' and try again", + }); + } + } else if (operation === 'getAll') { + } else if (operation === 'update') { + if ( + errorCode === 'BadRequest' && + errorMessage === 'Empty Payload. JSON content expected.' + ) { + // Ignore empty payload error. Currently n8n deletes the empty body object from the request. + return data; + } + if (errorCode === 'Request_ResourceNotFound') { + throw new NodeApiError(this.getNode(), response as unknown as JsonObject, { + message: "The required group doesn't match any existing one", + description: "Double-check the value in the parameter 'Group to Update' and try again", + }); + } + } + } else if (resource === 'user') { + if (operation === 'addGroup') { + if ( + errorCode === 'Request_BadRequest' && + errorMessage === + "One or more added object references already exist for the following modified properties: 'members'." + ) { + throw new NodeApiError(this.getNode(), response as unknown as JsonObject, { + message: 'The user is already in the group', + description: + 'The specified user cannot be added to the group because they are already a member', + }); + } else if (errorCode === 'Request_ResourceNotFound') { + const group = this.getNodeParameter('group.value') as string; + if (errorMessage.includes(group)) { + throw new NodeApiError(this.getNode(), response as unknown as JsonObject, { + message: "The required group doesn't match any existing one", + description: "Double-check the value in the parameter 'Group' and try again", + }); + } else { + throw new NodeApiError(this.getNode(), response as unknown as JsonObject, { + message: "The required user doesn't match any existing one", + description: "Double-check the value in the parameter 'User to Add' and try again", + }); + } + } + } else if (operation === 'create') { + } else if (operation === 'delete') { + if (errorCode === 'Request_ResourceNotFound') { + throw new NodeApiError(this.getNode(), response as unknown as JsonObject, { + message: "The required user doesn't match any existing one", + description: "Double-check the value in the parameter 'User to Delete' and try again", + }); + } + } else if (operation === 'get') { + if (errorCode === 'Request_ResourceNotFound') { + throw new NodeApiError(this.getNode(), response as unknown as JsonObject, { + message: "The required user doesn't match any existing one", + description: "Double-check the value in the parameter 'User to Get' and try again", + }); + } + } else if (operation === 'getAll') { + } else if (operation === 'removeGroup') { + if (errorCode === 'Request_ResourceNotFound') { + throw new NodeApiError(this.getNode(), response as unknown as JsonObject, { + message: 'The user is not in the group', + description: + 'The specified user cannot be removed from the group because they are not a member of the group', + }); + } else if ( + errorCode === 'Request_UnsupportedQuery' && + errorMessage === + "Unsupported referenced-object resource identifier for link property 'members'." + ) { + throw new NodeApiError(this.getNode(), response as unknown as JsonObject, { + message: 'The user ID is invalid', + description: 'The ID should be in the format e.g. 02bd9fd6-8f93-4758-87c3-1fb73740a315', + }); + } + } else if (operation === 'update') { + if ( + errorCode === 'BadRequest' && + errorMessage === 'Empty Payload. JSON content expected.' + ) { + // Ignore empty payload error. Currently n8n deletes the empty body object from the request. + return data; + } + if (errorCode === 'Request_ResourceNotFound') { + throw new NodeApiError(this.getNode(), response as unknown as JsonObject, { + message: "The required user doesn't match any existing one", + description: "Double-check the value in the parameter 'User to Update' and try again", + }); + } + } + } + + // Generic errors + if ( + errorCode === 'Request_BadRequest' && + errorMessage.startsWith('Invalid object identifier') + ) { + const group = this.getNodeParameter('group.value', '') as string; + const parameterResource = + resource === 'group' || errorMessage.includes(group) ? 'group' : 'user'; + + throw new NodeApiError(this.getNode(), response as unknown as JsonObject, { + message: `The ${parameterResource} ID is invalid`, + description: 'The ID should be in the format e.g. 02bd9fd6-8f93-4758-87c3-1fb73740a315', + }); + } + if (errorDetails?.some((x) => x.code === 'ObjectConflict' || x.code === 'ConflictingObjects')) { + throw new NodeApiError(this.getNode(), response as unknown as JsonObject, { + message: `The ${resource} already exists`, + description: errorMessage, + }); + } + + throw new NodeApiError(this.getNode(), response as unknown as JsonObject); + } + + return data; +} + +export async function getGroupProperties( + this: ILoadOptionsFunctions, +): Promise { + const returnData: INodePropertyOptions[] = []; + const response = await microsoftApiRequest.call(this, 'GET', '/$metadata#groups'); + const metadata = await parseStringPromise(response as string, { + explicitArray: false, + }); + + /* eslint-disable */ + const entities = metadata['edmx:Edmx']['edmx:DataServices']['Schema'] + .find((x: any) => x['$']['Namespace'] === 'microsoft.graph') + ['EntityType'].filter((x: any) => + ['entity', 'directoryObject', 'group'].includes(x['$']['Name']), + ); + let properties = entities + .flatMap((x: any) => x['Property']) + .map((x: any) => x['$']['Name']) as string[]; + /* eslint-enable */ + + properties = properties.filter( + (x) => !['id', 'isArchived', 'hasMembersWithLicenseErrors'].includes(x), + ); + + properties = properties.sort(); + + for (const property of properties) { + returnData.push({ + name: property, + value: property, + }); + } + + return returnData; +} + +export async function getUserProperties( + this: ILoadOptionsFunctions, +): Promise { + const returnData: INodePropertyOptions[] = []; + const response = await microsoftApiRequest.call(this, 'GET', '/$metadata#users'); + const metadata = await parseStringPromise(response as string, { + explicitArray: false, + }); + + /* eslint-disable */ + const entities = metadata['edmx:Edmx']['edmx:DataServices']['Schema'] + .find((x: any) => x['$']['Namespace'] === 'microsoft.graph') + ['EntityType'].filter((x: any) => + ['entity', 'directoryObject', 'user'].includes(x['$']['Name']), + ); + let properties = entities + .flatMap((x: any) => x['Property']) + .map((x: any) => x['$']['Name']) as string[]; + /* eslint-enable */ + + // signInActivity requires AuditLog.Read.All + // mailboxSettings MailboxSettings.Read + properties = properties.filter( + (x) => + !['id', 'deviceEnrollmentLimit', 'mailboxSettings', 'print', 'signInActivity'].includes(x), + ); + + properties = properties.sort(); + + for (const property of properties) { + returnData.push({ + name: property, + value: property, + }); + } + return returnData; +} + +export async function getGroups( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +): Promise { + let response: any; + if (paginationToken) { + response = await microsoftApiRequest.call( + this, + 'GET', + '/groups', + {}, + undefined, + undefined, + paginationToken, + ); + } else { + const qs: IDataObject = { + $select: 'id,displayName', + }; + const headers: IDataObject = {}; + if (filter) { + headers.ConsistencyLevel = 'eventual'; + qs.$search = `"displayName:${filter}"`; + } + response = await microsoftApiRequest.call(this, 'GET', '/groups', {}, qs, headers); + } + + const groups: Array<{ + id: string; + displayName: string; + }> = response.value; + + const results: INodeListSearchItems[] = groups + .map((g) => ({ + name: g.displayName, + value: g.id, + })) + .sort((a, b) => + a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }), + ); + + return { results, paginationToken: response['@odata.nextLink'] }; +} + +export async function getUsers( + this: ILoadOptionsFunctions, + filter?: string, + paginationToken?: string, +): Promise { + let response: any; + if (paginationToken) { + response = await microsoftApiRequest.call( + this, + 'GET', + '/users', + {}, + undefined, + undefined, + paginationToken, + ); + } else { + const qs: IDataObject = { + $select: 'id,displayName', + }; + const headers: IDataObject = {}; + if (filter) { + qs.$filter = `startsWith(displayName, '${filter}') OR startsWith(userPrincipalName, '${filter}')`; + } + response = await microsoftApiRequest.call(this, 'GET', '/users', {}, qs, headers); + } + + const users: Array<{ + id: string; + displayName: string; + }> = response.value; + + const results: INodeListSearchItems[] = users + .map((u) => ({ + name: u.displayName, + value: u.id, + })) + .sort((a, b) => + a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: 'base' }), + ); + + return { results, paginationToken: response['@odata.nextLink'] }; +} diff --git a/packages/nodes-base/nodes/Microsoft/Entra/MicrosoftEntra.node.json b/packages/nodes-base/nodes/Microsoft/Entra/MicrosoftEntra.node.json new file mode 100644 index 0000000000000..3e45875d3fe03 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Entra/MicrosoftEntra.node.json @@ -0,0 +1,18 @@ +{ + "node": "n8n-nodes-base.microsoftEntra", + "nodeVersion": "1.0", + "codexVersion": "1.0", + "categories": ["Development", "Developer Tools"], + "resources": { + "credentialDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/credentials/microsoft/" + } + ], + "primaryDocumentation": [ + { + "url": "https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.microsoftentra/" + } + ] + } +} diff --git a/packages/nodes-base/nodes/Microsoft/Entra/MicrosoftEntra.node.ts b/packages/nodes-base/nodes/Microsoft/Entra/MicrosoftEntra.node.ts new file mode 100644 index 0000000000000..d07255b86d00b --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Entra/MicrosoftEntra.node.ts @@ -0,0 +1,115 @@ +import type { + ILoadOptionsFunctions, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; +import { NodeConnectionType } from 'n8n-workflow'; + +import { groupFields, groupOperations, userFields, userOperations } from './descriptions'; +import { getGroupProperties, getGroups, getUserProperties, getUsers } from './GenericFunctions'; + +export class MicrosoftEntra implements INodeType { + description: INodeTypeDescription = { + displayName: 'Microsoft Entra ID', + name: 'microsoftEntra', + icon: { + light: 'file:microsoftEntra.svg', + dark: 'file:microsoftEntra.dark.svg', + }, + group: ['transform'], + version: 1, + subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}', + description: 'Interact with Micosoft Entra ID API', + defaults: { + name: 'Micosoft Entra ID', + }, + inputs: [NodeConnectionType.Main], + outputs: [NodeConnectionType.Main], + credentials: [ + { + name: 'microsoftEntraOAuth2Api', + required: true, + }, + ], + requestDefaults: { + baseURL: 'https://graph.microsoft.com/v1.0', + headers: { + 'Content-Type': 'application/json', + }, + }, + properties: [ + { + displayName: 'Resource', + name: 'resource', + type: 'options', + noDataExpression: true, + options: [ + { + name: 'Group', + value: 'group', + }, + { + name: 'User', + value: 'user', + }, + ], + default: 'user', + }, + + ...groupOperations, + ...groupFields, + ...userOperations, + ...userFields, + ], + }; + + methods = { + loadOptions: { + getGroupProperties, + + async getGroupPropertiesGetAll(this: ILoadOptionsFunctions): Promise { + // Filter items not supported for list endpoint + return (await getGroupProperties.call(this)).filter( + (x) => + ![ + 'allowExternalSenders', + 'autoSubscribeNewMembers', + 'hideFromAddressLists', + 'hideFromOutlookClients', + 'isSubscribedByMail', + 'unseenCount', + ].includes(x.value as string), + ); + }, + + getUserProperties, + + async getUserPropertiesGetAll(this: ILoadOptionsFunctions): Promise { + // Filter items not supported for list endpoint + return (await getUserProperties.call(this)).filter( + (x) => + ![ + 'aboutMe', + 'birthday', + 'hireDate', + 'interests', + 'mySite', + 'pastProjects', + 'preferredName', + 'responsibilities', + 'schools', + 'skills', + 'mailboxSettings', + ].includes(x.value as string), + ); + }, + }, + + listSearch: { + getGroups, + + getUsers, + }, + }; +} diff --git a/packages/nodes-base/nodes/Microsoft/Entra/descriptions/GroupDescription.ts b/packages/nodes-base/nodes/Microsoft/Entra/descriptions/GroupDescription.ts new file mode 100644 index 0000000000000..07f5ef908bb57 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Entra/descriptions/GroupDescription.ts @@ -0,0 +1,1179 @@ +import { merge } from 'lodash'; +import type { + IDataObject, + IExecuteSingleFunctions, + IHttpRequestOptions, + IN8nHttpFullResponse, + INodeExecutionData, + INodeProperties, +} from 'n8n-workflow'; +import { NodeOperationError } from 'n8n-workflow'; + +import { handleErrorPostReceive, microsoftApiRequest } from '../GenericFunctions'; + +export const groupOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['group'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a group', + routing: { + request: { + ignoreHttpStatusErrors: true, + method: 'POST', + url: '/groups', + }, + output: { + postReceive: [handleErrorPostReceive], + }, + }, + action: 'Create group', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a group', + routing: { + request: { + ignoreHttpStatusErrors: true, + method: 'DELETE', + url: '=/groups/{{ $parameter["group"] }}', + }, + output: { + postReceive: [ + handleErrorPostReceive, + { + type: 'set', + properties: { + value: '={{ { "deleted": true } }}', + }, + }, + ], + }, + }, + action: 'Delete group', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve data for a specific group', + routing: { + request: { + ignoreHttpStatusErrors: true, + method: 'GET', + url: '=/groups/{{ $parameter["group"] }}', + }, + output: { + postReceive: [handleErrorPostReceive], + }, + }, + action: 'Get group', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Retrieve a list of groups', + routing: { + request: { + ignoreHttpStatusErrors: true, + method: 'GET', + url: '/groups', + }, + output: { + postReceive: [ + handleErrorPostReceive, + { + type: 'rootProperty', + properties: { + property: 'value', + }, + }, + ], + }, + }, + action: 'Get many groups', + }, + { + name: 'Update', + value: 'update', + description: 'Update a group', + routing: { + request: { + ignoreHttpStatusErrors: true, + method: 'PATCH', + url: '=/groups/{{ $parameter["group"] }}', + }, + output: { + postReceive: [ + handleErrorPostReceive, + { + type: 'set', + properties: { + value: '={{ { "updated": true } }}', + }, + }, + ], + }, + }, + action: 'Update group', + }, + ], + default: 'getAll', + }, +]; + +const createFields: INodeProperties[] = [ + { + displayName: 'Group Type', + name: 'groupType', + default: '', + displayOptions: { + show: { + resource: ['group'], + operation: ['create'], + }, + }, + options: [ + { + name: 'Microsoft 365', + value: 'Unified', + }, + { + name: 'Security', + value: '', + }, + ], + routing: { + send: { + preSend: [ + async function ( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, + ): Promise { + const groupType = this.getNodeParameter('groupType') as string; + const body = requestOptions.body as IDataObject; + if (groupType) { + body.groupTypes ??= [] as string[]; + (body.groupTypes as string[]).push(groupType); + } else { + // Properties mailEnabled and securityEnabled are not visible for Security group, but are required. So we add them here. + body.mailEnabled = false; + body.securityEnabled = true; + } + return requestOptions; + }, + ], + }, + }, + type: 'options', + }, + { + displayName: 'Group Name', + name: 'displayName', + default: '', + description: 'The name to display in the address book for the group', + displayOptions: { + show: { + resource: ['group'], + operation: ['create'], + }, + }, + required: true, + routing: { + send: { + property: 'displayName', + type: 'body', + preSend: [ + async function ( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, + ): Promise { + const displayName = this.getNodeParameter('displayName') as string; + if (displayName?.length > 256) { + throw new NodeOperationError( + this.getNode(), + "'Display Name' should have a maximum length of 256", + ); + } + return requestOptions; + }, + ], + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Group Email Address', + name: 'mailNickname', + default: '', + description: 'The mail alias for the group. Only enter the local-part without the domain.', + displayOptions: { + show: { + resource: ['group'], + operation: ['create'], + }, + }, + placeholder: 'e.g. alias', + required: true, + routing: { + send: { + property: 'mailNickname', + type: 'body', + preSend: [ + async function ( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, + ): Promise { + const mailNickname = this.getNodeParameter('mailNickname') as string; + if (mailNickname?.includes('@')) { + throw new NodeOperationError( + this.getNode(), + `'Group Email Address' should only include the local-part of the email address, without ${mailNickname.slice(mailNickname.indexOf('@'))}`, + ); + } + if (mailNickname?.length > 64) { + throw new NodeOperationError( + this.getNode(), + "'Group Email Address' should have a maximum length of 64", + ); + } + if (mailNickname && !/^((?![@()\[\]"\\;:<> ,])[\x00-\x7F])*$/.test(mailNickname)) { + throw new NodeOperationError( + this.getNode(), + "'Group Email Address' should only contain characters in the ASCII character set 0 - 127 except the following: @ () \\ [] \" ; : <> , SPACE", + ); + } + return requestOptions; + }, + ], + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Mail Enabled', + name: 'mailEnabled', + default: false, + description: 'Whether the group is mail-enabled', + displayOptions: { + show: { + resource: ['group'], + operation: ['create'], + groupType: ['Unified'], + }, + }, + required: true, + routing: { + send: { + property: 'mailEnabled', + type: 'body', + }, + }, + type: 'boolean', + validateType: 'boolean', + }, + { + displayName: 'Membership Type', + name: 'membershipType', + default: '', + displayOptions: { + show: { + resource: ['group'], + operation: ['create'], + }, + }, + options: [ + { + name: 'Assigned', + value: '', + description: + 'Lets you add specific users as members of a group and have unique permissions', + }, + { + name: 'Dynamic', + value: 'DynamicMembership', + description: 'Lets you use rules to automatically add and remove users as members', + }, + ], + routing: { + send: { + preSend: [ + async function ( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, + ): Promise { + const membershipType = this.getNodeParameter('membershipType') as string; + if (membershipType) { + const body = requestOptions.body as IDataObject; + body.groupTypes ??= [] as string[]; + (body.groupTypes as string[]).push(membershipType); + } + return requestOptions; + }, + ], + }, + }, + type: 'options', + }, + { + displayName: 'Security Enabled', + name: 'securityEnabled', + default: true, + description: 'Whether the group is a security group', + displayOptions: { + show: { + resource: ['group'], + operation: ['create'], + groupType: ['Unified'], + }, + }, + routing: { + send: { + property: 'securityEnabled', + type: 'body', + }, + }, + type: 'boolean', + validateType: 'boolean', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + default: {}, + displayOptions: { + show: { + resource: ['group'], + operation: ['create'], + }, + }, + options: [ + { + displayName: 'Assignable to Role', + name: 'isAssignableToRole', + default: false, + description: 'Whether Microsoft Entra roles can be assigned to the group', + displayOptions: { + hide: { + '/membershipType': ['DynamicMembership'], + }, + }, + routing: { + send: { + property: 'isAssignableToRole', + type: 'body', + preSend: [ + async function ( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, + ): Promise { + const isAssignableToRole = this.getNodeParameter( + 'additionalFields.isAssignableToRole', + ) as boolean; + if (isAssignableToRole) { + const securityEnabled = this.getNodeParameter('securityEnabled', true) as boolean; + const visibility = this.getNodeParameter( + 'additionalFields.visibility', + '', + ) as string; + const groupType = this.getNodeParameter('groupType') as string; + const mailEnabled = this.getNodeParameter('mailEnabled', false) as boolean; + if (!securityEnabled) { + throw new NodeOperationError( + this.getNode(), + "'Security Enabled' must be set to true if 'Assignable to Role' is set", + ); + } + if (visibility !== 'Private') { + throw new NodeOperationError( + this.getNode(), + "'Visibility' must be set to 'Private' if 'Assignable to Role' is set", + ); + } + if (groupType === 'Unified' && !mailEnabled) { + throw new NodeOperationError( + this.getNode(), + "'Mail Enabled' must be set to true if 'Assignable to Role' is set", + ); + } + } + return requestOptions; + }, + ], + }, + }, + type: 'boolean', + validateType: 'boolean', + }, + { + displayName: 'Description', + name: 'description', + default: '', + description: 'Description for the group', + type: 'string', + validateType: 'string', + }, + { + displayName: 'Membership Rule', + name: 'membershipRule', + default: '', + description: + 'The dynamic membership rules', + displayOptions: { + show: { + '/membershipType': ['DynamicMembership'], + }, + }, + placeholder: 'e.g. user.department -eq "Marketing"', + routing: { + send: { + property: 'membershipRule', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Membership Rule Processing State', + name: 'membershipRuleProcessingState', + default: 'On', + description: 'Indicates whether the dynamic membership processing is on or paused', + displayOptions: { + show: { + '/membershipType': ['DynamicMembership'], + }, + }, + options: [ + { + name: 'On', + value: 'On', + }, + { + name: 'Paused', + value: 'Paused', + }, + ], + routing: { + send: { + property: 'membershipRuleProcessingState', + type: 'body', + }, + }, + type: 'options', + validateType: 'options', + }, + { + displayName: 'Preferred Data Location', + name: 'preferredDataLocation', + default: '', + description: + 'A property set for the group that Office 365 services use to provision the corresponding data-at-rest resources (mailbox, OneDrive, groups sites, and so on)', + displayOptions: { + show: { + '/groupType': ['Unified'], + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Unique Name', + name: 'uniqueName', + default: '', + description: + 'The unique identifier for the group, can only be updated if null, and is immutable once set', + type: 'string', + validateType: 'string', + }, + { + displayName: 'Visibility', + name: 'visibility', + default: 'Public', + description: 'Specifies the visibility of the group', + options: [ + { + name: 'Private', + value: 'Private', + }, + { + name: 'Public', + value: 'Public', + }, + ], + type: 'options', + validateType: 'options', + }, + ], + placeholder: 'Add Field', + routing: { + output: { + postReceive: [ + async function ( + this: IExecuteSingleFunctions, + items: INodeExecutionData[], + _response: IN8nHttpFullResponse, + ): Promise { + for (const item of items) { + const groupId = item.json.id as string; + const fields = this.getNodeParameter('additionalFields', item.index) as IDataObject; + delete fields.isAssignableToRole; + delete fields.membershipRule; + delete fields.membershipRuleProcessingState; + if (Object.keys(fields).length) { + const body: IDataObject = { + ...fields, + }; + if (body.assignedLabels) { + body.assignedLabels = [(body.assignedLabels as IDataObject).labelValues]; + } + + try { + await microsoftApiRequest.call(this, 'PATCH', `/groups/${groupId}`, body); + merge(item.json, body); + } catch (error) { + try { + await microsoftApiRequest.call(this, 'DELETE', `/groups/${groupId}`); + } catch {} + throw error; + } + } + } + return items; + }, + ], + }, + }, + type: 'collection', + }, +]; + +const deleteFields: INodeProperties[] = [ + { + displayName: 'Group to Delete', + name: 'group', + default: { + mode: 'list', + value: '', + }, + displayOptions: { + show: { + resource: ['group'], + operation: ['delete'], + }, + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'getGroups', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + placeholder: 'e.g. 02bd9fd6-8f93-4758-87c3-1fb73740a315', + type: 'string', + }, + ], + required: true, + type: 'resourceLocator', + }, +]; + +const getFields: INodeProperties[] = [ + { + displayName: 'Group to Get', + name: 'group', + default: { + mode: 'list', + value: '', + }, + displayOptions: { + show: { + resource: ['group'], + operation: ['get'], + }, + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'getGroups', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + placeholder: 'e.g. 02bd9fd6-8f93-4758-87c3-1fb73740a315', + type: 'string', + }, + ], + required: true, + type: 'resourceLocator', + }, + { + displayName: 'Output', + name: 'output', + default: 'simple', + displayOptions: { + show: { + resource: ['group'], + operation: ['get'], + }, + }, + options: [ + { + name: 'Simplified', + value: 'simple', + routing: { + send: { + property: '$select', + type: 'query', + value: + 'id,createdDateTime,description,displayName,mail,mailEnabled,mailNickname,securityEnabled,securityIdentifier,visibility', + }, + }, + }, + { + name: 'Raw', + value: 'raw', + }, + { + name: 'Selected Fields', + value: 'fields', + }, + ], + type: 'options', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options + displayName: 'Fields', + name: 'fields', + default: [], + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options + description: 'The fields to add to the output', + displayOptions: { + show: { + resource: ['group'], + operation: ['get'], + output: ['fields'], + }, + }, + routing: { + send: { + property: '$select', + type: 'query', + value: '={{ $value.concat("id").join(",") }}', + }, + }, + typeOptions: { + loadOptionsMethod: 'getGroupProperties', + }, + type: 'multiOptions', + }, + { + displayName: 'Options', + name: 'options', + default: {}, + displayOptions: { + show: { + resource: ['group'], + operation: ['get'], + }, + }, + options: [ + { + displayName: 'Include Members', + name: 'includeMembers', + default: false, + routing: { + send: { + property: '$expand', + type: 'query', + value: + '={{ $value ? "members($select=id,accountEnabled,createdDateTime,displayName,employeeId,mail,securityIdentifier,userPrincipalName,userType)" : undefined }}', + }, + }, + type: 'boolean', + validateType: 'boolean', + }, + ], + placeholder: 'Add Option', + type: 'collection', + }, +]; + +const getAllFields: INodeProperties[] = [ + { + displayName: 'Return All', + name: 'returnAll', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: ['group'], + operation: ['getAll'], + }, + }, + routing: { + send: { + paginate: '={{ $value }}', + }, + operations: { + pagination: { + type: 'generic', + properties: { + continue: '={{ !!$response.body?.["@odata.nextLink"] }}', + request: { + url: '={{ $response.body?.["@odata.nextLink"] ?? $request.url }}', + qs: { + $filter: + '={{ !!$response.body?.["@odata.nextLink"] ? undefined : $request.qs?.$filter }}', + $select: + '={{ !!$response.body?.["@odata.nextLink"] ? undefined : $request.qs?.$select }}', + }, + }, + }, + }, + }, + }, + type: 'boolean', + }, + { + displayName: 'Limit', + name: 'limit', + default: 50, + description: 'Max number of results to return', + displayOptions: { + show: { + resource: ['group'], + operation: ['getAll'], + returnAll: [false], + }, + }, + routing: { + send: { + property: '$top', + type: 'query', + value: '={{ $value }}', + }, + }, + type: 'number', + typeOptions: { + minValue: 1, + }, + validateType: 'number', + }, + { + displayName: 'Filter', + name: 'filter', + default: '', + description: + 'Query parameter to filter results by', + displayOptions: { + show: { + resource: ['group'], + operation: ['getAll'], + }, + }, + hint: 'If empty, all the groups will be returned', + placeholder: "e.g. startswith(displayName, 'a')", + routing: { + send: { + property: '$filter', + type: 'query', + value: '={{ $value ? $value : undefined }}', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Output', + name: 'output', + default: 'simple', + displayOptions: { + show: { + resource: ['group'], + operation: ['getAll'], + }, + }, + options: [ + { + name: 'Simplified', + value: 'simple', + routing: { + send: { + property: '$select', + type: 'query', + value: + 'id,createdDateTime,description,displayName,mail,mailEnabled,mailNickname,securityEnabled,securityIdentifier,visibility', + }, + }, + }, + { + name: 'Raw', + value: 'raw', + }, + { + name: 'Selected Fields', + value: 'fields', + }, + ], + type: 'options', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options + displayName: 'Fields', + name: 'fields', + default: [], + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options + description: 'The fields to add to the output', + displayOptions: { + show: { + resource: ['group'], + operation: ['getAll'], + output: ['fields'], + }, + }, + routing: { + send: { + property: '$select', + type: 'query', + value: '={{ $value.concat("id").join(",") }}', + }, + }, + typeOptions: { + loadOptionsMethod: 'getGroupPropertiesGetAll', + }, + type: 'multiOptions', + }, +]; + +const updateFields: INodeProperties[] = [ + { + displayName: 'Group to Update', + name: 'group', + default: { + mode: 'list', + value: '', + }, + displayOptions: { + show: { + resource: ['group'], + operation: ['update'], + }, + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'getGroups', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + placeholder: 'e.g. 02bd9fd6-8f93-4758-87c3-1fb73740a315', + type: 'string', + }, + ], + required: true, + type: 'resourceLocator', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + default: {}, + displayOptions: { + show: { + resource: ['group'], + operation: ['update'], + }, + }, + options: [ + { + displayName: 'Allow External Senders', + name: 'allowExternalSenders', + default: false, + description: + 'Whether people external to the organization can send messages to the group. Wait a few seconds before editing this field in a newly created group.', + type: 'boolean', + validateType: 'boolean', + }, + { + displayName: 'Auto Subscribe New Members', + name: 'autoSubscribeNewMembers', + default: false, + description: + 'Whether new members added to the group will be auto-subscribed to receive email notifications. Wait a few seconds before editing this field in a newly created group.', + type: 'boolean', + validateType: 'boolean', + }, + { + displayName: 'Description', + name: 'description', + default: '', + description: 'Description for the group', + routing: { + send: { + property: 'description', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Group Name', + name: 'displayName', + default: '', + description: 'The name to display in the address book for the group', + routing: { + send: { + property: 'displayName', + type: 'body', + preSend: [ + async function ( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, + ): Promise { + const displayName = this.getNodeParameter('updateFields.displayName') as string; + if (displayName?.length > 256) { + throw new NodeOperationError( + this.getNode(), + "'Display Name' should have a maximum length of 256", + ); + } + return requestOptions; + }, + ], + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Group Email Address', + name: 'mailNickname', + default: '', + description: 'The mail alias for the group. Only enter the local-part without the domain.', + placeholder: 'e.g. alias', + routing: { + send: { + property: 'mailNickname', + type: 'body', + preSend: [ + async function ( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, + ): Promise { + const mailNickname = this.getNodeParameter('updateFields.mailNickname') as string; + if (mailNickname?.includes('@')) { + throw new NodeOperationError( + this.getNode(), + `'Group Email Address' should only include the local-part of the email address, without ${mailNickname.slice(mailNickname.indexOf('@'))}`, + ); + } + if (mailNickname?.length > 64) { + throw new NodeOperationError( + this.getNode(), + "'Group Email Address' should have a maximum length of 64", + ); + } + if (mailNickname && !/^((?![@()\[\]"\\;:<> ,])[\x00-\x7F])*$/.test(mailNickname)) { + throw new NodeOperationError( + this.getNode(), + "'Group Email Address' should only contain characters in the ASCII character set 0 - 127 except the following: @ () \\ [] \" ; : <> , SPACE", + ); + } + return requestOptions; + }, + ], + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Membership Rule', + name: 'membershipRule', + default: '', + description: + 'The dynamic membership rules', + placeholder: 'e.g. user.department -eq "Marketing"', + routing: { + send: { + property: 'membershipRule', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Membership Rule Processing State', + name: 'membershipRuleProcessingState', + default: 'On', + description: 'Indicates whether the dynamic membership processing is on or paused', + options: [ + { + name: 'On', + value: 'On', + }, + { + name: 'Paused', + value: 'Paused', + }, + ], + routing: { + send: { + property: 'membershipRuleProcessingState', + type: 'body', + }, + }, + type: 'options', + validateType: 'options', + }, + { + displayName: 'Preferred Data Location', + name: 'preferredDataLocation', + default: '', + description: + 'A property set for the group that Office 365 services use to provision the corresponding data-at-rest resources (mailbox, OneDrive, groups sites, and so on)', + routing: { + send: { + property: 'preferredDataLocation', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Security Enabled', + name: 'securityEnabled', + default: true, + description: 'Whether the group is a security group', + routing: { + send: { + property: 'securityEnabled', + type: 'body', + }, + }, + type: 'boolean', + validateType: 'boolean', + }, + { + displayName: 'Unique Name', + name: 'uniqueName', + default: '', + description: + 'The unique identifier for the group, can only be updated if null, and is immutable once set', + routing: { + send: { + property: 'uniqueName', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Visibility', + name: 'visibility', + default: 'Public', + description: 'Specifies the visibility of the group', + options: [ + { + name: 'Private', + value: 'Private', + }, + { + name: 'Public', + value: 'Public', + }, + ], + routing: { + send: { + property: 'visibility', + type: 'body', + }, + }, + type: 'options', + validateType: 'options', + }, + ], + placeholder: 'Add Field', + routing: { + output: { + postReceive: [ + async function ( + this: IExecuteSingleFunctions, + items: INodeExecutionData[], + _response: IN8nHttpFullResponse, + ): Promise { + for (const item of items) { + const groupId = this.getNodeParameter('group.value', item.index) as string; + const fields = this.getNodeParameter('updateFields', item.index) as IDataObject; + // To update the following properties, you must specify them in their own PATCH request, without including the other properties + const separateProperties = [ + 'allowExternalSenders', + 'autoSubscribeNewMembers', + // 'hideFromAddressLists', + // 'hideFromOutlookClients', + // 'isSubscribedByMail', + // 'unseenCount', + ]; + const separateFields = Object.keys(fields) + .filter((key) => separateProperties.includes(key)) + .reduce((obj, key) => { + return { + ...obj, + [key]: fields[key], + }; + }, {}); + if (Object.keys(separateFields).length) { + const body: IDataObject = { + ...separateFields, + }; + await microsoftApiRequest.call(this, 'PATCH', `/groups/${groupId}`, body); + } + } + return items; + }, + ], + }, + }, + type: 'collection', + }, +]; + +export const groupFields: INodeProperties[] = [ + ...createFields, + ...deleteFields, + ...getFields, + ...getAllFields, + ...updateFields, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Entra/descriptions/UserDescription.ts b/packages/nodes-base/nodes/Microsoft/Entra/descriptions/UserDescription.ts new file mode 100644 index 0000000000000..4e4914e796bd3 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Entra/descriptions/UserDescription.ts @@ -0,0 +1,2109 @@ +import { merge } from 'lodash'; +import type { DateTime } from 'luxon'; +import { + NodeOperationError, + type IDataObject, + type IExecuteSingleFunctions, + type IHttpRequestOptions, + type IN8nHttpFullResponse, + type INodeExecutionData, + type INodeProperties, +} from 'n8n-workflow'; + +import { handleErrorPostReceive, microsoftApiRequest } from '../GenericFunctions'; + +export const userOperations: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['user'], + }, + }, + options: [ + { + name: 'Add to Group', + value: 'addGroup', + description: 'Add user to group', + routing: { + request: { + method: 'POST', + url: '=/groups/{{ $parameter["group"] }}/members/$ref', + ignoreHttpStatusErrors: true, + }, + output: { + postReceive: [ + handleErrorPostReceive, + { + type: 'set', + properties: { + value: '={{ { "added": true } }}', + }, + }, + ], + }, + }, + action: 'Add user to group', + }, + { + name: 'Create', + value: 'create', + description: 'Create a user', + routing: { + request: { + method: 'POST', + url: '/users', + ignoreHttpStatusErrors: true, + }, + output: { + postReceive: [handleErrorPostReceive], + }, + }, + action: 'Create user', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a user', + routing: { + request: { + method: 'DELETE', + url: '=/users/{{ $parameter["user"] }}', + ignoreHttpStatusErrors: true, + }, + output: { + postReceive: [ + handleErrorPostReceive, + { + type: 'set', + properties: { + value: '={{ { "deleted": true } }}', + }, + }, + ], + }, + }, + action: 'Delete user', + }, + { + name: 'Get', + value: 'get', + description: 'Retrieve data for a specific user', + routing: { + request: { + method: 'GET', + url: '=/users/{{ $parameter["user"] }}', + ignoreHttpStatusErrors: true, + }, + output: { + postReceive: [handleErrorPostReceive], + }, + }, + action: 'Get user', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Retrieve a list of users', + routing: { + request: { + method: 'GET', + url: '/users', + ignoreHttpStatusErrors: true, + }, + output: { + postReceive: [ + handleErrorPostReceive, + { + type: 'rootProperty', + properties: { + property: 'value', + }, + }, + ], + }, + }, + action: 'Get many users', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased + name: 'Remove from Group', + value: 'removeGroup', + description: 'Remove user from group', + routing: { + request: { + method: 'DELETE', + url: '=/groups/{{ $parameter["group"] }}/members/{{ $parameter["user"] }}/$ref', + ignoreHttpStatusErrors: true, + }, + output: { + postReceive: [ + handleErrorPostReceive, + { + type: 'set', + properties: { + value: '={{ { "removed": true } }}', + }, + }, + ], + }, + }, + action: 'Remove user from group', + }, + { + name: 'Update', + value: 'update', + description: 'Update a user', + routing: { + request: { + method: 'PATCH', + url: '=/users/{{ $parameter["user"] }}', + ignoreHttpStatusErrors: true, + }, + output: { + postReceive: [ + handleErrorPostReceive, + { + type: 'set', + properties: { + value: '={{ { "updated": true } }}', + }, + }, + ], + }, + }, + action: 'Update user', + }, + ], + default: 'getAll', + }, +]; + +const addGroupFields: INodeProperties[] = [ + { + displayName: 'Group', + name: 'group', + default: { + mode: 'list', + value: '', + }, + displayOptions: { + show: { + resource: ['user'], + operation: ['addGroup'], + }, + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'getGroups', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + placeholder: 'e.g. 02bd9fd6-8f93-4758-87c3-1fb73740a315', + type: 'string', + }, + ], + required: true, + type: 'resourceLocator', + }, + { + displayName: 'User to Add', + name: 'user', + default: { + mode: 'list', + value: '', + }, + displayOptions: { + show: { + resource: ['user'], + operation: ['addGroup'], + }, + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'getUsers', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + placeholder: 'e.g. 02bd9fd6-8f93-4758-87c3-1fb73740a315', + type: 'string', + }, + ], + required: true, + routing: { + send: { + property: '@odata.id', + propertyInDotNotation: false, + type: 'body', + value: '=https://graph.microsoft.com/v1.0/directoryObjects/{{ $value }}', + }, + }, + type: 'resourceLocator', + }, +]; + +const createFields: INodeProperties[] = [ + { + displayName: 'Account Enabled', + name: 'accountEnabled', + default: true, + description: 'Whether the account is enabled', + displayOptions: { + show: { + resource: ['user'], + operation: ['create'], + }, + }, + required: true, + routing: { + send: { + property: 'accountEnabled', + type: 'body', + }, + }, + type: 'boolean', + validateType: 'boolean', + }, + { + displayName: 'Display Name', + name: 'displayName', + default: '', + description: 'The name to display in the address book for the user', + displayOptions: { + show: { + resource: ['user'], + operation: ['create'], + }, + }, + placeholder: 'e.g. Nathan Smith', + required: true, + routing: { + send: { + property: 'displayName', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'User Principal Name', + name: 'userPrincipalName', + default: '', + description: 'The user principal name (UPN)', + displayOptions: { + show: { + resource: ['user'], + operation: ['create'], + }, + }, + placeholder: 'e.g. NathanSmith@contoso.com', + required: true, + routing: { + send: { + property: 'userPrincipalName', + type: 'body', + preSend: [ + async function ( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, + ): Promise { + const userPrincipalName = this.getNodeParameter('userPrincipalName') as string; + if (!/^[A-Za-z0-9'._\-!#^~@]+$/.test(userPrincipalName)) { + throw new NodeOperationError( + this.getNode(), + "Only the following characters are allowed for 'User Principal Name': A-Z, a-z, 0-9, ' . - _ ! # ^ ~", + ); + } + return requestOptions; + }, + ], + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Mail Nickname', + name: 'mailNickname', + default: '', + description: 'The mail alias for the user', + displayOptions: { + show: { + resource: ['user'], + operation: ['create'], + }, + }, + placeholder: 'e.g. NathanSmith', + required: true, + routing: { + send: { + property: 'mailNickname', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Password', + name: 'password', + default: '', + description: 'The password for the user', + displayOptions: { + show: { + resource: ['user'], + operation: ['create'], + }, + }, + required: true, + routing: { + send: { + property: 'passwordProfile.password', + type: 'body', + }, + }, + type: 'string', + typeOptions: { + password: true, + }, + validateType: 'string', + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + default: {}, + displayOptions: { + show: { + resource: ['user'], + operation: ['create'], + }, + }, + options: [ + { + displayName: 'About Me', + name: 'aboutMe', + default: '', + description: 'A freeform text entry field for the user to describe themselves', + type: 'string', + validateType: 'string', + }, + { + displayName: 'Age Group', + name: 'ageGroup', + default: 'Adult', + description: 'Sets the age group of the user', + options: [ + { + name: 'Adult', + value: 'Adult', + }, + { + name: 'Minor', + value: 'Minor', + }, + { + name: 'Not Adult', + value: 'NotAdult', + }, + ], + type: 'options', + validateType: 'options', + }, + { + displayName: 'Birthday', + name: 'birthday', + default: '', + description: 'The birthday of the user', + type: 'dateTime', + validateType: 'dateTime', + }, + { + displayName: 'Business Phone', + name: 'businessPhones', + default: '', + description: 'The telephone number for the user', + type: 'string', + validateType: 'string', + }, + { + displayName: 'City', + name: 'city', + default: '', + description: 'The city in which the user is located', + type: 'string', + validateType: 'string', + }, + { + displayName: 'Company Name', + name: 'companyName', + default: '', + description: 'The name of the company associated with the user', + routing: { + send: { + preSend: [ + async function ( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, + ): Promise { + const companyName = this.getNodeParameter('additionalFields.companyName') as string; + if (companyName?.length > 64) { + throw new NodeOperationError( + this.getNode(), + "'Company Name' should have a maximum length of 64", + ); + } + return requestOptions; + }, + ], + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Consent Provided', + name: 'consentProvidedForMinor', + default: 'Denied', + description: 'Specifies if consent is provided for minors', + options: [ + { + name: 'Denied', + value: 'Denied', + }, + { + name: 'Granted', + value: 'Granted', + }, + { + name: 'Not Required', + value: 'NotRequired', + }, + ], + type: 'options', + validateType: 'options', + }, + { + displayName: 'Country', + name: 'country', + default: '', + description: 'The country/region of the user', + placeholder: 'e.g. US', + type: 'string', + validateType: 'string', + }, + { + displayName: 'Department', + name: 'department', + default: '', + description: 'The department name where the user works', + type: 'string', + validateType: 'string', + }, + { + displayName: 'Employee ID', + name: 'employeeId', + default: '', + description: 'Employee identifier assigned by the organization', + routing: { + send: { + preSend: [ + async function ( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, + ): Promise { + const employeeId = this.getNodeParameter('additionalFields.employeeId') as string; + if (employeeId?.length > 16) { + throw new NodeOperationError( + this.getNode(), + "'Employee ID' should have a maximum length of 16", + ); + } + return requestOptions; + }, + ], + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Employee Type', + name: 'employeeType', + default: '', + description: 'Defines enterprise worker type', + placeholder: 'e.g. Contractor', + type: 'string', + validateType: 'string', + }, + { + displayName: 'Employee Hire Date', + name: 'employeeHireDate', + default: '', + description: 'The hire date of the user', + placeholder: 'e.g. 2014-01-01T00:00:00Z', + type: 'dateTime', + validateType: 'dateTime', + }, + { + displayName: 'Employee Leave Date', + name: 'employeeLeaveDateTime', + default: '', + description: 'The date and time when the user left or will leave the organization', + placeholder: 'e.g. 2014-01-01T00:00:00Z', + type: 'dateTime', + validateType: 'dateTime', + }, + { + displayName: 'Employee Organization Data', + name: 'employeeOrgData', + default: {}, + description: + 'Represents organization data (for example, division and costCenter) associated with a user', + options: [ + { + displayName: 'Employee Organization Data', + name: 'employeeOrgValues', + values: [ + { + displayName: 'Cost Center', + name: 'costCenter', + description: 'The cost center associated with the user', + type: 'string', + default: '', + }, + { + displayName: 'Division', + name: 'division', + description: 'The name of the division in which the user works', + type: 'string', + default: '', + }, + ], + }, + ], + type: 'fixedCollection', + validateType: 'string', + }, + { + displayName: 'First Name', + name: 'givenName', + default: '', + description: 'The given name (first name) of the user', + type: 'string', + validateType: 'string', + }, + { + displayName: 'Force Change Password', + name: 'forceChangePassword', + default: 'forceChangePasswordNextSignIn', + description: 'Whether the user must change their password on the next sign-in', + options: [ + { + name: 'Next Sign In', + value: 'forceChangePasswordNextSignIn', + }, + { + name: 'Next Sign In with MFA', + value: 'forceChangePasswordNextSignInWithMfa', + }, + ], + type: 'options', + validateType: 'options', + }, + { + displayName: 'Interests', + name: 'interests', + default: [], + description: 'A list for the user to describe their interests', + type: 'string', + typeOptions: { + multipleValues: true, + }, + validateType: 'array', + }, + { + displayName: 'Job Title', + name: 'jobTitle', + default: '', + description: "The user's job title", + type: 'string', + validateType: 'string', + }, + { + displayName: 'Last Name', + name: 'surname', + default: '', + description: "The user's last name (family name)", + type: 'string', + validateType: 'string', + }, + { + displayName: 'Mail', + name: 'mail', + default: '', + description: 'The SMTP address for the user', + placeholder: 'e.g. jeff@contoso.com', + type: 'string', + validateType: 'string', + }, + { + displayName: 'Mobile Phone', + name: 'mobilePhone', + default: '', + description: 'The primary cellular telephone number for the user', + type: 'string', + validateType: 'string', + }, + { + displayName: 'My Site', + name: 'mySite', + default: '', + description: "The URL for the user's personal site", + type: 'string', + validateType: 'string', + }, + { + displayName: 'Office Location', + name: 'officeLocation', + default: '', + description: 'The office location for the user', + type: 'string', + validateType: 'string', + }, + { + displayName: 'On Premises Immutable ID', + name: 'onPremisesImmutableId', + default: '', + description: + 'This property is used to associate an on-premises Active Directory user account to their Microsoft Entra user object', + type: 'string', + validateType: 'string', + }, + { + displayName: 'Other Emails', + name: 'otherMails', + default: [], + description: 'Additional email addresses for the user', + type: 'string', + typeOptions: { + multipleValues: true, + }, + validateType: 'array', + }, + { + displayName: 'Password Policies', + name: 'passwordPolicies', + default: [], + description: 'Specifies password policies', + options: [ + { + name: 'Disable Password Expiration', + value: 'DisablePasswordExpiration', + }, + { + name: 'Disable Strong Password', + value: 'DisableStrongPassword', + }, + ], + type: 'multiOptions', + }, + { + displayName: 'Past Projects', + name: 'pastProjects', + default: [], + description: 'A list of past projects the user has worked on', + type: 'string', + typeOptions: { + multipleValues: true, + }, + validateType: 'array', + }, + { + displayName: 'Postal Code', + name: 'postalCode', + default: '', + description: "The postal code for the user's address", + type: 'string', + validateType: 'string', + }, + { + displayName: 'Preferred Language', + name: 'preferredLanguage', + default: '', + description: "User's preferred language in ISO 639-1 code", + placeholder: 'e.g. en-US', + type: 'string', + validateType: 'string', + }, + { + displayName: 'Responsibilities', + name: 'responsibilities', + default: [], + description: 'A list of responsibilities the user has', + type: 'string', + typeOptions: { + multipleValues: true, + }, + validateType: 'array', + }, + { + displayName: 'Schools Attended', + name: 'schools', + default: [], + description: 'A list of schools the user attended', + type: 'string', + typeOptions: { + multipleValues: true, + }, + validateType: 'array', + }, + { + displayName: 'Skills', + name: 'skills', + default: [], + description: 'A list of skills the user possesses', + type: 'string', + typeOptions: { + multipleValues: true, + }, + validateType: 'array', + }, + { + displayName: 'State', + name: 'state', + default: '', + description: "The state or province of the user's address", + type: 'string', + validateType: 'string', + }, + { + displayName: 'Street Address', + name: 'streetAddress', + default: '', + description: "The street address of the user's place of business", + type: 'string', + validateType: 'string', + }, + { + displayName: 'Usage Location', + name: 'usageLocation', + default: '', + description: 'Two-letter country code where the user is located', + placeholder: 'e.g. US', + type: 'string', + validateType: 'string', + }, + { + displayName: 'User Type', + name: 'userType', + default: 'Guest', + description: 'Classifies the user type', + options: [ + { + name: 'Guest', + value: 'Guest', + }, + { + name: 'Member', + value: 'Member', + }, + ], + type: 'options', + validateType: 'options', + }, + ], + placeholder: 'Add Field', + routing: { + output: { + postReceive: [ + async function ( + this: IExecuteSingleFunctions, + items: INodeExecutionData[], + _response: IN8nHttpFullResponse, + ): Promise { + for (const item of items) { + const userId = item.json.id as string; + const fields = this.getNodeParameter('additionalFields', item.index) as IDataObject; + if (Object.keys(fields).length) { + const body: IDataObject = { + ...fields, + }; + if (body.birthday) { + body.birthday = (body.birthday as DateTime).toUTC().toISO(); + } + if (body.businessPhones) { + body.businessPhones = [body.businessPhones as string]; + } + if (body.employeeHireDate) { + body.employeeHireDate = (body.employeeHireDate as DateTime).toUTC().toISO(); + } + if (body.employeeLeaveDateTime) { + body.employeeLeaveDateTime = (body.employeeLeaveDateTime as DateTime) + .toUTC() + .toISO(); + } + if (body.employeeOrgData) { + body.employeeOrgData = (body.employeeOrgData as IDataObject).employeeOrgValues; + } + if (body.passwordPolicies) { + body.passwordPolicies = (body.passwordPolicies as string[]).join(','); + } + // forceChangePasswordNextSignInWithMfa doesn't seem to take effect when providing it in the initial create request, + // so we add it in the update request + if (body.forceChangePassword) { + if (body.forceChangePassword === 'forceChangePasswordNextSignIn') { + body.passwordProfile ??= {}; + (body.passwordProfile as IDataObject).forceChangePasswordNextSignIn = true; + } else if (body.forceChangePassword === 'forceChangePasswordNextSignInWithMfa') { + body.passwordProfile ??= {}; + (body.passwordProfile as IDataObject).forceChangePasswordNextSignInWithMfa = + true; + } + delete body.forceChangePassword; + } + + // To update the following properties, you must specify them in their own PATCH request, without including the other properties + const separateProperties = [ + 'aboutMe', + 'birthday', + 'interests', + 'mySite', + 'pastProjects', + 'responsibilities', + 'schools', + 'skills', + ]; + const separateBody: IDataObject = {}; + for (const [key, value] of Object.entries(body)) { + if (separateProperties.includes(key)) { + separateBody[key] = value; + delete body[key]; + } + } + + try { + if (Object.keys(separateBody).length) { + await microsoftApiRequest.call(this, 'PATCH', `/users/${userId}`, separateBody); + merge(item.json, separateBody); + } + if (Object.keys(body).length) { + await microsoftApiRequest.call(this, 'PATCH', `/users/${userId}`, body); + merge(item.json, body); + } + } catch (error) { + try { + await microsoftApiRequest.call(this, 'DELETE', `/users/${userId}`); + } catch {} + throw error; + } + } + } + return items; + }, + ], + }, + }, + type: 'collection', + }, +]; + +const deleteFields: INodeProperties[] = [ + { + displayName: 'User to Delete', + name: 'user', + default: { + mode: 'list', + value: '', + }, + displayOptions: { + show: { + resource: ['user'], + operation: ['delete'], + }, + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'getUsers', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + placeholder: 'e.g. 02bd9fd6-8f93-4758-87c3-1fb73740a315', + type: 'string', + }, + ], + required: true, + type: 'resourceLocator', + }, +]; + +const getFields: INodeProperties[] = [ + { + displayName: 'User to Get', + name: 'user', + default: { + mode: 'list', + value: '', + }, + displayOptions: { + show: { + resource: ['user'], + operation: ['get'], + }, + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'getUsers', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + placeholder: 'e.g. 02bd9fd6-8f93-4758-87c3-1fb73740a315', + type: 'string', + }, + ], + required: true, + type: 'resourceLocator', + }, + { + displayName: 'Output', + name: 'output', + default: 'simple', + displayOptions: { + show: { + resource: ['user'], + operation: ['get'], + }, + }, + options: [ + { + name: 'Simplified', + value: 'simple', + routing: { + send: { + property: '$select', + type: 'query', + value: + 'id,createdDateTime,displayName,userPrincipalName,mail,mailNickname,securityIdentifier', + }, + }, + }, + { + name: 'Raw', + value: 'raw', + routing: { + send: { + property: '$select', + type: 'query', + value: + 'id,accountEnabled,ageGroup,assignedLicenses,assignedPlans,authorizationInfo,businessPhones,city,companyName,consentProvidedForMinor,country,createdDateTime,creationType,customSecurityAttributes,deletedDateTime,department,displayName,employeeHireDate,employeeId,employeeLeaveDateTime,employeeOrgData,employeeType,externalUserState,externalUserStateChangeDateTime,faxNumber,givenName,identities,imAddresses,isManagementRestricted,isResourceAccount,jobTitle,lastPasswordChangeDateTime,legalAgeGroupClassification,licenseAssignmentStates,mail,mailNickname,mobilePhone,officeLocation,onPremisesDistinguishedName,onPremisesDomainName,onPremisesExtensionAttributes,onPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesProvisioningErrors,onPremisesSamAccountName,onPremisesSecurityIdentifier,onPremisesSyncEnabled,onPremisesUserPrincipalName,otherMails,passwordPolicies,passwordProfile,postalCode,preferredDataLocation,preferredLanguage,provisionedPlans,proxyAddresses,securityIdentifier,serviceProvisioningErrors,showInAddressList,signInSessionsValidFromDateTime,state,streetAddress,surname,usageLocation,userPrincipalName,userType', + }, + }, + }, + { + name: 'Selected Fields', + value: 'fields', + }, + ], + type: 'options', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options + displayName: 'Fields', + name: 'fields', + default: [], + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options + description: 'The fields to add to the output', + displayOptions: { + show: { + resource: ['user'], + operation: ['get'], + output: ['fields'], + }, + }, + routing: { + send: { + property: '$select', + type: 'query', + value: '={{ $value.concat("id").join(",") }}', + }, + }, + typeOptions: { + loadOptionsMethod: 'getUserProperties', + }, + type: 'multiOptions', + }, +]; + +const getAllFields: INodeProperties[] = [ + { + displayName: 'Return All', + name: 'returnAll', + default: false, + description: 'Whether to return all results or only up to a given limit', + displayOptions: { + show: { + resource: ['user'], + operation: ['getAll'], + }, + }, + routing: { + send: { + paginate: '={{ $value }}', + }, + operations: { + pagination: { + type: 'generic', + properties: { + continue: '={{ !!$response.body?.["@odata.nextLink"] }}', + request: { + url: '={{ $response.body?.["@odata.nextLink"] ?? $request.url }}', + qs: { + $filter: + '={{ !!$response.body?.["@odata.nextLink"] ? undefined : $request.qs?.$filter }}', + $select: + '={{ !!$response.body?.["@odata.nextLink"] ? undefined : $request.qs?.$select }}', + }, + }, + }, + }, + }, + }, + type: 'boolean', + }, + { + displayName: 'Limit', + name: 'limit', + default: 50, + description: 'Max number of results to return', + displayOptions: { + show: { + resource: ['user'], + operation: ['getAll'], + returnAll: [false], + }, + }, + routing: { + send: { + property: '$top', + type: 'query', + value: '={{ $value }}', + }, + }, + type: 'number', + typeOptions: { + minValue: 1, + }, + validateType: 'number', + }, + { + displayName: 'Filter', + name: 'filter', + default: '', + description: + 'Query parameter to filter results by', + displayOptions: { + show: { + resource: ['user'], + operation: ['getAll'], + }, + }, + placeholder: "e.g. startswith(displayName, 'a')", + routing: { + send: { + property: '$filter', + type: 'query', + value: '={{ $value ? $value : undefined }}', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Output', + name: 'output', + default: 'simple', + displayOptions: { + show: { + resource: ['user'], + operation: ['getAll'], + }, + }, + options: [ + { + name: 'Simplified', + value: 'simple', + routing: { + send: { + property: '$select', + type: 'query', + value: + 'id,createdDateTime,displayName,userPrincipalName,mail,mailNickname,securityIdentifier', + }, + }, + }, + { + name: 'Raw', + value: 'raw', + routing: { + send: { + property: '$select', + type: 'query', + value: + 'id,accountEnabled,ageGroup,assignedLicenses,assignedPlans,authorizationInfo,businessPhones,city,companyName,consentProvidedForMinor,country,createdDateTime,creationType,customSecurityAttributes,deletedDateTime,department,displayName,employeeHireDate,employeeId,employeeLeaveDateTime,employeeOrgData,employeeType,externalUserState,externalUserStateChangeDateTime,faxNumber,givenName,identities,imAddresses,isManagementRestricted,isResourceAccount,jobTitle,lastPasswordChangeDateTime,legalAgeGroupClassification,licenseAssignmentStates,mail,mailNickname,mobilePhone,officeLocation,onPremisesDistinguishedName,onPremisesDomainName,onPremisesExtensionAttributes,onPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesProvisioningErrors,onPremisesSamAccountName,onPremisesSecurityIdentifier,onPremisesSyncEnabled,onPremisesUserPrincipalName,otherMails,passwordPolicies,passwordProfile,postalCode,preferredDataLocation,preferredLanguage,provisionedPlans,proxyAddresses,securityIdentifier,serviceProvisioningErrors,showInAddressList,signInSessionsValidFromDateTime,state,streetAddress,surname,usageLocation,userPrincipalName,userType', + }, + }, + }, + { + name: 'Selected Fields', + value: 'fields', + }, + ], + type: 'options', + }, + { + // eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-multi-options + displayName: 'Fields', + name: 'fields', + default: [], + // eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-multi-options + description: 'The fields to add to the output', + displayOptions: { + show: { + resource: ['user'], + operation: ['getAll'], + output: ['fields'], + }, + }, + routing: { + send: { + property: '$select', + type: 'query', + value: '={{ $value.concat("id").join(",") }}', + }, + }, + typeOptions: { + loadOptionsMethod: 'getUserPropertiesGetAll', + }, + type: 'multiOptions', + }, +]; + +const removeGroupFields: INodeProperties[] = [ + { + displayName: 'Group', + name: 'group', + default: { + mode: 'list', + value: '', + }, + displayOptions: { + show: { + resource: ['user'], + operation: ['removeGroup'], + }, + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'getGroups', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + placeholder: 'e.g. 02bd9fd6-8f93-4758-87c3-1fb73740a315', + type: 'string', + }, + ], + required: true, + type: 'resourceLocator', + }, + { + displayName: 'User to Remove', + name: 'user', + default: { + mode: 'list', + value: '', + }, + displayOptions: { + show: { + resource: ['user'], + operation: ['removeGroup'], + }, + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'getUsers', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + placeholder: 'e.g. 02bd9fd6-8f93-4758-87c3-1fb73740a315', + type: 'string', + }, + ], + required: true, + type: 'resourceLocator', + }, +]; + +const updateFields: INodeProperties[] = [ + { + displayName: 'User to Update', + name: 'user', + default: { + mode: 'list', + value: '', + }, + displayOptions: { + show: { + resource: ['user'], + operation: ['update'], + }, + }, + modes: [ + { + displayName: 'From List', + name: 'list', + type: 'list', + typeOptions: { + searchListMethod: 'getUsers', + searchable: true, + }, + }, + { + displayName: 'By ID', + name: 'id', + placeholder: 'e.g. 02bd9fd6-8f93-4758-87c3-1fb73740a315', + type: 'string', + }, + ], + required: true, + type: 'resourceLocator', + }, + { + displayName: 'Update Fields', + name: 'updateFields', + default: {}, + displayOptions: { + show: { + resource: ['user'], + operation: ['update'], + }, + }, + options: [ + { + displayName: 'About Me', + name: 'aboutMe', + default: '', + description: 'A freeform text entry field for the user to describe themselves', + type: 'string', + validateType: 'string', + }, + { + displayName: 'Account Enabled', + name: 'accountEnabled', + default: true, + description: 'Whether the account is enabled', + routing: { + send: { + property: 'accountEnabled', + type: 'body', + }, + }, + type: 'boolean', + validateType: 'boolean', + }, + { + displayName: 'Age Group', + name: 'ageGroup', + default: 'Adult', + description: 'Sets the age group of the user', + options: [ + { + name: 'Adult', + value: 'Adult', + }, + { + name: 'Minor', + value: 'Minor', + }, + { + name: 'Not Adult', + value: 'NotAdult', + }, + ], + routing: { + send: { + property: 'ageGroup', + type: 'body', + }, + }, + type: 'options', + validateType: 'options', + }, + { + displayName: 'Birthday', + name: 'birthday', + default: '', + description: 'The birthday of the user', + type: 'dateTime', + validateType: 'dateTime', + }, + { + displayName: 'Business Phone', + name: 'businessPhones', + default: '', + description: 'The telephone number for the user', + routing: { + send: { + property: 'businessPhones', + type: 'body', + value: '={{ $value ? [$value] : [] }}', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'City', + name: 'city', + default: '', + description: 'The city in which the user is located', + routing: { + send: { + property: 'city', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Company Name', + name: 'companyName', + default: '', + description: 'The name of the company associated with the user', + routing: { + send: { + property: 'companyName', + type: 'body', + preSend: [ + async function ( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, + ): Promise { + const companyName = this.getNodeParameter('updateFields.companyName') as string; + if (companyName?.length > 64) { + throw new NodeOperationError( + this.getNode(), + "'Company Name' should have a maximum length of 64", + ); + } + return requestOptions; + }, + ], + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Consent Provided', + name: 'consentProvidedForMinor', + default: 'Denied', + description: 'Specifies if consent is provided for minors', + options: [ + { + name: 'Denied', + value: 'Denied', + }, + { + name: 'Granted', + value: 'Granted', + }, + { + name: 'Not Required', + value: 'NotRequired', + }, + ], + routing: { + send: { + property: 'consentProvidedForMinor', + type: 'body', + }, + }, + type: 'options', + validateType: 'options', + }, + { + displayName: 'Country', + name: 'country', + default: '', + description: 'The country/region of the user', + placeholder: 'e.g. US', + routing: { + send: { + property: 'country', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Department', + name: 'department', + default: '', + description: 'The department name where the user works', + routing: { + send: { + property: 'department', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Display Name', + name: 'displayName', + default: '', + description: 'The name to display in the address book for the user', + routing: { + send: { + property: 'displayName', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + + { + displayName: 'Employee ID', + name: 'employeeId', + default: '', + description: 'Employee identifier assigned by the organization', + routing: { + send: { + property: 'employeeId', + type: 'body', + preSend: [ + async function ( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, + ): Promise { + const employeeId = this.getNodeParameter('updateFields.employeeId') as string; + if (employeeId?.length > 16) { + throw new NodeOperationError( + this.getNode(), + "'Employee ID' should have a maximum length of 16", + ); + } + return requestOptions; + }, + ], + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Employee Type', + name: 'employeeType', + default: '', + description: 'Defines enterprise worker type', + placeholder: 'e.g. Contractor', + routing: { + send: { + property: 'employeeType', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'First Name', + name: 'givenName', + default: '', + description: 'The given name (first name) of the user', + routing: { + send: { + property: 'givenName', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Employee Hire Date', + name: 'employeeHireDate', + default: '', + description: 'The hire date of the user', + placeholder: 'e.g. 2014-01-01T00:00:00Z', + routing: { + send: { + property: 'employeeHireDate', + type: 'body', + value: '={{ $value?.toUTC().toISO() }}', + }, + }, + type: 'dateTime', + validateType: 'dateTime', + }, + { + displayName: 'Employee Leave Date', + name: 'employeeLeaveDateTime', + default: '', + description: 'The date and time when the user left or will leave the organization', + placeholder: 'e.g. 2014-01-01T00:00:00Z', + routing: { + send: { + property: 'employeeLeaveDateTime', + type: 'body', + value: '={{ $value?.toUTC().toISO() }}', + }, + }, + type: 'dateTime', + validateType: 'dateTime', + }, + { + displayName: 'Employee Organization Data', + name: 'employeeOrgData', + default: {}, + description: + 'Represents organization data (for example, division and costCenter) associated with a user', + options: [ + { + displayName: 'Employee Organization Data', + name: 'employeeOrgValues', + values: [ + { + displayName: 'Cost Center', + name: 'costCenter', + description: 'The cost center associated with the user', + routing: { + send: { + property: 'employeeOrgData.costCenter', + type: 'body', + }, + }, + type: 'string', + default: '', + }, + { + displayName: 'Division', + name: 'division', + description: 'The name of the division in which the user works', + routing: { + send: { + property: 'employeeOrgData.division', + type: 'body', + }, + }, + type: 'string', + default: '', + }, + ], + }, + ], + type: 'fixedCollection', + validateType: 'string', + }, + { + displayName: 'Force Change Password', + name: 'forceChangePassword', + default: 'forceChangePasswordNextSignIn', + description: 'Whether the user must change their password on the next sign-in', + options: [ + { + name: 'Next Sign In', + value: 'forceChangePasswordNextSignIn', + }, + { + name: 'Next Sign In with MFA', + value: 'forceChangePasswordNextSignInWithMfa', + }, + ], + routing: { + send: { + preSend: [ + async function ( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, + ): Promise { + const forceChangePassword = this.getNodeParameter( + 'updateFields.forceChangePassword', + ) as string; + if (forceChangePassword === 'forceChangePasswordNextSignIn') { + (requestOptions.body as IDataObject).passwordProfile ??= {}; + ( + (requestOptions.body as IDataObject).passwordProfile as IDataObject + ).forceChangePasswordNextSignIn = true; + } else if (forceChangePassword === 'forceChangePasswordNextSignInWithMfa') { + ( + (requestOptions.body as IDataObject).passwordProfile as IDataObject + ).forceChangePasswordNextSignInWithMfa = true; + } + return requestOptions; + }, + ], + }, + }, + type: 'options', + validateType: 'options', + }, + { + displayName: 'Interests', + name: 'interests', + default: [], + description: 'A list for the user to describe their interests', + type: 'string', + typeOptions: { + multipleValues: true, + }, + validateType: 'array', + }, + { + displayName: 'Job Title', + name: 'jobTitle', + default: '', + description: "The user's job title", + routing: { + send: { + property: 'jobTitle', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Last Name', + name: 'surname', + default: '', + description: "The user's last name (family name)", + routing: { + send: { + property: 'surname', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Mail', + name: 'mail', + default: '', + description: 'The SMTP address for the user', + placeholder: 'e.g. jeff@contoso.com', + routing: { + send: { + property: 'mail', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Mail Nickname', + name: 'mailNickname', + default: '', + description: 'The mail alias for the user', + routing: { + send: { + property: 'mailNickname', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Mobile Phone', + name: 'mobilePhone', + default: '', + description: 'The primary cellular telephone number for the user', + routing: { + send: { + property: 'mobilePhone', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'My Site', + name: 'mySite', + default: '', + description: "The URL for the user's personal site", + type: 'string', + validateType: 'string', + }, + { + displayName: 'Office Location', + name: 'officeLocation', + default: '', + description: 'The office location for the user', + routing: { + send: { + property: 'officeLocation', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'On Premises Immutable ID', + name: 'onPremisesImmutableId', + default: '', + description: + 'This property is used to associate an on-premises Active Directory user account to their Microsoft Entra user object', + routing: { + send: { + property: 'onPremisesImmutableId', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Other Emails', + name: 'otherMails', + default: [], + description: 'Additional email addresses for the user', + routing: { + send: { + property: 'otherMails', + type: 'body', + }, + }, + type: 'string', + typeOptions: { + multipleValues: true, + }, + validateType: 'array', + }, + { + displayName: 'Password', + name: 'password', + default: '', + description: + 'The password for the user. The password must satisfy minimum requirements as specified by the passwordPolicies property.', + routing: { + send: { + property: 'passwordProfile.password', + type: 'body', + }, + }, + type: 'string', + typeOptions: { + password: true, + }, + validateType: 'string', + }, + { + displayName: 'Password Policies', + name: 'passwordPolicies', + default: [], + description: 'Specifies password policies', + options: [ + { + name: 'Disable Password Expiration', + value: 'DisablePasswordExpiration', + }, + { + name: 'Disable Strong Password', + value: 'DisableStrongPassword', + }, + ], + routing: { + send: { + property: 'passwordPolicies', + type: 'body', + value: '={{ $value?.join(",") }}', + }, + }, + type: 'multiOptions', + }, + { + displayName: 'Past Projects', + name: 'pastProjects', + default: [], + description: 'A list of past projects the user has worked on', + type: 'string', + typeOptions: { + multipleValues: true, + }, + validateType: 'array', + }, + { + displayName: 'Postal Code', + name: 'postalCode', + default: '', + description: "The postal code for the user's address", + routing: { + send: { + property: 'postalCode', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Preferred Language', + name: 'preferredLanguage', + default: '', + description: "User's preferred language in ISO 639-1 code", + placeholder: 'e.g. en-US', + routing: { + send: { + property: 'preferredLanguage', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Responsibilities', + name: 'responsibilities', + default: [], + description: 'A list of responsibilities the user has', + type: 'string', + typeOptions: { + multipleValues: true, + }, + validateType: 'array', + }, + { + displayName: 'Schools Attended', + name: 'schools', + default: [], + description: 'A list of schools the user attended', + type: 'string', + typeOptions: { + multipleValues: true, + }, + validateType: 'array', + }, + { + displayName: 'Skills', + name: 'skills', + default: [], + description: 'A list of skills the user possesses', + type: 'string', + typeOptions: { + multipleValues: true, + }, + validateType: 'array', + }, + { + displayName: 'State', + name: 'state', + default: '', + description: "The state or province of the user's address", + routing: { + send: { + property: 'state', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Street Address', + name: 'streetAddress', + default: '', + description: "The street address of the user's place of business", + routing: { + send: { + property: 'streetAddress', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'Usage Location', + name: 'usageLocation', + default: '', + description: 'Two-letter country code where the user is located', + placeholder: 'e.g. US', + routing: { + send: { + property: 'usageLocation', + type: 'body', + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'User Principal Name', + name: 'userPrincipalName', + default: '', + description: 'The user principal name (UPN)', + placeholder: 'e.g. AdeleV@contoso.com', + routing: { + send: { + property: 'userPrincipalName', + type: 'body', + preSend: [ + async function ( + this: IExecuteSingleFunctions, + requestOptions: IHttpRequestOptions, + ): Promise { + const userPrincipalName = this.getNodeParameter( + 'updateFields.userPrincipalName', + ) as string; + if (!/^[A-Za-z0-9'._\-!#^~@]+$/.test(userPrincipalName)) { + throw new NodeOperationError( + this.getNode(), + "Only the following characters are allowed for 'User Principal Name': A-Z, a-z, 0-9, ' . - _ ! # ^ ~", + ); + } + return requestOptions; + }, + ], + }, + }, + type: 'string', + validateType: 'string', + }, + { + displayName: 'User Type', + name: 'userType', + default: 'Guest', + description: 'Classifies the user type', + options: [ + { + name: 'Guest', + value: 'Guest', + }, + { + name: 'Member', + value: 'Member', + }, + ], + routing: { + send: { + property: 'userType', + type: 'body', + }, + }, + type: 'options', + validateType: 'options', + }, + ], + placeholder: 'Add Field', + routing: { + output: { + postReceive: [ + async function ( + this: IExecuteSingleFunctions, + items: INodeExecutionData[], + _response: IN8nHttpFullResponse, + ): Promise { + for (const item of items) { + const userId = this.getNodeParameter('user.value', item.index) as string; + const fields = this.getNodeParameter('updateFields', item.index) as IDataObject; + // To update the following properties, you must specify them in their own PATCH request, without including the other properties + const separateProperties = [ + 'aboutMe', + 'birthday', + 'interests', + 'mySite', + 'pastProjects', + 'responsibilities', + 'schools', + 'skills', + ]; + const separateFields = Object.keys(fields) + .filter((key) => separateProperties.includes(key)) + .reduce((obj, key) => { + return { + ...obj, + [key]: fields[key], + }; + }, {}); + if (Object.keys(separateFields).length) { + const body: IDataObject = { + ...separateFields, + }; + if (body.birthday) { + body.birthday = (body.birthday as DateTime).toUTC().toISO(); + } + await microsoftApiRequest.call(this, 'PATCH', `/users/${userId}`, body); + } + } + return items; + }, + ], + }, + }, + type: 'collection', + }, +]; + +export const userFields: INodeProperties[] = [ + ...addGroupFields, + ...createFields, + ...deleteFields, + ...getFields, + ...getAllFields, + ...removeGroupFields, + ...updateFields, +]; diff --git a/packages/nodes-base/nodes/Microsoft/Entra/descriptions/index.ts b/packages/nodes-base/nodes/Microsoft/Entra/descriptions/index.ts new file mode 100644 index 0000000000000..ee8ab1abc00e9 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Entra/descriptions/index.ts @@ -0,0 +1,2 @@ +export * from './GroupDescription'; +export * from './UserDescription'; diff --git a/packages/nodes-base/nodes/Microsoft/Entra/microsoftEntra.dark.svg b/packages/nodes-base/nodes/Microsoft/Entra/microsoftEntra.dark.svg new file mode 100644 index 0000000000000..25b70ef180c36 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Entra/microsoftEntra.dark.svg @@ -0,0 +1,4 @@ + + + + diff --git a/packages/nodes-base/nodes/Microsoft/Entra/microsoftEntra.svg b/packages/nodes-base/nodes/Microsoft/Entra/microsoftEntra.svg new file mode 100644 index 0000000000000..de31695579b37 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Entra/microsoftEntra.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/packages/nodes-base/nodes/Microsoft/Entra/test/GroupDescription.test.ts b/packages/nodes-base/nodes/Microsoft/Entra/test/GroupDescription.test.ts new file mode 100644 index 0000000000000..632792ea966fd --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Entra/test/GroupDescription.test.ts @@ -0,0 +1,775 @@ +import { NodeConnectionType } from 'n8n-workflow'; +import nock from 'nock'; + +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; +import * as Helpers from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; + +import { microsoftEntraApiResponse, microsoftEntraNodeResponse } from './mocks'; + +describe('Gong Node', () => { + const baseUrl = 'https://graph.microsoft.com/v1.0'; + + beforeEach(() => { + // https://github.com/nock/nock/issues/2057#issuecomment-663665683 + if (!nock.isActive()) { + nock.activate(); + } + }); + + describe('Group description', () => { + const tests: WorkflowTestData[] = [ + { + description: 'should create group', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '416e4fc1-5055-4e61-854e-a6265256ac26', + name: "When clicking 'Test workflow'", + type: 'n8n-nodes-base.manualTrigger', + position: [820, 380], + typeVersion: 1, + }, + { + parameters: { + resource: 'group', + operation: 'create', + displayName: 'Group Display Name', + groupType: 'Unified', + mailEnabled: true, + mailNickname: 'MailNickname', + membershipType: 'DynamicMembership', + securityEnabled: true, + additionalFields: { + isAssignableToRole: true, + description: 'Group Description', + membershipRule: 'department -eq "Marketing"', + membershipRuleProcessingState: 'On', + preferredDataLocation: 'Preferred Data Location', + uniqueName: 'UniqueName', + visibility: 'Public', + }, + requestOptions: {}, + }, + type: 'n8n-nodes-base.microsoftEntra', + typeVersion: 1, + position: [220, 0], + id: '3429f7f2-dfca-4b72-8913-43a582e96e66', + name: 'Micosoft Entra ID', + credentials: { + microsoftEntraOAuth2Api: { + id: 'Hot2KwSMSoSmMVqd', + name: 'Microsoft Entra ID (Azure Active Directory) account', + }, + }, + }, + ], + connections: { + "When clicking 'Test workflow'": { + main: [ + [ + { + node: 'Micosoft Entra ID', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'Micosoft Entra ID': [microsoftEntraNodeResponse.createGroup], + }, + }, + nock: { + baseUrl, + mocks: [ + { + method: 'post', + path: '/groups', + statusCode: 201, + requestBody: { + displayName: 'Group Display Name', + mailNickname: 'MailNickname', + mailEnabled: true, + membershipRule: 'department -eq "Marketing"', + membershipRuleProcessingState: 'On', + securityEnabled: true, + groupTypes: ['Unified', 'DynamicMembership'], + }, + responseBody: microsoftEntraApiResponse.postGroup, + }, + { + method: 'patch', + path: `/groups/${microsoftEntraApiResponse.postGroup.id}`, + statusCode: 204, + requestBody: { + description: 'Group Description', + preferredDataLocation: 'Preferred Data Location', + uniqueName: 'UniqueName', + visibility: 'Public', + }, + responseBody: {}, + }, + ], + }, + }, + { + description: 'should delete group', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '416e4fc1-5055-4e61-854e-a6265256ac26', + name: "When clicking 'Test workflow'", + type: 'n8n-nodes-base.manualTrigger', + position: [820, 380], + typeVersion: 1, + }, + { + parameters: { + resource: 'group', + operation: 'delete', + group: { + __rl: true, + value: 'a8eb60e3-0145-4d7e-85ef-c6259784761b', + mode: 'id', + }, + options: {}, + requestOptions: {}, + }, + type: 'n8n-nodes-base.microsoftEntra', + typeVersion: 1, + position: [220, 0], + id: '3429f7f2-dfca-4b72-8913-43a582e96e66', + name: 'Micosoft Entra ID', + credentials: { + microsoftEntraOAuth2Api: { + id: 'Hot2KwSMSoSmMVqd', + name: 'Microsoft Entra ID (Azure Active Directory) account', + }, + }, + }, + ], + connections: { + "When clicking 'Test workflow'": { + main: [ + [ + { + node: 'Micosoft Entra ID', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'Micosoft Entra ID': [microsoftEntraNodeResponse.deleteGroup], + }, + }, + nock: { + baseUrl, + mocks: [ + { + method: 'delete', + path: '/groups/a8eb60e3-0145-4d7e-85ef-c6259784761b', + statusCode: 204, + responseBody: {}, + }, + ], + }, + }, + { + description: 'should get group', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '416e4fc1-5055-4e61-854e-a6265256ac26', + name: "When clicking 'Test workflow'", + type: 'n8n-nodes-base.manualTrigger', + position: [820, 380], + typeVersion: 1, + }, + { + parameters: { + resource: 'group', + operation: 'get', + group: { + __rl: true, + value: 'a8eb60e3-0145-4d7e-85ef-c6259784761b', + mode: 'id', + }, + output: 'raw', + requestOptions: {}, + }, + type: 'n8n-nodes-base.microsoftEntra', + typeVersion: 1, + position: [220, 0], + id: '3429f7f2-dfca-4b72-8913-43a582e96e66', + name: 'Micosoft Entra ID', + credentials: { + microsoftEntraOAuth2Api: { + id: 'Hot2KwSMSoSmMVqd', + name: 'Microsoft Entra ID (Azure Active Directory) account', + }, + }, + }, + ], + connections: { + "When clicking 'Test workflow'": { + main: [ + [ + { + node: 'Micosoft Entra ID', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'Micosoft Entra ID': [microsoftEntraNodeResponse.getGroup], + }, + }, + nock: { + baseUrl, + mocks: [ + { + method: 'get', + path: '/groups/a8eb60e3-0145-4d7e-85ef-c6259784761b', + statusCode: 200, + responseBody: microsoftEntraApiResponse.getGroup, + }, + ], + }, + }, + { + description: 'should get group with fields output and members', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '416e4fc1-5055-4e61-854e-a6265256ac26', + name: "When clicking 'Test workflow'", + type: 'n8n-nodes-base.manualTrigger', + position: [820, 380], + typeVersion: 1, + }, + { + parameters: { + resource: 'group', + operation: 'get', + group: { + __rl: true, + value: 'a8eb60e3-0145-4d7e-85ef-c6259784761b', + mode: 'id', + }, + output: 'fields', + fields: [ + 'assignedLabels', + 'assignedLicenses', + 'createdDateTime', + 'classification', + 'deletedDateTime', + 'description', + 'displayName', + 'expirationDateTime', + 'groupTypes', + 'visibility', + 'unseenCount', + 'theme', + 'uniqueName', + 'serviceProvisioningErrors', + 'securityIdentifier', + 'renewedDateTime', + 'securityEnabled', + 'autoSubscribeNewMembers', + 'allowExternalSenders', + 'licenseProcessingState', + 'isManagementRestricted', + 'isSubscribedByMail', + 'isAssignableToRole', + 'id', + 'hideFromOutlookClients', + 'hideFromAddressLists', + 'onPremisesProvisioningErrors', + 'onPremisesSecurityIdentifier', + 'onPremisesSamAccountName', + 'onPremisesNetBiosName', + 'onPremisesSyncEnabled', + 'preferredDataLocation', + 'preferredLanguage', + 'proxyAddresses', + 'onPremisesLastSyncDateTime', + 'onPremisesDomainName', + 'membershipRuleProcessingState', + 'membershipRule', + 'mailNickname', + 'mailEnabled', + 'mail', + ], + options: { + includeMembers: true, + }, + requestOptions: {}, + }, + type: 'n8n-nodes-base.microsoftEntra', + typeVersion: 1, + position: [220, 0], + id: '3429f7f2-dfca-4b72-8913-43a582e96e66', + name: 'Micosoft Entra ID', + credentials: { + microsoftEntraOAuth2Api: { + id: 'Hot2KwSMSoSmMVqd', + name: 'Microsoft Entra ID (Azure Active Directory) account', + }, + }, + }, + ], + connections: { + "When clicking 'Test workflow'": { + main: [ + [ + { + node: 'Micosoft Entra ID', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'Micosoft Entra ID': [microsoftEntraNodeResponse.getGroupWithProperties], + }, + }, + nock: { + baseUrl, + mocks: [ + { + method: 'get', + path: '/groups/a8eb60e3-0145-4d7e-85ef-c6259784761b?$select=assignedLabels,assignedLicenses,createdDateTime,classification,deletedDateTime,description,displayName,expirationDateTime,groupTypes,visibility,unseenCount,theme,uniqueName,serviceProvisioningErrors,securityIdentifier,renewedDateTime,securityEnabled,autoSubscribeNewMembers,allowExternalSenders,licenseProcessingState,isManagementRestricted,isSubscribedByMail,isAssignableToRole,id,hideFromOutlookClients,hideFromAddressLists,onPremisesProvisioningErrors,onPremisesSecurityIdentifier,onPremisesSamAccountName,onPremisesNetBiosName,onPremisesSyncEnabled,preferredDataLocation,preferredLanguage,proxyAddresses,onPremisesLastSyncDateTime,onPremisesDomainName,membershipRuleProcessingState,membershipRule,mailNickname,mailEnabled,mail,id&$expand=members($select=id,accountEnabled,createdDateTime,displayName,employeeId,mail,securityIdentifier,userPrincipalName,userType)', + statusCode: 200, + responseBody: microsoftEntraApiResponse.getGroupWithProperties, + }, + ], + }, + }, + { + description: 'should get all groups with simple output', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '416e4fc1-5055-4e61-854e-a6265256ac26', + name: "When clicking 'Test workflow'", + type: 'n8n-nodes-base.manualTrigger', + position: [820, 380], + typeVersion: 1, + }, + { + parameters: { + resource: 'group', + operation: 'getAll', + returnAll: true, + filter: '', + output: 'simple', + requestOptions: {}, + }, + type: 'n8n-nodes-base.microsoftEntra', + typeVersion: 1, + position: [220, 0], + id: '3429f7f2-dfca-4b72-8913-43a582e96e66', + name: 'Micosoft Entra ID', + credentials: { + microsoftEntraOAuth2Api: { + id: 'Hot2KwSMSoSmMVqd', + name: 'Microsoft Entra ID (Azure Active Directory) account', + }, + }, + }, + ], + connections: { + "When clicking 'Test workflow'": { + main: [ + [ + { + node: 'Micosoft Entra ID', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'Micosoft Entra ID': [new Array(102).fill(microsoftEntraNodeResponse.getGroup[0])], + }, + }, + nock: { + baseUrl, + mocks: [ + { + method: 'get', + path: '/groups?$select=id,createdDateTime,description,displayName,mail,mailEnabled,mailNickname,securityEnabled,securityIdentifier,visibility', + statusCode: 200, + responseBody: { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#groups', + '@odata.nextLink': + 'https://graph.microsoft.com/v1.0/groups?$select=id,createdDateTime,description,displayName,mail,mailEnabled,mailNickname,securityEnabled,securityIdentifier,visibility&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA', + value: new Array(100).fill(microsoftEntraApiResponse.getGroup), + }, + }, + { + method: 'get', + path: '/groups?$select=id,createdDateTime,description,displayName,mail,mailEnabled,mailNickname,securityEnabled,securityIdentifier,visibility&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA', + statusCode: 200, + responseBody: { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#groups', + value: new Array(2).fill(microsoftEntraApiResponse.getGroup), + }, + }, + ], + }, + }, + { + description: 'should get limit 10 groups with raw output', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '416e4fc1-5055-4e61-854e-a6265256ac26', + name: "When clicking 'Test workflow'", + type: 'n8n-nodes-base.manualTrigger', + position: [820, 380], + typeVersion: 1, + }, + { + parameters: { + resource: 'group', + operation: 'getAll', + limit: 10, + filter: '', + output: 'raw', + requestOptions: {}, + }, + type: 'n8n-nodes-base.microsoftEntra', + typeVersion: 1, + position: [220, 0], + id: '3429f7f2-dfca-4b72-8913-43a582e96e66', + name: 'Micosoft Entra ID', + credentials: { + microsoftEntraOAuth2Api: { + id: 'Hot2KwSMSoSmMVqd', + name: 'Microsoft Entra ID (Azure Active Directory) account', + }, + }, + }, + ], + connections: { + "When clicking 'Test workflow'": { + main: [ + [ + { + node: 'Micosoft Entra ID', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'Micosoft Entra ID': [new Array(10).fill(microsoftEntraNodeResponse.getGroup[0])], + }, + }, + nock: { + baseUrl, + mocks: [ + { + method: 'get', + path: '/groups?$top=10', + statusCode: 200, + responseBody: { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#groups', + '@odata.nextLink': + 'https://graph.microsoft.com/v1.0/groups?$top=10&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA', + value: new Array(10).fill(microsoftEntraApiResponse.getGroup), + }, + }, + ], + }, + }, + { + description: 'should get all groups with options and filter', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '416e4fc1-5055-4e61-854e-a6265256ac26', + name: "When clicking 'Test workflow'", + type: 'n8n-nodes-base.manualTrigger', + position: [820, 380], + typeVersion: 1, + }, + { + parameters: { + resource: 'group', + operation: 'getAll', + returnAll: true, + filter: "startswith(displayName,'group')", + output: 'fields', + fields: [ + 'assignedLabels', + 'assignedLicenses', + 'createdDateTime', + 'classification', + 'deletedDateTime', + 'description', + 'displayName', + 'expirationDateTime', + 'groupTypes', + 'visibility', + 'theme', + 'uniqueName', + 'serviceProvisioningErrors', + 'securityIdentifier', + 'renewedDateTime', + 'securityEnabled', + 'licenseProcessingState', + 'isManagementRestricted', + 'isAssignableToRole', + 'onPremisesProvisioningErrors', + 'onPremisesSecurityIdentifier', + 'onPremisesSamAccountName', + 'onPremisesNetBiosName', + 'onPremisesSyncEnabled', + 'preferredDataLocation', + 'preferredLanguage', + 'proxyAddresses', + 'onPremisesLastSyncDateTime', + 'onPremisesDomainName', + 'membershipRuleProcessingState', + 'membershipRule', + 'mailNickname', + 'mailEnabled', + 'mail', + ], + requestOptions: {}, + }, + type: 'n8n-nodes-base.microsoftEntra', + typeVersion: 1, + position: [220, 0], + id: '3429f7f2-dfca-4b72-8913-43a582e96e66', + name: 'Micosoft Entra ID', + credentials: { + microsoftEntraOAuth2Api: { + id: 'Hot2KwSMSoSmMVqd', + name: 'Microsoft Entra ID (Azure Active Directory) account', + }, + }, + }, + ], + connections: { + "When clicking 'Test workflow'": { + main: [ + [ + { + node: 'Micosoft Entra ID', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'Micosoft Entra ID': [ + new Array(102).fill(microsoftEntraNodeResponse.getGroupWithProperties[0]), + ], + }, + }, + nock: { + baseUrl, + mocks: [ + { + method: 'get', + path: "/groups?$filter=startswith(displayName,'group')&$select=assignedLabels,assignedLicenses,createdDateTime,classification,deletedDateTime,description,displayName,expirationDateTime,groupTypes,visibility,theme,uniqueName,serviceProvisioningErrors,securityIdentifier,renewedDateTime,securityEnabled,licenseProcessingState,isManagementRestricted,isAssignableToRole,onPremisesProvisioningErrors,onPremisesSecurityIdentifier,onPremisesSamAccountName,onPremisesNetBiosName,onPremisesSyncEnabled,preferredDataLocation,preferredLanguage,proxyAddresses,onPremisesLastSyncDateTime,onPremisesDomainName,membershipRuleProcessingState,membershipRule,mailNickname,mailEnabled,mail,id", + statusCode: 200, + responseBody: { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#groups', + '@odata.nextLink': + "https://graph.microsoft.com/v1.0/groups?$filter=startswith(displayName,'group')&$select=assignedLabels,assignedLicenses,createdDateTime,classification,deletedDateTime,description,displayName,expirationDateTime,groupTypes,visibility,theme,uniqueName,serviceProvisioningErrors,securityIdentifier,renewedDateTime,securityEnabled,licenseProcessingState,isManagementRestricted,isAssignableToRole,onPremisesProvisioningErrors,onPremisesSecurityIdentifier,onPremisesSamAccountName,onPremisesNetBiosName,onPremisesSyncEnabled,preferredDataLocation,preferredLanguage,proxyAddresses,onPremisesLastSyncDateTime,onPremisesDomainName,membershipRuleProcessingState,membershipRule,mailNickname,mailEnabled,mail,id&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA", + value: new Array(100).fill(microsoftEntraApiResponse.getGroupWithProperties), + }, + }, + { + method: 'get', + path: "/groups?$filter=startswith(displayName,'group')&$select=assignedLabels,assignedLicenses,createdDateTime,classification,deletedDateTime,description,displayName,expirationDateTime,groupTypes,visibility,theme,uniqueName,serviceProvisioningErrors,securityIdentifier,renewedDateTime,securityEnabled,licenseProcessingState,isManagementRestricted,isAssignableToRole,onPremisesProvisioningErrors,onPremisesSecurityIdentifier,onPremisesSamAccountName,onPremisesNetBiosName,onPremisesSyncEnabled,preferredDataLocation,preferredLanguage,proxyAddresses,onPremisesLastSyncDateTime,onPremisesDomainName,membershipRuleProcessingState,membershipRule,mailNickname,mailEnabled,mail,id&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA", + statusCode: 200, + responseBody: { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#groups', + value: new Array(2).fill(microsoftEntraApiResponse.getGroupWithProperties), + }, + }, + ], + }, + }, + { + description: 'should update group', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '416e4fc1-5055-4e61-854e-a6265256ac26', + name: "When clicking 'Test workflow'", + type: 'n8n-nodes-base.manualTrigger', + position: [820, 380], + typeVersion: 1, + }, + { + parameters: { + resource: 'group', + operation: 'update', + group: { + __rl: true, + value: 'a8eb60e3-0145-4d7e-85ef-c6259784761b', + mode: 'id', + }, + updateFields: { + allowExternalSenders: true, + autoSubscribeNewMembers: true, + description: 'Group Description', + displayName: 'Group Display Name', + mailNickname: 'MailNickname', + membershipRule: 'department -eq "Marketing"', + membershipRuleProcessingState: 'On', + preferredDataLocation: 'Preferred Data Location', + securityEnabled: true, + uniqueName: 'UniqueName', + visibility: 'Public', + }, + requestOptions: {}, + }, + type: 'n8n-nodes-base.microsoftEntra', + typeVersion: 1, + position: [220, 0], + id: '3429f7f2-dfca-4b72-8913-43a582e96e66', + name: 'Micosoft Entra ID', + credentials: { + microsoftEntraOAuth2Api: { + id: 'Hot2KwSMSoSmMVqd', + name: 'Microsoft Entra ID (Azure Active Directory) account', + }, + }, + }, + ], + connections: { + "When clicking 'Test workflow'": { + main: [ + [ + { + node: 'Micosoft Entra ID', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'Micosoft Entra ID': [microsoftEntraNodeResponse.updateGroup], + }, + }, + nock: { + baseUrl, + mocks: [ + { + method: 'patch', + path: `/groups/${microsoftEntraApiResponse.postGroup.id}`, + statusCode: 204, + requestBody: { + description: 'Group Description', + displayName: 'Group Display Name', + mailNickname: 'MailNickname', + membershipRule: 'department -eq "Marketing"', + membershipRuleProcessingState: 'On', + preferredDataLocation: 'Preferred Data Location', + securityEnabled: true, + uniqueName: 'UniqueName', + visibility: 'Public', + }, + responseBody: {}, + }, + { + method: 'patch', + path: `/groups/${microsoftEntraApiResponse.postGroup.id}`, + statusCode: 204, + requestBody: { + allowExternalSenders: true, + autoSubscribeNewMembers: true, + }, + responseBody: {}, + }, + ], + }, + }, + ]; + + const nodeTypes = Helpers.setup(tests); + + test.each(tests)('$description', async (testData) => { + const { result } = await executeWorkflow(testData, nodeTypes); + + const resultNodeData = Helpers.getResultNodeData(result, testData); + resultNodeData.forEach(({ nodeName, resultData }) => + expect(resultData).toEqual(testData.output.nodeData[nodeName]), + ); + expect(result.status).toEqual('success'); + }); + }); +}); diff --git a/packages/nodes-base/nodes/Microsoft/Entra/test/MicrosoftEntra.node.test.ts b/packages/nodes-base/nodes/Microsoft/Entra/test/MicrosoftEntra.node.test.ts new file mode 100644 index 0000000000000..5b15152624248 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Entra/test/MicrosoftEntra.node.test.ts @@ -0,0 +1,249 @@ +import type { + ICredentialDataDecryptedObject, + IDataObject, + IHttpRequestOptions, + ILoadOptionsFunctions, +} from 'n8n-workflow'; +import { NodeConnectionType } from 'n8n-workflow'; +import nock from 'nock'; + +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; +import * as Helpers from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; + +import { microsoftEntraApiResponse, microsoftEntraNodeResponse } from './mocks'; +import { FAKE_CREDENTIALS_DATA } from '../../../../test/nodes/FakeCredentialsMap'; +import { MicrosoftEntra } from '../MicrosoftEntra.node'; + +describe('Gong Node', () => { + const baseUrl = 'https://graph.microsoft.com/v1.0'; + + beforeEach(() => { + // https://github.com/nock/nock/issues/2057#issuecomment-663665683 + if (!nock.isActive()) { + nock.activate(); + } + }); + + describe('Credentials', () => { + const tests: WorkflowTestData[] = [ + { + description: 'should use correct credentials', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + type: 'n8n-nodes-base.manualTrigger', + typeVersion: 1, + position: [0, 0], + id: '1307e408-a8a5-464e-b858-494953e2f43b', + name: "When clicking 'Test workflow'", + }, + { + parameters: { + resource: 'group', + operation: 'get', + group: { + __rl: true, + value: 'a8eb60e3-0145-4d7e-85ef-c6259784761b', + mode: 'id', + }, + filter: '', + output: 'raw', + requestOptions: {}, + }, + type: 'n8n-nodes-base.microsoftEntra', + typeVersion: 1, + position: [220, 0], + id: '3429f7f2-dfca-4b72-8913-43a582e96e66', + name: 'Micosoft Entra ID', + credentials: { + microsoftEntraOAuth2Api: { + id: 'Hot2KwSMSoSmMVqd', + name: 'Microsoft Entra ID (Azure Active Directory) account', + }, + }, + }, + ], + connections: { + "When clicking 'Test workflow'": { + main: [ + [ + { + node: 'Micosoft Entra ID', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'Micosoft Entra ID': [microsoftEntraNodeResponse.getGroup], + }, + }, + }, + ]; + + beforeAll(() => { + nock.disableNetConnect(); + + jest + .spyOn(Helpers.CredentialsHelper.prototype, 'authenticate') + .mockImplementation( + async ( + credentials: ICredentialDataDecryptedObject, + typeName: string, + requestParams: IHttpRequestOptions, + ): Promise => { + if (typeName === 'microsoftEntraOAuth2Api') { + return { + ...requestParams, + headers: { + authorization: + 'bearer ' + (credentials.oauthTokenData as IDataObject).access_token, + }, + }; + } else { + return requestParams; + } + }, + ); + }); + + afterAll(() => { + nock.restore(); + jest.restoreAllMocks(); + }); + + nock(baseUrl) + .get(`/groups/${microsoftEntraApiResponse.getGroup.id}`) + .matchHeader( + 'authorization', + 'bearer ' + FAKE_CREDENTIALS_DATA.microsoftEntraOAuth2Api.oauthTokenData.access_token, + ) + .reply(200, { + ...microsoftEntraApiResponse.getGroup, + }); + + const nodeTypes = Helpers.setup(tests); + + test.each(tests)('$description', async (testData) => { + const { result } = await executeWorkflow(testData, nodeTypes); + const resultNodeData = Helpers.getResultNodeData(result, testData); + resultNodeData.forEach(({ nodeName, resultData }) => + expect(resultData).toEqual(testData.output.nodeData[nodeName]), + ); + expect(result.status).toEqual('success'); + }); + }); + + describe('Load options', () => { + it('should load group properties', async () => { + const mockContext = { + helpers: { + requestWithAuthentication: jest + .fn() + .mockReturnValue(microsoftEntraApiResponse.metadata.groups), + }, + getCurrentNodeParameter: jest.fn(), + } as unknown as ILoadOptionsFunctions; + const node = new MicrosoftEntra(); + + const properties = await node.methods.loadOptions.getGroupProperties.call(mockContext); + + expect(properties).toEqual(microsoftEntraNodeResponse.loadOptions.getGroupProperties); + }); + + it('should load user properties', async () => { + const mockContext = { + helpers: { + requestWithAuthentication: jest + .fn() + .mockReturnValue(microsoftEntraApiResponse.metadata.users), + }, + getCurrentNodeParameter: jest.fn(), + } as unknown as ILoadOptionsFunctions; + const node = new MicrosoftEntra(); + + const properties = await node.methods.loadOptions.getUserProperties.call(mockContext); + + expect(properties).toEqual(microsoftEntraNodeResponse.loadOptions.getUserProperties); + }); + }); + + describe('List search', () => { + it('should list search groups', async () => { + const mockResponse = { + value: Array.from({ length: 2 }, (_, i) => ({ + id: (i + 1).toString(), + displayName: `Group ${i + 1}`, + })), + '@odata.nextLink': '', + }; + const mockRequestWithAuthentication = jest.fn().mockReturnValue(mockResponse); + const mockContext = { + helpers: { + requestWithAuthentication: mockRequestWithAuthentication, + }, + } as unknown as ILoadOptionsFunctions; + const node = new MicrosoftEntra(); + + const listSearchResult = await node.methods.listSearch.getGroups.call(mockContext); + + expect(mockRequestWithAuthentication).toHaveBeenCalledWith('microsoftEntraOAuth2Api', { + method: 'GET', + url: 'https://graph.microsoft.com/v1.0/groups', + json: true, + headers: {}, + body: {}, + qs: { + $select: 'id,displayName', + }, + }); + expect(listSearchResult).toEqual({ + results: mockResponse.value.map((x) => ({ name: x.displayName, value: x.id })), + paginationToken: mockResponse['@odata.nextLink'], + }); + }); + + it('should list search users', async () => { + const mockResponse = { + value: Array.from({ length: 2 }, (_, i) => ({ + id: (i + 1).toString(), + displayName: `User ${i + 1}`, + })), + '@odata.nextLink': '', + }; + const mockRequestWithAuthentication = jest.fn().mockReturnValue(mockResponse); + const mockContext = { + helpers: { + requestWithAuthentication: mockRequestWithAuthentication, + }, + } as unknown as ILoadOptionsFunctions; + const node = new MicrosoftEntra(); + + const listSearchResult = await node.methods.listSearch.getUsers.call(mockContext); + + expect(mockRequestWithAuthentication).toHaveBeenCalledWith('microsoftEntraOAuth2Api', { + method: 'GET', + url: 'https://graph.microsoft.com/v1.0/users', + json: true, + headers: {}, + body: {}, + qs: { + $select: 'id,displayName', + }, + }); + expect(listSearchResult).toEqual({ + results: mockResponse.value.map((x) => ({ name: x.displayName, value: x.id })), + paginationToken: mockResponse['@odata.nextLink'], + }); + }); + }); +}); diff --git a/packages/nodes-base/nodes/Microsoft/Entra/test/UserDescription.test.ts b/packages/nodes-base/nodes/Microsoft/Entra/test/UserDescription.test.ts new file mode 100644 index 0000000000000..b245e12f705a3 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Entra/test/UserDescription.test.ts @@ -0,0 +1,1161 @@ +import { NodeConnectionType } from 'n8n-workflow'; +import nock from 'nock'; + +import { executeWorkflow } from '@test/nodes/ExecuteWorkflow'; +import * as Helpers from '@test/nodes/Helpers'; +import type { WorkflowTestData } from '@test/nodes/types'; + +import { microsoftEntraApiResponse, microsoftEntraNodeResponse } from './mocks'; + +describe('Gong Node', () => { + const baseUrl = 'https://graph.microsoft.com/v1.0'; + + beforeEach(() => { + // https://github.com/nock/nock/issues/2057#issuecomment-663665683 + if (!nock.isActive()) { + nock.activate(); + } + }); + + describe('User description', () => { + const tests: WorkflowTestData[] = [ + { + description: 'should add user to group', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '416e4fc1-5055-4e61-854e-a6265256ac26', + name: "When clicking 'Test workflow'", + type: 'n8n-nodes-base.manualTrigger', + position: [820, 380], + typeVersion: 1, + }, + { + parameters: { + resource: 'user', + operation: 'addGroup', + group: { + __rl: true, + mode: 'id', + value: 'a8eb60e3-0145-4d7e-85ef-c6259784761b', + }, + user: { + __rl: true, + mode: 'id', + value: '87d349ed-44d7-43e1-9a83-5f2406dee5bd', + }, + requestOptions: {}, + }, + type: 'n8n-nodes-base.microsoftEntra', + typeVersion: 1, + position: [220, 0], + id: '3429f7f2-dfca-4b72-8913-43a582e96e66', + name: 'Micosoft Entra ID', + credentials: { + microsoftEntraOAuth2Api: { + id: 'Hot2KwSMSoSmMVqd', + name: 'Microsoft Entra ID (Azure Active Directory) account', + }, + }, + }, + ], + connections: { + "When clicking 'Test workflow'": { + main: [ + [ + { + node: 'Micosoft Entra ID', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'Micosoft Entra ID': [microsoftEntraNodeResponse.addUserToGroup], + }, + }, + nock: { + baseUrl, + mocks: [ + { + method: 'post', + path: '/groups/a8eb60e3-0145-4d7e-85ef-c6259784761b/members/$ref', + statusCode: 204, + requestBody: { + '@odata.id': + 'https://graph.microsoft.com/v1.0/directoryObjects/87d349ed-44d7-43e1-9a83-5f2406dee5bd', + }, + responseBody: {}, + }, + ], + }, + }, + { + description: 'should create user', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '416e4fc1-5055-4e61-854e-a6265256ac26', + name: "When clicking 'Test workflow'", + type: 'n8n-nodes-base.manualTrigger', + position: [820, 380], + typeVersion: 1, + }, + { + parameters: { + operation: 'create', + displayName: 'John Doe', + mailNickname: 'johndoe', + password: 'Test!12345', + userPrincipalName: 'johndoe@contoso.com', + additionalFields: { + aboutMe: 'About me', + ageGroup: 'Adult', + birthday: '2024-11-12T00:00:00Z', + businessPhones: '0123456789', + city: 'New York', + companyName: 'Contoso', + consentProvidedForMinor: 'Granted', + country: 'US', + department: 'IT', + employeeId: 'employee-id-123', + employeeType: 'Contractor', + forceChangePassword: 'forceChangePasswordNextSignInWithMfa', + givenName: 'John', + employeeHireDate: '2024-11-13T00:00:00Z', + employeeLeaveDateTime: '2024-11-18T00:00:00Z', + employeeOrgData: { + employeeOrgValues: { + costCenter: 'Cost Center 1', + division: 'Division 1', + }, + }, + interests: ['interest1', 'interest2'], + jobTitle: 'Project manager', + surname: 'Doe', + mail: 'johndoe@contoso.com', + mobilePhone: '+123456789', + mySite: 'My Site', + officeLocation: 'New York', + onPremisesImmutableId: 'premiseid123', + otherMails: ['johndoe2@contoso.com', 'johndoe3@contoso.com'], + passwordPolicies: ['DisablePasswordExpiration', 'DisableStrongPassword'], + pastProjects: ['project1', 'project2'], + postalCode: '0123456', + preferredLanguage: 'en-US', + responsibilities: ['responsibility1', 'responsibility2'], + schools: ['school1', 'school2'], + skills: ['skill1', 'skill2'], + state: 'New York', + streetAddress: 'Street 123', + usageLocation: 'US', + userType: 'Guest', + }, + requestOptions: {}, + }, + type: 'n8n-nodes-base.microsoftEntra', + typeVersion: 1, + position: [220, 0], + id: '3429f7f2-dfca-4b72-8913-43a582e96e66', + name: 'Micosoft Entra ID', + credentials: { + microsoftEntraOAuth2Api: { + id: 'Hot2KwSMSoSmMVqd', + name: 'Microsoft Entra ID (Azure Active Directory) account', + }, + }, + }, + ], + connections: { + "When clicking 'Test workflow'": { + main: [ + [ + { + node: 'Micosoft Entra ID', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'Micosoft Entra ID': [microsoftEntraNodeResponse.createUser], + }, + }, + nock: { + baseUrl, + mocks: [ + { + method: 'post', + path: '/users', + statusCode: 201, + requestBody: { + accountEnabled: true, + displayName: 'John Doe', + mailNickname: 'johndoe', + passwordProfile: { + password: 'Test!12345', + }, + userPrincipalName: 'johndoe@contoso.com', + }, + responseBody: microsoftEntraApiResponse.postUser, + }, + { + method: 'patch', + path: `/users/${microsoftEntraApiResponse.postUser.id}`, + statusCode: 204, + requestBody: { + ageGroup: 'Adult', + businessPhones: ['0123456789'], + city: 'New York', + companyName: 'Contoso', + consentProvidedForMinor: 'Granted', + country: 'US', + department: 'IT', + employeeId: 'employee-id-123', + employeeType: 'Contractor', + givenName: 'John', + employeeHireDate: '2024-11-13T00:00:00.000Z', + employeeLeaveDateTime: '2024-11-18T00:00:00.000Z', + employeeOrgData: { + costCenter: 'Cost Center 1', + division: 'Division 1', + }, + jobTitle: 'Project manager', + surname: 'Doe', + mail: 'johndoe@contoso.com', + mobilePhone: '+123456789', + officeLocation: 'New York', + onPremisesImmutableId: 'premiseid123', + otherMails: ['johndoe2@contoso.com', 'johndoe3@contoso.com'], + passwordPolicies: 'DisablePasswordExpiration,DisableStrongPassword', + passwordProfile: { + forceChangePasswordNextSignInWithMfa: true, + }, + postalCode: '0123456', + preferredLanguage: 'en-US', + state: 'New York', + streetAddress: 'Street 123', + usageLocation: 'US', + userType: 'Guest', + }, + responseBody: {}, + }, + { + method: 'patch', + path: `/users/${microsoftEntraApiResponse.postUser.id}`, + statusCode: 204, + requestBody: { + aboutMe: 'About me', + birthday: '2024-11-12T00:00:00.000Z', + interests: ['interest1', 'interest2'], + mySite: 'My Site', + pastProjects: ['project1', 'project2'], + responsibilities: ['responsibility1', 'responsibility2'], + schools: ['school1', 'school2'], + skills: ['skill1', 'skill2'], + }, + responseBody: {}, + }, + ], + }, + }, + { + description: 'should delete user', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '416e4fc1-5055-4e61-854e-a6265256ac26', + name: "When clicking 'Test workflow'", + type: 'n8n-nodes-base.manualTrigger', + position: [820, 380], + typeVersion: 1, + }, + { + parameters: { + resource: 'user', + operation: 'delete', + user: { + __rl: true, + value: '87d349ed-44d7-43e1-9a83-5f2406dee5bd', + mode: 'id', + }, + options: {}, + requestOptions: {}, + }, + type: 'n8n-nodes-base.microsoftEntra', + typeVersion: 1, + position: [220, 0], + id: '3429f7f2-dfca-4b72-8913-43a582e96e66', + name: 'Micosoft Entra ID', + credentials: { + microsoftEntraOAuth2Api: { + id: 'Hot2KwSMSoSmMVqd', + name: 'Microsoft Entra ID (Azure Active Directory) account', + }, + }, + }, + ], + connections: { + "When clicking 'Test workflow'": { + main: [ + [ + { + node: 'Micosoft Entra ID', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'Micosoft Entra ID': [microsoftEntraNodeResponse.deleteUser], + }, + }, + nock: { + baseUrl, + mocks: [ + { + method: 'delete', + path: '/users/87d349ed-44d7-43e1-9a83-5f2406dee5bd', + statusCode: 204, + responseBody: {}, + }, + ], + }, + }, + { + description: 'should get user', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '416e4fc1-5055-4e61-854e-a6265256ac26', + name: "When clicking 'Test workflow'", + type: 'n8n-nodes-base.manualTrigger', + position: [820, 380], + typeVersion: 1, + }, + { + parameters: { + resource: 'user', + operation: 'get', + user: { + __rl: true, + value: '87d349ed-44d7-43e1-9a83-5f2406dee5bd', + mode: 'id', + }, + output: 'raw', + requestOptions: {}, + }, + type: 'n8n-nodes-base.microsoftEntra', + typeVersion: 1, + position: [220, 0], + id: '3429f7f2-dfca-4b72-8913-43a582e96e66', + name: 'Micosoft Entra ID', + credentials: { + microsoftEntraOAuth2Api: { + id: 'Hot2KwSMSoSmMVqd', + name: 'Microsoft Entra ID (Azure Active Directory) account', + }, + }, + }, + ], + connections: { + "When clicking 'Test workflow'": { + main: [ + [ + { + node: 'Micosoft Entra ID', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'Micosoft Entra ID': [microsoftEntraNodeResponse.getUser], + }, + }, + nock: { + baseUrl, + mocks: [ + { + method: 'get', + path: '/users/87d349ed-44d7-43e1-9a83-5f2406dee5bd?$select=id,accountEnabled,ageGroup,assignedLicenses,assignedPlans,authorizationInfo,businessPhones,city,companyName,consentProvidedForMinor,country,createdDateTime,creationType,customSecurityAttributes,deletedDateTime,department,displayName,employeeHireDate,employeeId,employeeLeaveDateTime,employeeOrgData,employeeType,externalUserState,externalUserStateChangeDateTime,faxNumber,givenName,identities,imAddresses,isManagementRestricted,isResourceAccount,jobTitle,lastPasswordChangeDateTime,legalAgeGroupClassification,licenseAssignmentStates,mail,mailNickname,mobilePhone,officeLocation,onPremisesDistinguishedName,onPremisesDomainName,onPremisesExtensionAttributes,onPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesProvisioningErrors,onPremisesSamAccountName,onPremisesSecurityIdentifier,onPremisesSyncEnabled,onPremisesUserPrincipalName,otherMails,passwordPolicies,passwordProfile,postalCode,preferredDataLocation,preferredLanguage,provisionedPlans,proxyAddresses,securityIdentifier,serviceProvisioningErrors,showInAddressList,signInSessionsValidFromDateTime,state,streetAddress,surname,usageLocation,userPrincipalName,userType', + statusCode: 200, + responseBody: microsoftEntraApiResponse.getUser, + }, + ], + }, + }, + { + description: 'should get user with options', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '416e4fc1-5055-4e61-854e-a6265256ac26', + name: "When clicking 'Test workflow'", + type: 'n8n-nodes-base.manualTrigger', + position: [820, 380], + typeVersion: 1, + }, + { + parameters: { + resource: 'user', + operation: 'get', + user: { + __rl: true, + value: '87d349ed-44d7-43e1-9a83-5f2406dee5bd', + mode: 'id', + }, + output: 'fields', + fields: [ + 'aboutMe', + 'accountEnabled', + 'ageGroup', + 'assignedLicenses', + 'assignedPlans', + 'authorizationInfo', + 'birthday', + 'businessPhones', + 'city', + 'companyName', + 'consentProvidedForMinor', + 'country', + 'createdDateTime', + 'creationType', + 'customSecurityAttributes', + 'deletedDateTime', + 'department', + 'displayName', + 'employeeHireDate', + 'employeeId', + 'employeeLeaveDateTime', + 'employeeOrgData', + 'externalUserStateChangeDateTime', + 'externalUserState', + 'employeeType', + 'faxNumber', + 'givenName', + 'hireDate', + 'identities', + 'imAddresses', + 'interests', + 'isManagementRestricted', + 'isResourceAccount', + 'jobTitle', + 'lastPasswordChangeDateTime', + 'legalAgeGroupClassification', + 'licenseAssignmentStates', + 'mail', + 'mailNickname', + 'mobilePhone', + 'mySite', + 'officeLocation', + 'onPremisesDistinguishedName', + 'onPremisesDomainName', + 'onPremisesExtensionAttributes', + 'onPremisesImmutableId', + 'onPremisesLastSyncDateTime', + 'onPremisesProvisioningErrors', + 'onPremisesSamAccountName', + 'onPremisesSecurityIdentifier', + 'onPremisesSyncEnabled', + 'onPremisesUserPrincipalName', + 'otherMails', + 'passwordPolicies', + 'passwordProfile', + 'pastProjects', + 'postalCode', + 'preferredDataLocation', + 'preferredLanguage', + 'preferredName', + 'provisionedPlans', + 'proxyAddresses', + 'userType', + 'userPrincipalName', + 'usageLocation', + 'surname', + 'streetAddress', + 'state', + 'skills', + 'signInSessionsValidFromDateTime', + 'showInAddressList', + 'serviceProvisioningErrors', + 'securityIdentifier', + 'schools', + 'responsibilities', + ], + requestOptions: {}, + }, + type: 'n8n-nodes-base.microsoftEntra', + typeVersion: 1, + position: [220, 0], + id: '3429f7f2-dfca-4b72-8913-43a582e96e66', + name: 'Micosoft Entra ID', + credentials: { + microsoftEntraOAuth2Api: { + id: 'Hot2KwSMSoSmMVqd', + name: 'Microsoft Entra ID (Azure Active Directory) account', + }, + }, + }, + ], + connections: { + "When clicking 'Test workflow'": { + main: [ + [ + { + node: 'Micosoft Entra ID', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'Micosoft Entra ID': [ + [ + { + json: { + '@odata.context': + 'https://graph.microsoft.com/v1.0/$metadata#users(id)/$entity', + id: '87d349ed-44d7-43e1-9a83-5f2406dee5bd', + }, + }, + ], + ], + }, + }, + nock: { + baseUrl, + mocks: [ + { + method: 'get', + path: '/users/87d349ed-44d7-43e1-9a83-5f2406dee5bd?$select=aboutMe,accountEnabled,ageGroup,assignedLicenses,assignedPlans,authorizationInfo,birthday,businessPhones,city,companyName,consentProvidedForMinor,country,createdDateTime,creationType,customSecurityAttributes,deletedDateTime,department,displayName,employeeHireDate,employeeId,employeeLeaveDateTime,employeeOrgData,externalUserStateChangeDateTime,externalUserState,employeeType,faxNumber,givenName,hireDate,identities,imAddresses,interests,isManagementRestricted,isResourceAccount,jobTitle,lastPasswordChangeDateTime,legalAgeGroupClassification,licenseAssignmentStates,mail,mailNickname,mobilePhone,mySite,officeLocation,onPremisesDistinguishedName,onPremisesDomainName,onPremisesExtensionAttributes,onPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesProvisioningErrors,onPremisesSamAccountName,onPremisesSecurityIdentifier,onPremisesSyncEnabled,onPremisesUserPrincipalName,otherMails,passwordPolicies,passwordProfile,pastProjects,postalCode,preferredDataLocation,preferredLanguage,preferredName,provisionedPlans,proxyAddresses,userType,userPrincipalName,usageLocation,surname,streetAddress,state,skills,signInSessionsValidFromDateTime,showInAddressList,serviceProvisioningErrors,securityIdentifier,schools,responsibilities,id', + statusCode: 200, + responseBody: { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#users(id)/$entity', + id: '87d349ed-44d7-43e1-9a83-5f2406dee5bd', + }, + }, + ], + }, + }, + { + description: 'should get all users with simple output', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '416e4fc1-5055-4e61-854e-a6265256ac26', + name: "When clicking 'Test workflow'", + type: 'n8n-nodes-base.manualTrigger', + position: [820, 380], + typeVersion: 1, + }, + { + parameters: { + resource: 'user', + operation: 'getAll', + returnAll: true, + filter: '', + output: 'simple', + requestOptions: {}, + }, + type: 'n8n-nodes-base.microsoftEntra', + typeVersion: 1, + position: [220, 0], + id: '3429f7f2-dfca-4b72-8913-43a582e96e66', + name: 'Micosoft Entra ID', + credentials: { + microsoftEntraOAuth2Api: { + id: 'Hot2KwSMSoSmMVqd', + name: 'Microsoft Entra ID (Azure Active Directory) account', + }, + }, + }, + ], + connections: { + "When clicking 'Test workflow'": { + main: [ + [ + { + node: 'Micosoft Entra ID', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'Micosoft Entra ID': [new Array(102).fill(microsoftEntraNodeResponse.getUser[0])], + }, + }, + nock: { + baseUrl, + mocks: [ + { + method: 'get', + path: '/users?$select=id,createdDateTime,displayName,userPrincipalName,mail,mailNickname,securityIdentifier', + statusCode: 200, + responseBody: { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#users', + '@odata.nextLink': + 'https://graph.microsoft.com/v1.0/users?$select=id,createdDateTime,displayName,userPrincipalName,mail,mailNickname,securityIdentifier&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA', + value: new Array(100).fill(microsoftEntraApiResponse.getUser), + }, + }, + { + method: 'get', + path: '/users?$select=id,createdDateTime,displayName,userPrincipalName,mail,mailNickname,securityIdentifier&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA', + statusCode: 200, + responseBody: { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#users', + value: new Array(2).fill(microsoftEntraApiResponse.getUser), + }, + }, + ], + }, + }, + { + description: 'should get limit 10 users with raw output', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '416e4fc1-5055-4e61-854e-a6265256ac26', + name: "When clicking 'Test workflow'", + type: 'n8n-nodes-base.manualTrigger', + position: [820, 380], + typeVersion: 1, + }, + { + parameters: { + resource: 'user', + operation: 'getAll', + limit: 10, + filter: '', + output: 'raw', + requestOptions: {}, + }, + type: 'n8n-nodes-base.microsoftEntra', + typeVersion: 1, + position: [220, 0], + id: '3429f7f2-dfca-4b72-8913-43a582e96e66', + name: 'Micosoft Entra ID', + credentials: { + microsoftEntraOAuth2Api: { + id: 'Hot2KwSMSoSmMVqd', + name: 'Microsoft Entra ID (Azure Active Directory) account', + }, + }, + }, + ], + connections: { + "When clicking 'Test workflow'": { + main: [ + [ + { + node: 'Micosoft Entra ID', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'Micosoft Entra ID': [new Array(10).fill(microsoftEntraNodeResponse.getUser[0])], + }, + }, + nock: { + baseUrl, + mocks: [ + { + method: 'get', + path: '/users?$top=10&$select=id,accountEnabled,ageGroup,assignedLicenses,assignedPlans,authorizationInfo,businessPhones,city,companyName,consentProvidedForMinor,country,createdDateTime,creationType,customSecurityAttributes,deletedDateTime,department,displayName,employeeHireDate,employeeId,employeeLeaveDateTime,employeeOrgData,employeeType,externalUserState,externalUserStateChangeDateTime,faxNumber,givenName,identities,imAddresses,isManagementRestricted,isResourceAccount,jobTitle,lastPasswordChangeDateTime,legalAgeGroupClassification,licenseAssignmentStates,mail,mailNickname,mobilePhone,officeLocation,onPremisesDistinguishedName,onPremisesDomainName,onPremisesExtensionAttributes,onPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesProvisioningErrors,onPremisesSamAccountName,onPremisesSecurityIdentifier,onPremisesSyncEnabled,onPremisesUserPrincipalName,otherMails,passwordPolicies,passwordProfile,postalCode,preferredDataLocation,preferredLanguage,provisionedPlans,proxyAddresses,securityIdentifier,serviceProvisioningErrors,showInAddressList,signInSessionsValidFromDateTime,state,streetAddress,surname,usageLocation,userPrincipalName,userType', + statusCode: 200, + responseBody: { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#users', + '@odata.nextLink': + 'https://graph.microsoft.com/v1.0/users?$top=10&$select=id,accountEnabled,ageGroup,assignedLicenses,assignedPlans,authorizationInfo,businessPhones,city,companyName,consentProvidedForMinor,country,createdDateTime,creationType,customSecurityAttributes,deletedDateTime,department,displayName,employeeHireDate,employeeId,employeeLeaveDateTime,employeeOrgData,employeeType,externalUserState,externalUserStateChangeDateTime,faxNumber,givenName,identities,imAddresses,isManagementRestricted,isResourceAccount,jobTitle,lastPasswordChangeDateTime,legalAgeGroupClassification,licenseAssignmentStates,mail,mailNickname,mobilePhone,officeLocation,onPremisesDistinguishedName,onPremisesDomainName,onPremisesExtensionAttributes,onPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesProvisioningErrors,onPremisesSamAccountName,onPremisesSecurityIdentifier,onPremisesSyncEnabled,onPremisesUserPrincipalName,otherMails,passwordPolicies,passwordProfile,postalCode,preferredDataLocation,preferredLanguage,provisionedPlans,proxyAddresses,securityIdentifier,serviceProvisioningErrors,showInAddressList,signInSessionsValidFromDateTime,state,streetAddress,surname,usageLocation,userPrincipalName,userType&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA', + value: new Array(10).fill(microsoftEntraApiResponse.getUser), + }, + }, + ], + }, + }, + { + description: 'should get all users with options and filter', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '416e4fc1-5055-4e61-854e-a6265256ac26', + name: "When clicking 'Test workflow'", + type: 'n8n-nodes-base.manualTrigger', + position: [820, 380], + typeVersion: 1, + }, + { + parameters: { + resource: 'user', + operation: 'getAll', + returnAll: true, + filter: "startswith(displayName,'user')", + output: 'fields', + fields: [ + 'accountEnabled', + 'ageGroup', + 'assignedLicenses', + 'assignedPlans', + 'businessPhones', + 'authorizationInfo', + 'city', + 'companyName', + 'consentProvidedForMinor', + 'country', + 'createdDateTime', + 'creationType', + 'customSecurityAttributes', + 'deletedDateTime', + 'department', + 'displayName', + 'employeeHireDate', + 'employeeId', + 'employeeLeaveDateTime', + 'employeeOrgData', + 'employeeType', + 'externalUserState', + 'externalUserStateChangeDateTime', + 'faxNumber', + 'givenName', + 'identities', + 'imAddresses', + 'isManagementRestricted', + 'isResourceAccount', + 'jobTitle', + 'lastPasswordChangeDateTime', + 'legalAgeGroupClassification', + 'licenseAssignmentStates', + 'mailNickname', + 'mail', + 'mobilePhone', + 'officeLocation', + 'onPremisesDistinguishedName', + 'onPremisesExtensionAttributes', + 'onPremisesDomainName', + 'onPremisesImmutableId', + 'onPremisesLastSyncDateTime', + 'onPremisesProvisioningErrors', + 'onPremisesSecurityIdentifier', + 'onPremisesSamAccountName', + 'onPremisesSyncEnabled', + 'onPremisesUserPrincipalName', + 'otherMails', + 'passwordPolicies', + 'passwordProfile', + 'postalCode', + 'preferredDataLocation', + 'preferredLanguage', + 'provisionedPlans', + 'proxyAddresses', + 'signInSessionsValidFromDateTime', + 'showInAddressList', + 'serviceProvisioningErrors', + 'securityIdentifier', + 'state', + 'streetAddress', + 'surname', + 'usageLocation', + 'userPrincipalName', + 'userType', + ], + requestOptions: {}, + }, + type: 'n8n-nodes-base.microsoftEntra', + typeVersion: 1, + position: [220, 0], + id: '3429f7f2-dfca-4b72-8913-43a582e96e66', + name: 'Micosoft Entra ID', + credentials: { + microsoftEntraOAuth2Api: { + id: 'Hot2KwSMSoSmMVqd', + name: 'Microsoft Entra ID (Azure Active Directory) account', + }, + }, + }, + ], + connections: { + "When clicking 'Test workflow'": { + main: [ + [ + { + node: 'Micosoft Entra ID', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'Micosoft Entra ID': [ + new Array(102).fill({ + json: { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#users(id)/$entity', + id: '87d349ed-44d7-43e1-9a83-5f2406dee5bd', + }, + }), + ], + }, + }, + nock: { + baseUrl, + mocks: [ + { + method: 'get', + path: "/users?$filter=startswith(displayName,'user')&$select=accountEnabled,ageGroup,assignedLicenses,assignedPlans,businessPhones,authorizationInfo,city,companyName,consentProvidedForMinor,country,createdDateTime,creationType,customSecurityAttributes,deletedDateTime,department,displayName,employeeHireDate,employeeId,employeeLeaveDateTime,employeeOrgData,employeeType,externalUserState,externalUserStateChangeDateTime,faxNumber,givenName,identities,imAddresses,isManagementRestricted,isResourceAccount,jobTitle,lastPasswordChangeDateTime,legalAgeGroupClassification,licenseAssignmentStates,mailNickname,mail,mobilePhone,officeLocation,onPremisesDistinguishedName,onPremisesExtensionAttributes,onPremisesDomainName,onPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesProvisioningErrors,onPremisesSecurityIdentifier,onPremisesSamAccountName,onPremisesSyncEnabled,onPremisesUserPrincipalName,otherMails,passwordPolicies,passwordProfile,postalCode,preferredDataLocation,preferredLanguage,provisionedPlans,proxyAddresses,signInSessionsValidFromDateTime,showInAddressList,serviceProvisioningErrors,securityIdentifier,state,streetAddress,surname,usageLocation,userPrincipalName,userType,id", + statusCode: 200, + responseBody: { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#users', + '@odata.nextLink': + "https://graph.microsoft.com/v1.0/users?$filter=startswith(displayName,'user')&$select=accountEnabled,ageGroup,assignedLicenses,assignedPlans,businessPhones,authorizationInfo,city,companyName,consentProvidedForMinor,country,createdDateTime,creationType,customSecurityAttributes,deletedDateTime,department,displayName,employeeHireDate,employeeId,employeeLeaveDateTime,employeeOrgData,employeeType,externalUserState,externalUserStateChangeDateTime,faxNumber,givenName,identities,imAddresses,isManagementRestricted,isResourceAccount,jobTitle,lastPasswordChangeDateTime,legalAgeGroupClassification,licenseAssignmentStates,mailNickname,mail,mobilePhone,officeLocation,onPremisesDistinguishedName,onPremisesExtensionAttributes,onPremisesDomainName,onPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesProvisioningErrors,onPremisesSecurityIdentifier,onPremisesSamAccountName,onPremisesSyncEnabled,onPremisesUserPrincipalName,otherMails,passwordPolicies,passwordProfile,postalCode,preferredDataLocation,preferredLanguage,provisionedPlans,proxyAddresses,signInSessionsValidFromDateTime,showInAddressList,serviceProvisioningErrors,securityIdentifier,state,streetAddress,surname,usageLocation,userPrincipalName,userType,id&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA", + value: new Array(100).fill({ + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#users(id)/$entity', + id: '87d349ed-44d7-43e1-9a83-5f2406dee5bd', + }), + }, + }, + { + method: 'get', + path: "/users?$filter=startswith(displayName,'user')&$select=accountEnabled,ageGroup,assignedLicenses,assignedPlans,businessPhones,authorizationInfo,city,companyName,consentProvidedForMinor,country,createdDateTime,creationType,customSecurityAttributes,deletedDateTime,department,displayName,employeeHireDate,employeeId,employeeLeaveDateTime,employeeOrgData,employeeType,externalUserState,externalUserStateChangeDateTime,faxNumber,givenName,identities,imAddresses,isManagementRestricted,isResourceAccount,jobTitle,lastPasswordChangeDateTime,legalAgeGroupClassification,licenseAssignmentStates,mailNickname,mail,mobilePhone,officeLocation,onPremisesDistinguishedName,onPremisesExtensionAttributes,onPremisesDomainName,onPremisesImmutableId,onPremisesLastSyncDateTime,onPremisesProvisioningErrors,onPremisesSecurityIdentifier,onPremisesSamAccountName,onPremisesSyncEnabled,onPremisesUserPrincipalName,otherMails,passwordPolicies,passwordProfile,postalCode,preferredDataLocation,preferredLanguage,provisionedPlans,proxyAddresses,signInSessionsValidFromDateTime,showInAddressList,serviceProvisioningErrors,securityIdentifier,state,streetAddress,surname,usageLocation,userPrincipalName,userType,id&$skiptoken=RFNwdAIAAQAAACpHcm91cF9jYzEzY2Y5Yy1lOWNiLTQ3NjUtODMzYS05MDIzZDhhMjhlZjMqR3JvdXBfY2MxM2NmOWMtZTljYi00NzY1LTgzM2EtOTAyM2Q4YTI4ZWYzAAAAAAAAAAAAAAA", + statusCode: 200, + responseBody: { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#users', + value: new Array(2).fill({ + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#users(id)/$entity', + id: '87d349ed-44d7-43e1-9a83-5f2406dee5bd', + }), + }, + }, + ], + }, + }, + { + description: 'should remove user from group', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '416e4fc1-5055-4e61-854e-a6265256ac26', + name: "When clicking 'Test workflow'", + type: 'n8n-nodes-base.manualTrigger', + position: [820, 380], + typeVersion: 1, + }, + { + parameters: { + resource: 'user', + operation: 'removeGroup', + group: { + __rl: true, + mode: 'id', + value: 'a8eb60e3-0145-4d7e-85ef-c6259784761b', + }, + user: { + __rl: true, + mode: 'id', + value: '87d349ed-44d7-43e1-9a83-5f2406dee5bd', + }, + requestOptions: {}, + }, + type: 'n8n-nodes-base.microsoftEntra', + typeVersion: 1, + position: [220, 0], + id: '3429f7f2-dfca-4b72-8913-43a582e96e66', + name: 'Micosoft Entra ID', + credentials: { + microsoftEntraOAuth2Api: { + id: 'Hot2KwSMSoSmMVqd', + name: 'Microsoft Entra ID (Azure Active Directory) account', + }, + }, + }, + ], + connections: { + "When clicking 'Test workflow'": { + main: [ + [ + { + node: 'Micosoft Entra ID', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'Micosoft Entra ID': [microsoftEntraNodeResponse.removeUserFromGroup], + }, + }, + nock: { + baseUrl, + mocks: [ + { + method: 'delete', + path: '/groups/a8eb60e3-0145-4d7e-85ef-c6259784761b/members/87d349ed-44d7-43e1-9a83-5f2406dee5bd/$ref', + statusCode: 204, + responseBody: {}, + }, + ], + }, + }, + { + description: 'should update user', + input: { + workflowData: { + nodes: [ + { + parameters: {}, + id: '416e4fc1-5055-4e61-854e-a6265256ac26', + name: "When clicking 'Test workflow'", + type: 'n8n-nodes-base.manualTrigger', + position: [820, 380], + typeVersion: 1, + }, + { + parameters: { + resource: 'user', + operation: 'update', + user: { + __rl: true, + value: '87d349ed-44d7-43e1-9a83-5f2406dee5bd', + mode: 'id', + }, + updateFields: { + aboutMe: 'About me', + accountEnabled: true, + ageGroup: 'Adult', + birthday: '2024-11-12T00:00:00Z', + businessPhones: '0123456789', + city: 'New York', + companyName: 'Contoso', + consentProvidedForMinor: 'Granted', + country: 'US', + department: 'IT', + displayName: 'Group Display Name', + employeeId: 'employee-id-123', + employeeType: 'Contractor', + forceChangePassword: 'forceChangePasswordNextSignInWithMfa', + givenName: 'John', + employeeHireDate: '2024-11-13T00:00:00Z', + employeeLeaveDateTime: '2024-11-18T00:00:00Z', + employeeOrgData: { + employeeOrgValues: { + costCenter: 'Cost Center 1', + division: 'Division 1', + }, + }, + interests: ['interest1', 'interest2'], + jobTitle: 'Project manager', + surname: 'Doe', + mail: 'johndoe@contoso.com', + mailNickname: 'MailNickname', + mobilePhone: '+123456789', + mySite: 'My Site', + officeLocation: 'New York', + onPremisesImmutableId: 'premiseid123', + otherMails: ['johndoe2@contoso.com', 'johndoe3@contoso.com'], + password: 'Test!12345', + passwordPolicies: ['DisablePasswordExpiration', 'DisableStrongPassword'], + pastProjects: ['project1', 'project2'], + postalCode: '0123456', + preferredLanguage: 'en-US', + responsibilities: ['responsibility1', 'responsibility2'], + schools: ['school1', 'school2'], + skills: ['skill1', 'skill2'], + state: 'New York', + streetAddress: 'Street 123', + usageLocation: 'US', + userPrincipalName: 'johndoe@contoso.com', + userType: 'Guest', + }, + requestOptions: {}, + }, + type: 'n8n-nodes-base.microsoftEntra', + typeVersion: 1, + position: [220, 0], + id: '3429f7f2-dfca-4b72-8913-43a582e96e66', + name: 'Micosoft Entra ID', + credentials: { + microsoftEntraOAuth2Api: { + id: 'Hot2KwSMSoSmMVqd', + name: 'Microsoft Entra ID (Azure Active Directory) account', + }, + }, + }, + ], + connections: { + "When clicking 'Test workflow'": { + main: [ + [ + { + node: 'Micosoft Entra ID', + type: NodeConnectionType.Main, + index: 0, + }, + ], + ], + }, + }, + }, + }, + output: { + nodeExecutionOrder: ['Start'], + nodeData: { + 'Micosoft Entra ID': [microsoftEntraNodeResponse.updateUser], + }, + }, + nock: { + baseUrl, + mocks: [ + { + method: 'patch', + path: `/users/${microsoftEntraApiResponse.postUser.id}`, + statusCode: 204, + requestBody: { + accountEnabled: true, + ageGroup: 'Adult', + businessPhones: ['0123456789'], + city: 'New York', + companyName: 'Contoso', + consentProvidedForMinor: 'Granted', + country: 'US', + department: 'IT', + displayName: 'Group Display Name', + employeeId: 'employee-id-123', + employeeType: 'Contractor', + givenName: 'John', + employeeHireDate: null, + employeeLeaveDateTime: null, + employeeOrgData: { + costCenter: 'Cost Center 1', + division: 'Division 1', + }, + jobTitle: 'Project manager', + surname: 'Doe', + mail: 'johndoe@contoso.com', + mailNickname: 'MailNickname', + mobilePhone: '+123456789', + officeLocation: 'New York', + onPremisesImmutableId: 'premiseid123', + otherMails: ['johndoe2@contoso.com', 'johndoe3@contoso.com'], + passwordProfile: { + password: 'Test!12345', + forceChangePasswordNextSignInWithMfa: true, + }, + passwordPolicies: 'DisablePasswordExpiration,DisableStrongPassword', + postalCode: '0123456', + preferredLanguage: 'en-US', + state: 'New York', + streetAddress: 'Street 123', + usageLocation: 'US', + userPrincipalName: 'johndoe@contoso.com', + userType: 'Guest', + }, + responseBody: {}, + }, + { + method: 'patch', + path: `/users/${microsoftEntraApiResponse.postUser.id}`, + statusCode: 204, + requestBody: { + aboutMe: 'About me', + birthday: '2024-11-12T00:00:00.000Z', + interests: ['interest1', 'interest2'], + mySite: 'My Site', + pastProjects: ['project1', 'project2'], + responsibilities: ['responsibility1', 'responsibility2'], + schools: ['school1', 'school2'], + skills: ['skill1', 'skill2'], + }, + responseBody: {}, + }, + ], + }, + }, + ]; + + const nodeTypes = Helpers.setup(tests); + + test.each(tests)('$description', async (testData) => { + const { result } = await executeWorkflow(testData, nodeTypes); + + const resultNodeData = Helpers.getResultNodeData(result, testData); + resultNodeData.forEach(({ nodeName, resultData }) => + expect(resultData).toEqual(testData.output.nodeData[nodeName]), + ); + expect(result.status).toEqual('success'); + }); + }); +}); diff --git a/packages/nodes-base/nodes/Microsoft/Entra/test/mocks.ts b/packages/nodes-base/nodes/Microsoft/Entra/test/mocks.ts new file mode 100644 index 0000000000000..003df7f193309 --- /dev/null +++ b/packages/nodes-base/nodes/Microsoft/Entra/test/mocks.ts @@ -0,0 +1,1270 @@ +/* eslint-disable n8n-nodes-base/node-param-display-name-miscased-id */ +/* eslint-disable n8n-nodes-base/node-param-display-name-miscased */ + +export const microsoftEntraApiResponse = { + postGroup: { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#groups/$entity', + id: 'a8eb60e3-0145-4d7e-85ef-c6259784761b', + deletedDateTime: null, + classification: null, + createdDateTime: '2024-11-17T15:46:32Z', + creationOptions: [], + description: null, + displayName: 'Group Display Name', + expirationDateTime: null, + groupTypes: ['DynamicMembership', 'Unified'], + isAssignableToRole: true, + mail: null, + mailEnabled: true, + mailNickname: 'MailNickname', + membershipRule: 'department -eq "Marketing"', + membershipRuleProcessingState: 'On', + onPremisesDomainName: null, + onPremisesLastSyncDateTime: null, + onPremisesNetBiosName: null, + onPremisesSamAccountName: null, + onPremisesSecurityIdentifier: null, + onPremisesSyncEnabled: null, + preferredDataLocation: null, + preferredLanguage: null, + proxyAddresses: [], + renewedDateTime: '2024-11-17T15:46:32Z', + resourceBehaviorOptions: [], + resourceProvisioningOptions: [], + securityEnabled: true, + securityIdentifier: 'S-1-12-1-2833998051-1300103493-633794437-460752023', + theme: null, + uniqueName: null, + visibility: null, + onPremisesProvisioningErrors: [], + serviceProvisioningErrors: [], + }, + + getGroup: { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#groups/$entity', + id: 'a8eb60e3-0145-4d7e-85ef-c6259784761b', + deletedDateTime: null, + classification: null, + createdDateTime: '2024-11-04T16:00:21Z', + creationOptions: ['YammerProvisioning'], + description: 'This is the default group for everyone in the network', + displayName: 'All Company', + expirationDateTime: null, + groupTypes: ['Unified'], + isAssignableToRole: null, + mail: 'HRTaskforce@contoso.com', + mailEnabled: true, + mailNickname: 'allcompany', + membershipRule: 'department -eq "Marketing"', + membershipRuleProcessingState: 'On', + onPremisesDomainName: null, + onPremisesLastSyncDateTime: null, + onPremisesNetBiosName: null, + onPremisesSamAccountName: null, + onPremisesSecurityIdentifier: null, + onPremisesSyncEnabled: null, + preferredDataLocation: null, + preferredLanguage: null, + proxyAddresses: ['SMTP:HRTaskforce@contoso.com'], + renewedDateTime: '2024-11-04T16:00:21Z', + resourceBehaviorOptions: ['CalendarMemberReadOnly'], + resourceProvisioningOptions: [], + securityEnabled: false, + securityIdentifier: 'S-1-12-1-1394793482-1222919313-3222267053-1234900903', + theme: null, + uniqueName: null, + visibility: 'Public', + onPremisesProvisioningErrors: [], + serviceProvisioningErrors: [], + }, + + getGroupWithProperties: { + '@odata.context': + 'https://graph.microsoft.com/v1.0/$metadata#groups(assignedLabels,assignedLicenses,createdDateTime,classification,deletedDateTime,description,displayName,expirationDateTime,groupTypes,visibility,unseenCount,theme,uniqueName,serviceProvisioningErrors,securityIdentifier,renewedDateTime,securityEnabled,autoSubscribeNewMembers,allowExternalSenders,licenseProcessingState,isManagementRestricted,isSubscribedByMail,isAssignableToRole,id,hideFromOutlookClients,hideFromAddressLists,onPremisesProvisioningErrors,onPremisesSecurityIdentifier,onPremisesSamAccountName,onPremisesNetBiosName,onPremisesSyncEnabled,preferredDataLocation,preferredLanguage,proxyAddresses,onPremisesLastSyncDateTime,onPremisesDomainName,membershipRuleProcessingState,membershipRule,mailNickname,mailEnabled,mail,members())/$entity', + createdDateTime: '2024-11-04T16:00:21Z', + classification: null, + deletedDateTime: null, + description: 'This is the default group for everyone in the network', + displayName: 'All Company', + expirationDateTime: null, + groupTypes: ['Unified'], + visibility: 'Public', + theme: null, + uniqueName: null, + securityIdentifier: 'S-1-12-1-1394793482-1222919313-3222267053-1234900903', + renewedDateTime: '2024-11-04T16:00:21Z', + securityEnabled: false, + isManagementRestricted: null, + isAssignableToRole: null, + id: 'a8eb60e3-0145-4d7e-85ef-c6259784761b', + onPremisesSecurityIdentifier: null, + onPremisesSamAccountName: null, + onPremisesNetBiosName: null, + onPremisesSyncEnabled: null, + preferredDataLocation: null, + preferredLanguage: null, + proxyAddresses: ['SMTP:HRTaskforce@contoso.com'], + onPremisesLastSyncDateTime: null, + onPremisesDomainName: null, + membershipRule: 'department -eq "Marketing"', + membershipRuleProcessingState: 'On', + mailNickname: 'allcompany', + mailEnabled: true, + mail: 'HRTaskforce@contoso.com', + licenseProcessingState: null, + allowExternalSenders: false, + autoSubscribeNewMembers: false, + isSubscribedByMail: false, + unseenCount: 0, + hideFromOutlookClients: false, + hideFromAddressLists: false, + assignedLabels: [], + assignedLicenses: [], + serviceProvisioningErrors: [], + onPremisesProvisioningErrors: [], + members: [ + { + '@odata.type': '#microsoft.graph.user', + id: '5f7afebb-121d-4664-882b-a09fe6584ce0', + deletedDateTime: null, + accountEnabled: true, + ageGroup: null, + businessPhones: ['4917600000000'], + city: null, + companyName: null, + consentProvidedForMinor: null, + country: 'US', + createdDateTime: '2024-11-03T16:15:19Z', + creationType: null, + department: null, + displayName: 'John Doe', + employeeId: null, + employeeHireDate: null, + employeeLeaveDateTime: null, + employeeType: null, + externalUserState: null, + externalUserStateChangeDateTime: null, + faxNumber: null, + givenName: 'John', + isLicenseReconciliationNeeded: false, + jobTitle: null, + legalAgeGroupClassification: null, + mail: 'john.doe@contoso.com', + mailNickname: 'johndoe', + mobilePhone: null, + onPremisesDistinguishedName: null, + onPremisesDomainName: null, + onPremisesImmutableId: null, + onPremisesLastSyncDateTime: null, + onPremisesSecurityIdentifier: null, + onPremisesSamAccountName: null, + onPremisesSyncEnabled: null, + onPremisesUserPrincipalName: null, + otherMails: ['matstallmann@gmail.com'], + passwordPolicies: null, + officeLocation: null, + postalCode: null, + preferredDataLocation: null, + preferredLanguage: 'en', + proxyAddresses: ['SMTP:john.doe@contoso.com'], + refreshTokensValidFromDateTime: '2024-11-03T16:15:18Z', + imAddresses: ['john.doe@contoso.com'], + isResourceAccount: null, + showInAddressList: null, + securityIdentifier: 'S-1-12-1-1601896123-1180963357-2678074248-3763099878', + signInSessionsValidFromDateTime: '2024-11-03T16:15:18Z', + state: null, + streetAddress: null, + surname: 'Doe', + usageLocation: 'US', + userPrincipalName: 'john.doe@contoso.com', + userType: 'Member', + employeeOrgData: null, + passwordProfile: null, + assignedLicenses: [ + { + disabledPlans: [], + skuId: 'cbdc14ab-d96c-4c30-b9f4-6ada7cdc1d46', + }, + ], + assignedPlans: [ + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'WhiteboardServices', + servicePlanId: 'b8afc642-032e-4de5-8c0a-507a7bba7e5d', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'exchange', + servicePlanId: 'a6520331-d7d4-4276-95f5-15c0933bc757', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'DynamicsNAV', + servicePlanId: '39b5c996-467e-4e60-bd62-46066f572726', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'WindowsDefenderATP', + servicePlanId: 'bfc1bbd9-981b-4f71-9b82-17c35fd0e2a4', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'SharePoint', + servicePlanId: 'e95bec33-7c88-4a70-8e19-b10bd9d0c014', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'exchange', + servicePlanId: '176a09a6-7ec5-4039-ac02-b2791c6ba793', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'AADPremiumService', + servicePlanId: '41781fb2-bc02-4b7c-bd55-b576c07bb09d', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'exchange', + servicePlanId: '33c4f319-9bdd-48d6-9c4d-410b750a4a5a', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'MicrosoftKaizala', + servicePlanId: '54fc630f-5a40-48ee-8965-af0503c1386e', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'To-Do', + servicePlanId: '5e62787c-c316-451f-b873-1d05acd4d12c', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'Bing', + servicePlanId: '0d0c0d31-fae7-41f2-b909-eaf4d7f26dba', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'ccibotsprod', + servicePlanId: 'ded3d325-1bdc-453e-8432-5bac26d7a014', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'SharePoint', + servicePlanId: 'a1ace008-72f3-4ea0-8dac-33b3a23a2472', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'exchange', + servicePlanId: '9aaf7827-d63c-4b61-89c3-182f06f82e5c', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'exchange', + servicePlanId: '9bec7e34-c9fa-40b7-a9d1-bd6d1165c7ed', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'MixedRealityCollaborationServices', + servicePlanId: 'f0ff6ac6-297d-49cd-be34-6dfef97f0c28', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'RMSOnline', + servicePlanId: '6c57d4b6-3b23-47a5-9bc9-69f17b4947b3', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'RMSOnline', + servicePlanId: 'bea4c11e-220a-4e6d-8eb8-8ea15d019f90', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'MicrosoftOffice', + servicePlanId: '094e7854-93fc-4d55-b2c0-3ab5369ebdc1', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'Sway', + servicePlanId: 'a23b959c-7ce8-4e57-9140-b90eb88a9e97', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'Microsoft.ProjectBabylon', + servicePlanId: 'c948ea65-2053-4a5a-8a62-9eaaaf11b522', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'exchange', + servicePlanId: '5bfe124c-bbdc-4494-8835-f1297d457d79', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'PowerAppsService', + servicePlanId: '92f7a6f3-b89b-4bbd-8c30-809e6da5ad1c', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'MixedRealityCollaborationServices', + servicePlanId: '3efbd4ed-8958-4824-8389-1321f8730af8', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'CRM', + servicePlanId: '28b0fa46-c39a-4188-89e2-58e979a6b014', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'MixedRealityCollaborationServices', + servicePlanId: 'dcf9d2f4-772e-4434-b757-77a453cfbc02', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'ProcessSimple', + servicePlanId: '0f9b09cb-62d1-4ff4-9129-43f4996f83f4', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'YammerEnterprise', + servicePlanId: 'a82fbf69-b4d7-49f4-83a6-915b2cf354f4', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'MultiFactorService', + servicePlanId: '8a256a2b-b617-496d-b51b-e76466e88db0', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'OfficeForms', + servicePlanId: '159f4cd6-e380-449f-a816-af1a9ef76344', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'MicrosoftCommunicationsOnline', + servicePlanId: '0feaeb32-d00e-4d66-bd5a-43b5b83db82c', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'Windows', + servicePlanId: '8e229017-d77b-43d5-9305-903395523b99', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'ProjectProgramsAndPortfolios', + servicePlanId: 'b21a6b06-1988-436e-a07b-51ec6d9f52ad', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'SCO', + servicePlanId: 'c1ec4a95-1f05-45b3-a911-aa3fa01094f5', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'WindowsUpdateforBusinessCloudExtensions', + servicePlanId: '7bf960f6-2cd9-443a-8046-5dbff9558365', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'YammerEnterprise', + servicePlanId: '7547a3fe-08ee-4ccb-b430-5077c5041653', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'CRM', + servicePlanId: 'afa73018-811e-46e9-988f-f75d2b1b8430', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'SharePoint', + servicePlanId: 'c7699d2e-19aa-44de-8edf-1736da088ca1', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'AADPremiumService', + servicePlanId: 'de377cbc-0019-4ec2-b77c-3f223947e102', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'exchange', + servicePlanId: '199a5c09-e0ca-4e37-8f7c-b05d533e1ea2', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'Adallom', + servicePlanId: '932ad362-64a8-4783-9106-97849a1a30b9', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'Chapter5FluidApp', + servicePlanId: 'c4b8c31a-fb44-4c65-9837-a21f55fcabda', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'LearningAppServiceInTeams', + servicePlanId: 'b76fb638-6ba6-402a-b9f9-83d28acb3d86', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'Deskless', + servicePlanId: '8c7d2df8-86f0-4902-b2ed-a0458298f3b3', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'MicrosoftStream', + servicePlanId: '743dd19e-1ce3-4c62-a3ad-49ba8f63a2f6', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'TeamspaceAPI', + servicePlanId: '57ff2da0-773e-42df-b2af-ffb7a2317929', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'MicrosoftOffice', + servicePlanId: '276d6e8a-f056-4f70-b7e8-4fc27f79f809', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'SCO', + servicePlanId: '8e9ff0ff-aa7a-4b20-83c1-2f636b600ac2', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'MicrosoftPrint', + servicePlanId: '795f6fe0-cc4d-4773-b050-5dde4dc704c9', + }, + { + assignedDateTime: '2024-11-04T15:54:26Z', + capabilityStatus: 'Enabled', + service: 'ProjectWorkManagement', + servicePlanId: 'b737dad2-2f6c-4c65-90e3-ca563267e8b9', + }, + ], + authorizationInfo: { + certificateUserIds: [], + }, + identities: [ + { + signInType: 'userPrincipalName', + issuer: 'contoso.com', + issuerAssignedId: 'john.doe@contoso.com', + }, + ], + onPremisesProvisioningErrors: [], + onPremisesExtensionAttributes: { + extensionAttribute1: null, + extensionAttribute2: null, + extensionAttribute3: null, + extensionAttribute4: null, + extensionAttribute5: null, + extensionAttribute6: null, + extensionAttribute7: null, + extensionAttribute8: null, + extensionAttribute9: null, + extensionAttribute10: null, + extensionAttribute11: null, + extensionAttribute12: null, + extensionAttribute13: null, + extensionAttribute14: null, + extensionAttribute15: null, + }, + provisionedPlans: [ + { + capabilityStatus: 'Enabled', + provisioningStatus: 'Success', + service: 'exchange', + }, + { + capabilityStatus: 'Enabled', + provisioningStatus: 'Success', + service: 'exchange', + }, + { + capabilityStatus: 'Enabled', + provisioningStatus: 'Success', + service: 'exchange', + }, + { + capabilityStatus: 'Enabled', + provisioningStatus: 'Success', + service: 'exchange', + }, + { + capabilityStatus: 'Enabled', + provisioningStatus: 'Success', + service: 'exchange', + }, + { + capabilityStatus: 'Enabled', + provisioningStatus: 'Success', + service: 'exchange', + }, + { + capabilityStatus: 'Enabled', + provisioningStatus: 'Success', + service: 'exchange', + }, + { + capabilityStatus: 'Enabled', + provisioningStatus: 'Success', + service: 'MicrosoftCommunicationsOnline', + }, + { + capabilityStatus: 'Enabled', + provisioningStatus: 'Success', + service: 'SharePoint', + }, + { + capabilityStatus: 'Enabled', + provisioningStatus: 'Success', + service: 'SharePoint', + }, + { + capabilityStatus: 'Enabled', + provisioningStatus: 'Success', + service: 'SharePoint', + }, + ], + serviceProvisioningErrors: [], + }, + ], + }, + + postUser: { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#users/$entity', + id: '87d349ed-44d7-43e1-9a83-5f2406dee5bd', + businessPhones: [], + displayName: 'John Doe', + givenName: null, + jobTitle: null, + mail: null, + mobilePhone: null, + officeLocation: null, + preferredLanguage: null, + surname: null, + userPrincipalName: 'johndoe@contoso.com', + }, + + getUser: { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#users/$entity', + businessPhones: ['0123456789'], + displayName: 'John Doe', + givenName: 'John', + jobTitle: 'Project manager', + mail: 'johndoe@contoso.com', + mobilePhone: '+123456789', + officeLocation: 'New York', + preferredLanguage: 'en-US', + surname: 'Doe', + userPrincipalName: 'johndoe@contoso.com', + id: '87d349ed-44d7-43e1-9a83-5f2406dee5bd', + }, + + metadata: { + groups: + '', + users: + '', + }, +}; + +export const microsoftEntraNodeResponse = { + createGroup: [ + { + json: { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#groups/$entity', + id: 'a8eb60e3-0145-4d7e-85ef-c6259784761b', + deletedDateTime: null, + classification: null, + createdDateTime: '2024-11-17T15:46:32Z', + creationOptions: [], + displayName: 'Group Display Name', + expirationDateTime: null, + groupTypes: ['DynamicMembership', 'Unified'], + isAssignableToRole: true, + mail: null, + mailEnabled: true, + mailNickname: 'MailNickname', + membershipRule: 'department -eq "Marketing"', + membershipRuleProcessingState: 'On', + onPremisesDomainName: null, + onPremisesLastSyncDateTime: null, + onPremisesNetBiosName: null, + onPremisesSamAccountName: null, + onPremisesSecurityIdentifier: null, + onPremisesSyncEnabled: null, + preferredLanguage: null, + proxyAddresses: [], + renewedDateTime: '2024-11-17T15:46:32Z', + resourceBehaviorOptions: [], + resourceProvisioningOptions: [], + securityEnabled: true, + securityIdentifier: 'S-1-12-1-2833998051-1300103493-633794437-460752023', + theme: null, + onPremisesProvisioningErrors: [], + serviceProvisioningErrors: [], + description: 'Group Description', + preferredDataLocation: 'Preferred Data Location', + uniqueName: 'UniqueName', + visibility: 'Public', + }, + }, + ], + + deleteGroup: [ + { + json: { + deleted: true, + }, + }, + ], + + getGroup: [{ json: { ...microsoftEntraApiResponse.getGroup } }], + + getGroupWithProperties: [{ json: { ...microsoftEntraApiResponse.getGroupWithProperties } }], + + updateGroup: [ + { + json: { + updated: true, + }, + }, + ], + + addUserToGroup: [ + { + json: { + added: true, + }, + }, + ], + + createUser: [ + { + json: { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#users/$entity', + id: '87d349ed-44d7-43e1-9a83-5f2406dee5bd', + businessPhones: ['0123456789'], + displayName: 'John Doe', + givenName: 'John', + jobTitle: 'Project manager', + mail: 'johndoe@contoso.com', + mobilePhone: '+123456789', + officeLocation: 'New York', + preferredLanguage: 'en-US', + // mailNickname: 'johndoe', + userPrincipalName: 'johndoe@contoso.com', + aboutMe: 'About me', + ageGroup: 'Adult', + birthday: '2024-11-12T00:00:00.000Z', + city: 'New York', + companyName: 'Contoso', + consentProvidedForMinor: 'Granted', + country: 'US', + department: 'IT', + employeeId: 'employee-id-123', + employeeType: 'Contractor', + employeeHireDate: '2024-11-13T00:00:00.000Z', + employeeLeaveDateTime: '2024-11-18T00:00:00.000Z', + employeeOrgData: { + costCenter: 'Cost Center 1', + division: 'Division 1', + }, + interests: ['interest1', 'interest2'], + surname: 'Doe', + mySite: 'My Site', + onPremisesImmutableId: 'premiseid123', + otherMails: ['johndoe2@contoso.com', 'johndoe3@contoso.com'], + passwordPolicies: 'DisablePasswordExpiration,DisableStrongPassword', + passwordProfile: { + forceChangePasswordNextSignInWithMfa: true, + }, + pastProjects: ['project1', 'project2'], + postalCode: '0123456', + responsibilities: ['responsibility1', 'responsibility2'], + schools: ['school1', 'school2'], + skills: ['skill1', 'skill2'], + state: 'New York', + streetAddress: 'Street 123', + usageLocation: 'US', + userType: 'Guest', + }, + }, + ], + + deleteUser: [ + { + json: { + deleted: true, + }, + }, + ], + + getUser: [ + { + json: { + '@odata.context': 'https://graph.microsoft.com/v1.0/$metadata#users/$entity', + businessPhones: ['0123456789'], + displayName: 'John Doe', + givenName: 'John', + jobTitle: 'Project manager', + mail: 'johndoe@contoso.com', + mobilePhone: '+123456789', + officeLocation: 'New York', + preferredLanguage: 'en-US', + surname: 'Doe', + userPrincipalName: 'johndoe@contoso.com', + id: '87d349ed-44d7-43e1-9a83-5f2406dee5bd', + }, + }, + ], + + removeUserFromGroup: [ + { + json: { + removed: true, + }, + }, + ], + + updateUser: [ + { + json: { + updated: true, + }, + }, + ], + + loadOptions: { + getGroupProperties: [ + { + name: 'allowExternalSenders', + value: 'allowExternalSenders', + }, + { + name: 'assignedLabels', + value: 'assignedLabels', + }, + { + name: 'assignedLicenses', + value: 'assignedLicenses', + }, + { + name: 'autoSubscribeNewMembers', + value: 'autoSubscribeNewMembers', + }, + { + name: 'classification', + value: 'classification', + }, + { + name: 'createdDateTime', + value: 'createdDateTime', + }, + { + name: 'deletedDateTime', + value: 'deletedDateTime', + }, + { + name: 'description', + value: 'description', + }, + { + name: 'displayName', + value: 'displayName', + }, + { + name: 'expirationDateTime', + value: 'expirationDateTime', + }, + { + name: 'groupTypes', + value: 'groupTypes', + }, + { + name: 'hideFromAddressLists', + value: 'hideFromAddressLists', + }, + { + name: 'hideFromOutlookClients', + value: 'hideFromOutlookClients', + }, + // { + // name: 'id', + // value: 'id', + // }, + { + name: 'isAssignableToRole', + value: 'isAssignableToRole', + }, + { + name: 'isManagementRestricted', + value: 'isManagementRestricted', + }, + { + name: 'isSubscribedByMail', + value: 'isSubscribedByMail', + }, + { + name: 'licenseProcessingState', + value: 'licenseProcessingState', + }, + { + name: 'mail', + value: 'mail', + }, + { + name: 'mailEnabled', + value: 'mailEnabled', + }, + { + name: 'mailNickname', + value: 'mailNickname', + }, + { + name: 'membershipRule', + value: 'membershipRule', + }, + { + name: 'membershipRuleProcessingState', + value: 'membershipRuleProcessingState', + }, + { + name: 'onPremisesDomainName', + value: 'onPremisesDomainName', + }, + { + name: 'onPremisesLastSyncDateTime', + value: 'onPremisesLastSyncDateTime', + }, + { + name: 'onPremisesNetBiosName', + value: 'onPremisesNetBiosName', + }, + { + name: 'onPremisesProvisioningErrors', + value: 'onPremisesProvisioningErrors', + }, + { + name: 'onPremisesSamAccountName', + value: 'onPremisesSamAccountName', + }, + { + name: 'onPremisesSecurityIdentifier', + value: 'onPremisesSecurityIdentifier', + }, + { + name: 'onPremisesSyncEnabled', + value: 'onPremisesSyncEnabled', + }, + { + name: 'preferredDataLocation', + value: 'preferredDataLocation', + }, + { + name: 'preferredLanguage', + value: 'preferredLanguage', + }, + { + name: 'proxyAddresses', + value: 'proxyAddresses', + }, + { + name: 'renewedDateTime', + value: 'renewedDateTime', + }, + { + name: 'securityEnabled', + value: 'securityEnabled', + }, + { + name: 'securityIdentifier', + value: 'securityIdentifier', + }, + { + name: 'serviceProvisioningErrors', + value: 'serviceProvisioningErrors', + }, + { + name: 'theme', + value: 'theme', + }, + { + name: 'uniqueName', + value: 'uniqueName', + }, + { + name: 'unseenCount', + value: 'unseenCount', + }, + { + name: 'visibility', + value: 'visibility', + }, + ], + + getUserProperties: [ + { + name: 'aboutMe', + value: 'aboutMe', + }, + { + name: 'accountEnabled', + value: 'accountEnabled', + }, + { + name: 'ageGroup', + value: 'ageGroup', + }, + { + name: 'assignedLicenses', + value: 'assignedLicenses', + }, + { + name: 'assignedPlans', + value: 'assignedPlans', + }, + { + name: 'authorizationInfo', + value: 'authorizationInfo', + }, + { + name: 'birthday', + value: 'birthday', + }, + { + name: 'businessPhones', + value: 'businessPhones', + }, + { + name: 'city', + value: 'city', + }, + { + name: 'companyName', + value: 'companyName', + }, + { + name: 'consentProvidedForMinor', + value: 'consentProvidedForMinor', + }, + { + name: 'country', + value: 'country', + }, + { + name: 'createdDateTime', + value: 'createdDateTime', + }, + { + name: 'creationType', + value: 'creationType', + }, + { + name: 'customSecurityAttributes', + value: 'customSecurityAttributes', + }, + { + name: 'deletedDateTime', + value: 'deletedDateTime', + }, + { + name: 'department', + value: 'department', + }, + { + name: 'displayName', + value: 'displayName', + }, + { + name: 'employeeHireDate', + value: 'employeeHireDate', + }, + { + name: 'employeeId', + value: 'employeeId', + }, + { + name: 'employeeLeaveDateTime', + value: 'employeeLeaveDateTime', + }, + { + name: 'employeeOrgData', + value: 'employeeOrgData', + }, + { + name: 'employeeType', + value: 'employeeType', + }, + { + name: 'externalUserState', + value: 'externalUserState', + }, + { + name: 'externalUserStateChangeDateTime', + value: 'externalUserStateChangeDateTime', + }, + { + name: 'faxNumber', + value: 'faxNumber', + }, + { + name: 'givenName', + value: 'givenName', + }, + { + name: 'hireDate', + value: 'hireDate', + }, + // { + // name: 'id', + // value: 'id', + // }, + { + name: 'identities', + value: 'identities', + }, + { + name: 'imAddresses', + value: 'imAddresses', + }, + { + name: 'interests', + value: 'interests', + }, + { + name: 'isManagementRestricted', + value: 'isManagementRestricted', + }, + { + name: 'isResourceAccount', + value: 'isResourceAccount', + }, + { + name: 'jobTitle', + value: 'jobTitle', + }, + { + name: 'lastPasswordChangeDateTime', + value: 'lastPasswordChangeDateTime', + }, + { + name: 'legalAgeGroupClassification', + value: 'legalAgeGroupClassification', + }, + { + name: 'licenseAssignmentStates', + value: 'licenseAssignmentStates', + }, + { + name: 'mail', + value: 'mail', + }, + { + name: 'mailNickname', + value: 'mailNickname', + }, + // { + // name: 'mailboxSettings', + // value: 'mailboxSettings', + // }, + { + name: 'mobilePhone', + value: 'mobilePhone', + }, + { + name: 'mySite', + value: 'mySite', + }, + { + name: 'officeLocation', + value: 'officeLocation', + }, + { + name: 'onPremisesDistinguishedName', + value: 'onPremisesDistinguishedName', + }, + { + name: 'onPremisesDomainName', + value: 'onPremisesDomainName', + }, + { + name: 'onPremisesExtensionAttributes', + value: 'onPremisesExtensionAttributes', + }, + { + name: 'onPremisesImmutableId', + value: 'onPremisesImmutableId', + }, + { + name: 'onPremisesLastSyncDateTime', + value: 'onPremisesLastSyncDateTime', + }, + { + name: 'onPremisesProvisioningErrors', + value: 'onPremisesProvisioningErrors', + }, + { + name: 'onPremisesSamAccountName', + value: 'onPremisesSamAccountName', + }, + { + name: 'onPremisesSecurityIdentifier', + value: 'onPremisesSecurityIdentifier', + }, + { + name: 'onPremisesSyncEnabled', + value: 'onPremisesSyncEnabled', + }, + { + name: 'onPremisesUserPrincipalName', + value: 'onPremisesUserPrincipalName', + }, + { + name: 'otherMails', + value: 'otherMails', + }, + { + name: 'passwordPolicies', + value: 'passwordPolicies', + }, + { + name: 'passwordProfile', + value: 'passwordProfile', + }, + { + name: 'pastProjects', + value: 'pastProjects', + }, + { + name: 'postalCode', + value: 'postalCode', + }, + { + name: 'preferredDataLocation', + value: 'preferredDataLocation', + }, + { + name: 'preferredLanguage', + value: 'preferredLanguage', + }, + { + name: 'preferredName', + value: 'preferredName', + }, + { + name: 'provisionedPlans', + value: 'provisionedPlans', + }, + { + name: 'proxyAddresses', + value: 'proxyAddresses', + }, + { + name: 'responsibilities', + value: 'responsibilities', + }, + { + name: 'schools', + value: 'schools', + }, + { + name: 'securityIdentifier', + value: 'securityIdentifier', + }, + { + name: 'serviceProvisioningErrors', + value: 'serviceProvisioningErrors', + }, + { + name: 'showInAddressList', + value: 'showInAddressList', + }, + // { + // name: 'signInActivity', + // value: 'signInActivity', + // }, + { + name: 'signInSessionsValidFromDateTime', + value: 'signInSessionsValidFromDateTime', + }, + { + name: 'skills', + value: 'skills', + }, + { + name: 'state', + value: 'state', + }, + { + name: 'streetAddress', + value: 'streetAddress', + }, + { + name: 'surname', + value: 'surname', + }, + { + name: 'usageLocation', + value: 'usageLocation', + }, + { + name: 'userPrincipalName', + value: 'userPrincipalName', + }, + { + name: 'userType', + value: 'userType', + }, + ], + }, +}; diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 51bbf93feaff0..8ee27fe195636 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -626,6 +626,7 @@ "dist/nodes/MessageBird/MessageBird.node.js", "dist/nodes/Metabase/Metabase.node.js", "dist/nodes/Microsoft/Dynamics/MicrosoftDynamicsCrm.node.js", + "dist/nodes/Microsoft/Entra/MicrosoftEntra.node.js", "dist/nodes/Microsoft/Excel/MicrosoftExcel.node.js", "dist/nodes/Microsoft/GraphSecurity/MicrosoftGraphSecurity.node.js", "dist/nodes/Microsoft/OneDrive/MicrosoftOneDrive.node.js", diff --git a/packages/nodes-base/test/nodes/FakeCredentialsMap.ts b/packages/nodes-base/test/nodes/FakeCredentialsMap.ts index 4daacbfd07012..96445973e92e7 100644 --- a/packages/nodes-base/test/nodes/FakeCredentialsMap.ts +++ b/packages/nodes-base/test/nodes/FakeCredentialsMap.ts @@ -80,6 +80,30 @@ BQIDAQAB }, baseUrl: 'https://api.gong.io', }, + microsoftEntraOAuth2Api: { + grantType: 'authorizationCode', + authUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', + accessTokenUrl: 'https://login.microsoftonline.com/common/oauth2/v2.0/token', + clientId: 'CLIENTID', + clientSecret: 'CLIENTSECRET', + scope: + 'openid offline_access AccessReview.ReadWrite.All Directory.ReadWrite.All NetworkAccessPolicy.ReadWrite.All DelegatedAdminRelationship.ReadWrite.All EntitlementManagement.ReadWrite.All User.ReadWrite.All Directory.AccessAsUser.All Sites.FullControl.All', + authQueryParameters: 'response_mode=query', + authentication: 'body', + oauthTokenData: { + token_type: 'Bearer', + scope: + 'AccessReview.ReadWrite.All DelegatedAdminRelationship.ReadWrite.All Directory.AccessAsUser.All Directory.Read.All Directory.ReadWrite.All EntitlementManagement.ReadWrite.All Group.ReadWrite.All NetworkAccessPolicy.ReadWrite.All openid Sites.FullControl.All User.DeleteRestore.All User.EnableDisableAccount.All User.Export.All User.Invite.All User.ManageIdentities.All User.Read User.Read.All User.ReadBasic.All User.ReadWrite User.ReadWrite.All User.RevokeSessions.All profile email', + expires_in: 4822, + ext_expires_in: 4822, + access_token: 'ACCESSTOKEN', + refresh_token: 'REFRESHTOKEN', + id_token: 'IDTOKEN', + callbackQueryString: { + session_state: 'SESSIONSTATE', + }, + }, + }, n8nApi: { apiKey: 'key123', baseUrl: 'https://test.app.n8n.cloud/api/v1', diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 5711de38ca824..2161569b7d35e 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2451,7 +2451,7 @@ export interface WorkflowTestData { nock?: { baseUrl: string; mocks: Array<{ - method: 'delete' | 'get' | 'post' | 'put'; + method: 'delete' | 'get' | 'patch' | 'post' | 'put'; path: string; requestBody?: RequestBodyMatcher; statusCode: number; From 8cce5882092a85a0371ac954f5998c60f94f1585 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Milorad=20FIlipovi=C4=87?= Date: Wed, 8 Jan 2025 12:11:11 +0100 Subject: [PATCH 2/4] test(editor): Update workflow actions tests for the new canvas (no-changelog) (#12245) Co-authored-by: Alex Grozav --- cypress/e2e/7-workflow-actions.cy.ts | 19 +++++++++++++++++-- packages/editor-ui/src/views/NodeView.v2.vue | 5 +++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/7-workflow-actions.cy.ts b/cypress/e2e/7-workflow-actions.cy.ts index 8571b174d9f43..f0f3ae019a2dd 100644 --- a/cypress/e2e/7-workflow-actions.cy.ts +++ b/cypress/e2e/7-workflow-actions.cy.ts @@ -171,9 +171,16 @@ describe('Workflow Actions', () => { cy.get('#node-creator').should('not.exist'); WorkflowPage.actions.hitSelectAll(); - cy.get('.jtk-drag-selected').should('have.length', 2); WorkflowPage.actions.hitCopy(); successToast().should('exist'); + // Both nodes should be copied + cy.window() + .its('navigator.clipboard') + .then((clip) => clip.readText()) + .then((text) => { + const copiedWorkflow = JSON.parse(text); + expect(copiedWorkflow.nodes).to.have.length(2); + }); }); it('should paste nodes (both current and old node versions)', () => { @@ -345,7 +352,15 @@ describe('Workflow Actions', () => { WorkflowPage.actions.hitDeleteAllNodes(); WorkflowPage.getters.canvasNodes().should('have.length', 0); // Button should be disabled - WorkflowPage.getters.executeWorkflowButton().should('be.disabled'); + cy.ifCanvasVersion( + () => { + WorkflowPage.getters.executeWorkflowButton().should('be.disabled'); + }, + () => { + // In new canvas, button does not exist when there are no nodes + WorkflowPage.getters.executeWorkflowButton().should('not.exist'); + }, + ); // Keyboard shortcut should not work WorkflowPage.actions.hitExecuteWorkflow(); successToast().should('not.exist'); diff --git a/packages/editor-ui/src/views/NodeView.v2.vue b/packages/editor-ui/src/views/NodeView.v2.vue index ee3fc857c1523..fda3f17e6f03d 100644 --- a/packages/editor-ui/src/views/NodeView.v2.vue +++ b/packages/editor-ui/src/views/NodeView.v2.vue @@ -697,6 +697,11 @@ function onPinNodes(ids: string[], source: PinDataSource) { } async function onSaveWorkflow() { + const workflowIsSaved = !uiStore.stateIsDirty; + + if (workflowIsSaved) { + return; + } const saved = await workflowHelpers.saveCurrentWorkflow(); if (saved) { canvasEventBus.emit('saved:workflow'); From 0ecce10faf60ae44d11007d45e87766b678d3a84 Mon Sep 17 00:00:00 2001 From: Alex Grozav Date: Wed, 8 Jan 2025 13:11:20 +0200 Subject: [PATCH 3/4] fix(editor): Improve configurable nodes design on new canvas (#12317) --- .../canvas/elements/nodes/CanvasNode.test.ts | 27 ++++++ .../canvas/elements/nodes/CanvasNode.vue | 97 +++++++++++-------- .../nodes/render-types/CanvasNodeDefault.vue | 6 ++ .../editor-ui/src/utils/canvasUtilsV2.test.ts | 74 +++++++++++++- packages/editor-ui/src/utils/canvasUtilsV2.ts | 20 ++++ 5 files changed, 182 insertions(+), 42 deletions(-) diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.test.ts b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.test.ts index 6b72cdf037afa..816310ac7d5d8 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.test.ts +++ b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.test.ts @@ -84,6 +84,33 @@ describe('CanvasNode', () => { expect(inputHandles.length).toBe(3); expect(outputHandles.length).toBe(2); }); + + it('should insert spacers after required non-main input handle', () => { + const { getAllByTestId } = renderComponent({ + props: { + ...createCanvasNodeProps({ + data: { + inputs: [ + { type: NodeConnectionType.Main, index: 0 }, + { type: NodeConnectionType.AiAgent, index: 0, required: true }, + { type: NodeConnectionType.AiTool, index: 0 }, + ], + outputs: [], + }, + }), + }, + global: { + stubs: { + Handle: true, + }, + }, + }); + + const inputHandles = getAllByTestId('canvas-node-input-handle'); + + expect(inputHandles[1]).toHaveStyle('left: 20%'); + expect(inputHandles[2]).toHaveStyle('left: 80%'); + }); }); describe('toolbar', () => { diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue index 5dbd08db32563..a0b812489d1b5 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/CanvasNode.vue @@ -28,7 +28,10 @@ import { useContextMenu } from '@/composables/useContextMenu'; import type { NodeProps, XYPosition } from '@vue-flow/core'; import { Position } from '@vue-flow/core'; import { useCanvas } from '@/composables/useCanvas'; -import { createCanvasConnectionHandleString } from '@/utils/canvasUtilsV2'; +import { + createCanvasConnectionHandleString, + insertSpacersBetweenEndpoints, +} from '@/utils/canvasUtilsV2'; import type { EventBus } from 'n8n-design-system'; import { createEventBus } from 'n8n-design-system'; import { isEqual } from 'lodash-es'; @@ -77,12 +80,18 @@ const nodeClasses = ref([]); const inputs = computed(() => props.data.inputs); const outputs = computed(() => props.data.outputs); const connections = computed(() => props.data.connections); -const { mainInputs, nonMainInputs, mainOutputs, nonMainOutputs, isValidConnection } = - useNodeConnections({ - inputs, - outputs, - connections, - }); +const { + mainInputs, + nonMainInputs, + requiredNonMainInputs, + mainOutputs, + nonMainOutputs, + isValidConnection, +} = useNodeConnections({ + inputs, + outputs, + connections, +}); const isDisabled = computed(() => props.data.disabled); @@ -114,23 +123,15 @@ function emitCanvasNodeEvent(event: CanvasEventBusEvents['nodes:action']) { * Inputs */ +const nonMainInputsWithSpacer = computed(() => + insertSpacersBetweenEndpoints(nonMainInputs.value, requiredNonMainInputs.value.length), +); + const mappedInputs = computed(() => { return [ - ...mainInputs.value.map( - createEndpointMappingFn({ - mode: CanvasConnectionMode.Input, - position: Position.Left, - offsetAxis: 'top', - }), - ), - ...nonMainInputs.value.map( - createEndpointMappingFn({ - mode: CanvasConnectionMode.Input, - position: Position.Bottom, - offsetAxis: 'left', - }), - ), - ]; + ...mainInputs.value.map(mainInputsMappingFn), + ...nonMainInputsWithSpacer.value.map(nonMainInputsMappingFn), + ].filter((endpoint) => !!endpoint); }); /** @@ -139,21 +140,9 @@ const mappedInputs = computed(() => { const mappedOutputs = computed(() => { return [ - ...mainOutputs.value.map( - createEndpointMappingFn({ - mode: CanvasConnectionMode.Output, - position: Position.Right, - offsetAxis: 'top', - }), - ), - ...nonMainOutputs.value.map( - createEndpointMappingFn({ - mode: CanvasConnectionMode.Output, - position: Position.Top, - offsetAxis: 'left', - }), - ), - ]; + ...mainOutputs.value.map(mainOutputsMappingFn), + ...nonMainOutputs.value.map(nonMainOutputsMappingFn), + ].filter((endpoint) => !!endpoint); }); /** @@ -179,10 +168,14 @@ const createEndpointMappingFn = offsetAxis: 'top' | 'left'; }) => ( - endpoint: CanvasConnectionPort, + endpoint: CanvasConnectionPort | null, index: number, - endpoints: CanvasConnectionPort[], - ): CanvasElementPortWithRenderData => { + endpoints: Array, + ): CanvasElementPortWithRenderData | undefined => { + if (!endpoint) { + return; + } + const handleId = createCanvasConnectionHandleString({ mode, type: endpoint.type, @@ -207,6 +200,30 @@ const createEndpointMappingFn = }; }; +const mainInputsMappingFn = createEndpointMappingFn({ + mode: CanvasConnectionMode.Input, + position: Position.Left, + offsetAxis: 'top', +}); + +const nonMainInputsMappingFn = createEndpointMappingFn({ + mode: CanvasConnectionMode.Input, + position: Position.Bottom, + offsetAxis: 'left', +}); + +const mainOutputsMappingFn = createEndpointMappingFn({ + mode: CanvasConnectionMode.Output, + position: Position.Right, + offsetAxis: 'top', +}); + +const nonMainOutputsMappingFn = createEndpointMappingFn({ + mode: CanvasConnectionMode.Output, + position: Position.Top, + offsetAxis: 'left', +}); + /** * Events */ diff --git a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue index 9c9f864a8601f..2481831249e9c 100644 --- a/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue +++ b/packages/editor-ui/src/components/canvas/elements/nodes/render-types/CanvasNodeDefault.vue @@ -201,6 +201,12 @@ function openContextMenu(event: MouseEvent) { var(--configurable-node--input-width) ); + justify-content: flex-start; + + :global(.n8n-node-icon) { + margin-left: var(--configurable-node--icon-offset); + } + .description { top: unset; position: relative; diff --git a/packages/editor-ui/src/utils/canvasUtilsV2.test.ts b/packages/editor-ui/src/utils/canvasUtilsV2.test.ts index 61425172b345f..63c73218088b5 100644 --- a/packages/editor-ui/src/utils/canvasUtilsV2.test.ts +++ b/packages/editor-ui/src/utils/canvasUtilsV2.test.ts @@ -1,14 +1,15 @@ import { + checkOverlap, createCanvasConnectionHandleString, createCanvasConnectionId, + insertSpacersBetweenEndpoints, mapCanvasConnectionToLegacyConnection, mapLegacyConnectionsToCanvasConnections, mapLegacyEndpointsToCanvasConnectionPort, parseCanvasConnectionHandleString, - checkOverlap, } from '@/utils/canvasUtilsV2'; +import type { IConnection, IConnections, INodeTypeDescription } from 'n8n-workflow'; import { NodeConnectionType } from 'n8n-workflow'; -import type { IConnections, INodeTypeDescription, IConnection } from 'n8n-workflow'; import type { CanvasConnection } from '@/types'; import { CanvasConnectionMode } from '@/types'; import type { INodeUi } from '@/Interface'; @@ -976,3 +977,72 @@ describe('checkOverlap', () => { expect(checkOverlap(node1, node2)).toBe(false); }); }); + +describe('insertSpacersBetweenEndpoints', () => { + it('should insert spacers when there are less than min endpoints count', () => { + const endpoints = [{ index: 0, required: true }]; + const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length; + const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4); + expect(result).toEqual([{ index: 0, required: true }, null, null, null]); + }); + + it('should not insert spacers when there are at least min endpoints count', () => { + const endpoints = [{ index: 0, required: true }, { index: 1 }, { index: 2 }, { index: 3 }]; + const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length; + const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4); + expect(result).toEqual(endpoints); + }); + + it('should handle zero required endpoints', () => { + const endpoints = [{ index: 0, required: false }]; + const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length; + const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4); + expect(result).toEqual([null, null, null, { index: 0, required: false }]); + }); + + it('should handle no endpoints', () => { + const endpoints: Array<{ index: number; required: boolean }> = []; + const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length; + const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4); + expect(result).toEqual([null, null, null, null]); + }); + + it('should handle required endpoints greater than min endpoints count', () => { + const endpoints = [ + { index: 0, required: true }, + { index: 1, required: true }, + { index: 2, required: true }, + { index: 3, required: true }, + { index: 4, required: true }, + ]; + const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length; + const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4); + expect(result).toEqual(endpoints); + }); + + it('should insert spacers between required and optional endpoints', () => { + const endpoints = [{ index: 0, required: true }, { index: 1, required: true }, { index: 2 }]; + const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length; + const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4); + expect(result).toEqual([ + { index: 0, required: true }, + { index: 1, required: true }, + null, + { index: 2 }, + ]); + }); + + it('should handle required endpoints count greater than endpoints length', () => { + const endpoints = [{ index: 0, required: true }]; + const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length; + const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 4); + expect(result).toEqual([{ index: 0, required: true }, null, null, null]); + }); + + it('should handle min endpoints count less than required endpoints count', () => { + const endpoints = [{ index: 0, required: false }]; + const requiredEndpointsCount = endpoints.filter((endpoint) => endpoint.required).length; + const result = insertSpacersBetweenEndpoints(endpoints, requiredEndpointsCount, 0); + expect(result).toEqual([{ index: 0, required: false }]); + }); +}); diff --git a/packages/editor-ui/src/utils/canvasUtilsV2.ts b/packages/editor-ui/src/utils/canvasUtilsV2.ts index d08f817d8252f..f8e412caea754 100644 --- a/packages/editor-ui/src/utils/canvasUtilsV2.ts +++ b/packages/editor-ui/src/utils/canvasUtilsV2.ts @@ -210,3 +210,23 @@ export function checkOverlap(node1: BoundingBox, node2: BoundingBox) { ) ); } + +export function insertSpacersBetweenEndpoints( + endpoints: T[], + requiredEndpointsCount = 0, + minEndpointsCount = 4, +) { + const endpointsWithSpacers: Array = [...endpoints]; + const optionalNonMainInputsCount = endpointsWithSpacers.length - requiredEndpointsCount; + const spacerCount = minEndpointsCount - requiredEndpointsCount - optionalNonMainInputsCount; + + // Insert `null` in between required non-main inputs and non-required non-main inputs + // to separate them visually if there are less than 4 inputs in total + if (endpointsWithSpacers.length < minEndpointsCount) { + for (let i = 0; i < spacerCount; i++) { + endpointsWithSpacers.splice(requiredEndpointsCount + i, 0, null); + } + } + + return endpointsWithSpacers; +} From 4e50c60bbac9576f56bd739bcc89f69ba9a3aafd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Wed, 8 Jan 2025 12:25:42 +0100 Subject: [PATCH 4/4] Update pnpm-lock.yaml --- pnpm-lock.yaml | 44 ++++++++++++++++++++++++++------------------ 1 file changed, 26 insertions(+), 18 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 808598c7fec76..679b1155d2297 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -440,7 +440,7 @@ importers: version: 3.666.0(@aws-sdk/client-sts@3.666.0) '@getzep/zep-cloud': specifier: 1.0.12 - version: 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i)) + version: 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu)) '@getzep/zep-js': specifier: 0.9.0 version: 0.9.0 @@ -467,7 +467,7 @@ importers: version: 0.3.1(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13) '@langchain/community': specifier: 0.3.15 - version: 0.3.15(v4qhcw25bevfr6xzz4fnsvjiqe) + version: 0.3.15(vc5hvyy27o4cmm4jplsptc2fqm) '@langchain/core': specifier: 'catalog:' version: 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) @@ -554,7 +554,7 @@ importers: version: 23.0.1 langchain: specifier: 0.3.6 - version: 0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i) + version: 0.3.6(e4rnrwhosnp2xiru36mqgdy2bu) lodash: specifier: 'catalog:' version: 4.17.21 @@ -15764,7 +15764,7 @@ snapshots: '@gar/promisify@1.1.3': optional: true - '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i))': + '@getzep/zep-cloud@1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu))': dependencies: form-data: 4.0.0 node-fetch: 2.7.0(encoding@0.1.13) @@ -15773,7 +15773,7 @@ snapshots: zod: 3.23.8 optionalDependencies: '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) - langchain: 0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i) + langchain: 0.3.6(e4rnrwhosnp2xiru36mqgdy2bu) transitivePeerDependencies: - encoding @@ -16237,7 +16237,7 @@ snapshots: - aws-crt - encoding - '@langchain/community@0.3.15(v4qhcw25bevfr6xzz4fnsvjiqe)': + '@langchain/community@0.3.15(vc5hvyy27o4cmm4jplsptc2fqm)': dependencies: '@ibm-cloud/watsonx-ai': 1.1.2 '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) @@ -16247,7 +16247,7 @@ snapshots: flat: 5.0.2 ibm-cloud-sdk-core: 5.1.0 js-yaml: 4.1.0 - langchain: 0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i) + langchain: 0.3.6(e4rnrwhosnp2xiru36mqgdy2bu) langsmith: 0.2.3(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) uuid: 10.0.0 zod: 3.23.8 @@ -16260,7 +16260,7 @@ snapshots: '@aws-sdk/client-s3': 3.666.0 '@aws-sdk/credential-provider-node': 3.666.0(@aws-sdk/client-sso-oidc@3.666.0(@aws-sdk/client-sts@3.666.0))(@aws-sdk/client-sts@3.666.0) '@azure/storage-blob': 12.18.0(encoding@0.1.13) - '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i)) + '@getzep/zep-cloud': 1.0.12(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13)(langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu)) '@getzep/zep-js': 0.9.0 '@google-ai/generativelanguage': 2.6.0(encoding@0.1.13) '@google-cloud/storage': 7.12.1(encoding@0.1.13) @@ -19553,6 +19553,14 @@ snapshots: transitivePeerDependencies: - debug + axios@1.7.4(debug@4.3.7): + dependencies: + follow-redirects: 1.15.6(debug@4.3.7) + form-data: 4.0.0 + proxy-from-env: 1.1.0 + transitivePeerDependencies: + - debug + axios@1.7.7: dependencies: follow-redirects: 1.15.6(debug@4.3.6) @@ -21276,7 +21284,7 @@ snapshots: eslint-import-resolver-node@0.3.9: dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) is-core-module: 2.13.1 resolve: 1.22.8 transitivePeerDependencies: @@ -21301,7 +21309,7 @@ snapshots: eslint-module-utils@2.8.0(@typescript-eslint/parser@7.2.0(eslint@8.57.0)(typescript@5.7.2))(eslint-import-resolver-node@0.3.9)(eslint-import-resolver-typescript@3.6.1)(eslint@8.57.0): dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) optionalDependencies: '@typescript-eslint/parser': 7.2.0(eslint@8.57.0)(typescript@5.7.2) eslint: 8.57.0 @@ -21321,7 +21329,7 @@ snapshots: array.prototype.findlastindex: 1.2.3 array.prototype.flat: 1.3.2 array.prototype.flatmap: 1.3.2 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) doctrine: 2.1.0 eslint: 8.57.0 eslint-import-resolver-node: 0.3.9 @@ -22100,7 +22108,7 @@ snapshots: array-parallel: 0.1.3 array-series: 0.1.5 cross-spawn: 4.0.2 - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) transitivePeerDependencies: - supports-color @@ -22406,7 +22414,7 @@ snapshots: '@types/debug': 4.1.12 '@types/node': 18.16.16 '@types/tough-cookie': 4.0.2 - axios: 1.7.4 + axios: 1.7.4(debug@4.3.7) camelcase: 6.3.0 debug: 4.3.7 dotenv: 16.4.5 @@ -22416,7 +22424,7 @@ snapshots: isstream: 0.1.2 jsonwebtoken: 9.0.2 mime-types: 2.1.35 - retry-axios: 2.6.0(axios@1.7.4(debug@4.3.7)) + retry-axios: 2.6.0(axios@1.7.4) tough-cookie: 4.1.3 transitivePeerDependencies: - supports-color @@ -23421,7 +23429,7 @@ snapshots: kuler@2.0.0: {} - langchain@0.3.6(4axcxpjbcq5bce7ff6ajxrpp4i): + langchain@0.3.6(e4rnrwhosnp2xiru36mqgdy2bu): dependencies: '@langchain/core': 0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)) '@langchain/openai': 0.3.14(@langchain/core@0.3.19(openai@4.73.1(encoding@0.1.13)(zod@3.23.8)))(encoding@0.1.13) @@ -24991,7 +24999,7 @@ snapshots: pdf-parse@1.1.1: dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) node-ensure: 0.0.0 transitivePeerDependencies: - supports-color @@ -25813,7 +25821,7 @@ snapshots: ret@0.1.15: {} - retry-axios@2.6.0(axios@1.7.4(debug@4.3.7)): + retry-axios@2.6.0(axios@1.7.4): dependencies: axios: 1.7.4 @@ -25840,7 +25848,7 @@ snapshots: rhea@1.0.24: dependencies: - debug: 3.2.7(supports-color@8.1.1) + debug: 3.2.7(supports-color@5.5.0) transitivePeerDependencies: - supports-color