n8n-nodes-notion-advanced 1.0.0-beta.1 → 1.1.0-beta.2
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.
package/README.md
CHANGED
@@ -1,6 +1,16 @@
|
|
1
|
-
# n8n Notion Advanced
|
1
|
+
# n8n Notion Advanced Nodes
|
2
2
|
|
3
|
-
A comprehensive n8n community
|
3
|
+
A comprehensive n8n community package that provides complete access to the Notion API v2022-06-28 with support for all block types, rich text formatting, and CRUD operations. Includes both a full-featured workflow node and an AI Agent Tool for intelligent automation.
|
4
|
+
|
5
|
+
## What's Included
|
6
|
+
|
7
|
+
### 1. **Notion Advanced** - Full-Featured Workflow Node
|
8
|
+
Complete Notion integration for traditional n8n workflows with comprehensive CRUD operations and all 25+ block types.
|
9
|
+
|
10
|
+
### 2. **Notion AI Tool** - AI Agent Integration
|
11
|
+
Specialized tool designed for n8n's AI Agent Nodes, enabling natural language Notion automation.
|
12
|
+
|
13
|
+
📖 **For AI Agent usage, see [AI-TOOL-USAGE.md](./AI-TOOL-USAGE.md)**
|
4
14
|
|
5
15
|
## Features
|
6
16
|
|
@@ -27,12 +37,41 @@ A comprehensive n8n community node that provides complete access to the Notion A
|
|
27
37
|
|
28
38
|
## Installation
|
29
39
|
|
40
|
+
### Via n8n GUI (Recommended)
|
41
|
+
1. Open n8n Settings → Community Nodes
|
42
|
+
2. Click "Install a community node"
|
43
|
+
3. Enter: `n8n-nodes-notion-advanced`
|
44
|
+
4. Click Install
|
45
|
+
|
46
|
+
### Via npm
|
30
47
|
```bash
|
31
|
-
npm install n8n-notion-advanced
|
48
|
+
npm install n8n-nodes-notion-advanced
|
32
49
|
```
|
33
50
|
|
34
51
|
📖 **For detailed installation instructions including Docker setup, development mode, and troubleshooting, see [INSTALLATION.md](./INSTALLATION.md)**
|
35
52
|
|
53
|
+
## Quick Start
|
54
|
+
|
55
|
+
### For AI Agents
|
56
|
+
```markdown
|
57
|
+
1. Add AI Agent Node to your workflow
|
58
|
+
2. Configure your chat model (OpenAI, Anthropic, etc.)
|
59
|
+
3. Add "Notion AI Tool" from available tools
|
60
|
+
4. Configure Notion credentials
|
61
|
+
5. AI Agent can now create/manage Notion content with natural language!
|
62
|
+
|
63
|
+
Example: "Create a project plan page with timeline and milestones"
|
64
|
+
```
|
65
|
+
|
66
|
+
### For Traditional Workflows
|
67
|
+
```markdown
|
68
|
+
1. Add "Notion Advanced" node to your workflow
|
69
|
+
2. Configure Notion credentials
|
70
|
+
3. Choose resource (Page/Block/Database/User)
|
71
|
+
4. Select operation and configure parameters
|
72
|
+
5. Execute comprehensive Notion operations
|
73
|
+
```
|
74
|
+
|
36
75
|
## Prerequisites
|
37
76
|
|
38
77
|
- n8n instance (self-hosted or cloud)
|
@@ -0,0 +1,622 @@
|
|
1
|
+
const { NodeConnectionType, NodeOperationError } = require('n8n-workflow');
|
2
|
+
|
3
|
+
const {
|
4
|
+
notionApiRequest,
|
5
|
+
validateCredentials,
|
6
|
+
createRichText,
|
7
|
+
resolvePageId,
|
8
|
+
} = require('./NotionUtils');
|
9
|
+
|
10
|
+
class NotionAITool {
|
11
|
+
constructor() {
|
12
|
+
this.description = {
|
13
|
+
displayName: 'Notion AI Tool',
|
14
|
+
name: 'notionAiTool',
|
15
|
+
icon: 'file:notion.svg',
|
16
|
+
group: ['ai'],
|
17
|
+
version: 1,
|
18
|
+
subtitle: '={{$parameter["operation"]}}',
|
19
|
+
description: 'AI-powered tool for creating and managing Notion content. Designed for use with AI Agent Nodes.',
|
20
|
+
defaults: {
|
21
|
+
name: 'Notion AI Tool',
|
22
|
+
},
|
23
|
+
inputs: [NodeConnectionType.Main],
|
24
|
+
outputs: [NodeConnectionType.Main],
|
25
|
+
credentials: [
|
26
|
+
{
|
27
|
+
name: 'notionApi',
|
28
|
+
required: true,
|
29
|
+
},
|
30
|
+
],
|
31
|
+
properties: [
|
32
|
+
{
|
33
|
+
displayName: 'Operation',
|
34
|
+
name: 'operation',
|
35
|
+
type: 'options',
|
36
|
+
noDataExpression: true,
|
37
|
+
options: [
|
38
|
+
{
|
39
|
+
name: 'Create Page with Content',
|
40
|
+
value: 'createPageWithContent',
|
41
|
+
description: 'Create a new Notion page with structured content including text, headings, lists, and formatting',
|
42
|
+
action: 'Create a Notion page with content',
|
43
|
+
},
|
44
|
+
{
|
45
|
+
name: 'Add Content to Page',
|
46
|
+
value: 'addContentToPage',
|
47
|
+
description: 'Append new content blocks (paragraphs, headings, lists, etc.) to an existing Notion page',
|
48
|
+
action: 'Add content to existing page',
|
49
|
+
},
|
50
|
+
{
|
51
|
+
name: 'Search and Retrieve Pages',
|
52
|
+
value: 'searchPages',
|
53
|
+
description: 'Search for Notion pages by title, content, or properties and retrieve their information',
|
54
|
+
action: 'Search and retrieve pages',
|
55
|
+
},
|
56
|
+
{
|
57
|
+
name: 'Update Page Properties',
|
58
|
+
value: 'updatePageProperties',
|
59
|
+
description: 'Update page title, properties, status, tags, or other metadata',
|
60
|
+
action: 'Update page properties',
|
61
|
+
},
|
62
|
+
{
|
63
|
+
name: 'Create Database Entry',
|
64
|
+
value: 'createDatabaseEntry',
|
65
|
+
description: 'Create a new entry in a Notion database with specified properties and values',
|
66
|
+
action: 'Create database entry',
|
67
|
+
},
|
68
|
+
{
|
69
|
+
name: 'Query Database',
|
70
|
+
value: 'queryDatabase',
|
71
|
+
description: 'Search and filter database entries based on criteria and retrieve matching records',
|
72
|
+
action: 'Query database',
|
73
|
+
},
|
74
|
+
],
|
75
|
+
default: 'createPageWithContent',
|
76
|
+
},
|
77
|
+
// CREATE PAGE WITH CONTENT
|
78
|
+
{
|
79
|
+
displayName: 'Page Title',
|
80
|
+
name: 'pageTitle',
|
81
|
+
type: 'string',
|
82
|
+
required: true,
|
83
|
+
displayOptions: {
|
84
|
+
show: {
|
85
|
+
operation: ['createPageWithContent'],
|
86
|
+
},
|
87
|
+
},
|
88
|
+
default: '',
|
89
|
+
description: 'The title of the new page to create',
|
90
|
+
},
|
91
|
+
{
|
92
|
+
displayName: 'Parent Page/Database ID',
|
93
|
+
name: 'parentId',
|
94
|
+
type: 'string',
|
95
|
+
required: true,
|
96
|
+
displayOptions: {
|
97
|
+
show: {
|
98
|
+
operation: ['createPageWithContent', 'createDatabaseEntry'],
|
99
|
+
},
|
100
|
+
},
|
101
|
+
default: '',
|
102
|
+
description: 'ID of the parent page or database where this should be created. Can be a Notion URL or page ID.',
|
103
|
+
},
|
104
|
+
{
|
105
|
+
displayName: 'Content',
|
106
|
+
name: 'content',
|
107
|
+
type: 'string',
|
108
|
+
typeOptions: {
|
109
|
+
rows: 6,
|
110
|
+
},
|
111
|
+
displayOptions: {
|
112
|
+
show: {
|
113
|
+
operation: ['createPageWithContent', 'addContentToPage'],
|
114
|
+
},
|
115
|
+
},
|
116
|
+
default: '',
|
117
|
+
description: 'The content to add. Use natural language - AI will structure it into appropriate blocks (headings, paragraphs, lists, etc.)',
|
118
|
+
placeholder: 'Example:\n# Main Heading\nThis is a paragraph with **bold** and *italic* text.\n\n## Subheading\n- First bullet point\n- Second bullet point\n\n> This is a quote block',
|
119
|
+
},
|
120
|
+
// ADD CONTENT TO PAGE
|
121
|
+
{
|
122
|
+
displayName: 'Target Page ID',
|
123
|
+
name: 'targetPageId',
|
124
|
+
type: 'string',
|
125
|
+
required: true,
|
126
|
+
displayOptions: {
|
127
|
+
show: {
|
128
|
+
operation: ['addContentToPage', 'updatePageProperties'],
|
129
|
+
},
|
130
|
+
},
|
131
|
+
default: '',
|
132
|
+
description: 'ID or URL of the existing page to modify',
|
133
|
+
},
|
134
|
+
// SEARCH PAGES
|
135
|
+
{
|
136
|
+
displayName: 'Search Query',
|
137
|
+
name: 'searchQuery',
|
138
|
+
type: 'string',
|
139
|
+
displayOptions: {
|
140
|
+
show: {
|
141
|
+
operation: ['searchPages'],
|
142
|
+
},
|
143
|
+
},
|
144
|
+
default: '',
|
145
|
+
description: 'Search terms to find pages. Leave empty to get all pages.',
|
146
|
+
},
|
147
|
+
{
|
148
|
+
displayName: 'Search Type',
|
149
|
+
name: 'searchType',
|
150
|
+
type: 'options',
|
151
|
+
displayOptions: {
|
152
|
+
show: {
|
153
|
+
operation: ['searchPages'],
|
154
|
+
},
|
155
|
+
},
|
156
|
+
options: [
|
157
|
+
{
|
158
|
+
name: 'All Content',
|
159
|
+
value: 'all',
|
160
|
+
description: 'Search in page titles and content',
|
161
|
+
},
|
162
|
+
{
|
163
|
+
name: 'Titles Only',
|
164
|
+
value: 'title',
|
165
|
+
description: 'Search only in page titles',
|
166
|
+
},
|
167
|
+
{
|
168
|
+
name: 'Recent Pages',
|
169
|
+
value: 'recent',
|
170
|
+
description: 'Get recently modified pages',
|
171
|
+
},
|
172
|
+
],
|
173
|
+
default: 'all',
|
174
|
+
},
|
175
|
+
// UPDATE PAGE PROPERTIES
|
176
|
+
{
|
177
|
+
displayName: 'Properties to Update',
|
178
|
+
name: 'propertiesToUpdate',
|
179
|
+
type: 'string',
|
180
|
+
typeOptions: {
|
181
|
+
rows: 4,
|
182
|
+
},
|
183
|
+
displayOptions: {
|
184
|
+
show: {
|
185
|
+
operation: ['updatePageProperties'],
|
186
|
+
},
|
187
|
+
},
|
188
|
+
default: '',
|
189
|
+
description: 'Properties to update in JSON format or natural language. Example: {"status": "In Progress", "priority": "High"} or "Set status to Done and priority to Low"',
|
190
|
+
},
|
191
|
+
// DATABASE OPERATIONS
|
192
|
+
{
|
193
|
+
displayName: 'Database ID',
|
194
|
+
name: 'databaseId',
|
195
|
+
type: 'string',
|
196
|
+
required: true,
|
197
|
+
displayOptions: {
|
198
|
+
show: {
|
199
|
+
operation: ['queryDatabase'],
|
200
|
+
},
|
201
|
+
},
|
202
|
+
default: '',
|
203
|
+
description: 'ID or URL of the database to query',
|
204
|
+
},
|
205
|
+
{
|
206
|
+
displayName: 'Entry Properties',
|
207
|
+
name: 'entryProperties',
|
208
|
+
type: 'string',
|
209
|
+
typeOptions: {
|
210
|
+
rows: 4,
|
211
|
+
},
|
212
|
+
displayOptions: {
|
213
|
+
show: {
|
214
|
+
operation: ['createDatabaseEntry'],
|
215
|
+
},
|
216
|
+
},
|
217
|
+
default: '',
|
218
|
+
description: 'Properties for the new database entry in JSON format or natural language description',
|
219
|
+
},
|
220
|
+
{
|
221
|
+
displayName: 'Query Filter',
|
222
|
+
name: 'queryFilter',
|
223
|
+
type: 'string',
|
224
|
+
displayOptions: {
|
225
|
+
show: {
|
226
|
+
operation: ['queryDatabase'],
|
227
|
+
},
|
228
|
+
},
|
229
|
+
default: '',
|
230
|
+
description: 'Filter criteria in natural language (e.g., "status is Done and priority is High") or JSON format',
|
231
|
+
},
|
232
|
+
// COMMON OPTIONS
|
233
|
+
{
|
234
|
+
displayName: 'Additional Options',
|
235
|
+
name: 'additionalOptions',
|
236
|
+
type: 'collection',
|
237
|
+
placeholder: 'Add Option',
|
238
|
+
default: {},
|
239
|
+
options: [
|
240
|
+
{
|
241
|
+
displayName: 'Icon',
|
242
|
+
name: 'icon',
|
243
|
+
type: 'string',
|
244
|
+
default: '',
|
245
|
+
description: 'Emoji icon for the page (e.g., 📝, 🎯, 📊)',
|
246
|
+
},
|
247
|
+
{
|
248
|
+
displayName: 'Cover Image URL',
|
249
|
+
name: 'coverUrl',
|
250
|
+
type: 'string',
|
251
|
+
default: '',
|
252
|
+
description: 'URL of cover image for the page',
|
253
|
+
},
|
254
|
+
{
|
255
|
+
displayName: 'Return Full Content',
|
256
|
+
name: 'returnFullContent',
|
257
|
+
type: 'boolean',
|
258
|
+
default: false,
|
259
|
+
description: 'Whether to return full page content or just metadata',
|
260
|
+
},
|
261
|
+
{
|
262
|
+
displayName: 'Max Results',
|
263
|
+
name: 'maxResults',
|
264
|
+
type: 'number',
|
265
|
+
default: 20,
|
266
|
+
description: 'Maximum number of results to return (1-100)',
|
267
|
+
},
|
268
|
+
],
|
269
|
+
},
|
270
|
+
],
|
271
|
+
};
|
272
|
+
}
|
273
|
+
|
274
|
+
async execute() {
|
275
|
+
const items = this.getInputData();
|
276
|
+
const responseData = [];
|
277
|
+
|
278
|
+
// Validate credentials
|
279
|
+
const isValid = await validateCredentials.call(this);
|
280
|
+
if (!isValid) {
|
281
|
+
throw new NodeOperationError(this.getNode(), 'Invalid Notion API credentials');
|
282
|
+
}
|
283
|
+
|
284
|
+
for (let i = 0; i < items.length; i++) {
|
285
|
+
try {
|
286
|
+
const operation = this.getNodeParameter('operation', i);
|
287
|
+
let result;
|
288
|
+
|
289
|
+
switch (operation) {
|
290
|
+
case 'createPageWithContent':
|
291
|
+
result = await this.createPageWithContent(i);
|
292
|
+
break;
|
293
|
+
case 'addContentToPage':
|
294
|
+
result = await this.addContentToPage(i);
|
295
|
+
break;
|
296
|
+
case 'searchPages':
|
297
|
+
result = await this.searchPages(i);
|
298
|
+
break;
|
299
|
+
case 'updatePageProperties':
|
300
|
+
result = await this.updatePageProperties(i);
|
301
|
+
break;
|
302
|
+
case 'createDatabaseEntry':
|
303
|
+
result = await this.createDatabaseEntry(i);
|
304
|
+
break;
|
305
|
+
case 'queryDatabase':
|
306
|
+
result = await this.queryDatabase(i);
|
307
|
+
break;
|
308
|
+
default:
|
309
|
+
throw new NodeOperationError(this.getNode(), `Unknown operation: ${operation}`);
|
310
|
+
}
|
311
|
+
|
312
|
+
responseData.push({
|
313
|
+
operation,
|
314
|
+
success: true,
|
315
|
+
...result,
|
316
|
+
});
|
317
|
+
} catch (error) {
|
318
|
+
if (this.continueOnFail()) {
|
319
|
+
responseData.push({
|
320
|
+
error: error.message,
|
321
|
+
success: false,
|
322
|
+
});
|
323
|
+
} else {
|
324
|
+
throw error;
|
325
|
+
}
|
326
|
+
}
|
327
|
+
}
|
328
|
+
|
329
|
+
return [this.helpers.returnJsonArray(responseData)];
|
330
|
+
}
|
331
|
+
|
332
|
+
async createPageWithContent(itemIndex) {
|
333
|
+
const pageTitle = this.getNodeParameter('pageTitle', itemIndex);
|
334
|
+
const parentId = this.getNodeParameter('parentId', itemIndex);
|
335
|
+
const content = this.getNodeParameter('content', itemIndex, '');
|
336
|
+
const additionalOptions = this.getNodeParameter('additionalOptions', itemIndex, {});
|
337
|
+
|
338
|
+
const resolvedParentId = await resolvePageId.call(this, parentId);
|
339
|
+
|
340
|
+
// Create the page first
|
341
|
+
const pageBody = {
|
342
|
+
parent: { page_id: resolvedParentId },
|
343
|
+
properties: {
|
344
|
+
title: {
|
345
|
+
title: [createRichText(pageTitle)],
|
346
|
+
},
|
347
|
+
},
|
348
|
+
};
|
349
|
+
|
350
|
+
// Add icon and cover if provided
|
351
|
+
if (additionalOptions.icon) {
|
352
|
+
pageBody.icon = { type: 'emoji', emoji: additionalOptions.icon };
|
353
|
+
}
|
354
|
+
if (additionalOptions.coverUrl) {
|
355
|
+
pageBody.cover = { type: 'external', external: { url: additionalOptions.coverUrl } };
|
356
|
+
}
|
357
|
+
|
358
|
+
const page = await notionApiRequest.call(this, 'POST', '/pages', pageBody);
|
359
|
+
|
360
|
+
// If content is provided, add it to the page
|
361
|
+
if (content) {
|
362
|
+
const blocks = this.parseContentToBlocks(content);
|
363
|
+
if (blocks.length > 0) {
|
364
|
+
await notionApiRequest.call(this, 'PATCH', `/blocks/${page.id}/children`, {
|
365
|
+
children: blocks,
|
366
|
+
});
|
367
|
+
}
|
368
|
+
}
|
369
|
+
|
370
|
+
return {
|
371
|
+
pageId: page.id,
|
372
|
+
title: pageTitle,
|
373
|
+
url: page.url,
|
374
|
+
message: `Created page "${pageTitle}" with content`,
|
375
|
+
};
|
376
|
+
}
|
377
|
+
|
378
|
+
async addContentToPage(itemIndex) {
|
379
|
+
const targetPageId = this.getNodeParameter('targetPageId', itemIndex);
|
380
|
+
const content = this.getNodeParameter('content', itemIndex);
|
381
|
+
|
382
|
+
const resolvedPageId = await resolvePageId.call(this, targetPageId);
|
383
|
+
const blocks = this.parseContentToBlocks(content);
|
384
|
+
|
385
|
+
if (blocks.length === 0) {
|
386
|
+
throw new NodeOperationError(this.getNode(), 'No valid content blocks found to add');
|
387
|
+
}
|
388
|
+
|
389
|
+
const result = await notionApiRequest.call(this, 'PATCH', `/blocks/${resolvedPageId}/children`, {
|
390
|
+
children: blocks,
|
391
|
+
});
|
392
|
+
|
393
|
+
return {
|
394
|
+
pageId: resolvedPageId,
|
395
|
+
blocksAdded: blocks.length,
|
396
|
+
message: `Added ${blocks.length} content blocks to page`,
|
397
|
+
result,
|
398
|
+
};
|
399
|
+
}
|
400
|
+
|
401
|
+
async searchPages(itemIndex) {
|
402
|
+
const searchQuery = this.getNodeParameter('searchQuery', itemIndex, '');
|
403
|
+
const searchType = this.getNodeParameter('searchType', itemIndex, 'all');
|
404
|
+
const additionalOptions = this.getNodeParameter('additionalOptions', itemIndex, {});
|
405
|
+
const maxResults = additionalOptions.maxResults || 20;
|
406
|
+
|
407
|
+
const body = {
|
408
|
+
page_size: Math.min(maxResults, 100),
|
409
|
+
};
|
410
|
+
|
411
|
+
if (searchQuery) {
|
412
|
+
body.query = searchQuery;
|
413
|
+
}
|
414
|
+
|
415
|
+
body.filter = {
|
416
|
+
property: 'object',
|
417
|
+
value: 'page',
|
418
|
+
};
|
419
|
+
|
420
|
+
const response = await notionApiRequest.call(this, 'POST', '/search', body);
|
421
|
+
|
422
|
+
return {
|
423
|
+
totalResults: response.results?.length || 0,
|
424
|
+
pages: response.results || [],
|
425
|
+
message: `Found ${response.results?.length || 0} pages`,
|
426
|
+
};
|
427
|
+
}
|
428
|
+
|
429
|
+
async updatePageProperties(itemIndex) {
|
430
|
+
const targetPageId = this.getNodeParameter('targetPageId', itemIndex);
|
431
|
+
const propertiesToUpdate = this.getNodeParameter('propertiesToUpdate', itemIndex);
|
432
|
+
|
433
|
+
const resolvedPageId = await resolvePageId.call(this, targetPageId);
|
434
|
+
const properties = this.parsePropertiesToUpdate(propertiesToUpdate);
|
435
|
+
|
436
|
+
const result = await notionApiRequest.call(this, 'PATCH', `/pages/${resolvedPageId}`, {
|
437
|
+
properties,
|
438
|
+
});
|
439
|
+
|
440
|
+
return {
|
441
|
+
pageId: resolvedPageId,
|
442
|
+
updatedProperties: Object.keys(properties),
|
443
|
+
message: `Updated ${Object.keys(properties).length} properties`,
|
444
|
+
result,
|
445
|
+
};
|
446
|
+
}
|
447
|
+
|
448
|
+
async createDatabaseEntry(itemIndex) {
|
449
|
+
const parentId = this.getNodeParameter('parentId', itemIndex);
|
450
|
+
const entryProperties = this.getNodeParameter('entryProperties', itemIndex);
|
451
|
+
|
452
|
+
const resolvedParentId = await resolvePageId.call(this, parentId);
|
453
|
+
const properties = this.parsePropertiesToUpdate(entryProperties);
|
454
|
+
|
455
|
+
const result = await notionApiRequest.call(this, 'POST', '/pages', {
|
456
|
+
parent: { database_id: resolvedParentId },
|
457
|
+
properties,
|
458
|
+
});
|
459
|
+
|
460
|
+
return {
|
461
|
+
entryId: result.id,
|
462
|
+
databaseId: resolvedParentId,
|
463
|
+
message: 'Created new database entry',
|
464
|
+
result,
|
465
|
+
};
|
466
|
+
}
|
467
|
+
|
468
|
+
async queryDatabase(itemIndex) {
|
469
|
+
const databaseId = this.getNodeParameter('databaseId', itemIndex);
|
470
|
+
const queryFilter = this.getNodeParameter('queryFilter', itemIndex, '');
|
471
|
+
const additionalOptions = this.getNodeParameter('additionalOptions', itemIndex, {});
|
472
|
+
const maxResults = additionalOptions.maxResults || 20;
|
473
|
+
|
474
|
+
const resolvedDatabaseId = await resolvePageId.call(this, databaseId);
|
475
|
+
const body = {
|
476
|
+
page_size: Math.min(maxResults, 100),
|
477
|
+
};
|
478
|
+
|
479
|
+
if (queryFilter) {
|
480
|
+
try {
|
481
|
+
body.filter = JSON.parse(queryFilter);
|
482
|
+
} catch {
|
483
|
+
// If not JSON, create a simple text filter
|
484
|
+
body.filter = {
|
485
|
+
property: 'Name',
|
486
|
+
title: {
|
487
|
+
contains: queryFilter,
|
488
|
+
},
|
489
|
+
};
|
490
|
+
}
|
491
|
+
}
|
492
|
+
|
493
|
+
const response = await notionApiRequest.call(this, 'POST', `/databases/${resolvedDatabaseId}/query`, body);
|
494
|
+
|
495
|
+
return {
|
496
|
+
databaseId: resolvedDatabaseId,
|
497
|
+
totalResults: response.results?.length || 0,
|
498
|
+
entries: response.results || [],
|
499
|
+
message: `Found ${response.results?.length || 0} database entries`,
|
500
|
+
};
|
501
|
+
}
|
502
|
+
|
503
|
+
parseContentToBlocks(content) {
|
504
|
+
const blocks = [];
|
505
|
+
const lines = content.split('\n');
|
506
|
+
|
507
|
+
for (let i = 0; i < lines.length; i++) {
|
508
|
+
const line = lines[i].trim();
|
509
|
+
if (!line) continue;
|
510
|
+
|
511
|
+
// Parse different content types
|
512
|
+
if (line.startsWith('# ')) {
|
513
|
+
blocks.push({
|
514
|
+
object: 'block',
|
515
|
+
type: 'heading_1',
|
516
|
+
heading_1: {
|
517
|
+
rich_text: [createRichText(line.substring(2))],
|
518
|
+
},
|
519
|
+
});
|
520
|
+
} else if (line.startsWith('## ')) {
|
521
|
+
blocks.push({
|
522
|
+
object: 'block',
|
523
|
+
type: 'heading_2',
|
524
|
+
heading_2: {
|
525
|
+
rich_text: [createRichText(line.substring(3))],
|
526
|
+
},
|
527
|
+
});
|
528
|
+
} else if (line.startsWith('### ')) {
|
529
|
+
blocks.push({
|
530
|
+
object: 'block',
|
531
|
+
type: 'heading_3',
|
532
|
+
heading_3: {
|
533
|
+
rich_text: [createRichText(line.substring(4))],
|
534
|
+
},
|
535
|
+
});
|
536
|
+
} else if (line.startsWith('- ') || line.startsWith('* ')) {
|
537
|
+
blocks.push({
|
538
|
+
object: 'block',
|
539
|
+
type: 'bulleted_list_item',
|
540
|
+
bulleted_list_item: {
|
541
|
+
rich_text: [createRichText(line.substring(2))],
|
542
|
+
},
|
543
|
+
});
|
544
|
+
} else if (line.match(/^\d+\. /)) {
|
545
|
+
blocks.push({
|
546
|
+
object: 'block',
|
547
|
+
type: 'numbered_list_item',
|
548
|
+
numbered_list_item: {
|
549
|
+
rich_text: [createRichText(line.replace(/^\d+\. /, ''))],
|
550
|
+
},
|
551
|
+
});
|
552
|
+
} else if (line.startsWith('> ')) {
|
553
|
+
blocks.push({
|
554
|
+
object: 'block',
|
555
|
+
type: 'quote',
|
556
|
+
quote: {
|
557
|
+
rich_text: [createRichText(line.substring(2))],
|
558
|
+
},
|
559
|
+
});
|
560
|
+
} else if (line.startsWith('```')) {
|
561
|
+
// Handle code blocks
|
562
|
+
const codeLines = [];
|
563
|
+
i++; // Skip the opening ```
|
564
|
+
while (i < lines.length && !lines[i].trim().startsWith('```')) {
|
565
|
+
codeLines.push(lines[i]);
|
566
|
+
i++;
|
567
|
+
}
|
568
|
+
blocks.push({
|
569
|
+
object: 'block',
|
570
|
+
type: 'code',
|
571
|
+
code: {
|
572
|
+
rich_text: [createRichText(codeLines.join('\n'))],
|
573
|
+
language: 'plain text',
|
574
|
+
},
|
575
|
+
});
|
576
|
+
} else {
|
577
|
+
// Regular paragraph
|
578
|
+
blocks.push({
|
579
|
+
object: 'block',
|
580
|
+
type: 'paragraph',
|
581
|
+
paragraph: {
|
582
|
+
rich_text: [createRichText(line)],
|
583
|
+
},
|
584
|
+
});
|
585
|
+
}
|
586
|
+
}
|
587
|
+
|
588
|
+
return blocks;
|
589
|
+
}
|
590
|
+
|
591
|
+
parsePropertiesToUpdate(propertiesString) {
|
592
|
+
try {
|
593
|
+
// Try to parse as JSON first
|
594
|
+
return JSON.parse(propertiesString);
|
595
|
+
} catch {
|
596
|
+
// If not JSON, try to parse natural language
|
597
|
+
const properties = {};
|
598
|
+
|
599
|
+
// Simple natural language parsing
|
600
|
+
const patterns = [
|
601
|
+
/set\s+(\w+)\s+to\s+(.+?)(?:\s+and|$)/gi,
|
602
|
+
/(\w+)\s*:\s*(.+?)(?:\s*,|$)/gi,
|
603
|
+
/(\w+)\s*=\s*(.+?)(?:\s*,|$)/gi,
|
604
|
+
];
|
605
|
+
|
606
|
+
for (const pattern of patterns) {
|
607
|
+
let match;
|
608
|
+
while ((match = pattern.exec(propertiesString)) !== null) {
|
609
|
+
const [, key, value] = match;
|
610
|
+
properties[key.trim()] = {
|
611
|
+
rich_text: [createRichText(value.trim())],
|
612
|
+
};
|
613
|
+
}
|
614
|
+
}
|
615
|
+
|
616
|
+
return properties;
|
617
|
+
}
|
618
|
+
}
|
619
|
+
}
|
620
|
+
|
621
|
+
module.exports = { NotionAITool };
|
622
|
+
module.exports = { NotionAITool };
|
@@ -0,0 +1,641 @@
|
|
1
|
+
import {
|
2
|
+
IExecuteFunctions,
|
3
|
+
INodeExecutionData,
|
4
|
+
INodeType,
|
5
|
+
INodeTypeDescription,
|
6
|
+
IDataObject,
|
7
|
+
NodeOperationError,
|
8
|
+
NodeConnectionType,
|
9
|
+
} from 'n8n-workflow';
|
10
|
+
|
11
|
+
import {
|
12
|
+
notionApiRequest,
|
13
|
+
validateCredentials,
|
14
|
+
createRichText,
|
15
|
+
resolvePageId,
|
16
|
+
} from './NotionUtils';
|
17
|
+
|
18
|
+
export class NotionAITool implements INodeType {
|
19
|
+
description: INodeTypeDescription = {
|
20
|
+
displayName: 'Notion AI Tool',
|
21
|
+
name: 'notionAiTool',
|
22
|
+
icon: 'file:notion.svg',
|
23
|
+
group: ['ai'],
|
24
|
+
version: 1,
|
25
|
+
subtitle: '={{$parameter["operation"]}}',
|
26
|
+
description: 'AI-powered tool for creating and managing Notion content. Designed for use with AI Agent Nodes.',
|
27
|
+
defaults: {
|
28
|
+
name: 'Notion AI Tool',
|
29
|
+
},
|
30
|
+
inputs: [NodeConnectionType.Main],
|
31
|
+
outputs: [NodeConnectionType.Main],
|
32
|
+
credentials: [
|
33
|
+
{
|
34
|
+
name: 'notionApi',
|
35
|
+
required: true,
|
36
|
+
},
|
37
|
+
],
|
38
|
+
properties: [
|
39
|
+
{
|
40
|
+
displayName: 'Operation',
|
41
|
+
name: 'operation',
|
42
|
+
type: 'options',
|
43
|
+
noDataExpression: true,
|
44
|
+
options: [
|
45
|
+
{
|
46
|
+
name: 'Create Page with Content',
|
47
|
+
value: 'createPageWithContent',
|
48
|
+
description: 'Create a new Notion page with structured content including text, headings, lists, and formatting',
|
49
|
+
action: 'Create a Notion page with content',
|
50
|
+
},
|
51
|
+
{
|
52
|
+
name: 'Add Content to Page',
|
53
|
+
value: 'addContentToPage',
|
54
|
+
description: 'Append new content blocks (paragraphs, headings, lists, etc.) to an existing Notion page',
|
55
|
+
action: 'Add content to existing page',
|
56
|
+
},
|
57
|
+
{
|
58
|
+
name: 'Search and Retrieve Pages',
|
59
|
+
value: 'searchPages',
|
60
|
+
description: 'Search for Notion pages by title, content, or properties and retrieve their information',
|
61
|
+
action: 'Search and retrieve pages',
|
62
|
+
},
|
63
|
+
{
|
64
|
+
name: 'Update Page Properties',
|
65
|
+
value: 'updatePageProperties',
|
66
|
+
description: 'Update page title, properties, status, tags, or other metadata',
|
67
|
+
action: 'Update page properties',
|
68
|
+
},
|
69
|
+
{
|
70
|
+
name: 'Create Database Entry',
|
71
|
+
value: 'createDatabaseEntry',
|
72
|
+
description: 'Create a new entry in a Notion database with specified properties and values',
|
73
|
+
action: 'Create database entry',
|
74
|
+
},
|
75
|
+
{
|
76
|
+
name: 'Query Database',
|
77
|
+
value: 'queryDatabase',
|
78
|
+
description: 'Search and filter database entries based on criteria and retrieve matching records',
|
79
|
+
action: 'Query database',
|
80
|
+
},
|
81
|
+
],
|
82
|
+
default: 'createPageWithContent',
|
83
|
+
},
|
84
|
+
|
85
|
+
// CREATE PAGE WITH CONTENT
|
86
|
+
{
|
87
|
+
displayName: 'Page Title',
|
88
|
+
name: 'pageTitle',
|
89
|
+
type: 'string',
|
90
|
+
required: true,
|
91
|
+
displayOptions: {
|
92
|
+
show: {
|
93
|
+
operation: ['createPageWithContent'],
|
94
|
+
},
|
95
|
+
},
|
96
|
+
default: '',
|
97
|
+
description: 'The title of the new page to create',
|
98
|
+
},
|
99
|
+
{
|
100
|
+
displayName: 'Parent Page/Database ID',
|
101
|
+
name: 'parentId',
|
102
|
+
type: 'string',
|
103
|
+
required: true,
|
104
|
+
displayOptions: {
|
105
|
+
show: {
|
106
|
+
operation: ['createPageWithContent', 'createDatabaseEntry'],
|
107
|
+
},
|
108
|
+
},
|
109
|
+
default: '',
|
110
|
+
description: 'ID of the parent page or database where this should be created. Can be a Notion URL or page ID.',
|
111
|
+
},
|
112
|
+
{
|
113
|
+
displayName: 'Content',
|
114
|
+
name: 'content',
|
115
|
+
type: 'string',
|
116
|
+
typeOptions: {
|
117
|
+
rows: 6,
|
118
|
+
},
|
119
|
+
displayOptions: {
|
120
|
+
show: {
|
121
|
+
operation: ['createPageWithContent', 'addContentToPage'],
|
122
|
+
},
|
123
|
+
},
|
124
|
+
default: '',
|
125
|
+
description: 'The content to add. Use natural language - AI will structure it into appropriate blocks (headings, paragraphs, lists, etc.)',
|
126
|
+
placeholder: 'Example:\n# Main Heading\nThis is a paragraph with **bold** and *italic* text.\n\n## Subheading\n- First bullet point\n- Second bullet point\n\n> This is a quote block',
|
127
|
+
},
|
128
|
+
|
129
|
+
// ADD CONTENT TO PAGE
|
130
|
+
{
|
131
|
+
displayName: 'Target Page ID',
|
132
|
+
name: 'targetPageId',
|
133
|
+
type: 'string',
|
134
|
+
required: true,
|
135
|
+
displayOptions: {
|
136
|
+
show: {
|
137
|
+
operation: ['addContentToPage', 'updatePageProperties'],
|
138
|
+
},
|
139
|
+
},
|
140
|
+
default: '',
|
141
|
+
description: 'ID or URL of the existing page to modify',
|
142
|
+
},
|
143
|
+
|
144
|
+
// SEARCH PAGES
|
145
|
+
{
|
146
|
+
displayName: 'Search Query',
|
147
|
+
name: 'searchQuery',
|
148
|
+
type: 'string',
|
149
|
+
displayOptions: {
|
150
|
+
show: {
|
151
|
+
operation: ['searchPages'],
|
152
|
+
},
|
153
|
+
},
|
154
|
+
default: '',
|
155
|
+
description: 'Search terms to find pages. Leave empty to get all pages.',
|
156
|
+
},
|
157
|
+
{
|
158
|
+
displayName: 'Search Type',
|
159
|
+
name: 'searchType',
|
160
|
+
type: 'options',
|
161
|
+
displayOptions: {
|
162
|
+
show: {
|
163
|
+
operation: ['searchPages'],
|
164
|
+
},
|
165
|
+
},
|
166
|
+
options: [
|
167
|
+
{
|
168
|
+
name: 'All Content',
|
169
|
+
value: 'all',
|
170
|
+
description: 'Search in page titles and content',
|
171
|
+
},
|
172
|
+
{
|
173
|
+
name: 'Titles Only',
|
174
|
+
value: 'title',
|
175
|
+
description: 'Search only in page titles',
|
176
|
+
},
|
177
|
+
{
|
178
|
+
name: 'Recent Pages',
|
179
|
+
value: 'recent',
|
180
|
+
description: 'Get recently modified pages',
|
181
|
+
},
|
182
|
+
],
|
183
|
+
default: 'all',
|
184
|
+
},
|
185
|
+
|
186
|
+
// UPDATE PAGE PROPERTIES
|
187
|
+
{
|
188
|
+
displayName: 'Properties to Update',
|
189
|
+
name: 'propertiesToUpdate',
|
190
|
+
type: 'string',
|
191
|
+
typeOptions: {
|
192
|
+
rows: 4,
|
193
|
+
},
|
194
|
+
displayOptions: {
|
195
|
+
show: {
|
196
|
+
operation: ['updatePageProperties'],
|
197
|
+
},
|
198
|
+
},
|
199
|
+
default: '',
|
200
|
+
description: 'Properties to update in JSON format or natural language. Example: {"status": "In Progress", "priority": "High"} or "Set status to Done and priority to Low"',
|
201
|
+
},
|
202
|
+
|
203
|
+
// DATABASE OPERATIONS
|
204
|
+
{
|
205
|
+
displayName: 'Database ID',
|
206
|
+
name: 'databaseId',
|
207
|
+
type: 'string',
|
208
|
+
required: true,
|
209
|
+
displayOptions: {
|
210
|
+
show: {
|
211
|
+
operation: ['queryDatabase'],
|
212
|
+
},
|
213
|
+
},
|
214
|
+
default: '',
|
215
|
+
description: 'ID or URL of the database to query',
|
216
|
+
},
|
217
|
+
{
|
218
|
+
displayName: 'Entry Properties',
|
219
|
+
name: 'entryProperties',
|
220
|
+
type: 'string',
|
221
|
+
typeOptions: {
|
222
|
+
rows: 4,
|
223
|
+
},
|
224
|
+
displayOptions: {
|
225
|
+
show: {
|
226
|
+
operation: ['createDatabaseEntry'],
|
227
|
+
},
|
228
|
+
},
|
229
|
+
default: '',
|
230
|
+
description: 'Properties for the new database entry in JSON format or natural language description',
|
231
|
+
},
|
232
|
+
{
|
233
|
+
displayName: 'Query Filter',
|
234
|
+
name: 'queryFilter',
|
235
|
+
type: 'string',
|
236
|
+
displayOptions: {
|
237
|
+
show: {
|
238
|
+
operation: ['queryDatabase'],
|
239
|
+
},
|
240
|
+
},
|
241
|
+
default: '',
|
242
|
+
description: 'Filter criteria in natural language (e.g., "status is Done and priority is High") or JSON format',
|
243
|
+
},
|
244
|
+
|
245
|
+
// COMMON OPTIONS
|
246
|
+
{
|
247
|
+
displayName: 'Additional Options',
|
248
|
+
name: 'additionalOptions',
|
249
|
+
type: 'collection',
|
250
|
+
placeholder: 'Add Option',
|
251
|
+
default: {},
|
252
|
+
options: [
|
253
|
+
{
|
254
|
+
displayName: 'Icon',
|
255
|
+
name: 'icon',
|
256
|
+
type: 'string',
|
257
|
+
default: '',
|
258
|
+
description: 'Emoji icon for the page (e.g., 📝, 🎯, 📊)',
|
259
|
+
},
|
260
|
+
{
|
261
|
+
displayName: 'Cover Image URL',
|
262
|
+
name: 'coverUrl',
|
263
|
+
type: 'string',
|
264
|
+
default: '',
|
265
|
+
description: 'URL of cover image for the page',
|
266
|
+
},
|
267
|
+
{
|
268
|
+
displayName: 'Return Full Content',
|
269
|
+
name: 'returnFullContent',
|
270
|
+
type: 'boolean',
|
271
|
+
default: false,
|
272
|
+
description: 'Whether to return full page content or just metadata',
|
273
|
+
},
|
274
|
+
{
|
275
|
+
displayName: 'Max Results',
|
276
|
+
name: 'maxResults',
|
277
|
+
type: 'number',
|
278
|
+
default: 20,
|
279
|
+
description: 'Maximum number of results to return (1-100)',
|
280
|
+
},
|
281
|
+
],
|
282
|
+
},
|
283
|
+
],
|
284
|
+
};
|
285
|
+
|
286
|
+
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
287
|
+
const items = this.getInputData();
|
288
|
+
const responseData: IDataObject[] = [];
|
289
|
+
|
290
|
+
// Validate credentials
|
291
|
+
const isValid = await validateCredentials.call(this);
|
292
|
+
if (!isValid) {
|
293
|
+
throw new NodeOperationError(this.getNode(), 'Invalid Notion API credentials');
|
294
|
+
}
|
295
|
+
|
296
|
+
for (let i = 0; i < items.length; i++) {
|
297
|
+
try {
|
298
|
+
const operation = this.getNodeParameter('operation', i) as string;
|
299
|
+
let result: IDataObject;
|
300
|
+
|
301
|
+
const nodeInstance = this.getNode() as any;
|
302
|
+
|
303
|
+
switch (operation) {
|
304
|
+
case 'createPageWithContent':
|
305
|
+
result = await NotionAITool.prototype.createPageWithContent.call(this, i);
|
306
|
+
break;
|
307
|
+
case 'addContentToPage':
|
308
|
+
result = await NotionAITool.prototype.addContentToPage.call(this, i);
|
309
|
+
break;
|
310
|
+
case 'searchPages':
|
311
|
+
result = await NotionAITool.prototype.searchPages.call(this, i);
|
312
|
+
break;
|
313
|
+
case 'updatePageProperties':
|
314
|
+
result = await NotionAITool.prototype.updatePageProperties.call(this, i);
|
315
|
+
break;
|
316
|
+
case 'createDatabaseEntry':
|
317
|
+
result = await NotionAITool.prototype.createDatabaseEntry.call(this, i);
|
318
|
+
break;
|
319
|
+
case 'queryDatabase':
|
320
|
+
result = await NotionAITool.prototype.queryDatabase.call(this, i);
|
321
|
+
break;
|
322
|
+
default:
|
323
|
+
throw new NodeOperationError(this.getNode(), `Unknown operation: ${operation}`);
|
324
|
+
}
|
325
|
+
|
326
|
+
responseData.push({
|
327
|
+
operation,
|
328
|
+
success: true,
|
329
|
+
...result,
|
330
|
+
});
|
331
|
+
} catch (error) {
|
332
|
+
if (this.continueOnFail()) {
|
333
|
+
responseData.push({
|
334
|
+
error: (error as Error).message,
|
335
|
+
success: false,
|
336
|
+
});
|
337
|
+
} else {
|
338
|
+
throw error;
|
339
|
+
}
|
340
|
+
}
|
341
|
+
}
|
342
|
+
|
343
|
+
return [this.helpers.returnJsonArray(responseData)];
|
344
|
+
}
|
345
|
+
|
346
|
+
private async createPageWithContent(this: IExecuteFunctions, itemIndex: number): Promise<IDataObject> {
|
347
|
+
const pageTitle = this.getNodeParameter('pageTitle', itemIndex) as string;
|
348
|
+
const parentId = this.getNodeParameter('parentId', itemIndex) as string;
|
349
|
+
const content = this.getNodeParameter('content', itemIndex, '') as string;
|
350
|
+
const additionalOptions = this.getNodeParameter('additionalOptions', itemIndex, {}) as IDataObject;
|
351
|
+
|
352
|
+
const resolvedParentId = await resolvePageId.call(this, parentId);
|
353
|
+
|
354
|
+
// Create the page first
|
355
|
+
const pageBody: IDataObject = {
|
356
|
+
parent: { page_id: resolvedParentId },
|
357
|
+
properties: {
|
358
|
+
title: {
|
359
|
+
title: [createRichText(pageTitle)],
|
360
|
+
},
|
361
|
+
},
|
362
|
+
};
|
363
|
+
|
364
|
+
// Add icon and cover if provided
|
365
|
+
if (additionalOptions.icon) {
|
366
|
+
pageBody.icon = { type: 'emoji', emoji: additionalOptions.icon as string };
|
367
|
+
}
|
368
|
+
if (additionalOptions.coverUrl) {
|
369
|
+
pageBody.cover = { type: 'external', external: { url: additionalOptions.coverUrl as string } };
|
370
|
+
}
|
371
|
+
|
372
|
+
const page = await notionApiRequest.call(this, 'POST', '/pages', pageBody);
|
373
|
+
|
374
|
+
// If content is provided, add it to the page
|
375
|
+
if (content) {
|
376
|
+
const blocks = NotionAITool.prototype.parseContentToBlocks.call(this, content);
|
377
|
+
if (blocks.length > 0) {
|
378
|
+
await notionApiRequest.call(this, 'PATCH', `/blocks/${page.id}/children`, {
|
379
|
+
children: blocks,
|
380
|
+
});
|
381
|
+
}
|
382
|
+
}
|
383
|
+
|
384
|
+
return {
|
385
|
+
pageId: page.id,
|
386
|
+
title: pageTitle,
|
387
|
+
url: page.url,
|
388
|
+
message: `Created page "${pageTitle}" with content`,
|
389
|
+
};
|
390
|
+
}
|
391
|
+
|
392
|
+
private async addContentToPage(this: IExecuteFunctions, itemIndex: number): Promise<IDataObject> {
|
393
|
+
const targetPageId = this.getNodeParameter('targetPageId', itemIndex) as string;
|
394
|
+
const content = this.getNodeParameter('content', itemIndex) as string;
|
395
|
+
|
396
|
+
const resolvedPageId = await resolvePageId.call(this, targetPageId);
|
397
|
+
const blocks = NotionAITool.prototype.parseContentToBlocks.call(this, content);
|
398
|
+
|
399
|
+
if (blocks.length === 0) {
|
400
|
+
throw new NodeOperationError(this.getNode(), 'No valid content blocks found to add');
|
401
|
+
}
|
402
|
+
|
403
|
+
const result = await notionApiRequest.call(this, 'PATCH', `/blocks/${resolvedPageId}/children`, {
|
404
|
+
children: blocks,
|
405
|
+
});
|
406
|
+
|
407
|
+
return {
|
408
|
+
pageId: resolvedPageId,
|
409
|
+
blocksAdded: blocks.length,
|
410
|
+
message: `Added ${blocks.length} content blocks to page`,
|
411
|
+
result,
|
412
|
+
};
|
413
|
+
}
|
414
|
+
|
415
|
+
private async searchPages(this: IExecuteFunctions, itemIndex: number): Promise<IDataObject> {
|
416
|
+
const searchQuery = this.getNodeParameter('searchQuery', itemIndex, '') as string;
|
417
|
+
const searchType = this.getNodeParameter('searchType', itemIndex, 'all') as string;
|
418
|
+
const additionalOptions = this.getNodeParameter('additionalOptions', itemIndex, {}) as IDataObject;
|
419
|
+
const maxResults = (additionalOptions.maxResults as number) || 20;
|
420
|
+
|
421
|
+
const body: IDataObject = {
|
422
|
+
page_size: Math.min(maxResults, 100),
|
423
|
+
};
|
424
|
+
|
425
|
+
if (searchQuery) {
|
426
|
+
body.query = searchQuery;
|
427
|
+
}
|
428
|
+
|
429
|
+
// Set search filter based on type
|
430
|
+
if (searchType === 'title') {
|
431
|
+
body.filter = {
|
432
|
+
property: 'object',
|
433
|
+
value: 'page',
|
434
|
+
};
|
435
|
+
} else {
|
436
|
+
body.filter = {
|
437
|
+
property: 'object',
|
438
|
+
value: 'page',
|
439
|
+
};
|
440
|
+
}
|
441
|
+
|
442
|
+
const response = await notionApiRequest.call(this, 'POST', '/search', body);
|
443
|
+
|
444
|
+
return {
|
445
|
+
totalResults: response.results?.length || 0,
|
446
|
+
pages: response.results || [],
|
447
|
+
message: `Found ${response.results?.length || 0} pages`,
|
448
|
+
};
|
449
|
+
}
|
450
|
+
|
451
|
+
private async updatePageProperties(this: IExecuteFunctions, itemIndex: number): Promise<IDataObject> {
|
452
|
+
const targetPageId = this.getNodeParameter('targetPageId', itemIndex) as string;
|
453
|
+
const propertiesToUpdate = this.getNodeParameter('propertiesToUpdate', itemIndex) as string;
|
454
|
+
|
455
|
+
const resolvedPageId = await resolvePageId.call(this, targetPageId);
|
456
|
+
const properties = NotionAITool.prototype.parsePropertiesToUpdate.call(this, propertiesToUpdate);
|
457
|
+
|
458
|
+
const result = await notionApiRequest.call(this, 'PATCH', `/pages/${resolvedPageId}`, {
|
459
|
+
properties,
|
460
|
+
});
|
461
|
+
|
462
|
+
return {
|
463
|
+
pageId: resolvedPageId,
|
464
|
+
updatedProperties: Object.keys(properties),
|
465
|
+
message: `Updated ${Object.keys(properties).length} properties`,
|
466
|
+
result,
|
467
|
+
};
|
468
|
+
}
|
469
|
+
|
470
|
+
private async createDatabaseEntry(this: IExecuteFunctions, itemIndex: number): Promise<IDataObject> {
|
471
|
+
const parentId = this.getNodeParameter('parentId', itemIndex) as string;
|
472
|
+
const entryProperties = this.getNodeParameter('entryProperties', itemIndex) as string;
|
473
|
+
|
474
|
+
const resolvedParentId = await resolvePageId.call(this, parentId);
|
475
|
+
const properties = NotionAITool.prototype.parsePropertiesToUpdate.call(this, entryProperties);
|
476
|
+
|
477
|
+
const result = await notionApiRequest.call(this, 'POST', '/pages', {
|
478
|
+
parent: { database_id: resolvedParentId },
|
479
|
+
properties,
|
480
|
+
});
|
481
|
+
|
482
|
+
return {
|
483
|
+
entryId: result.id,
|
484
|
+
databaseId: resolvedParentId,
|
485
|
+
message: 'Created new database entry',
|
486
|
+
result,
|
487
|
+
};
|
488
|
+
}
|
489
|
+
|
490
|
+
private async queryDatabase(this: IExecuteFunctions, itemIndex: number): Promise<IDataObject> {
|
491
|
+
const databaseId = this.getNodeParameter('databaseId', itemIndex) as string;
|
492
|
+
const queryFilter = this.getNodeParameter('queryFilter', itemIndex, '') as string;
|
493
|
+
const additionalOptions = this.getNodeParameter('additionalOptions', itemIndex, {}) as IDataObject;
|
494
|
+
const maxResults = (additionalOptions.maxResults as number) || 20;
|
495
|
+
|
496
|
+
const resolvedDatabaseId = await resolvePageId.call(this, databaseId);
|
497
|
+
const body: IDataObject = {
|
498
|
+
page_size: Math.min(maxResults, 100),
|
499
|
+
};
|
500
|
+
|
501
|
+
if (queryFilter) {
|
502
|
+
try {
|
503
|
+
body.filter = JSON.parse(queryFilter);
|
504
|
+
} catch {
|
505
|
+
// If not JSON, create a simple text filter
|
506
|
+
body.filter = {
|
507
|
+
property: 'Name',
|
508
|
+
title: {
|
509
|
+
contains: queryFilter,
|
510
|
+
},
|
511
|
+
};
|
512
|
+
}
|
513
|
+
}
|
514
|
+
|
515
|
+
const response = await notionApiRequest.call(this, 'POST', `/databases/${resolvedDatabaseId}/query`, body);
|
516
|
+
|
517
|
+
return {
|
518
|
+
databaseId: resolvedDatabaseId,
|
519
|
+
totalResults: response.results?.length || 0,
|
520
|
+
entries: response.results || [],
|
521
|
+
message: `Found ${response.results?.length || 0} database entries`,
|
522
|
+
};
|
523
|
+
}
|
524
|
+
|
525
|
+
private parseContentToBlocks(content: string): IDataObject[] {
|
526
|
+
const blocks: IDataObject[] = [];
|
527
|
+
const lines = content.split('\n');
|
528
|
+
|
529
|
+
for (let i = 0; i < lines.length; i++) {
|
530
|
+
const line = lines[i].trim();
|
531
|
+
if (!line) continue;
|
532
|
+
|
533
|
+
// Parse different content types
|
534
|
+
if (line.startsWith('# ')) {
|
535
|
+
blocks.push({
|
536
|
+
object: 'block',
|
537
|
+
type: 'heading_1',
|
538
|
+
heading_1: {
|
539
|
+
rich_text: [createRichText(line.substring(2))],
|
540
|
+
},
|
541
|
+
});
|
542
|
+
} else if (line.startsWith('## ')) {
|
543
|
+
blocks.push({
|
544
|
+
object: 'block',
|
545
|
+
type: 'heading_2',
|
546
|
+
heading_2: {
|
547
|
+
rich_text: [createRichText(line.substring(3))],
|
548
|
+
},
|
549
|
+
});
|
550
|
+
} else if (line.startsWith('### ')) {
|
551
|
+
blocks.push({
|
552
|
+
object: 'block',
|
553
|
+
type: 'heading_3',
|
554
|
+
heading_3: {
|
555
|
+
rich_text: [createRichText(line.substring(4))],
|
556
|
+
},
|
557
|
+
});
|
558
|
+
} else if (line.startsWith('- ') || line.startsWith('* ')) {
|
559
|
+
blocks.push({
|
560
|
+
object: 'block',
|
561
|
+
type: 'bulleted_list_item',
|
562
|
+
bulleted_list_item: {
|
563
|
+
rich_text: [createRichText(line.substring(2))],
|
564
|
+
},
|
565
|
+
});
|
566
|
+
} else if (line.match(/^\d+\. /)) {
|
567
|
+
blocks.push({
|
568
|
+
object: 'block',
|
569
|
+
type: 'numbered_list_item',
|
570
|
+
numbered_list_item: {
|
571
|
+
rich_text: [createRichText(line.replace(/^\d+\. /, ''))],
|
572
|
+
},
|
573
|
+
});
|
574
|
+
} else if (line.startsWith('> ')) {
|
575
|
+
blocks.push({
|
576
|
+
object: 'block',
|
577
|
+
type: 'quote',
|
578
|
+
quote: {
|
579
|
+
rich_text: [createRichText(line.substring(2))],
|
580
|
+
},
|
581
|
+
});
|
582
|
+
} else if (line.startsWith('```')) {
|
583
|
+
// Handle code blocks
|
584
|
+
const codeLines: string[] = [];
|
585
|
+
i++; // Skip the opening ```
|
586
|
+
while (i < lines.length && !lines[i].trim().startsWith('```')) {
|
587
|
+
codeLines.push(lines[i]);
|
588
|
+
i++;
|
589
|
+
}
|
590
|
+
blocks.push({
|
591
|
+
object: 'block',
|
592
|
+
type: 'code',
|
593
|
+
code: {
|
594
|
+
rich_text: [createRichText(codeLines.join('\n'))],
|
595
|
+
language: 'plain text',
|
596
|
+
},
|
597
|
+
});
|
598
|
+
} else {
|
599
|
+
// Regular paragraph
|
600
|
+
blocks.push({
|
601
|
+
object: 'block',
|
602
|
+
type: 'paragraph',
|
603
|
+
paragraph: {
|
604
|
+
rich_text: [createRichText(line)],
|
605
|
+
},
|
606
|
+
});
|
607
|
+
}
|
608
|
+
}
|
609
|
+
|
610
|
+
return blocks;
|
611
|
+
}
|
612
|
+
|
613
|
+
private parsePropertiesToUpdate(propertiesString: string): IDataObject {
|
614
|
+
try {
|
615
|
+
// Try to parse as JSON first
|
616
|
+
return JSON.parse(propertiesString);
|
617
|
+
} catch {
|
618
|
+
// If not JSON, try to parse natural language
|
619
|
+
const properties: IDataObject = {};
|
620
|
+
|
621
|
+
// Simple natural language parsing
|
622
|
+
const patterns = [
|
623
|
+
/set\s+(\w+)\s+to\s+(.+?)(?:\s+and|$)/gi,
|
624
|
+
/(\w+)\s*:\s*(.+?)(?:\s*,|$)/gi,
|
625
|
+
/(\w+)\s*=\s*(.+?)(?:\s*,|$)/gi,
|
626
|
+
];
|
627
|
+
|
628
|
+
for (const pattern of patterns) {
|
629
|
+
let match;
|
630
|
+
while ((match = pattern.exec(propertiesString)) !== null) {
|
631
|
+
const [, key, value] = match;
|
632
|
+
properties[key.trim()] = {
|
633
|
+
rich_text: [createRichText(value.trim())],
|
634
|
+
};
|
635
|
+
}
|
636
|
+
}
|
637
|
+
|
638
|
+
return properties;
|
639
|
+
}
|
640
|
+
}
|
641
|
+
}
|
package/dist/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "n8n-nodes-notion-advanced",
|
3
|
-
"version": "1.
|
4
|
-
"description": "Advanced n8n Notion
|
3
|
+
"version": "1.1.0-beta.2",
|
4
|
+
"description": "Advanced n8n Notion nodes: Full-featured workflow node + AI Agent Tool for intelligent Notion automation with 25+ block types (BETA)",
|
5
5
|
"main": "dist/index.js",
|
6
6
|
"scripts": {
|
7
7
|
"build": "node dev-notes/build-for-install.js",
|
@@ -20,7 +20,8 @@
|
|
20
20
|
"n8n": {
|
21
21
|
"n8nNodesApiVersion": 1,
|
22
22
|
"nodes": [
|
23
|
-
"dist/nodes/NotionAdvanced/NotionAdvanced.node.js"
|
23
|
+
"dist/nodes/NotionAdvanced/NotionAdvanced.node.js",
|
24
|
+
"dist/nodes/NotionAdvanced/NotionAITool.node.js"
|
24
25
|
]
|
25
26
|
},
|
26
27
|
"keywords": [
|
@@ -37,7 +38,15 @@
|
|
37
38
|
"integration",
|
38
39
|
"notion-api",
|
39
40
|
"block-types",
|
40
|
-
"formatting"
|
41
|
+
"formatting",
|
42
|
+
"ai",
|
43
|
+
"ai-agent",
|
44
|
+
"ai-tool",
|
45
|
+
"chatgpt",
|
46
|
+
"claude",
|
47
|
+
"llm",
|
48
|
+
"natural-language",
|
49
|
+
"intelligent-automation"
|
41
50
|
],
|
42
51
|
"license": "MIT",
|
43
52
|
"homepage": "https://github.com/your-username/n8n-notion-advanced-node#readme",
|
package/package.json
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
{
|
2
2
|
"name": "n8n-nodes-notion-advanced",
|
3
|
-
"version": "1.
|
4
|
-
"description": "Advanced n8n Notion
|
3
|
+
"version": "1.1.0-beta.2",
|
4
|
+
"description": "Advanced n8n Notion nodes: Full-featured workflow node + AI Agent Tool for intelligent Notion automation with 25+ block types (BETA)",
|
5
5
|
"main": "dist/index.js",
|
6
6
|
"scripts": {
|
7
7
|
"build": "node dev-notes/build-for-install.js",
|
@@ -20,7 +20,8 @@
|
|
20
20
|
"n8n": {
|
21
21
|
"n8nNodesApiVersion": 1,
|
22
22
|
"nodes": [
|
23
|
-
"dist/nodes/NotionAdvanced/NotionAdvanced.node.js"
|
23
|
+
"dist/nodes/NotionAdvanced/NotionAdvanced.node.js",
|
24
|
+
"dist/nodes/NotionAdvanced/NotionAITool.node.js"
|
24
25
|
]
|
25
26
|
},
|
26
27
|
"keywords": [
|
@@ -37,7 +38,15 @@
|
|
37
38
|
"integration",
|
38
39
|
"notion-api",
|
39
40
|
"block-types",
|
40
|
-
"formatting"
|
41
|
+
"formatting",
|
42
|
+
"ai",
|
43
|
+
"ai-agent",
|
44
|
+
"ai-tool",
|
45
|
+
"chatgpt",
|
46
|
+
"claude",
|
47
|
+
"llm",
|
48
|
+
"natural-language",
|
49
|
+
"intelligent-automation"
|
41
50
|
],
|
42
51
|
"license": "MIT",
|
43
52
|
"homepage": "https://github.com/your-username/n8n-notion-advanced-node#readme",
|