@blickwerk/n8n-nodes-microsoft-planner 1.4.0 → 1.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -6,6 +6,7 @@ class MicrosoftPlannerOAuth2Api {
6
6
  this.name = 'microsoftPlannerOAuth2Api';
7
7
  this.extends = ['oAuth2Api'];
8
8
  this.displayName = 'Microsoft Planner OAuth2 API';
9
+ // eslint-disable-next-line n8n-nodes-base/cred-class-field-documentation-url-miscased
9
10
  this.documentationUrl = 'https://docs.microsoft.com/en-us/graph/auth-v2-user';
10
11
  this.properties = [
11
12
  {
@@ -17,14 +18,14 @@ class MicrosoftPlannerOAuth2Api {
17
18
  {
18
19
  displayName: 'Authorization URL',
19
20
  name: 'authUrl',
20
- type: 'hidden',
21
+ type: 'string',
21
22
  default: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
22
23
  required: true,
23
24
  },
24
25
  {
25
26
  displayName: 'Access Token URL',
26
27
  name: 'accessTokenUrl',
27
- type: 'hidden',
28
+ type: 'string',
28
29
  default: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
29
30
  required: true,
30
31
  },
@@ -0,0 +1,3 @@
1
+ import { INodeProperties } from 'n8n-workflow';
2
+ export declare const bucketOperations: INodeProperties[];
3
+ export declare const bucketFields: INodeProperties[];
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.bucketFields = exports.bucketOperations = void 0;
4
+ // ----------------------------------
5
+ // Bucket operations
6
+ // ----------------------------------
7
+ exports.bucketOperations = [
8
+ {
9
+ displayName: 'Operation',
10
+ name: 'operation',
11
+ type: 'options',
12
+ noDataExpression: true,
13
+ displayOptions: {
14
+ show: {
15
+ resource: ['bucket'],
16
+ },
17
+ },
18
+ options: [
19
+ {
20
+ name: 'Create',
21
+ value: 'create',
22
+ description: 'Create a new bucket in a plan',
23
+ action: 'Create a bucket',
24
+ },
25
+ {
26
+ name: 'Get',
27
+ value: 'get',
28
+ description: 'Get a bucket',
29
+ action: 'Get a bucket',
30
+ },
31
+ {
32
+ name: 'Get Many',
33
+ value: 'getAll',
34
+ description: 'Get many buckets in a plan',
35
+ action: 'Get many buckets',
36
+ },
37
+ {
38
+ name: 'Update',
39
+ value: 'update',
40
+ description: 'Update a bucket',
41
+ action: 'Update a bucket',
42
+ },
43
+ {
44
+ name: 'Delete',
45
+ value: 'delete',
46
+ description: 'Delete a bucket',
47
+ action: 'Delete a bucket',
48
+ },
49
+ ],
50
+ default: 'getAll',
51
+ },
52
+ ];
53
+ // ----------------------------------
54
+ // Bucket fields
55
+ // ----------------------------------
56
+ exports.bucketFields = [
57
+ // ----------------------------------
58
+ // bucket:create
59
+ // ----------------------------------
60
+ {
61
+ displayName: 'Plan ID',
62
+ name: 'planId',
63
+ type: 'string',
64
+ required: true,
65
+ displayOptions: {
66
+ show: {
67
+ resource: ['bucket'],
68
+ operation: ['create', 'getAll'],
69
+ },
70
+ },
71
+ default: '',
72
+ description: 'The ID of the plan this bucket belongs to',
73
+ },
74
+ // ----------------------------------
75
+ // bucket:getAll
76
+ // ----------------------------------
77
+ // No Return All / Limit options; Graph does not support $top/$limit for Planner.
78
+ {
79
+ displayName: 'Name',
80
+ name: 'name',
81
+ type: 'string',
82
+ required: true,
83
+ displayOptions: {
84
+ show: {
85
+ resource: ['bucket'],
86
+ operation: ['create'],
87
+ },
88
+ },
89
+ default: '',
90
+ description: 'Name of the bucket',
91
+ },
92
+ {
93
+ displayName: 'Upsert',
94
+ name: 'upsert',
95
+ type: 'boolean',
96
+ default: false,
97
+ displayOptions: {
98
+ show: {
99
+ resource: ['bucket'],
100
+ operation: ['create'],
101
+ },
102
+ },
103
+ description: 'Whether to return the existing bucket if one with the same name already exists in the plan',
104
+ },
105
+ {
106
+ displayName: 'Additional Fields',
107
+ name: 'additionalFields',
108
+ type: 'collection',
109
+ placeholder: 'Add Field',
110
+ default: {},
111
+ displayOptions: {
112
+ show: {
113
+ resource: ['bucket'],
114
+ operation: ['getAll'],
115
+ },
116
+ },
117
+ options: [
118
+ {
119
+ displayName: 'Select Properties',
120
+ name: 'select',
121
+ type: 'string',
122
+ default: '',
123
+ description: 'Comma-separated list of properties to specify which fields to return (e.g. id,name)',
124
+ },
125
+ ],
126
+ },
127
+ // ----------------------------------
128
+ // bucket:get / bucket:update / bucket:delete
129
+ // ----------------------------------
130
+ {
131
+ displayName: 'Bucket ID',
132
+ name: 'bucketId',
133
+ type: 'string',
134
+ required: true,
135
+ displayOptions: {
136
+ show: {
137
+ resource: ['bucket'],
138
+ operation: ['get', 'update', 'delete'],
139
+ },
140
+ },
141
+ default: '',
142
+ description: 'The ID of the bucket',
143
+ },
144
+ {
145
+ displayName: 'Update Fields',
146
+ name: 'updateFields',
147
+ type: 'collection',
148
+ placeholder: 'Add Field',
149
+ default: {},
150
+ displayOptions: {
151
+ show: {
152
+ resource: ['bucket'],
153
+ operation: ['update'],
154
+ },
155
+ },
156
+ options: [
157
+ {
158
+ displayName: 'Name',
159
+ name: 'name',
160
+ type: 'string',
161
+ default: '',
162
+ description: 'Name of the bucket',
163
+ },
164
+ {
165
+ displayName: 'Order Hint',
166
+ name: 'orderHint',
167
+ type: 'string',
168
+ default: '',
169
+ description: 'Used to sort buckets in the plan',
170
+ },
171
+ ],
172
+ },
173
+ ];
@@ -0,0 +1,3 @@
1
+ import { INodeProperties } from 'n8n-workflow';
2
+ export declare const commentOperations: INodeProperties[];
3
+ export declare const commentFields: INodeProperties[];
@@ -0,0 +1,160 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.commentFields = exports.commentOperations = void 0;
4
+ // ----------------------------------
5
+ // Comment operations
6
+ // ----------------------------------
7
+ exports.commentOperations = [
8
+ {
9
+ displayName: 'Operation',
10
+ name: 'operation',
11
+ type: 'options',
12
+ noDataExpression: true,
13
+ displayOptions: {
14
+ show: {
15
+ resource: ['comment'],
16
+ },
17
+ },
18
+ options: [
19
+ {
20
+ name: 'Create',
21
+ value: 'create',
22
+ description: 'Create a new comment on a task',
23
+ action: 'Create a comment',
24
+ },
25
+ {
26
+ name: 'Get Many',
27
+ value: 'getAll',
28
+ description: 'Get all comments from a task',
29
+ action: 'Get many comments',
30
+ },
31
+ ],
32
+ default: 'create',
33
+ },
34
+ ];
35
+ // ----------------------------------
36
+ // Comment fields
37
+ // ----------------------------------
38
+ exports.commentFields = [
39
+ // ----------------------------------
40
+ // comment:create
41
+ // ----------------------------------
42
+ {
43
+ displayName: 'Task',
44
+ name: 'taskId',
45
+ type: 'resourceLocator',
46
+ default: { mode: 'id', value: '' },
47
+ required: true,
48
+ displayOptions: {
49
+ show: {
50
+ resource: ['comment'],
51
+ operation: ['create'],
52
+ },
53
+ },
54
+ modes: [
55
+ {
56
+ displayName: 'By ID',
57
+ name: 'id',
58
+ type: 'string',
59
+ placeholder: 'e.g. rz1EH6N_a0aLpRm-2QifxZgAF5OL',
60
+ },
61
+ ],
62
+ description: 'The task to comment on',
63
+ },
64
+ {
65
+ displayName: 'Content',
66
+ name: 'content',
67
+ type: 'string',
68
+ typeOptions: {
69
+ rows: 5,
70
+ },
71
+ required: true,
72
+ displayOptions: {
73
+ show: {
74
+ resource: ['comment'],
75
+ operation: ['create'],
76
+ },
77
+ },
78
+ default: '',
79
+ description: 'The content of the comment',
80
+ },
81
+ {
82
+ displayName: 'Additional Fields',
83
+ name: 'additionalFields',
84
+ type: 'collection',
85
+ placeholder: 'Add Field',
86
+ default: {},
87
+ displayOptions: {
88
+ show: {
89
+ resource: ['comment'],
90
+ operation: ['create'],
91
+ },
92
+ },
93
+ options: [
94
+ {
95
+ displayName: 'Content Type',
96
+ name: 'contentType',
97
+ type: 'options',
98
+ options: [
99
+ {
100
+ name: 'HTML',
101
+ value: 'html',
102
+ },
103
+ {
104
+ name: 'Text',
105
+ value: 'text',
106
+ },
107
+ ],
108
+ default: 'text',
109
+ description: 'The format of the comment content',
110
+ },
111
+ ],
112
+ },
113
+ // ----------------------------------
114
+ // comment:getAll
115
+ // ----------------------------------
116
+ {
117
+ displayName: 'Task',
118
+ name: 'taskId',
119
+ type: 'resourceLocator',
120
+ default: { mode: 'id', value: '' },
121
+ required: true,
122
+ displayOptions: {
123
+ show: {
124
+ resource: ['comment'],
125
+ operation: ['getAll'],
126
+ },
127
+ },
128
+ modes: [
129
+ {
130
+ displayName: 'By ID',
131
+ name: 'id',
132
+ type: 'string',
133
+ placeholder: 'e.g. rz1EH6N_a0aLpRm-2QifxZgAF5OL',
134
+ },
135
+ ],
136
+ description: 'The task to get comments from',
137
+ },
138
+ {
139
+ displayName: 'Additional Fields',
140
+ name: 'additionalFields',
141
+ type: 'collection',
142
+ placeholder: 'Add Field',
143
+ default: {},
144
+ displayOptions: {
145
+ show: {
146
+ resource: ['comment'],
147
+ operation: ['getAll'],
148
+ },
149
+ },
150
+ options: [
151
+ {
152
+ displayName: 'Select Properties',
153
+ name: 'select',
154
+ type: 'string',
155
+ default: '',
156
+ description: 'Comma-separated list of properties to specify which fields to return (e.g. id,content)',
157
+ },
158
+ ],
159
+ },
160
+ ];
@@ -3,6 +3,22 @@ export declare function microsoftApiRequest(this: IHookFunctions | IExecuteFunct
3
3
  export declare function microsoftApiRequestAllItems(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, propertyName: string, method: IHttpRequestMethods, endpoint: string, body?: IDataObject, query?: IDataObject): Promise<any>;
4
4
  export declare function cleanETag(eTag: string): string;
5
5
  export declare function formatDateTime(dateTime: string | undefined): string | undefined;
6
- export declare function getUserIdByEmail(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, email: string): Promise<string | null>;
7
- export declare function parseAssignments(assignmentsString: string): string[];
6
+ export declare function getUserIdByEmail(this: IHookFunctions | IExecuteFunctions | ILoadOptionsFunctions, email: string): Promise<string>;
7
+ export declare function parseAssignments(assignments: string | string[]): string[];
8
8
  export declare function createAssignmentsObject(userIds: string[]): IDataObject;
9
+ /**
10
+ * Encodes a URL for use as a key in plannerExternalReferences.
11
+ * OData requires keys in open types to be free of certain special characters like '.',
12
+ * which must be encoded.
13
+ */
14
+ export declare function encodeReferenceKey(url: string): string;
15
+ /**
16
+ * Generates a GUID/UUID v4.
17
+ * Useful for creating new ID keys for checklist items.
18
+ */
19
+ export declare function generateGuid(): string;
20
+ /**
21
+ * Formats comment content for Microsoft Graph API conversation posts.
22
+ * Wraps plain text in HTML paragraph tags if needed.
23
+ */
24
+ export declare function formatCommentContent(content: string, contentType?: string): IDataObject;
@@ -7,6 +7,9 @@ exports.formatDateTime = formatDateTime;
7
7
  exports.getUserIdByEmail = getUserIdByEmail;
8
8
  exports.parseAssignments = parseAssignments;
9
9
  exports.createAssignmentsObject = createAssignmentsObject;
10
+ exports.encodeReferenceKey = encodeReferenceKey;
11
+ exports.generateGuid = generateGuid;
12
+ exports.formatCommentContent = formatCommentContent;
10
13
  const n8n_workflow_1 = require("n8n-workflow");
11
14
  async function microsoftApiRequest(method, resource, body = {}, qs = {}, uri, headers = {}) {
12
15
  const options = {
@@ -26,7 +29,7 @@ async function microsoftApiRequest(method, resource, body = {}, qs = {}, uri, he
26
29
  if (Object.keys(body).length === 0) {
27
30
  delete options.body;
28
31
  }
29
- return await this.helpers.requestOAuth2.call(this, 'microsoftPlannerOAuth2Api', options);
32
+ return await this.helpers.httpRequestWithAuthentication.call(this, 'microsoftPlannerOAuth2Api', options);
30
33
  }
31
34
  catch (error) {
32
35
  throw new n8n_workflow_1.NodeApiError(this.getNode(), error);
@@ -37,7 +40,8 @@ async function microsoftApiRequestAllItems(propertyName, method, endpoint, body
37
40
  let responseData;
38
41
  let uri;
39
42
  do {
40
- responseData = await microsoftApiRequest.call(this, method, endpoint, body, query, uri);
43
+ const activeQuery = uri ? {} : query;
44
+ responseData = await microsoftApiRequest.call(this, method, endpoint, body, activeQuery, uri);
41
45
  uri = responseData['@odata.nextLink'];
42
46
  returnData.push.apply(returnData, responseData[propertyName]);
43
47
  } while (responseData['@odata.nextLink'] !== undefined);
@@ -65,21 +69,37 @@ function formatDateTime(dateTime) {
65
69
  return date.toISOString();
66
70
  }
67
71
  async function getUserIdByEmail(email) {
72
+ // 1. Check if it's already a UUID
73
+ if (/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(email)) {
74
+ return email;
75
+ }
68
76
  try {
69
- const response = await microsoftApiRequest.call(this, 'GET', `/users/${encodeURIComponent(email)}`);
77
+ // 2. Try direct lookup (fastest)
78
+ const response = await microsoftApiRequest.call(this, 'GET', `/users/${encodeURIComponent(email)}`, {}, { $select: 'id' });
70
79
  return response.id;
71
80
  }
72
81
  catch (error) {
73
- // Log error for debugging
74
- console.error(`Failed to get user ID for email ${email}:`, error.message || error);
75
- return null;
82
+ // 3. Try filter lookup (handling UPN mismatch or restricted visibility)
83
+ try {
84
+ const users = await microsoftApiRequestAllItems.call(this, 'value', 'GET', '/users', {}, { $filter: `mail eq '${email}' or userPrincipalName eq '${email}'`, $select: 'id' });
85
+ if (users && users.length > 0) {
86
+ return users[0].id;
87
+ }
88
+ }
89
+ catch (filterError) {
90
+ // Ignore filter error, throw original or new error
91
+ }
92
+ throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Could not find user with email/ID: ${email}`);
76
93
  }
77
94
  }
78
- function parseAssignments(assignmentsString) {
79
- if (!assignmentsString || assignmentsString.trim() === '') {
95
+ function parseAssignments(assignments) {
96
+ if (Array.isArray(assignments)) {
97
+ return assignments;
98
+ }
99
+ if (!assignments || assignments.trim() === '') {
80
100
  return [];
81
101
  }
82
- return assignmentsString
102
+ return assignments
83
103
  .split(',')
84
104
  .map((email) => email.trim())
85
105
  .filter((email) => email.length > 0);
@@ -94,3 +114,48 @@ function createAssignmentsObject(userIds) {
94
114
  }
95
115
  return assignments;
96
116
  }
117
+ /**
118
+ * Encodes a URL for use as a key in plannerExternalReferences.
119
+ * OData requires keys in open types to be free of certain special characters like '.',
120
+ * which must be encoded.
121
+ */
122
+ function encodeReferenceKey(url) {
123
+ // Use encodeURI to handle spaces and % but preserve URI structure (/, ?, &, =)
124
+ let encoded = encodeURI(url);
125
+ // Manually replace OData forbidden characters that encodeURI preserves
126
+ // Forbidden: . : % @ #
127
+ // encodeURI handles % (encodes to %25 unless it's a valid escape sequence)
128
+ encoded = encoded
129
+ .replace(/\./g, '%2E')
130
+ .replace(/:/g, '%3A')
131
+ .replace(/@/g, '%40')
132
+ .replace(/#/g, '%23');
133
+ return encoded;
134
+ }
135
+ /**
136
+ * Generates a GUID/UUID v4.
137
+ * Useful for creating new ID keys for checklist items.
138
+ */
139
+ function generateGuid() {
140
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
141
+ const r = (Math.random() * 16) | 0;
142
+ const v = c === 'x' ? r : (r & 0x3) | 0x8;
143
+ return v.toString(16);
144
+ });
145
+ }
146
+ /**
147
+ * Formats comment content for Microsoft Graph API conversation posts.
148
+ * Wraps plain text in HTML paragraph tags if needed.
149
+ */
150
+ function formatCommentContent(content, contentType = 'text') {
151
+ // When we wrap text in HTML tags, we need to set contentType to 'html'
152
+ // Otherwise the tags will be displayed as literal text in Planner
153
+ const isHtml = contentType === 'html';
154
+ const formattedContent = isHtml ? content : `<p>${content}</p>`;
155
+ return {
156
+ body: {
157
+ contentType: 'html', // Always use 'html' since we're wrapping in tags
158
+ content: formattedContent,
159
+ },
160
+ };
161
+ }
@@ -9,6 +9,9 @@ export declare class MicrosoftPlanner implements INodeType {
9
9
  getTasks(this: ILoadOptionsFunctions): Promise<{
10
10
  results: any;
11
11
  }>;
12
+ getGroups(this: ILoadOptionsFunctions): Promise<{
13
+ results: any;
14
+ }>;
12
15
  };
13
16
  };
14
17
  execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;