mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 01:56:46 +00:00
feat(n8n Microsoft Teams Node): New trigger node (#12875)
This commit is contained in:
@@ -17,5 +17,18 @@ export class MicrosoftTeamsOAuth2Api implements ICredentialType {
|
|||||||
type: 'hidden',
|
type: 'hidden',
|
||||||
default: 'openid offline_access User.ReadWrite.All Group.ReadWrite.All Chat.ReadWrite',
|
default: 'openid offline_access User.ReadWrite.All Group.ReadWrite.All Chat.ReadWrite',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
displayName: `
|
||||||
|
Microsoft Teams Trigger requires the following permissions:
|
||||||
|
<br><code>ChannelMessage.Read.All</code>
|
||||||
|
<br><code>Chat.Read.All</code>
|
||||||
|
<br><code>Team.ReadBasic.All</code>
|
||||||
|
<br><code>Subscription.ReadWrite.All</code>
|
||||||
|
<br>Configure these permissions in <a href="https://portal.azure.com">Microsoft Entra</a>
|
||||||
|
`,
|
||||||
|
name: 'notice',
|
||||||
|
type: 'notice',
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"node": "n8n-nodes-base.microsoftTeamsTrigger",
|
||||||
|
"nodeVersion": "1.0",
|
||||||
|
"codexVersion": "1.0",
|
||||||
|
"categories": ["Communication"],
|
||||||
|
"resources": {
|
||||||
|
"credentialDocumentation": [
|
||||||
|
{
|
||||||
|
"url": "https://docs.n8n.io/integrations/builtin/credentials/microsoft/"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"primaryDocumentation": [
|
||||||
|
{
|
||||||
|
"url": "https://docs.n8n.io/integrations/builtin/trigger-nodes/n8n-nodes-base.microsoftteamstrigger/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,397 @@
|
|||||||
|
import type {
|
||||||
|
IExecuteFunctions,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeDescription,
|
||||||
|
IHookFunctions,
|
||||||
|
IWebhookFunctions,
|
||||||
|
IWebhookResponseData,
|
||||||
|
IDataObject,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
JsonObject,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import { NodeApiError, NodeConnectionTypes } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import type { WebhookNotification, SubscriptionResponse } from './v2/helpers/types';
|
||||||
|
import { createSubscription, getResourcePath } from './v2/helpers/utils-trigger';
|
||||||
|
import { listSearch } from './v2/methods';
|
||||||
|
import { microsoftApiRequest, microsoftApiRequestAllItems } from './v2/transport';
|
||||||
|
|
||||||
|
export class MicrosoftTeamsTrigger implements INodeType {
|
||||||
|
description: INodeTypeDescription = {
|
||||||
|
displayName: 'Microsoft Teams Trigger',
|
||||||
|
name: 'microsoftTeamsTrigger',
|
||||||
|
icon: 'file:teams.svg',
|
||||||
|
group: ['trigger'],
|
||||||
|
version: 1,
|
||||||
|
description:
|
||||||
|
'Triggers workflows in n8n based on events from Microsoft Teams, such as new messages or team updates, using specified configurations.',
|
||||||
|
subtitle: 'Microsoft Teams Trigger',
|
||||||
|
defaults: {
|
||||||
|
name: 'Microsoft Teams Trigger',
|
||||||
|
},
|
||||||
|
credentials: [
|
||||||
|
{
|
||||||
|
name: 'microsoftTeamsOAuth2Api',
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
inputs: [],
|
||||||
|
outputs: [NodeConnectionTypes.Main],
|
||||||
|
webhooks: [
|
||||||
|
{
|
||||||
|
name: 'default',
|
||||||
|
httpMethod: 'POST',
|
||||||
|
responseMode: 'onReceived',
|
||||||
|
path: 'webhook',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Trigger On',
|
||||||
|
name: 'event',
|
||||||
|
type: 'options',
|
||||||
|
default: 'newChannelMessage',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'New Channel',
|
||||||
|
value: 'newChannel',
|
||||||
|
description: 'A new channel is created',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'New Channel Message',
|
||||||
|
value: 'newChannelMessage',
|
||||||
|
description: 'A message is posted to a channel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'New Chat',
|
||||||
|
value: 'newChat',
|
||||||
|
description: 'A new chat is created',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'New Chat Message',
|
||||||
|
value: 'newChatMessage',
|
||||||
|
description: 'A message is posted to a chat',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'New Team Member',
|
||||||
|
value: 'newTeamMember',
|
||||||
|
description: 'A new member is added to a team',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
description: 'Select the event to trigger the workflow',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Watch All Teams',
|
||||||
|
name: 'watchAllTeams',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Whether to watch for the event in all the available teams',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
event: ['newChannel', 'newChannelMessage', 'newTeamMember'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Team',
|
||||||
|
name: 'teamId',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
default: {
|
||||||
|
mode: 'list',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
description: 'Select a team from the list, enter an ID or a URL',
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From List',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
placeholder: 'Select a team...',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'getTeams',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: 'e.g., 61165b04-e4cc-4026-b43f-926b4e2a7182',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By URL',
|
||||||
|
name: 'url',
|
||||||
|
type: 'string',
|
||||||
|
placeholder:
|
||||||
|
'e.g., https://teams.microsoft.com/l/team/19%3A...groupId=your-team-id&tenantId=...',
|
||||||
|
extractValue: {
|
||||||
|
type: 'regex',
|
||||||
|
regex: /groupId=([0-9a-fA-F-]{36})/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
event: ['newChannel', 'newChannelMessage', 'newTeamMember'],
|
||||||
|
watchAllTeams: [false],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Watch All Channels',
|
||||||
|
name: 'watchAllChannels',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Whether to watch for the event in all the available channels',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
event: ['newChannelMessage'],
|
||||||
|
watchAllTeams: [false],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Channel',
|
||||||
|
name: 'channelId',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
default: {
|
||||||
|
mode: 'list',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
description: 'Select a channel from the list, enter an ID or a URL',
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From List',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
placeholder: 'Select a channel...',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'getChannels',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: 'e.g., 19:-xlxyqXNSCxpI1SDzgQ_L9ZvzSR26pgphq1BJ9y7QJE1@thread.tacv2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By URL',
|
||||||
|
name: 'url',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: 'e.g., https://teams.microsoft.com/l/channel/19%3A...@thread.tacv2/...',
|
||||||
|
extractValue: {
|
||||||
|
type: 'regex',
|
||||||
|
regex: /channel\/([^\/?]+)/,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
event: ['newChannelMessage'],
|
||||||
|
watchAllTeams: [false],
|
||||||
|
watchAllChannels: [false],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Watch All Chats',
|
||||||
|
name: 'watchAllChats',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description: 'Whether to watch for the event in all the available chats',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
event: ['newChatMessage'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Chat',
|
||||||
|
name: 'chatId',
|
||||||
|
type: 'resourceLocator',
|
||||||
|
default: {
|
||||||
|
mode: 'list',
|
||||||
|
value: '',
|
||||||
|
},
|
||||||
|
required: true,
|
||||||
|
description: 'Select a chat from the list, enter an ID or a URL',
|
||||||
|
modes: [
|
||||||
|
{
|
||||||
|
displayName: 'From List',
|
||||||
|
name: 'list',
|
||||||
|
type: 'list',
|
||||||
|
placeholder: 'Select a chat...',
|
||||||
|
typeOptions: {
|
||||||
|
searchListMethod: 'getChats',
|
||||||
|
searchable: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By ID',
|
||||||
|
name: 'id',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: '19:7e2f1174-e8ee-4859-b8b1-a8d1cc63d276@unq.gbl.spaces',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'By URL',
|
||||||
|
name: 'url',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: 'https://teams.microsoft.com/_#/conversations/CHAT_ID',
|
||||||
|
extractValue: {
|
||||||
|
type: 'regex',
|
||||||
|
regex: /conversations\/([^\/?]+)/i,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
event: ['newChatMessage'],
|
||||||
|
watchAllChats: [false],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
methods = {
|
||||||
|
listSearch,
|
||||||
|
};
|
||||||
|
|
||||||
|
webhookMethods = {
|
||||||
|
default: {
|
||||||
|
async checkExists(this: IHookFunctions): Promise<boolean> {
|
||||||
|
const event = this.getNodeParameter('event', 0) as string;
|
||||||
|
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||||
|
const webhookData = this.getWorkflowStaticData('node');
|
||||||
|
|
||||||
|
try {
|
||||||
|
const subscriptions = (await microsoftApiRequestAllItems.call(
|
||||||
|
this as unknown as ILoadOptionsFunctions,
|
||||||
|
'value',
|
||||||
|
'GET',
|
||||||
|
'/v1.0/subscriptions',
|
||||||
|
)) as SubscriptionResponse[];
|
||||||
|
|
||||||
|
const matchingSubscriptions = subscriptions.filter(
|
||||||
|
(subscription) => subscription.notificationUrl === webhookUrl,
|
||||||
|
);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const thresholdMs = 5 * 60 * 1000;
|
||||||
|
const validSubscriptions = matchingSubscriptions.filter((subscription) => {
|
||||||
|
const expiration = new Date(subscription.expirationDateTime);
|
||||||
|
return expiration.getTime() - now.getTime() > thresholdMs;
|
||||||
|
});
|
||||||
|
|
||||||
|
const resourcePaths = await getResourcePath.call(this, event);
|
||||||
|
const requiredResources = Array.isArray(resourcePaths) ? resourcePaths : [resourcePaths];
|
||||||
|
|
||||||
|
const subscribedResources = validSubscriptions.map((sub) => sub.resource);
|
||||||
|
const allResourcesSubscribed = requiredResources.every((resource) =>
|
||||||
|
subscribedResources.includes(resource),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (allResourcesSubscribed) {
|
||||||
|
webhookData.subscriptionIds = validSubscriptions.map((sub) => sub.id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
async create(this: IHookFunctions): Promise<boolean> {
|
||||||
|
const event = this.getNodeParameter('event', 0) as string;
|
||||||
|
const webhookUrl = this.getNodeWebhookUrl('default');
|
||||||
|
const webhookData = this.getWorkflowStaticData('node');
|
||||||
|
|
||||||
|
if (!webhookUrl || !webhookUrl.startsWith('https://')) {
|
||||||
|
throw new NodeApiError(this.getNode(), {
|
||||||
|
message: 'Invalid Notification URL',
|
||||||
|
description: `The webhook URL "${webhookUrl}" is invalid. Microsoft Graph requires an HTTPS URL.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const resourcePaths = await getResourcePath.call(this, event);
|
||||||
|
const subscriptionIds: string[] = [];
|
||||||
|
|
||||||
|
if (Array.isArray(resourcePaths)) {
|
||||||
|
await Promise.all(
|
||||||
|
resourcePaths.map(async (resource) => {
|
||||||
|
const subscription = await createSubscription.call(this, webhookUrl, resource);
|
||||||
|
subscriptionIds.push(subscription.id);
|
||||||
|
return subscription;
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
webhookData.subscriptionIds = subscriptionIds;
|
||||||
|
} else {
|
||||||
|
const subscription = await createSubscription.call(this, webhookUrl, resourcePaths);
|
||||||
|
webhookData.subscriptionIds = [subscription.id];
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
async delete(this: IHookFunctions): Promise<boolean> {
|
||||||
|
const webhookData = this.getWorkflowStaticData('node');
|
||||||
|
const storedIds = webhookData.subscriptionIds as string[] | undefined;
|
||||||
|
|
||||||
|
if (!Array.isArray(storedIds)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await Promise.all(
|
||||||
|
storedIds.map(async (subscriptionId) => {
|
||||||
|
try {
|
||||||
|
await microsoftApiRequest.call(
|
||||||
|
this as unknown as IExecuteFunctions,
|
||||||
|
'DELETE',
|
||||||
|
`/v1.0/subscriptions/${subscriptionId}`,
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
if ((error as JsonObject).httpStatusCode !== 404) {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
delete webhookData.subscriptionIds;
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async webhook(this: IWebhookFunctions): Promise<IWebhookResponseData> {
|
||||||
|
const req = this.getRequestObject();
|
||||||
|
const res = this.getResponseObject();
|
||||||
|
|
||||||
|
// Handle Microsoft Graph validation request
|
||||||
|
if (req.query.validationToken) {
|
||||||
|
res.status(200).send(req.query.validationToken);
|
||||||
|
return { noWebhookResponse: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventNotifications = req.body.value as WebhookNotification[];
|
||||||
|
const response: IWebhookResponseData = {
|
||||||
|
workflowData: eventNotifications.map((event) => [
|
||||||
|
{
|
||||||
|
json: (event.resourceData as IDataObject) ?? event,
|
||||||
|
},
|
||||||
|
]),
|
||||||
|
};
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,232 @@
|
|||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
|
||||||
|
import { MicrosoftTeamsTrigger } from '../../MicrosoftTeamsTrigger.node';
|
||||||
|
import { microsoftApiRequest, microsoftApiRequestAllItems } from '../../v2/transport';
|
||||||
|
|
||||||
|
jest.mock('../../v2/transport', () => ({
|
||||||
|
microsoftApiRequest: {
|
||||||
|
call: jest.fn(),
|
||||||
|
},
|
||||||
|
microsoftApiRequestAllItems: {
|
||||||
|
call: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Microsoft Teams Trigger Node', () => {
|
||||||
|
let mockWebhookFunctions: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockWebhookFunctions = mock();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('webhookMethods', () => {
|
||||||
|
describe('checkExists', () => {
|
||||||
|
it('should return true if the subscription exists', async () => {
|
||||||
|
(microsoftApiRequestAllItems.call as jest.Mock).mockResolvedValue([
|
||||||
|
{
|
||||||
|
id: 'sub1',
|
||||||
|
notificationUrl: 'https://webhook.url',
|
||||||
|
resource: '/me/chats',
|
||||||
|
expirationDateTime: new Date(Date.now() + 3600000).toISOString(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
mockWebhookFunctions.getNodeWebhookUrl.mockReturnValue('https://webhook.url');
|
||||||
|
mockWebhookFunctions.getWorkflowStaticData.mockReturnValue({
|
||||||
|
node: {
|
||||||
|
subscriptionIds: ['sub1'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
mockWebhookFunctions.getNodeParameter.mockImplementation((paramName: string) => {
|
||||||
|
if (paramName === 'event') return 'newChat';
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await new MicrosoftTeamsTrigger().webhookMethods.default.checkExists.call(
|
||||||
|
mockWebhookFunctions,
|
||||||
|
);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
it('should return false if the subscription does not exist', async () => {
|
||||||
|
(microsoftApiRequestAllItems.call as jest.Mock).mockResolvedValue([]);
|
||||||
|
|
||||||
|
mockWebhookFunctions.getNodeWebhookUrl.mockReturnValue('https://webhook.url');
|
||||||
|
|
||||||
|
mockWebhookFunctions.getWorkflowStaticData.mockReturnValue({
|
||||||
|
node: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await new MicrosoftTeamsTrigger().webhookMethods.default.checkExists.call(
|
||||||
|
mockWebhookFunctions,
|
||||||
|
);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the API request fails', async () => {
|
||||||
|
(microsoftApiRequestAllItems.call as jest.Mock).mockRejectedValue(
|
||||||
|
new Error('API request failed'),
|
||||||
|
);
|
||||||
|
|
||||||
|
mockWebhookFunctions.getNodeWebhookUrl.mockReturnValue('https://webhook.url');
|
||||||
|
mockWebhookFunctions.getWorkflowStaticData.mockReturnValue({
|
||||||
|
node: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await new MicrosoftTeamsTrigger().webhookMethods.default.checkExists.call(
|
||||||
|
mockWebhookFunctions,
|
||||||
|
);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('create', () => {
|
||||||
|
it('should create a subscription successfully', async () => {
|
||||||
|
(microsoftApiRequest.call as jest.Mock).mockResolvedValue({ id: 'subscription123' });
|
||||||
|
|
||||||
|
mockWebhookFunctions.getNodeWebhookUrl.mockReturnValue('https://webhook.url');
|
||||||
|
mockWebhookFunctions.getNodeParameter.mockReturnValue('newChat');
|
||||||
|
mockWebhookFunctions.getWorkflowStaticData.mockReturnValue({
|
||||||
|
node: {
|
||||||
|
subscriptionIds: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
(microsoftApiRequest.call as jest.Mock).mockResolvedValue({
|
||||||
|
value: [{ id: 'team1', displayName: 'Team 1' }],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await new MicrosoftTeamsTrigger().webhookMethods.default.create.call(
|
||||||
|
mockWebhookFunctions,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(microsoftApiRequest.call).toHaveBeenCalledWith(
|
||||||
|
mockWebhookFunctions,
|
||||||
|
'POST',
|
||||||
|
'/v1.0/subscriptions',
|
||||||
|
expect.objectContaining({
|
||||||
|
changeType: 'created',
|
||||||
|
notificationUrl: 'https://webhook.url',
|
||||||
|
resource: '/me/chats',
|
||||||
|
expirationDateTime: expect.any(String),
|
||||||
|
latestSupportedTlsVersion: 'v1_2',
|
||||||
|
lifecycleNotificationUrl: 'https://webhook.url',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the URL is invalid', async () => {
|
||||||
|
mockWebhookFunctions.getNodeWebhookUrl.mockReturnValue('invalid-url');
|
||||||
|
await expect(
|
||||||
|
new MicrosoftTeamsTrigger().webhookMethods.default.create.call(mockWebhookFunctions),
|
||||||
|
).rejects.toThrow('Invalid Notification URL');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('delete', () => {
|
||||||
|
it('should delete subscriptions using stored IDs and clean static data', async () => {
|
||||||
|
const mockWebhookData = {
|
||||||
|
subscriptionIds: ['subscription123'],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockWebhookFunctions.getWorkflowStaticData.mockReturnValue(mockWebhookData);
|
||||||
|
|
||||||
|
(microsoftApiRequest.call as jest.Mock).mockResolvedValue({});
|
||||||
|
|
||||||
|
const result = await new MicrosoftTeamsTrigger().webhookMethods.default.delete.call(
|
||||||
|
mockWebhookFunctions,
|
||||||
|
);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
expect(microsoftApiRequest.call).toHaveBeenCalledWith(
|
||||||
|
mockWebhookFunctions,
|
||||||
|
'DELETE',
|
||||||
|
'/v1.0/subscriptions/subscription123',
|
||||||
|
);
|
||||||
|
expect(mockWebhookData.subscriptionIds).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if no subscription matches', async () => {
|
||||||
|
(microsoftApiRequestAllItems.call as jest.Mock).mockResolvedValue([]);
|
||||||
|
mockWebhookFunctions.getNodeWebhookUrl.mockReturnValue('https://webhook.url');
|
||||||
|
mockWebhookFunctions.getWorkflowStaticData.mockReturnValue({
|
||||||
|
node: {
|
||||||
|
subscriptionIds: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await new MicrosoftTeamsTrigger().webhookMethods.default.delete.call(
|
||||||
|
mockWebhookFunctions,
|
||||||
|
);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if the API request fails', async () => {
|
||||||
|
(microsoftApiRequestAllItems.call as jest.Mock).mockResolvedValue([
|
||||||
|
{ id: 'subscription123', notificationUrl: 'https://webhook.url' },
|
||||||
|
]);
|
||||||
|
(microsoftApiRequest.call as jest.Mock).mockRejectedValue(new Error('API request failed'));
|
||||||
|
mockWebhookFunctions.getNodeWebhookUrl.mockReturnValue('https://webhook.url');
|
||||||
|
mockWebhookFunctions.getWorkflowStaticData.mockReturnValue({
|
||||||
|
node: {
|
||||||
|
subscriptionIds: ['subscription123'],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await new MicrosoftTeamsTrigger().webhookMethods.default.delete.call(
|
||||||
|
mockWebhookFunctions,
|
||||||
|
);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('webhook', () => {
|
||||||
|
it('should handle Microsoft Graph validation request correctly', async () => {
|
||||||
|
const mockRequest = {
|
||||||
|
query: {
|
||||||
|
validationToken: 'validation-token',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const mockResponse = {
|
||||||
|
status: jest.fn().mockReturnThis(),
|
||||||
|
send: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mockWebhookFunctions.getRequestObject.mockReturnValue(mockRequest);
|
||||||
|
mockWebhookFunctions.getResponseObject.mockReturnValue(mockResponse);
|
||||||
|
|
||||||
|
const result = await new MicrosoftTeamsTrigger().webhook.call(mockWebhookFunctions);
|
||||||
|
expect(mockResponse.status).toHaveBeenCalledWith(200);
|
||||||
|
expect(mockResponse.send).toHaveBeenCalledWith('validation-token');
|
||||||
|
expect(result.noWebhookResponse).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should process incoming event notifications', async () => {
|
||||||
|
const mockRequest = {
|
||||||
|
body: {
|
||||||
|
value: [{ resourceData: { message: 'test message' } }],
|
||||||
|
},
|
||||||
|
query: {},
|
||||||
|
};
|
||||||
|
const mockResponse = {
|
||||||
|
status: jest.fn().mockReturnThis(),
|
||||||
|
send: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
mockWebhookFunctions.getRequestObject.mockReturnValue(mockRequest);
|
||||||
|
mockWebhookFunctions.getResponseObject.mockReturnValue(mockResponse);
|
||||||
|
|
||||||
|
const result = await new MicrosoftTeamsTrigger().webhook.call(mockWebhookFunctions);
|
||||||
|
|
||||||
|
expect(result.workflowData).toEqual([
|
||||||
|
[
|
||||||
|
{
|
||||||
|
json: { message: 'test message' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
import { mock } from 'jest-mock-extended';
|
||||||
|
import { NodeApiError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
fetchAllTeams,
|
||||||
|
fetchAllChannels,
|
||||||
|
createSubscription,
|
||||||
|
getResourcePath,
|
||||||
|
} from '../../v2/helpers/utils-trigger';
|
||||||
|
import { microsoftApiRequest } from '../../v2/transport';
|
||||||
|
|
||||||
|
jest.mock('../../v2/transport', () => ({
|
||||||
|
microsoftApiRequest: {
|
||||||
|
call: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('Microsoft Teams Helpers Functions', () => {
|
||||||
|
let mockLoadOptionsFunctions: any;
|
||||||
|
let mockHookFunctions: any;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockLoadOptionsFunctions = mock();
|
||||||
|
mockHookFunctions = mock();
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchAllTeams', () => {
|
||||||
|
it('should fetch all teams and map them correctly', async () => {
|
||||||
|
(microsoftApiRequest.call as jest.Mock).mockResolvedValue({
|
||||||
|
value: [
|
||||||
|
{ id: 'team1', displayName: 'Team 1' },
|
||||||
|
{ id: 'team2', displayName: 'Team 2' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await fetchAllTeams.call(mockLoadOptionsFunctions);
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ id: 'team1', displayName: 'Team 1' },
|
||||||
|
{ id: 'team2', displayName: 'Team 2' },
|
||||||
|
]);
|
||||||
|
expect(microsoftApiRequest.call).toHaveBeenCalledWith(
|
||||||
|
mockLoadOptionsFunctions,
|
||||||
|
'GET',
|
||||||
|
'/v1.0/me/joinedTeams',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if getTeams fails', async () => {
|
||||||
|
(microsoftApiRequest.call as jest.Mock).mockRejectedValue(new Error('Failed to fetch teams'));
|
||||||
|
|
||||||
|
await expect(fetchAllTeams.call(mockLoadOptionsFunctions)).rejects.toThrow(
|
||||||
|
'Failed to fetch teams',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetchAllChannels', () => {
|
||||||
|
it('should fetch all channels for a team and map them correctly', async () => {
|
||||||
|
(microsoftApiRequest.call as jest.Mock).mockResolvedValue({
|
||||||
|
value: [
|
||||||
|
{ id: 'channel1', displayName: 'Channel 1' },
|
||||||
|
{ id: 'channel2', displayName: 'Channel 2' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await fetchAllChannels.call(mockLoadOptionsFunctions, 'team1');
|
||||||
|
|
||||||
|
expect(result).toEqual([
|
||||||
|
{ id: 'channel1', displayName: 'Channel 1' },
|
||||||
|
{ id: 'channel2', displayName: 'Channel 2' },
|
||||||
|
]);
|
||||||
|
expect(microsoftApiRequest.call).toHaveBeenCalledWith(
|
||||||
|
mockLoadOptionsFunctions,
|
||||||
|
'GET',
|
||||||
|
'/v1.0/teams/team1/channels',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw an error if getChannels fails', async () => {
|
||||||
|
(microsoftApiRequest.call as jest.Mock).mockRejectedValue(
|
||||||
|
new Error('Failed to fetch channels'),
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(fetchAllChannels.call(mockLoadOptionsFunctions, 'team1')).rejects.toThrow(
|
||||||
|
'Failed to fetch channels',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('createSubscription', () => {
|
||||||
|
it('should create a subscription and return the subscription ID', async () => {
|
||||||
|
(microsoftApiRequest.call as jest.Mock).mockResolvedValue({
|
||||||
|
id: 'subscription123',
|
||||||
|
resource: '/resource/path',
|
||||||
|
notificationUrl: 'https://webhook.url',
|
||||||
|
expirationDateTime: '2024-01-01T00:00:00Z',
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await createSubscription.call(
|
||||||
|
mockHookFunctions,
|
||||||
|
'https://webhook.url',
|
||||||
|
'/resource/path',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
id: 'subscription123',
|
||||||
|
resource: '/resource/path',
|
||||||
|
notificationUrl: 'https://webhook.url',
|
||||||
|
expirationDateTime: '2024-01-01T00:00:00Z',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should throw a NodeApiError if the API request fails', async () => {
|
||||||
|
const error = new NodeApiError(mockHookFunctions.getNode(), {
|
||||||
|
message: 'API request failed',
|
||||||
|
httpCode: '400',
|
||||||
|
});
|
||||||
|
(microsoftApiRequest.call as jest.Mock).mockRejectedValue(error);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
createSubscription.call(mockHookFunctions, 'https://webhook.url', '/resource/path'),
|
||||||
|
).rejects.toThrow(NodeApiError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getResourcePath', () => {
|
||||||
|
it('should return the correct resource path for newChat event', async () => {
|
||||||
|
const result = await getResourcePath.call(mockHookFunctions, 'newChat');
|
||||||
|
expect(result).toBe('/me/chats');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct resource path for newChatMessage event with watchAllChats', async () => {
|
||||||
|
mockHookFunctions.getNodeParameter.mockReturnValueOnce(true);
|
||||||
|
const result = await getResourcePath.call(mockHookFunctions, 'newChatMessage');
|
||||||
|
expect(result).toBe('/me/chats/getAllMessages');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct resource path for newChatMessage event with chatId', async () => {
|
||||||
|
mockHookFunctions.getNodeParameter.mockReturnValueOnce(false).mockReturnValueOnce('chat123');
|
||||||
|
|
||||||
|
const result = await getResourcePath.call(mockHookFunctions, 'newChatMessage');
|
||||||
|
expect(result).toBe('/chats/chat123/messages');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct resource path for newChatMessage event with chatId missing', async () => {
|
||||||
|
mockHookFunctions.getNodeParameter.mockReturnValueOnce(false);
|
||||||
|
mockHookFunctions.getNodeParameter.mockReturnValueOnce(undefined);
|
||||||
|
|
||||||
|
const result = await getResourcePath.call(mockHookFunctions, 'newChatMessage');
|
||||||
|
expect(result).toBe('/chats/undefined/messages');
|
||||||
|
});
|
||||||
|
it('should return the correct resource path for newChannel event', async () => {
|
||||||
|
mockHookFunctions.getNodeParameter.mockReturnValueOnce(false);
|
||||||
|
mockHookFunctions.getNodeParameter.mockReturnValueOnce('team123');
|
||||||
|
|
||||||
|
const result = await getResourcePath.call(mockHookFunctions, 'newChannel');
|
||||||
|
expect(result).toBe('/teams/team123/channels');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct resource path for newChannel event with teamId missing', async () => {
|
||||||
|
mockHookFunctions.getNodeParameter.mockReturnValueOnce(undefined);
|
||||||
|
|
||||||
|
const result = await getResourcePath.call(mockHookFunctions, 'newChannel');
|
||||||
|
expect(result).toBe('/teams/undefined/channels');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct resource path for newChannelMessage event with a specific team and channel', async () => {
|
||||||
|
mockHookFunctions.getNodeParameter
|
||||||
|
.mockReturnValueOnce(false)
|
||||||
|
.mockReturnValueOnce('team123')
|
||||||
|
.mockReturnValueOnce(false)
|
||||||
|
.mockReturnValueOnce('channel123');
|
||||||
|
|
||||||
|
const result = await getResourcePath.call(mockHookFunctions, 'newChannelMessage');
|
||||||
|
expect(result).toBe('/teams/team123/channels/channel123/messages');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct resource path for newTeamMember event', async () => {
|
||||||
|
mockHookFunctions.getNodeParameter.mockReturnValueOnce(false);
|
||||||
|
mockHookFunctions.getNodeParameter.mockReturnValueOnce('team123');
|
||||||
|
|
||||||
|
const result = await getResourcePath.call(mockHookFunctions, 'newTeamMember');
|
||||||
|
expect(result).toBe('/teams/team123/members');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct resource path for newTeamMember event with teamId missing', async () => {
|
||||||
|
mockHookFunctions.getNodeParameter.mockReturnValueOnce(undefined);
|
||||||
|
|
||||||
|
const result = await getResourcePath.call(mockHookFunctions, 'newTeamMember');
|
||||||
|
expect(result).toBe('/teams/undefined/members');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct resource path for newTeamMember event with watchAllTeams', async () => {
|
||||||
|
mockHookFunctions.getNodeParameter.mockReturnValueOnce(true);
|
||||||
|
|
||||||
|
(microsoftApiRequest.call as jest.Mock).mockResolvedValueOnce({
|
||||||
|
value: [
|
||||||
|
{ id: 'team1', displayName: 'Team 1' },
|
||||||
|
{ id: 'team2', displayName: 'Team 2' },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await getResourcePath.call(mockHookFunctions, 'newTeamMember');
|
||||||
|
expect(result).toEqual(['/teams/team1/members', '/teams/team2/members']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
export interface TeamResponse {
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ChannelResponse {
|
||||||
|
id: string;
|
||||||
|
displayName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WebhookNotification {
|
||||||
|
subscriptionId: string;
|
||||||
|
resource: string;
|
||||||
|
resourceData: ResourceData;
|
||||||
|
tenantId: string;
|
||||||
|
subscriptionExpirationDateTime: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ResourceData {
|
||||||
|
id: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubscriptionResponse {
|
||||||
|
id: string;
|
||||||
|
expirationDateTime: string;
|
||||||
|
notificationUrl: string;
|
||||||
|
resource: string;
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
import type { IHookFunctions, IDataObject } from 'n8n-workflow';
|
||||||
|
import { NodeOperationError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
import type { TeamResponse, ChannelResponse, SubscriptionResponse } from './types';
|
||||||
|
import { microsoftApiRequest } from '../transport';
|
||||||
|
|
||||||
|
export async function fetchAllTeams(this: IHookFunctions): Promise<TeamResponse[]> {
|
||||||
|
const { value: teams } = (await microsoftApiRequest.call(
|
||||||
|
this,
|
||||||
|
'GET',
|
||||||
|
'/v1.0/me/joinedTeams',
|
||||||
|
)) as { value: TeamResponse[] };
|
||||||
|
return teams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchAllChannels(
|
||||||
|
this: IHookFunctions,
|
||||||
|
teamId: string,
|
||||||
|
): Promise<ChannelResponse[]> {
|
||||||
|
const { value: channels } = (await microsoftApiRequest.call(
|
||||||
|
this,
|
||||||
|
'GET',
|
||||||
|
`/v1.0/teams/${teamId}/channels`,
|
||||||
|
)) as { value: ChannelResponse[] };
|
||||||
|
return channels;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createSubscription(
|
||||||
|
this: IHookFunctions,
|
||||||
|
webhookUrl: string,
|
||||||
|
resourcePath: string,
|
||||||
|
): Promise<SubscriptionResponse> {
|
||||||
|
const expirationTime = new Date(Date.now() + 4318 * 60 * 1000).toISOString();
|
||||||
|
const body: IDataObject = {
|
||||||
|
changeType: 'created',
|
||||||
|
notificationUrl: webhookUrl,
|
||||||
|
resource: resourcePath,
|
||||||
|
expirationDateTime: expirationTime,
|
||||||
|
latestSupportedTlsVersion: 'v1_2',
|
||||||
|
lifecycleNotificationUrl: webhookUrl,
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = (await microsoftApiRequest.call(
|
||||||
|
this,
|
||||||
|
'POST',
|
||||||
|
'/v1.0/subscriptions',
|
||||||
|
body,
|
||||||
|
)) as SubscriptionResponse;
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getResourcePath(
|
||||||
|
this: IHookFunctions,
|
||||||
|
event: string,
|
||||||
|
): Promise<string | string[]> {
|
||||||
|
switch (event) {
|
||||||
|
case 'newChat': {
|
||||||
|
return '/me/chats';
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'newChatMessage': {
|
||||||
|
const watchAllChats = this.getNodeParameter('watchAllChats', false, {
|
||||||
|
extractValue: true,
|
||||||
|
}) as boolean;
|
||||||
|
|
||||||
|
if (watchAllChats) {
|
||||||
|
return '/me/chats/getAllMessages';
|
||||||
|
} else {
|
||||||
|
const chatId = this.getNodeParameter('chatId', undefined, { extractValue: true }) as string;
|
||||||
|
return `/chats/${decodeURIComponent(chatId)}/messages`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'newChannel': {
|
||||||
|
const watchAllTeams = this.getNodeParameter('watchAllTeams', false, {
|
||||||
|
extractValue: true,
|
||||||
|
}) as boolean;
|
||||||
|
|
||||||
|
if (watchAllTeams) {
|
||||||
|
const teams = await fetchAllTeams.call(this);
|
||||||
|
return teams.map((team) => `/teams/${team.id}/channels`);
|
||||||
|
} else {
|
||||||
|
const teamId = this.getNodeParameter('teamId', undefined, { extractValue: true }) as string;
|
||||||
|
return `/teams/${teamId}/channels`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'newChannelMessage': {
|
||||||
|
const watchAllTeams = this.getNodeParameter('watchAllTeams', false, {
|
||||||
|
extractValue: true,
|
||||||
|
}) as boolean;
|
||||||
|
|
||||||
|
if (watchAllTeams) {
|
||||||
|
const teams = await fetchAllTeams.call(this);
|
||||||
|
const teamChannels = await Promise.all(
|
||||||
|
teams.map(async (team) => {
|
||||||
|
const channels = await fetchAllChannels.call(this, team.id);
|
||||||
|
return channels.map((channel) => `/teams/${team.id}/channels/${channel.id}/messages`);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
return teamChannels.flat();
|
||||||
|
} else {
|
||||||
|
const teamId = this.getNodeParameter('teamId', undefined, { extractValue: true }) as string;
|
||||||
|
const watchAllChannels = this.getNodeParameter('watchAllChannels', false, {
|
||||||
|
extractValue: true,
|
||||||
|
}) as boolean;
|
||||||
|
|
||||||
|
if (watchAllChannels) {
|
||||||
|
const channels = await fetchAllChannels.call(this, teamId);
|
||||||
|
return channels.map((channel) => `/teams/${teamId}/channels/${channel.id}/messages`);
|
||||||
|
} else {
|
||||||
|
const channelId = this.getNodeParameter('channelId', undefined, {
|
||||||
|
extractValue: true,
|
||||||
|
}) as string;
|
||||||
|
return `/teams/${teamId}/channels/${decodeURIComponent(channelId)}/messages`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'newTeamMember': {
|
||||||
|
const watchAllTeams = this.getNodeParameter('watchAllTeams', false, {
|
||||||
|
extractValue: true,
|
||||||
|
}) as boolean;
|
||||||
|
|
||||||
|
if (watchAllTeams) {
|
||||||
|
const teams = await fetchAllTeams.call(this);
|
||||||
|
return teams.map((team) => `/teams/${team.id}/members`);
|
||||||
|
} else {
|
||||||
|
const teamId = this.getNodeParameter('teamId', undefined, { extractValue: true }) as string;
|
||||||
|
return `/teams/${teamId}/members`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
throw new NodeOperationError(this.getNode(), {
|
||||||
|
message: `Invalid event: ${event}`,
|
||||||
|
description: `The selected event "${event}" is not recognized.`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,13 +5,14 @@ import type {
|
|||||||
JsonObject,
|
JsonObject,
|
||||||
IHttpRequestMethods,
|
IHttpRequestMethods,
|
||||||
IRequestOptions,
|
IRequestOptions,
|
||||||
|
IHookFunctions,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { NodeApiError } from 'n8n-workflow';
|
import { NodeApiError } from 'n8n-workflow';
|
||||||
|
|
||||||
import { capitalize } from '../../../../../utils/utilities';
|
import { capitalize } from '../../../../../utils/utilities';
|
||||||
|
|
||||||
export async function microsoftApiRequest(
|
export async function microsoftApiRequest(
|
||||||
this: IExecuteFunctions | ILoadOptionsFunctions,
|
this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions,
|
||||||
method: IHttpRequestMethods,
|
method: IHttpRequestMethods,
|
||||||
resource: string,
|
resource: string,
|
||||||
body: any = {},
|
body: any = {},
|
||||||
@@ -56,7 +57,6 @@ export async function microsoftApiRequestAllItems(
|
|||||||
propertyName: string,
|
propertyName: string,
|
||||||
method: IHttpRequestMethods,
|
method: IHttpRequestMethods,
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
|
|
||||||
body: any = {},
|
body: any = {},
|
||||||
query: IDataObject = {},
|
query: IDataObject = {},
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
@@ -83,7 +83,6 @@ export async function microsoftApiRequestAllItemsSkip(
|
|||||||
propertyName: string,
|
propertyName: string,
|
||||||
method: IHttpRequestMethods,
|
method: IHttpRequestMethods,
|
||||||
endpoint: string,
|
endpoint: string,
|
||||||
|
|
||||||
body: any = {},
|
body: any = {},
|
||||||
query: IDataObject = {},
|
query: IDataObject = {},
|
||||||
): Promise<any> {
|
): Promise<any> {
|
||||||
|
|||||||
@@ -655,6 +655,7 @@
|
|||||||
"dist/nodes/Microsoft/Sql/MicrosoftSql.node.js",
|
"dist/nodes/Microsoft/Sql/MicrosoftSql.node.js",
|
||||||
"dist/nodes/Microsoft/Storage/AzureStorage.node.js",
|
"dist/nodes/Microsoft/Storage/AzureStorage.node.js",
|
||||||
"dist/nodes/Microsoft/Teams/MicrosoftTeams.node.js",
|
"dist/nodes/Microsoft/Teams/MicrosoftTeams.node.js",
|
||||||
|
"dist/nodes/Microsoft/Teams/MicrosoftTeamsTrigger.node.js",
|
||||||
"dist/nodes/Microsoft/ToDo/MicrosoftToDo.node.js",
|
"dist/nodes/Microsoft/ToDo/MicrosoftToDo.node.js",
|
||||||
"dist/nodes/Mindee/Mindee.node.js",
|
"dist/nodes/Mindee/Mindee.node.js",
|
||||||
"dist/nodes/Misp/Misp.node.js",
|
"dist/nodes/Misp/Misp.node.js",
|
||||||
|
|||||||
Reference in New Issue
Block a user