1
1
mirror of https://github.com/n8n-io/n8n.git synced 2024-10-08 02:28:57 +03:00

Add Notion node V2 (#2437)

*  Add versioning

*  Add credentials verification

*  Add folmula filtering

*  Add file support

*  Apply internal review

*  Improvements

*  Add page updated event to trigger

*  Use name instead of id when setting expression in select type

*  improvements

*  Improvements

*  Improvement to descriptions

*  Add filter to databasePage:getAll

*  Improvements

*  Add database:search operation

*  Add page:archive operation

*  Allow clearing fields date type

*  Allow setting single value in people type field

* asasas

* asasas

* aaaaa

*  Improvements

*  Fix merging issues

* 🐛 Fix filename

*  Minor fix

Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com>
This commit is contained in:
Ricardo Espinoza 2021-12-29 17:23:22 -05:00 committed by GitHub
parent aab5f5ddab
commit 7a8425a152
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 2434 additions and 722 deletions

View File

@ -1436,7 +1436,7 @@ class App {
const testFunctionSearch =
credential.name === credentialType && !!credential.testedBy;
if (testFunctionSearch) {
foundTestFunction = (node as unknown as INodeType).methods!.credentialTest![
foundTestFunction = (nodeType as unknown as INodeType).methods!.credentialTest![
credential.testedBy!
];
}

View File

@ -6,7 +6,7 @@ import {
blocks,
} from './Blocks';
export const blockOperations: INodeProperties[] = [
export const blockOperations = [
{
displayName: 'Operation',
name: 'operation',
@ -20,28 +20,27 @@ export const blockOperations: INodeProperties[] = [
},
options: [
{
name: 'Append',
name: 'Append After',
value: 'append',
description: 'Append a block',
},
{
name: 'Get All',
name: 'Get Child Blocks',
value: 'getAll',
description: 'Get all children blocks',
},
],
default: 'append',
description: 'The operation to perform.',
},
];
] as INodeProperties[];
export const blockFields: INodeProperties[] = [
export const blockFields = [
/* -------------------------------------------------------------------------- */
/* block:append */
/* -------------------------------------------------------------------------- */
{
displayName: 'Block ID',
displayName: 'Block ID or Link',
name: 'blockId',
type: 'string',
default: '',
@ -56,14 +55,14 @@ export const blockFields: INodeProperties[] = [
],
},
},
description: `The ID of block. A page it is also considered a block. Hence, a Page ID can be used as well.`,
description: `The Block URL from Notion's 'copy link' functionality (or just the ID contained within the URL). Pages are also blocks, so you can use a page URL/ID here too`,
},
...blocks('block', 'append'),
/* -------------------------------------------------------------------------- */
/* block:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Block ID',
displayName: 'Block ID or Link',
name: 'blockId',
type: 'string',
default: '',
@ -78,6 +77,7 @@ export const blockFields: INodeProperties[] = [
],
},
},
description: `The Block URL from Notion's 'copy link' functionality (or just the ID contained within the URL). Pages are also blocks, so you can use a page URL/ID here too`,
},
{
displayName: 'Return All',
@ -94,7 +94,7 @@ export const blockFields: INodeProperties[] = [
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
description: 'If all results should be returned or only up to a given limit',
},
{
displayName: 'Limit',
@ -118,6 +118,6 @@ export const blockFields: INodeProperties[] = [
maxValue: 100,
},
default: 50,
description: 'How many results to return.',
description: 'How many results to return',
},
];
] as INodeProperties[];

View File

@ -95,35 +95,35 @@ const annotation: INodeProperties[] = [
name: 'bold',
type: 'boolean',
default: false,
description: 'Whether the text is bolded.',
description: 'Whether the text is bolded',
},
{
displayName: 'Italic',
name: 'italic',
type: 'boolean',
default: false,
description: 'Whether the text is italicized.',
description: 'Whether the text is italicized',
},
{
displayName: 'Strikethrough',
name: 'strikethrough',
type: 'boolean',
default: false,
description: 'Whether the text is struck through.',
description: 'Whether the text is struck through',
},
{
displayName: 'Underline',
name: 'underline',
type: 'boolean',
default: false,
description: 'Whether the text is underlined.',
description: 'Whether the text is underlined',
},
{
displayName: 'Code',
name: 'code',
type: 'boolean',
default: false,
description: 'Whether the text is code style.',
description: 'Whether the text is code style',
},
{
displayName: 'Color',
@ -131,10 +131,10 @@ const annotation: INodeProperties[] = [
type: 'options',
options: colors,
default: '',
description: 'Color of the text.',
description: 'Color of the text',
},
],
description: 'All annotations that apply to this rich text.',
description: 'All annotations that apply to this rich text',
},
];
@ -169,7 +169,7 @@ const typeMention: INodeProperties[] = [
},
],
default: '',
description: `An inline mention of a user, page, database, or date. In the app these are created by typing @ followed by the name of a user, page, database, or a date.`,
description: `An inline mention of a user, page, database, or date. In the app these are created by typing @ followed by the name of a user, page, database, or a date`,
},
{
displayName: 'User ID',
@ -186,7 +186,7 @@ const typeMention: INodeProperties[] = [
},
},
default: '',
description: 'The id of the user being mentioned.',
description: 'The ID of the user being mentioned',
},
{
displayName: 'Page ID',
@ -200,7 +200,7 @@ const typeMention: INodeProperties[] = [
},
},
default: '',
description: 'The id of the page being mentioned.',
description: 'The ID of the page being mentioned',
},
{
displayName: 'Database ID',
@ -217,7 +217,7 @@ const typeMention: INodeProperties[] = [
},
},
default: '',
description: 'The id of the database being mentioned.',
description: 'The ID of the database being mentioned',
},
{
displayName: 'Range',
@ -231,7 +231,7 @@ const typeMention: INodeProperties[] = [
},
type: 'boolean',
default: false,
description: 'Weather or not you want to define a date range.',
description: 'Weather or not you want to define a date range',
},
{
displayName: 'Date',
@ -248,7 +248,7 @@ const typeMention: INodeProperties[] = [
},
type: 'dateTime',
default: '',
description: 'An ISO 8601 format date, with optional time.',
description: 'An ISO 8601 format date, with optional time',
},
{
displayName: 'Date Start',
@ -265,7 +265,7 @@ const typeMention: INodeProperties[] = [
},
type: 'dateTime',
default: '',
description: 'An ISO 8601 format date, with optional time.',
description: 'An ISO 8601 format date, with optional time',
},
{
displayName: 'Date End',
@ -282,7 +282,7 @@ const typeMention: INodeProperties[] = [
},
type: 'dateTime',
default: '',
description: `An ISO 8601 formatted date, with optional time. Represents the end of a date range.`,
description: `An ISO 8601 formatted date, with optional time. Represents the end of a date range`,
},
];
@ -316,7 +316,8 @@ const typeText: INodeProperties[] = [
},
type: 'string',
default: '',
description: `Text content. This field contains the actual content of your text and is probably the field you'll use most often.`,
description: `Text content. This field contains the actual content
of your text and is probably the field you'll use most often`,
},
{
displayName: 'Is Link',
@ -346,7 +347,7 @@ const typeText: INodeProperties[] = [
},
type: 'string',
default: '',
description: 'The URL that this link points to.',
description: 'The URL that this link points to',
},
];
@ -395,8 +396,8 @@ export const text = (displayOptions: IDisplayOptions): INodeProperties[] => [
],
},
],
description: 'Rich text in the block.',
}];
description: 'Rich text in the block',
}] as INodeProperties[];
const todo = (type: string): INodeProperties[] => [{
@ -411,8 +412,8 @@ const todo = (type: string): INodeProperties[] => [{
],
},
},
description: 'Whether the to_do is checked or not.',
}];
description: 'Whether the to_do is checked or not',
}] as INodeProperties[];
const title = (type: string): INodeProperties[] => [{
displayName: 'Title',
@ -426,8 +427,8 @@ const title = (type: string): INodeProperties[] => [{
],
},
},
description: 'Plain text of page title.',
}];
description: 'Plain text of page title',
}] as INodeProperties[];
const richText = (displayOptions: IDisplayOptions): INodeProperties[] => [
{
@ -449,7 +450,7 @@ const textContent = (displayOptions: IDisplayOptions): INodeProperties[] => [
},
];
const block = (blockType: string) => {
const block = (blockType: string): INodeProperties[] => {
const data: INodeProperties[] = [];
switch (blockType) {
case 'to_do':
@ -549,7 +550,6 @@ export const blocks = (resource: string, operation: string): INodeProperties[] =
typeOptions: {
loadOptionsMethod: 'getBlockTypes',
},
description: 'Type of block',
default: 'paragraph',
},
...block('paragraph'),
@ -566,4 +566,3 @@ export const blocks = (resource: string, operation: string): INodeProperties[] =
],
},
];

View File

@ -2,13 +2,49 @@ import {
INodeProperties,
} from 'n8n-workflow';
export const databaseOperations: INodeProperties[] = [
export const databaseOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
version: [
2,
],
resource: [
'database',
],
},
},
options: [
{
name: 'Get',
value: 'get',
description: 'Get a database',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all databases',
},
{
name: 'Search',
value: 'search',
description: 'search databases using text search',
},
],
default: 'get',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
version: [
1,
],
resource: [
'database',
],
@ -27,17 +63,16 @@ export const databaseOperations: INodeProperties[] = [
},
],
default: 'get',
description: 'The operation to perform.',
},
];
] as INodeProperties[];
export const databaseFields: INodeProperties[] = [
export const databaseFields = [
/* -------------------------------------------------------------------------- */
/* database:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Database ID',
displayName: 'Database Link or ID',
name: 'databaseId',
type: 'string',
default: '',
@ -52,6 +87,7 @@ export const databaseFields: INodeProperties[] = [
],
},
},
description: `The Database URL from Notion's 'copy link' functionality (or just the ID contained within the URL)`,
},
/* -------------------------------------------------------------------------- */
/* database:getAll */
@ -71,7 +107,7 @@ export const databaseFields: INodeProperties[] = [
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
description: 'If all results should be returned or only up to a given limit',
},
{
displayName: 'Limit',
@ -95,6 +131,172 @@ export const databaseFields: INodeProperties[] = [
maxValue: 100,
},
default: 50,
description: 'How many results to return.',
description: 'How many results to return',
},
];
{
displayName: 'Simplify Output',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
version: [
2,
],
resource: [
'database',
],
operation: [
'getAll',
'get',
],
},
},
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
/* -------------------------------------------------------------------------- */
/* database:search */
/* -------------------------------------------------------------------------- */
{
displayName: 'Search Text',
name: 'text',
type: 'string',
default: '',
displayOptions: {
show: {
resource: [
'database',
],
operation: [
'search',
],
},
},
description: 'The text to search for',
},
{
displayName: 'Return All',
name: 'returnAll',
type: 'boolean',
displayOptions: {
show: {
resource: [
'database',
],
operation: [
'search',
],
},
},
default: false,
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
name: 'limit',
type: 'number',
displayOptions: {
show: {
resource: [
'database',
],
operation: [
'search',
],
returnAll: [
false,
],
},
},
typeOptions: {
minValue: 1,
maxValue: 100,
},
default: 50,
description: 'How many results to return',
},
{
displayName: 'Simplify Output',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
resource: [
'database',
],
operation: [
'search',
],
},
},
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
{
displayName: 'Options',
name: 'options',
type: 'collection',
displayOptions: {
show: {
resource: [
'database',
],
operation: [
'search',
],
},
},
default: {},
placeholder: 'Add Field',
options: [
{
displayName: 'Sort',
name: 'sort',
placeholder: 'Add Sort',
type: 'fixedCollection',
typeOptions: {
multipleValues: false,
},
default: {},
options: [
{
displayName: 'Sort',
name: 'sortValue',
values: [
{
displayName: 'Direction',
name: 'direction',
type: 'options',
options: [
{
name: 'Ascending',
value: 'ascending',
},
{
name: 'Descending',
value: 'descending',
},
],
default: 'descending',
description: 'The direction to sort',
},
{
displayName: 'Timestamp',
name: 'timestamp',
type: 'options',
options: [
{
name: 'Last Edited Time',
value: 'last_edited_time',
},
],
default: 'last_edited_time',
description: `The name of the timestamp to sort against`,
},
],
},
],
},
],
},
] as INodeProperties[];

View File

@ -2,6 +2,11 @@ import {
INodeProperties,
} from 'n8n-workflow';
import {
getConditions,
getSearchFilters,
} from './GenericFunctions';
import {
blocks,
text,
@ -11,13 +16,54 @@ import {
filters,
} from './Filters';
export const databasePageOperations: INodeProperties[] = [
export const databasePageOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
version: [
2,
],
resource: [
'databasePage',
],
},
},
options: [
{
name: 'Create',
value: 'create',
description: 'Create a pages in a database',
},
{
name: 'Get',
value: 'get',
description: 'Get a page in a database',
},
{
name: 'Get All',
value: 'getAll',
description: 'Get all pages in a database',
},
{
name: 'Update',
value: 'update',
description: 'Update pages in a database',
},
],
default: 'create',
},
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
version: [
1,
],
resource: [
'databasePage',
],
@ -41,17 +87,16 @@ export const databasePageOperations: INodeProperties[] = [
},
],
default: 'create',
description: 'The operation to perform.',
},
];
] as INodeProperties[];
export const databasePageFields: INodeProperties[] = [
export const databasePageFields = [
/* -------------------------------------------------------------------------- */
/* databasePage:create */
/* databasePage:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Database ID',
displayName: 'Database Name or ID',
name: 'databaseId',
type: 'options',
default: '',
@ -69,10 +114,30 @@ export const databasePageFields: INodeProperties[] = [
],
},
},
description: 'The ID of the database that this databasePage belongs to.',
description: `The Database Page URL from Notion's 'copy link' functionality (or just the ID contained within the URL)`,
},
{
displayName: 'Simple',
displayName: 'Title',
name: 'title',
type: 'string',
default: '',
displayOptions: {
show: {
version: [
2,
],
resource: [
'databasePage',
],
operation: [
'create',
],
},
},
description: 'Page title. Appears at the top of the page and can be found via Quick Find',
},
{
displayName: 'Simplify Output',
name: 'simple',
type: 'boolean',
displayOptions: {
@ -86,7 +151,7 @@ export const databasePageFields: INodeProperties[] = [
},
},
default: true,
description: 'When set to true a simplify version of the response will be used else the raw data.',
description: 'Whether to return a simplified version of the response instead of the raw data',
},
{
displayName: 'Properties',
@ -194,7 +259,7 @@ export const databasePageFields: INodeProperties[] = [
},
},
default: '',
description: `Phone number. No structure is enforced.`,
description: `Phone number. No structure is enforced`,
},
{
displayName: 'Options',
@ -212,7 +277,7 @@ export const databasePageFields: INodeProperties[] = [
},
default: [],
description: `Name of the options you want to set.
Multiples can be defined separated by comma.`,
Multiples can be defined separated by comma`,
},
{
displayName: 'Option',
@ -229,7 +294,7 @@ export const databasePageFields: INodeProperties[] = [
},
},
default: '',
description: `Name of the option you want to set.`,
description: `Name of the option you want to set`,
},
{
displayName: 'Email',
@ -243,7 +308,7 @@ export const databasePageFields: INodeProperties[] = [
},
},
default: '',
description: 'Email address.',
description: 'Email address',
},
{
displayName: 'URL',
@ -257,7 +322,7 @@ export const databasePageFields: INodeProperties[] = [
},
},
default: '',
description: 'Web address.',
description: 'Web address',
},
{
displayName: 'User IDs',
@ -274,7 +339,7 @@ export const databasePageFields: INodeProperties[] = [
},
},
default: [],
description: 'List of users. Multiples can be defined separated by comma.',
description: 'List of users. Multiples can be defined separated by comma',
},
{
displayName: 'Relation IDs',
@ -291,7 +356,7 @@ export const databasePageFields: INodeProperties[] = [
},
},
default: [],
description: 'List of databases that belong to another database. Multiples can be defined separated by comma.',
description: 'List of databases that belong to another database. Multiples can be defined separated by comma',
},
{
displayName: 'Checked',
@ -319,7 +384,7 @@ export const databasePageFields: INodeProperties[] = [
},
type: 'number',
default: 0,
description: 'Number value.',
description: 'Number value',
},
{
displayName: 'Range',
@ -333,7 +398,7 @@ export const databasePageFields: INodeProperties[] = [
},
type: 'boolean',
default: false,
description: 'Weather or not you want to define a date range.',
description: 'Weather or not you want to define a date range',
},
{
displayName: 'Include Time',
@ -347,7 +412,7 @@ export const databasePageFields: INodeProperties[] = [
},
type: 'boolean',
default: true,
description: 'Weather or not to include the time in the date.',
description: 'Weather or not to include the time in the date',
},
{
displayName: 'Date',
@ -364,7 +429,7 @@ export const databasePageFields: INodeProperties[] = [
},
type: 'dateTime',
default: '',
description: 'An ISO 8601 format date, with optional time.',
description: 'An ISO 8601 format date, with optional time',
},
{
displayName: 'Date Start',
@ -381,7 +446,7 @@ export const databasePageFields: INodeProperties[] = [
},
type: 'dateTime',
default: '',
description: 'An ISO 8601 format date, with optional time.',
description: 'An ISO 8601 format date, with optional time',
},
{
displayName: 'Date End',
@ -399,7 +464,7 @@ export const databasePageFields: INodeProperties[] = [
type: 'dateTime',
default: '',
description: `
An ISO 8601 formatted date, with optional time. Represents the end of a date range.`,
An ISO 8601 formatted date, with optional time. Represents the end of a date range`,
},
{
displayName: 'Timezone',
@ -416,7 +481,49 @@ export const databasePageFields: INodeProperties[] = [
loadOptionsMethod: 'getTimezones',
},
default: 'default',
description: 'Time zone to use. By default n8n timezone is used.',
description: 'Time zone to use. By default n8n timezone is used',
},
{
displayName: 'File URLs',
name: 'fileUrls',
placeholder: 'Add File',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
sortable: true,
},
displayOptions: {
show: {
'/version': [
2,
],
type: [
'files',
],
},
},
default: {},
options: [
{
name: 'fileUrl',
displayName: 'File',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'File URL',
name: 'url',
type: 'string',
default: '',
description: 'Link to externally hosted file',
},
],
},
],
},
],
},
@ -424,10 +531,10 @@ export const databasePageFields: INodeProperties[] = [
},
...blocks('databasePage', 'create'),
/* -------------------------------------------------------------------------- */
/* databasePage:update */
/* databasePage:update */
/* -------------------------------------------------------------------------- */
{
displayName: 'Page ID',
displayName: 'Database Page Link or ID',
name: 'pageId',
type: 'string',
default: '',
@ -442,10 +549,10 @@ export const databasePageFields: INodeProperties[] = [
],
},
},
description: 'The ID of the databasePage to update.',
description: `The Database Page URL from Notion's 'copy link' functionality (or just the ID contained within the URL)`,
},
{
displayName: 'Simplify Response',
displayName: 'Simplify Output',
name: 'simple',
type: 'boolean',
displayOptions: {
@ -459,7 +566,7 @@ export const databasePageFields: INodeProperties[] = [
},
},
default: true,
description: 'Return a simplified version of the response instead of the raw data.',
description: 'Whether to return a simplified version of the response instead of the raw data',
},
{
displayName: 'Properties',
@ -567,7 +674,7 @@ export const databasePageFields: INodeProperties[] = [
},
},
default: '',
description: `Phone number. No structure is enforced.`,
description: `Phone number. No structure is enforced`,
},
{
displayName: 'Options',
@ -584,8 +691,6 @@ export const databasePageFields: INodeProperties[] = [
},
},
default: [],
description: `Name of the options you want to set.
Multiples can be defined separated by comma.`,
},
{
displayName: 'Option',
@ -602,7 +707,6 @@ export const databasePageFields: INodeProperties[] = [
},
},
default: '',
description: `Name of the option you want to set.`,
},
{
displayName: 'Email',
@ -616,7 +720,6 @@ export const databasePageFields: INodeProperties[] = [
},
},
default: '',
description: 'Email address.',
},
{
displayName: 'URL',
@ -630,7 +733,7 @@ export const databasePageFields: INodeProperties[] = [
},
},
default: '',
description: 'Web address.',
description: 'Web address',
},
{
displayName: 'User IDs',
@ -647,7 +750,7 @@ export const databasePageFields: INodeProperties[] = [
},
},
default: [],
description: 'List of users. Multiples can be defined separated by comma.',
description: 'List of users. Multiples can be defined separated by comma',
},
{
displayName: 'Relation IDs',
@ -664,7 +767,7 @@ export const databasePageFields: INodeProperties[] = [
},
},
default: [],
description: 'List of databases that belong to another database. Multiples can be defined separated by comma.',
description: 'List of databases that belong to another database. Multiples can be defined separated by comma',
},
{
displayName: 'Checked',
@ -692,7 +795,7 @@ export const databasePageFields: INodeProperties[] = [
},
type: 'number',
default: 0,
description: 'Number value.',
description: 'Number value',
},
{
displayName: 'Range',
@ -706,7 +809,7 @@ export const databasePageFields: INodeProperties[] = [
},
type: 'boolean',
default: false,
description: 'Weather or not you want to define a date range.',
description: 'Weather or not you want to define a date range',
},
{
displayName: 'Include Time',
@ -720,7 +823,7 @@ export const databasePageFields: INodeProperties[] = [
},
type: 'boolean',
default: true,
description: 'Weather or not to include the time in the date.',
description: 'Weather or not to include the time in the date',
},
{
displayName: 'Date',
@ -737,7 +840,7 @@ export const databasePageFields: INodeProperties[] = [
},
type: 'dateTime',
default: '',
description: 'An ISO 8601 format date, with optional time.',
description: 'An ISO 8601 format date, with optional time',
},
{
displayName: 'Date Start',
@ -754,7 +857,7 @@ export const databasePageFields: INodeProperties[] = [
},
type: 'dateTime',
default: '',
description: 'An ISO 8601 format date, with optional time.',
description: 'An ISO 8601 format date, with optional time',
},
{
displayName: 'Date End',
@ -772,7 +875,7 @@ export const databasePageFields: INodeProperties[] = [
type: 'dateTime',
default: '',
description: `
An ISO 8601 formatted date, with optional time. Represents the end of a date range.`,
An ISO 8601 formatted date, with optional time. Represents the end of a date range`,
},
{
displayName: 'Timezone',
@ -789,17 +892,103 @@ export const databasePageFields: INodeProperties[] = [
loadOptionsMethod: 'getTimezones',
},
default: 'default',
description: 'Time zone to use. By default n8n timezone is used.',
description: 'Time zone to use. By default n8n timezone is used',
},
{
displayName: 'File URLs',
name: 'fileUrls',
placeholder: 'Add File',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
sortable: true,
},
displayOptions: {
show: {
'/version': [
2,
],
type: [
'files',
],
},
},
default: {},
options: [
{
name: 'fileUrl',
displayName: 'File',
values: [
{
displayName: 'Name',
name: 'name',
type: 'string',
default: '',
},
{
displayName: 'File URL',
name: 'url',
type: 'string',
default: '',
description: 'Link to externally hosted file',
},
],
},
],
},
],
},
],
},
/* -------------------------------------------------------------------------- */
/* databasePage:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Database Page Link or ID',
name: 'pageId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
version: [
2,
],
resource: [
'databasePage',
],
operation: [
'get',
],
},
},
description: `The Database Page URL from Notion's 'copy link' functionality (or just the ID contained within the URL)`,
},
{
displayName: 'Simplify Output',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
version: [
2,
],
resource: [
'databasePage',
],
operation: [
'get',
],
},
},
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
/* -------------------------------------------------------------------------- */
/* databasePage:getAll */
/* -------------------------------------------------------------------------- */
{
displayName: 'Database ID',
displayName: 'Database Name or ID',
name: 'databaseId',
type: 'options',
typeOptions: {
@ -833,7 +1022,7 @@ export const databasePageFields: INodeProperties[] = [
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
@ -857,10 +1046,10 @@ export const databasePageFields: INodeProperties[] = [
maxValue: 100,
},
default: 50,
description: 'How many results to return.',
description: 'How many results to return',
},
{
displayName: 'Simple',
displayName: 'Simplify Output',
name: 'simple',
type: 'boolean',
displayOptions: {
@ -874,8 +1063,9 @@ export const databasePageFields: INodeProperties[] = [
},
},
default: true,
description: 'When set to true a simplify version of the response will be used else the raw data.',
description: 'Whether to return a simplified version of the response instead of the raw data',
},
...getSearchFilters('databasePage'),
{
displayName: 'Options',
name: 'options',
@ -893,6 +1083,26 @@ export const databasePageFields: INodeProperties[] = [
default: {},
placeholder: 'Add Field',
options: [
{
displayName: 'Download Files',
name: 'downloadFiles',
type: 'boolean',
displayOptions: {
show: {
'/version': [
2,
],
'/resource': [
'databasePage',
],
'/operation': [
'getAll',
],
},
},
default: false,
description: 'If a database field contains a file, whether to download it',
},
{
displayName: 'Filters',
name: 'filter',
@ -901,13 +1111,20 @@ export const databasePageFields: INodeProperties[] = [
typeOptions: {
multipleValues: false,
},
displayOptions: {
show: {
'/version': [
1,
],
},
},
default: {},
options: [
{
displayName: 'Single Condition',
name: 'singleCondition',
values: [
...filters,
...filters(getConditions()),
],
},
{
@ -928,14 +1145,14 @@ export const databasePageFields: INodeProperties[] = [
displayName: 'OR',
name: 'or',
values: [
...filters,
...filters(getConditions()),
],
},
{
displayName: 'AND',
name: 'and',
values: [
...filters,
...filters(getConditions()),
],
},
],
@ -963,7 +1180,7 @@ export const databasePageFields: INodeProperties[] = [
name: 'timestamp',
type: 'boolean',
default: false,
description: `Whether or not to use the record's timestamp to sort the response.`,
description: `Whether or not to use the record's timestamp to sort the response`,
},
{
displayName: 'Property Name',
@ -983,7 +1200,7 @@ export const databasePageFields: INodeProperties[] = [
],
},
default: '',
description: 'The name of the property to filter by.',
description: 'The name of the property to filter by',
},
{
displayName: 'Property Name',
@ -1007,7 +1224,7 @@ export const databasePageFields: INodeProperties[] = [
},
},
default: '',
description: 'The name of the property to filter by.',
description: 'The name of the property to filter by',
},
{
displayName: 'Type',
@ -1037,7 +1254,7 @@ export const databasePageFields: INodeProperties[] = [
},
],
default: '',
description: 'The direction to sort.',
description: 'The direction to sort',
},
],
},
@ -1045,4 +1262,4 @@ export const databasePageFields: INodeProperties[] = [
},
],
},
];
] as INodeProperties[];

View File

@ -1,12 +1,7 @@
import {
INodeProperties
} from 'n8n-workflow';
import {
getConditions
} from './GenericFunctions';
export const filters: INodeProperties[] = [{
// tslint:disable-next-line: no-any
export const filters = (conditions: any) => [{
displayName: 'Property Name',
name: 'key',
type: 'options',
@ -17,7 +12,7 @@ export const filters: INodeProperties[] = [{
],
},
default: '',
description: 'The name of the property to filter by.',
description: 'The name of the property to filter by',
},
{
displayName: 'Type',
@ -25,7 +20,7 @@ export const filters: INodeProperties[] = [{
type: 'hidden',
default: '={{$parameter["&key"].split("|")[1]}}',
},
...getConditions(),
...conditions,
{
displayName: 'Title',
name: 'titleValue',
@ -82,7 +77,7 @@ export const filters: INodeProperties[] = [{
},
},
default: '',
description: `Phone number. No structure is enforced.`,
description: `Phone number. No structure is enforced`,
},
{
displayName: 'Option',
@ -105,8 +100,6 @@ export const filters: INodeProperties[] = [{
},
},
default: [],
description: `Name of the options you want to set.
Multiples can be defined separated by comma.`,
},
{
displayName: 'Option',
@ -129,7 +122,6 @@ export const filters: INodeProperties[] = [{
},
},
default: '',
description: `Name of the option you want to set.`,
},
{
displayName: 'Email',
@ -149,7 +141,6 @@ export const filters: INodeProperties[] = [{
},
},
default: '',
description: 'Email address.',
},
{
displayName: 'URL',
@ -169,7 +160,6 @@ export const filters: INodeProperties[] = [{
},
},
default: '',
description: 'Web address.',
},
{
displayName: 'User ID',
@ -192,7 +182,7 @@ export const filters: INodeProperties[] = [{
},
},
default: '',
description: 'List of users. Multiples can be defined separated by comma.',
description: 'List of users. Multiples can be defined separated by comma',
},
{
displayName: 'User ID',
@ -215,7 +205,7 @@ export const filters: INodeProperties[] = [{
},
},
default: '',
description: 'List of users. Multiples can be defined separated by comma.',
description: 'List of users. Multiples can be defined separated by comma',
},
{
displayName: 'User ID',
@ -238,7 +228,7 @@ export const filters: INodeProperties[] = [{
},
},
default: '',
description: 'List of users. Multiples can be defined separated by comma.',
description: 'List of users. Multiples can be defined separated by comma',
},
{
displayName: 'Relation ID',
@ -271,7 +261,7 @@ export const filters: INodeProperties[] = [{
},
type: 'boolean',
default: false,
description: 'Whether or not the checkbox is checked. <code>true</code> represents checked. <code>false</code> represents unchecked.',
description: 'Whether or not the checkbox is checked. <code>true</code> represents checked. <code>false</code> represents unchecked',
},
{
displayName: 'Number',
@ -291,7 +281,7 @@ export const filters: INodeProperties[] = [{
},
type: 'number',
default: 0,
description: 'Number value.',
description: 'Number value',
},
{
displayName: 'Date',
@ -317,7 +307,7 @@ export const filters: INodeProperties[] = [{
},
type: 'dateTime',
default: '',
description: 'An ISO 8601 format date, with optional time.',
description: 'An ISO 8601 format date, with optional time',
},
{
displayName: 'Created Time',
@ -343,7 +333,7 @@ export const filters: INodeProperties[] = [{
},
type: 'dateTime',
default: '',
description: 'An ISO 8601 format date, with optional time.',
description: 'An ISO 8601 format date, with optional time',
},
{
displayName: 'Last Edited Time',
@ -369,5 +359,99 @@ export const filters: INodeProperties[] = [{
},
type: 'dateTime',
default: '',
description: 'An ISO 8601 format date, with optional time.',
}];
description: 'An ISO 8601 format date, with optional time',
},
//formula types
{
displayName: 'Number',
name: 'numberValue',
displayOptions: {
show: {
type: [
'formula',
],
returnType: [
'number',
],
},
hide: {
condition: [
'is_empty',
'is_not_empty',
],
},
},
type: 'number',
default: 0,
description: 'Number value',
},
{
displayName: 'Text',
name: 'textValue',
type: 'string',
displayOptions: {
show: {
type: [
'formula',
],
returnType: [
'text',
],
},
hide: {
condition: [
'is_empty',
'is_not_empty',
],
},
},
default: '',
},
{
displayName: 'Boolean',
name: 'checkboxValue',
displayOptions: {
show: {
type: [
'formula',
],
returnType: [
'checkbox',
],
},
},
type: 'boolean',
default: false,
description: 'Whether or not the checkbox is checked. <code>true</code> represents checked. <code>false</code> represents unchecked',
},
{
displayName: 'Date',
name: 'dateValue',
displayOptions: {
show: {
type: [
'formula',
],
returnType: [
'date',
],
},
hide: {
condition: [
'is_empty',
'is_not_empty',
'past_week',
'past_month',
'past_year',
'next_week',
'next_month',
'next_year',
],
},
},
type: 'dateTime',
default: '',
description: 'An ISO 8601 format date, with optional time',
},
];

View File

@ -10,8 +10,12 @@ import {
} from 'n8n-core';
import {
IBinaryKeyData,
ICredentialDataDecryptedObject,
ICredentialTestFunctions,
IDataObject,
IDisplayOptions,
INodeExecutionData,
INodeProperties,
IPollFunctions,
NodeApiError,
@ -22,16 +26,27 @@ import {
capitalCase,
} from 'change-case';
import {
filters,
} from './Filters';
import * as moment from 'moment-timezone';
import { validate as uuidValidate } from 'uuid';
import { snakeCase } from 'change-case';
const apiVersion: { [key: number]: string } = {
1: '2021-05-13',
2: '2021-08-16',
};
export async function notionApiRequest(this: IHookFunctions | IExecuteFunctions | IExecuteSingleFunctions | ILoadOptionsFunctions | IPollFunctions, method: string, resource: string, body: any = {}, qs: IDataObject = {}, uri?: string, option: IDataObject = {}): Promise<any> { // tslint:disable-line:no-any
try {
let options: OptionsWithUri = {
headers: {
'Notion-Version': '2021-05-13',
'Notion-Version': apiVersion[this.getNode().typeVersion],
},
method,
qs,
@ -39,10 +54,15 @@ export async function notionApiRequest(this: IHookFunctions | IExecuteFunctions
uri: uri || `https://api.notion.com/v1${resource}`,
json: true,
};
options = Object.assign({}, options, option);
const credentials = await this.getCredentials('notionApi') as IDataObject;
options!.headers!['Authorization'] = `Bearer ${credentials.apiKey}`;
if (!uri) {
//do not include the API Key when downloading files, else the request fails
options!.headers!['Authorization'] = `Bearer ${credentials.apiKey}`;
}
if (Object.keys(body).length === 0) {
delete options.body;
}
return this.helpers.request!(options);
} catch (error) {
@ -209,7 +229,7 @@ export function formatBlocks(blocks: IDataObject[]) {
object: 'block',
type: block.type,
[block.type as string]: {
...(block.type === 'to_do') ? { checked: block.checked } : { checked: false },
...(block.type === 'to_do') ? { checked: block.checked } : {},
//@ts-expect-error
// tslint:disable-next-line: no-any
text: (block.richText === false) ? formatText(block.textContent).text : getTexts(block.text.text as any || []),
@ -220,7 +240,7 @@ export function formatBlocks(blocks: IDataObject[]) {
}
// tslint:disable-next-line: no-any
function getPropertyKeyValue(value: any, type: string, timezone: string) {
function getPropertyKeyValue(value: any, type: string, timezone: string, version = 1) {
let result = {};
switch (type) {
case 'rich_text':
@ -268,6 +288,11 @@ function getPropertyKeyValue(value: any, type: string, timezone: string) {
};
break;
case 'people':
//if expression it's a single value, make it an array
if (!Array.isArray(value.peopleValue)) {
value.peopleValue = [value.peopleValue];
}
result = {
type: 'people', people: value.peopleValue.map((option: string) => ({ id: option })),
};
@ -279,7 +304,7 @@ function getPropertyKeyValue(value: any, type: string, timezone: string) {
break;
case 'select':
result = {
type: 'select', select: { id: value.selectValue },
type: 'select', select: (version === 1) ? { id: value.selectValue } : { name: value.selectValue },
};
break;
case 'date':
@ -302,6 +327,20 @@ function getPropertyKeyValue(value: any, type: string, timezone: string) {
},
};
}
//if the date was left empty, set it to null so it resets the value in notion
if (value.date === '' ||
(value.dateStart === '' && value.dateEnd === '')) {
//@ts-ignore
result.date = null;
}
break;
case 'files':
result = {
type: 'files', files: value.fileUrls.fileUrl
.map((file: { name: string, url: string }) => ({ name: file.name, type: 'external', external: { url: file.url } })),
};
break;
default:
}
@ -323,9 +362,9 @@ function getNameAndType(key: string) {
};
}
export function mapProperties(properties: IDataObject[], timezone: string) {
export function mapProperties(properties: IDataObject[], timezone: string, version = 1) {
return properties.reduce((obj, value) => Object.assign(obj, {
[`${(value.key as string).split('|')[0]}`]: getPropertyKeyValue(value, (value.key as string).split('|')[1], timezone),
[`${(value.key as string).split('|')[0]}`]: getPropertyKeyValue(value, (value.key as string).split('|')[1], timezone, version),
}), {});
}
@ -339,6 +378,7 @@ export function mapSorting(data: [{ key: string, type: string, direction: string
}
export function mapFilters(filters: IDataObject[], timezone: string) {
// tslint:disable-next-line: no-any
return filters.reduce((obj, value: { [key: string]: any }) => {
let key = getNameAndType(value.key).type;
@ -352,12 +392,25 @@ export function mapFilters(filters: IDataObject[], timezone: string) {
} else if (['past_week', 'past_month', 'past_year', 'next_week', 'next_month', 'next_year'].includes(value.condition as string)) {
valuePropertyName = {};
}
if (key === 'rich_text') {
if (key === 'rich_text' || key === 'text') {
key = 'text';
} else if (key === 'phone_number') {
key = 'phone';
} else if (key === 'date' && !['is_empty', 'is_not_empty'].includes(value.condition as string)) {
valuePropertyName = (valuePropertyName !== undefined && !Object.keys(valuePropertyName).length) ? {} : moment.tz(value.date, timezone).utc().format();
} else if (key === 'number') {
key = 'text';
} else if (key === 'boolean') {
key = 'checkbox';
}
if (value.type === 'formula') {
const valuePropertyName = value[`${camelCase(value.returnType)}Value`];
return Object.assign(obj, {
['property']: getNameAndType(value.key).name,
[key]: { [value.returnType]: { [`${value.condition}`]: valuePropertyName } },
});
}
return Object.assign(obj, {
@ -389,11 +442,11 @@ export function simplifyProperties(properties: any) {
results[`${key}`] = '';
}
} else if (['created_by', 'last_edited_by', 'select'].includes(properties[key].type)) {
results[`${key}`] = properties[key][type].name;
results[`${key}`] = (properties[key][type]) ? properties[key][type].name : null;
} else if (['people'].includes(properties[key].type)) {
if (Array.isArray(properties[key][type])) {
// tslint:disable-next-line: no-any
results[`${key}`] = properties[key][type].map((person: any) => person.person.email || {});
results[`${key}`] = properties[key][type].map((person: any) => person.person?.email || {});
} else {
results[`${key}`] = properties[key][type];
}
@ -415,32 +468,49 @@ export function simplifyProperties(properties: any) {
} else if (['rollup'].includes(properties[key].type)) {
//TODO figure how to resolve rollup field type
// results[`${key}`] = properties[key][type][properties[key][type].type];
} else if (['files'].includes(properties[key].type)) {
// tslint:disable-next-line: no-any
results[`${key}`] = properties[key][type].map((file: { type: string, [key: string]: any }) => (file[file.type].url));
}
}
return results;
}
// tslint:disable-next-line: no-any
export function simplifyObjects(objects: any) {
export function simplifyObjects(objects: any, download = false, version = 2) {
if (!Array.isArray(objects)) {
objects = [objects];
}
const results: IDataObject[] = [];
for (const { object, id, properties, parent, title } of objects) {
for (const { object, id, properties, parent, title, json, binary, url, created_time, last_edited_time } of objects) {
if (object === 'page' && (parent.type === 'page_id' || parent.type === 'workspace')) {
results.push({
id,
title: properties.title.title[0].plain_text,
name: properties.title.title[0].plain_text,
...version === 2 ? { url } : {},
});
} else if (object === 'page' && parent.type === 'database_id') {
results.push({
id,
...simplifyProperties(properties),
...(version === 2) ? { name: getPropertyTitle(properties) } : {},
...(version === 2) ? { url } : {},
...(version === 2) ? { ...prepend('property', simplifyProperties(properties)) } : { ...simplifyProperties(properties) },
});
} else if (download && json.object === 'page' && json.parent.type === 'database_id') {
results.push({
json: {
id,
...(version === 2) ? { name: getPropertyTitle(json.properties) } : {},
...(version === 2) ? { url } : {},
...(version === 2) ? { ...prepend('property', simplifyProperties(json.properties)) } : { ...simplifyProperties(json.properties) },
},
binary,
});
} else if (object === 'database') {
results.push({
id,
title: title[0].plain_text,
...version === 2 ? { name: title[0]?.plain_text || '' } : { title: title[0]?.plain_text || '' },
...version === 2 ? { url } : {},
});
}
}
@ -549,11 +619,20 @@ export function getConditions() {
'is_empty',
'is_not_empty',
],
formula: [
'contains',
'does_not_contain',
'is_empty',
'is_not_empty',
};
const formula: { [key: string]: string[] } = {
text: [
...typeConditions.rich_text,
],
checkbox: [
...typeConditions.checkbox,
],
number: [
...typeConditions.number,
],
date: [
...typeConditions.date,
],
};
@ -576,5 +655,283 @@ export function getConditions() {
} as INodeProperties,
);
}
elements.push(
{
displayName: 'Return Type',
name: 'returnType',
type: 'options',
displayOptions: {
show: {
type: [
'formula',
],
},
} as IDisplayOptions,
options: Object.keys(formula).map((key: string) => ({ name: capitalCase(key), value: key })),
default: '',
description: 'The formula return type',
} as INodeProperties,
);
for (const key of Object.keys(formula)) {
elements.push(
{
displayName: 'Condition',
name: 'condition',
type: 'options',
displayOptions: {
show: {
type: [
'formula',
],
returnType: [
key,
],
},
} as IDisplayOptions,
options: formula[key].map((key: string) => ({ name: capitalCase(key), value: key })),
default: '',
description: 'The value of the property to filter by.',
} as INodeProperties,
);
}
return elements;
}
export function validateCrendetials(this: ICredentialTestFunctions, credentials: ICredentialDataDecryptedObject) {
const options: OptionsWithUri = {
headers: {
'Authorization': `Bearer ${credentials.apiKey}`,
'Notion-Version': apiVersion[2],
},
method: 'GET',
uri: `https://api.notion.com/v1/users/me`,
json: true,
};
return this.helpers.request!(options);
}
// tslint:disable-next-line: no-any
export async function downloadFiles(this: IExecuteFunctions | IPollFunctions, records: [{ properties: { [key: string]: any | { id: string, type: string, files: [{ external: { url: string } } | { file: { url: string } }] } } }]): Promise<INodeExecutionData[]> {
const elements: INodeExecutionData[] = [];
for (const record of records) {
const element: INodeExecutionData = { json: {}, binary: {} };
element.json = record as unknown as IDataObject;
for (const key of Object.keys(record.properties)) {
if (record.properties[key].type === 'files') {
if (record.properties[key].files.length) {
for (const [index, file] of record.properties[key].files.entries()) {
const data = await notionApiRequest.call(this, 'GET', '', {}, {}, file?.file?.url || file?.external?.url, { json: false, encoding: null });
element.binary![`${key}_${index}`] = await this.helpers.prepareBinaryData(data);
}
}
}
}
if (Object.keys(element.binary as IBinaryKeyData).length === 0) {
delete element.binary;
}
elements.push(element);
}
return elements;
}
export function extractPageId(page: string) {
if (page.includes('p=')) {
return page.split('p=')[1];
} else if (page.includes('-') && page.includes('https')) {
return page.split('-')[page.split('-').length - 1];
}
return page;
}
export function extractDatabaseId(database: string) {
if (database.includes('?v=')) {
const data = database.split('?v=')[0].split('/');
const index = data.length - 1;
return data[index];
} else if (database.includes('/')) {
const index = database.split('/').length - 1;
return database.split('/')[index];
} else {
return database;
}
}
// tslint:disable-next-line: no-any
function prepend(stringKey: string, properties: { [key: string]: any }) {
for (const key of Object.keys(properties)) {
properties[`${stringKey}_${snakeCase(key)}`] = properties[key];
delete properties[key];
}
return properties;
}
// tslint:disable-next-line: no-any
export function getPropertyTitle(properties: { [key: string]: any }) {
return Object.values(properties).filter(property => property.type === 'title')[0].title[0]?.plain_text || '';
}
export function getSearchFilters(resource: string) {
return [
{
displayName: 'Filter',
name: 'filterType',
type: 'options',
options: [
{
name: 'None',
value: 'none',
},
{
name: 'Build Manually',
value: 'manual',
},
{
name: 'JSON',
value: 'json',
},
],
displayOptions: {
show: {
version: [
2,
],
resource: [
resource,
],
operation: [
'getAll',
],
},
},
default: 'none',
},
{
displayName: 'Must Match',
name: 'matchType',
type: 'options',
options: [
{
name: 'Any filter',
value: 'anyFilter',
},
{
name: 'All Filters',
value: 'allFilters',
},
],
displayOptions: {
show: {
version: [
2,
],
resource: [
resource,
],
operation: [
'getAll',
],
filterType: [
'manual',
],
},
},
default: 'anyFilter',
},
{
displayName: 'Filters',
name: 'filters',
type: 'fixedCollection',
typeOptions: {
multipleValues: true,
},
displayOptions: {
show: {
version: [
2,
],
resource: [
resource,
],
operation: [
'getAll',
],
filterType: [
'manual',
],
},
},
default: '',
placeholder: 'Add Condition',
options: [
{
displayName: 'Conditions',
name: 'conditions',
values: [
...filters(getConditions()),
],
},
],
},
{
displayName: 'See <a href="https://developers.notion.com/reference/post-database-query#post-database-query-filter" target="_blank">Notion guide</a> to creating filters',
name: 'jsonNotice',
type: 'notice',
displayOptions: {
show: {
version: [
2,
],
resource: [
resource,
],
operation: [
'getAll',
],
filterType: [
'json',
],
},
},
default: '',
},
{
displayName: 'Filters (JSON)',
name: 'filterJson',
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
},
displayOptions: {
show: {
version: [
2,
],
resource: [
resource,
],
operation: [
'getAll',
],
filterType: [
'json',
],
},
},
default: '',
description: '',
},
];
}
export function validateJSON(json: string | undefined): any { // tslint:disable-line:no-any
let result;
try {
result = JSON.parse(json!);
} catch (exception) {
result = undefined;
}
return result;
}

View File

@ -1,547 +1,37 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeDescription,
INodeTypeBaseDescription,
INodeVersionedType,
} from 'n8n-workflow';
import {
formatBlocks,
formatTitle,
getBlockTypes,
mapFilters,
mapProperties,
mapSorting,
notionApiRequest,
notionApiRequestAllItems,
simplifyObjects,
} from './GenericFunctions';
NotionV1,
} from './v1/NotionV1.node';
import {
databaseFields,
databaseOperations,
} from './DatabaseDescription';
NotionV2,
} from './v2/NotionV2.node';
import {
userFields,
userOperations,
} from './UserDescription';
NodeVersionedType,
} from '../../src/NodeVersionedType';
import {
pageFields,
pageOperations,
} from './PageDescription';
export class Notion extends NodeVersionedType {
constructor() {
const baseDescription: INodeTypeBaseDescription = {
displayName: 'Notion (Beta)',
name: 'notion',
icon: 'file:notion.svg',
group: ['output'],
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Notion API (Beta)',
defaultVersion: 2,
};
import {
blockFields,
blockOperations,
} from './BlockDescription';
const nodeVersions: INodeVersionedType['nodeVersions'] = {
1: new NotionV1(baseDescription),
2: new NotionV2(baseDescription),
};
import {
databasePageFields,
databasePageOperations,
} from './DatabasePageDescription';
import * as moment from 'moment-timezone';
export class Notion implements INodeType {
description: INodeTypeDescription = {
displayName: 'Notion (Beta)',
name: 'notion',
icon: 'file:notion.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Notion API (Beta)',
defaults: {
name: 'Notion',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'notionApi',
required: true,
// displayOptions: {
// show: {
// authentication: [
// 'apiKey',
// ],
// },
// },
},
// {
// name: 'notionOAuth2Api',
// required: true,
// displayOptions: {
// show: {
// authentication: [
// 'oAuth2',
// ],
// },
// },
// },
],
properties: [
// {
// displayName: 'Authentication',
// name: 'authentication',
// type: 'options',
// options: [
// {
// name: 'API Key',
// value: 'apiKey',
// },
// {
// name: 'OAuth2',
// value: 'oAuth2',
// },
// ],
// default: 'apiKey',
// description: 'The resource to operate on.',
// },
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Block',
value: 'block',
},
{
name: 'Database',
value: 'database',
},
{
name: 'Database Page',
value: 'databasePage',
},
{
name: 'Page',
value: 'page',
},
{
name: 'User',
value: 'user',
},
],
default: 'page',
description: 'Resource to consume.',
},
...blockOperations,
...blockFields,
...databaseOperations,
...databaseFields,
...databasePageOperations,
...databasePageFields,
...pageOperations,
...pageFields,
...userOperations,
...userFields,
],
};
methods = {
loadOptions: {
async getDatabases(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const body: IDataObject = {
page_size: 100,
filter: { property: 'object', value: 'database' },
};
const databases = await notionApiRequestAllItems.call(this, 'results', 'POST', `/search`, body);
for (const database of databases) {
returnData.push({
name: database.title[0]?.plain_text || database.id,
value: database.id,
});
}
returnData.sort((a, b) => {
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { return -1; }
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) { return 1; }
return 0;
});
return returnData;
},
async getDatabaseProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const databaseId = this.getCurrentNodeParameter('databaseId') as string;
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
for (const key of Object.keys(properties)) {
//remove parameters that cannot be set from the API.
if (!['created_time', 'last_edited_time', 'created_by', 'last_edited_by', 'formula', 'files'].includes(properties[key].type)) {
returnData.push({
name: `${key} - (${properties[key].type})`,
value: `${key}|${properties[key].type}`,
});
}
}
returnData.sort((a, b) => {
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { return -1; }
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) { return 1; }
return 0;
});
return returnData;
},
async getFilterProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const databaseId = this.getCurrentNodeParameter('databaseId') as string;
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
for (const key of Object.keys(properties)) {
returnData.push({
name: `${key} - (${properties[key].type})`,
value: `${key}|${properties[key].type}`,
});
}
returnData.sort((a, b) => {
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { return -1; }
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) { return 1; }
return 0;
});
return returnData;
},
async getBlockTypes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
return getBlockTypes();
},
async getPropertySelectValues(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const [name, type] = (this.getCurrentNodeParameter('&key') as string).split('|');
const databaseId = this.getCurrentNodeParameter('databaseId') as string;
const resource = this.getCurrentNodeParameter('resource') as string;
const operation = this.getCurrentNodeParameter('operation') as string;
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
if (resource === 'databasePage') {
if (['multi_select', 'select'].includes(type) && operation === 'getAll') {
return (properties[name][type].options)
.map((option: IDataObject) => ({ name: option.name, value: option.name }));
} else if (['multi_select'].includes(type) && ['create', 'update'].includes(operation)) {
return (properties[name][type].options)
.map((option: IDataObject) => ({ name: option.name, value: option.name }));
}
}
return (properties[name][type].options).map((option: IDataObject) => ({ name: option.name, value: option.id }));
},
async getUsers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const users = await notionApiRequestAllItems.call(this, 'results', 'GET', '/users');
for (const user of users) {
returnData.push({
name: user.name,
value: user.id,
});
}
return returnData;
},
async getDatabaseIdFromPage(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const pageId = this.getCurrentNodeParameter('pageId') as string;
const { parent: { database_id: databaseId } } = await notionApiRequest.call(this, 'GET', `/pages/${pageId}`);
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
for (const key of Object.keys(properties)) {
//remove parameters that cannot be set from the API.
if (!['created_time', 'last_edited_time', 'created_by', 'last_edited_by', 'formula', 'files'].includes(properties[key].type)) {
returnData.push({
name: `${key} - (${properties[key].type})`,
value: `${key}|${properties[key].type}`,
});
}
}
returnData.sort((a, b) => {
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { return -1; }
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) { return 1; }
return 0;
});
return returnData;
},
async getDatabaseOptionsFromPage(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const pageId = this.getCurrentNodeParameter('pageId') as string;
const [name, type] = (this.getCurrentNodeParameter('&key') as string).split('|');
const { parent: { database_id: databaseId } } = await notionApiRequest.call(this, 'GET', `/pages/${pageId}`);
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
return (properties[name][type].options).map((option: IDataObject) => ({ name: option.name, value: option.id }));
},
// Get all the timezones to display them to user so that he can
// select them easily
async getTimezones(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
for (const timezone of moment.tz.names()) {
const timezoneName = timezone;
const timezoneId = timezone;
returnData.push({
name: timezoneName,
value: timezoneId,
});
}
returnData.unshift({
name: 'Default',
value: 'default',
description: 'Timezone set in n8n',
});
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
let responseData;
const qs: IDataObject = {};
const timezone = this.getTimezone();
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
if (resource === 'block') {
if (operation === 'append') {
for (let i = 0; i < length; i++) {
const blockId = this.getNodeParameter('blockId', i) as string;
const body: IDataObject = {
children: formatBlocks(this.getNodeParameter('blockUi.blockValues', i, []) as IDataObject[]),
};
const block = await notionApiRequest.call(this, 'PATCH', `/blocks/${blockId}/children`, body);
returnData.push(block);
}
}
if (operation === 'getAll') {
for (let i = 0; i < length; i++) {
const blockId = this.getNodeParameter('blockId', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'GET', `/blocks/${blockId}/children`, {});
} else {
qs.page_size = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequest.call(this, 'GET', `/blocks/${blockId}/children`, {});
responseData = responseData.results;
}
returnData.push.apply(returnData, responseData);
}
}
}
if (resource === 'database') {
if (operation === 'get') {
for (let i = 0; i < length; i++) {
const databaseId = this.getNodeParameter('databaseId', i) as string;
responseData = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
returnData.push(responseData);
}
}
if (operation === 'getAll') {
for (let i = 0; i < length; i++) {
const body: IDataObject = {
filter: { property: 'object', value: 'database' },
};
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', `/search`, body);
} else {
body['page_size'] = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequest.call(this, 'POST', `/search`, body);
responseData = responseData.results;
}
returnData.push.apply(returnData, responseData);
}
}
}
if (resource === 'databasePage') {
if (operation === 'create') {
for (let i = 0; i < length; i++) {
const simple = this.getNodeParameter('simple', i) as boolean;
// tslint:disable-next-line: no-any
const body: { [key: string]: any } = {
parent: {},
properties: {},
};
body.parent['database_id'] = this.getNodeParameter('databaseId', i) as string;
const properties = this.getNodeParameter('propertiesUi.propertyValues', i, []) as IDataObject[];
if (properties.length !== 0) {
body.properties = mapProperties(properties, timezone) as IDataObject;
}
body.children = formatBlocks(this.getNodeParameter('blockUi.blockValues', i, []) as IDataObject[]);
responseData = await notionApiRequest.call(this, 'POST', '/pages', body);
if (simple === true) {
responseData = simplifyObjects(responseData);
}
returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]);
}
}
if (operation === 'getAll') {
for (let i = 0; i < length; i++) {
const simple = this.getNodeParameter('simple', 0) as boolean;
const databaseId = this.getNodeParameter('databaseId', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const filters = this.getNodeParameter('options.filter', i, {}) as IDataObject;
const sort = this.getNodeParameter('options.sort.sortValue', i, []) as IDataObject[];
const body: IDataObject = {
filter: {},
};
if (filters.singleCondition) {
body['filter'] = mapFilters([filters.singleCondition] as IDataObject[], timezone);
}
if (filters.multipleCondition) {
const { or, and } = (filters.multipleCondition as IDataObject).condition as IDataObject;
if (Array.isArray(or) && or.length !== 0) {
Object.assign(body.filter, { or: (or as IDataObject[]).map((data) => mapFilters([data], timezone)) });
}
if (Array.isArray(and) && and.length !== 0) {
Object.assign(body.filter, { and: (and as IDataObject[]).map((data) => mapFilters([data], timezone)) });
}
}
if (!Object.keys(body.filter as IDataObject).length) {
delete body.filter;
}
if (sort) {
//@ts-expect-error
body['sorts'] = mapSorting(sort);
}
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', `/databases/${databaseId}/query`, body, {});
} else {
body.page_size = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequest.call(this, 'POST', `/databases/${databaseId}/query`, body, qs);
responseData = responseData.results;
}
if (simple === true) {
responseData = simplifyObjects(responseData);
}
returnData.push.apply(returnData, responseData);
}
}
if (operation === 'update') {
for (let i = 0; i < length; i++) {
const pageId = this.getNodeParameter('pageId', i) as string;
const simple = this.getNodeParameter('simple', i) as boolean;
const properties = this.getNodeParameter('propertiesUi.propertyValues', i, []) as IDataObject[];
// tslint:disable-next-line: no-any
const body: { [key: string]: any } = {
properties: {},
};
if (properties.length !== 0) {
body.properties = mapProperties(properties, timezone) as IDataObject;
}
responseData = await notionApiRequest.call(this, 'PATCH', `/pages/${pageId}`, body);
if (simple === true) {
responseData = simplifyObjects(responseData);
}
returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]);
}
}
}
if (resource === 'user') {
if (operation === 'get') {
for (let i = 0; i < length; i++) {
const userId = this.getNodeParameter('userId', i) as string;
responseData = await notionApiRequest.call(this, 'GET', `/users/${userId}`);
returnData.push(responseData);
}
}
if (operation === 'getAll') {
for (let i = 0; i < length; i++) {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'GET', '/users');
} else {
qs.limit = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequestAllItems.call(this, 'results', 'GET', '/users');
responseData = responseData.splice(0, qs.limit);
}
returnData.push.apply(returnData, responseData);
}
}
}
if (resource === 'page') {
if (operation === 'create') {
for (let i = 0; i < length; i++) {
const simple = this.getNodeParameter('simple', i) as boolean;
// tslint:disable-next-line: no-any
const body: { [key: string]: any } = {
parent: {},
properties: {},
};
body.parent['page_id'] = this.getNodeParameter('pageId', i) as string;
body.properties = formatTitle(this.getNodeParameter('title', i) as string);
body.children = formatBlocks(this.getNodeParameter('blockUi.blockValues', i, []) as IDataObject[]);
responseData = await notionApiRequest.call(this, 'POST', '/pages', body);
if (simple === true) {
responseData = simplifyObjects(responseData);
}
returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]);
}
}
if (operation === 'get') {
for (let i = 0; i < length; i++) {
const pageId = this.getNodeParameter('pageId', i) as string;
const simple = this.getNodeParameter('simple', i) as boolean;
responseData = await notionApiRequest.call(this, 'GET', `/pages/${pageId}`);
if (simple === true) {
responseData = simplifyObjects(responseData);
}
returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]);
}
}
if (operation === 'search') {
for (let i = 0; i < length; i++) {
const text = this.getNodeParameter('text', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const simple = this.getNodeParameter('simple', i) as boolean;
const body: IDataObject = {};
if (text) {
body['query'] = text;
}
if (options.filter) {
const filter = (options.filter as IDataObject || {}).filters as IDataObject[] || [];
body['filter'] = filter;
}
if (options.sort) {
const sort = (options.sort as IDataObject || {}).sortValue as IDataObject || {};
body['sort'] = sort;
}
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', '/search', body);
} else {
qs.limit = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', '/search', body);
responseData = responseData.splice(0, qs.limit);
}
if (simple === true) {
responseData = simplifyObjects(responseData);
}
returnData.push.apply(returnData, responseData);
}
}
}
return [this.helpers.returnJsonArray(returnData)];
super(nodeVersions, baseDescription);
}
}
}

View File

@ -34,6 +34,7 @@ export class NotionTrigger implements INodeType {
{
name: 'notionApi',
required: true,
testedBy: 'notionApiCredentialTest',
},
],
polling: true,
@ -49,10 +50,10 @@ export class NotionTrigger implements INodeType {
name: 'Page Added to Database',
value: 'pageAddedToDatabase',
},
// {
// name: 'Record Updated',
// value: 'recordUpdated',
// },
{
name: 'Paged Updated in Database',
value: 'pagedUpdatedInDatabase',
},
],
required: true,
default: '',
@ -68,26 +69,28 @@ export class NotionTrigger implements INodeType {
show: {
event: [
'pageAddedToDatabase',
'pagedUpdatedInDatabase',
],
},
},
default: '',
required: true,
description: 'The ID of this database.',
description: 'The ID of this database',
},
{
displayName: 'Simple',
displayName: 'Simplify Output',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
event: [
'pageAddedToDatabase',
'pagedUpdatedInDatabase',
],
},
},
default: true,
description: 'When set to true a simplify version of the response will be used else the raw data.',
description: 'Whether to return a simplified version of the response instead of the raw data',
},
],
};
@ -148,7 +151,7 @@ export class NotionTrigger implements INodeType {
if (this.getMode() === 'manual') {
if (simple === true) {
data = simplifyObjects(data);
data = simplifyObjects(data, false, 1);
}
if (Array.isArray(data) && data.length) {
return [this.helpers.returnJsonArray(data)];
@ -172,7 +175,7 @@ export class NotionTrigger implements INodeType {
}
if (simple === true) {
records = simplifyObjects(records);
records = simplifyObjects(records, false, 1);
}
webhookData.lastRecordProccesed = data[0].id;

View File

@ -6,13 +6,16 @@ import {
blocks,
} from './Blocks';
export const pageOperations: INodeProperties[] = [
export const pageOperations = [
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
version: [
1,
],
resource: [
'page',
],
@ -36,17 +39,93 @@ export const pageOperations: INodeProperties[] = [
},
],
default: 'create',
description: 'The operation to perform.',
},
];
{
displayName: 'Operation',
name: 'operation',
type: 'options',
displayOptions: {
show: {
version: [
2,
],
resource: [
'page',
],
},
},
options: [
{
name: 'Archive',
value: 'archive',
description: 'Archive a page',
},
{
name: 'Create',
value: 'create',
description: 'Create a page',
},
{
name: 'Search',
value: 'search',
description: 'Text search of pages',
},
],
default: 'create',
},
] as INodeProperties[];
export const pageFields: INodeProperties[] = [
export const pageFields = [
/* -------------------------------------------------------------------------- */
/* page:archive */
/* -------------------------------------------------------------------------- */
{
displayName: 'Page Link or ID',
name: 'pageId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
version: [
2,
],
resource: [
'page',
],
operation: [
'archive',
],
},
},
description: `The Page URL from Notion's 'copy link' functionality (or just the ID contained within the URL)`,
},
{
displayName: 'Simplify Output',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
version: [
2,
],
resource: [
'page',
],
operation: [
'archive',
],
},
},
default: true,
description: 'Whether to return a simplified version of the response instead of the raw data',
},
/* -------------------------------------------------------------------------- */
/* page:create */
/* -------------------------------------------------------------------------- */
{
displayName: 'Parent Page ID',
displayName: 'Parent Page ID or Link',
name: 'pageId',
type: 'string',
default: '',
@ -61,7 +140,7 @@ export const pageFields: INodeProperties[] = [
],
},
},
description: 'The ID of the parent page that this child page belongs to.',
description: `The URL from Notion's 'copy link' functionality (or just the ID contained within the URL)`,
},
{
displayName: 'Title',
@ -79,10 +158,10 @@ export const pageFields: INodeProperties[] = [
],
},
},
description: 'Page title. Appears at the top of the page and can be found via Quick Find.',
description: 'Page title. Appears at the top of the page and can be found via Quick Find',
},
{
displayName: 'Simple',
displayName: 'Simplify Output',
name: 'simple',
type: 'boolean',
displayOptions: {
@ -96,20 +175,23 @@ export const pageFields: INodeProperties[] = [
},
},
default: true,
description: 'When set to true a simplify version of the response will be used else the raw data.',
description: 'Whether to return a simplified version of the response instead of the raw data',
},
...blocks('page', 'create'),
/* -------------------------------------------------------------------------- */
/* page:get */
/* -------------------------------------------------------------------------- */
{
displayName: 'Page ID',
displayName: 'Page Link or ID',
name: 'pageId',
type: 'string',
default: '',
required: true,
displayOptions: {
show: {
version: [
1,
],
resource: [
'page',
],
@ -118,13 +200,17 @@ export const pageFields: INodeProperties[] = [
],
},
},
description: `The Page URL from Notion's 'copy link' functionality (or just the ID contained within the URL)`,
},
{
displayName: 'Simple',
displayName: 'Simplify Output',
name: 'simple',
type: 'boolean',
displayOptions: {
show: {
version: [
1,
],
resource: [
'page',
],
@ -134,7 +220,7 @@ export const pageFields: INodeProperties[] = [
},
},
default: true,
description: 'When set to true a simplify version of the response will be used else the raw data.',
description: 'Whether to return a simplified version of the response instead of the raw data',
},
/* -------------------------------------------------------------------------- */
/* page:search */
@ -154,7 +240,7 @@ export const pageFields: INodeProperties[] = [
],
},
},
description: 'The text to search for.',
description: 'The text to search for',
},
{
displayName: 'Return All',
@ -171,7 +257,7 @@ export const pageFields: INodeProperties[] = [
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
@ -195,10 +281,10 @@ export const pageFields: INodeProperties[] = [
maxValue: 100,
},
default: 50,
description: 'How many results to return.',
description: 'How many results to return',
},
{
displayName: 'Simple',
displayName: 'Simplify Output',
name: 'simple',
type: 'boolean',
displayOptions: {
@ -212,7 +298,7 @@ export const pageFields: INodeProperties[] = [
},
},
default: true,
description: 'When set to true a simplify version of the response will be used else the raw data.',
description: 'Whether to return a simplified version of the response instead of the raw data',
},
{
displayName: 'Options',
@ -256,7 +342,7 @@ export const pageFields: INodeProperties[] = [
},
],
default: 'object',
description: 'The name of the property to filter by.',
description: 'The name of the property to filter by',
},
{
displayName: 'Value',
@ -273,7 +359,7 @@ export const pageFields: INodeProperties[] = [
},
],
default: '',
description: 'The value of the property to filter by.',
description: 'The value of the property to filter by',
},
],
},
@ -307,8 +393,8 @@ export const pageFields: INodeProperties[] = [
value: 'descending',
},
],
default: '',
description: 'The direction to sort.',
default: 'descending',
description: 'The direction to sort',
},
{
displayName: 'Timestamp',
@ -321,7 +407,7 @@ export const pageFields: INodeProperties[] = [
},
],
default: 'last_edited_time',
description: `The name of the timestamp to sort against.`,
description: `The name of the timestamp to sort against`,
},
],
},
@ -329,4 +415,4 @@ export const pageFields: INodeProperties[] = [
},
],
},
];
] as INodeProperties[];

View File

@ -2,7 +2,7 @@ import {
INodeProperties,
} from 'n8n-workflow';
export const userOperations: INodeProperties[] = [
export const userOperations = [
{
displayName: 'Operation',
name: 'operation',
@ -27,11 +27,10 @@ export const userOperations: INodeProperties[] = [
},
],
default: 'get',
description: 'The operation to perform.',
},
];
] as INodeProperties[];
export const userFields: INodeProperties[] = [
export const userFields = [
/* -------------------------------------------------------------------------- */
/* user:get */
@ -71,7 +70,7 @@ export const userFields: INodeProperties[] = [
},
},
default: false,
description: 'If all results should be returned or only up to a given limit.',
description: 'Whether to return all results or only up to a given limit',
},
{
displayName: 'Limit',
@ -95,6 +94,6 @@ export const userFields: INodeProperties[] = [
maxValue: 100,
},
default: 50,
description: 'How many results to return.',
description: 'How many results to return',
},
];
] as INodeProperties[];

View File

@ -0,0 +1,444 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeBaseDescription,
INodeTypeDescription,
} from 'n8n-workflow';
import {
extractDatabaseId,
extractPageId,
formatBlocks,
formatTitle,
getBlockTypes,
mapFilters,
mapProperties,
mapSorting,
notionApiRequest,
notionApiRequestAllItems,
simplifyObjects,
} from '../GenericFunctions';
import * as moment from 'moment-timezone';
import {
versionDescription
} from './VersionDescription';
export class NotionV1 implements INodeType {
description: INodeTypeDescription;
constructor(baseDescription: INodeTypeBaseDescription) {
this.description = {
...baseDescription,
...versionDescription,
};
}
methods = {
loadOptions: {
async getDatabases(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const body: IDataObject = {
page_size: 100,
filter: { property: 'object', value: 'database' },
};
const databases = await notionApiRequestAllItems.call(this, 'results', 'POST', `/search`, body);
for (const database of databases) {
returnData.push({
name: database.title[0]?.plain_text || database.id,
value: database.id,
});
}
returnData.sort((a, b) => {
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { return -1; }
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) { return 1; }
return 0;
});
return returnData;
},
async getDatabaseProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const databaseId = this.getCurrentNodeParameter('databaseId') as string;
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
for (const key of Object.keys(properties)) {
//remove parameters that cannot be set from the API.
if (!['created_time', 'last_edited_time', 'created_by', 'last_edited_by', 'formula', 'files', 'rollup'].includes(properties[key].type)) {
returnData.push({
name: `${key} - (${properties[key].type})`,
value: `${key}|${properties[key].type}`,
});
}
}
returnData.sort((a, b) => {
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { return -1; }
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) { return 1; }
return 0;
});
return returnData;
},
async getFilterProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const databaseId = this.getCurrentNodeParameter('databaseId') as string;
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
for (const key of Object.keys(properties)) {
returnData.push({
name: `${key} - (${properties[key].type})`,
value: `${key}|${properties[key].type}`,
});
}
returnData.sort((a, b) => {
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { return -1; }
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) { return 1; }
return 0;
});
return returnData;
},
async getBlockTypes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
return getBlockTypes();
},
async getPropertySelectValues(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const [name, type] = (this.getCurrentNodeParameter('&key') as string).split('|');
const databaseId = this.getCurrentNodeParameter('databaseId') as string;
const resource = this.getCurrentNodeParameter('resource') as string;
const operation = this.getCurrentNodeParameter('operation') as string;
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
if (resource === 'databasePage') {
if (['multi_select', 'select'].includes(type) && operation === 'getAll') {
return (properties[name][type].options)
.map((option: IDataObject) => ({ name: option.name, value: option.name }));
} else if (['multi_select'].includes(type) && ['create', 'update'].includes(operation)) {
return (properties[name][type].options)
.map((option: IDataObject) => ({ name: option.name, value: option.name }));
}
}
return (properties[name][type].options).map((option: IDataObject) => ({ name: option.name, value: option.id }));
},
async getUsers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const users = await notionApiRequestAllItems.call(this, 'results', 'GET', '/users');
for (const user of users) {
returnData.push({
name: user.name,
value: user.id,
});
}
return returnData;
},
async getDatabaseIdFromPage(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const pageId = this.getCurrentNodeParameter('pageId') as string;
const { parent: { database_id: databaseId } } = await notionApiRequest.call(this, 'GET', `/pages/${pageId}`);
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
for (const key of Object.keys(properties)) {
//remove parameters that cannot be set from the API.
if (!['created_time', 'last_edited_time', 'created_by', 'last_edited_by', 'formula', 'files'].includes(properties[key].type)) {
returnData.push({
name: `${key} - (${properties[key].type})`,
value: `${key}|${properties[key].type}`,
});
}
}
returnData.sort((a, b) => {
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { return -1; }
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) { return 1; }
return 0;
});
return returnData;
},
async getDatabaseOptionsFromPage(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const pageId = this.getCurrentNodeParameter('pageId') as string;
const [name, type] = (this.getCurrentNodeParameter('&key') as string).split('|');
const { parent: { database_id: databaseId } } = await notionApiRequest.call(this, 'GET', `/pages/${pageId}`);
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
return (properties[name][type].options).map((option: IDataObject) => ({ name: option.name, value: option.id }));
},
// Get all the timezones to display them to user so that he can
// select them easily
async getTimezones(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
for (const timezone of moment.tz.names()) {
const timezoneName = timezone;
const timezoneId = timezone;
returnData.push({
name: timezoneName,
value: timezoneId,
});
}
returnData.unshift({
name: 'Default',
value: 'default',
description: 'Timezone set in n8n',
});
return returnData;
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
let responseData;
const qs: IDataObject = {};
const timezone = this.getTimezone();
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
if (resource === 'block') {
if (operation === 'append') {
for (let i = 0; i < length; i++) {
const blockId = extractPageId(this.getNodeParameter('blockId', i) as string);
const body: IDataObject = {
children: formatBlocks(this.getNodeParameter('blockUi.blockValues', i, []) as IDataObject[]),
};
const block = await notionApiRequest.call(this, 'PATCH', `/blocks/${blockId}/children`, body);
returnData.push(block);
}
}
if (operation === 'getAll') {
for (let i = 0; i < length; i++) {
const blockId = extractPageId(this.getNodeParameter('blockId', i) as string);
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'GET', `/blocks/${blockId}/children`, {});
} else {
qs.page_size = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequest.call(this, 'GET', `/blocks/${blockId}/children`, {}, qs);
responseData = responseData.results;
}
returnData.push.apply(returnData, responseData);
}
}
}
if (resource === 'database') {
if (operation === 'get') {
for (let i = 0; i < length; i++) {
const databaseId = extractDatabaseId(this.getNodeParameter('databaseId', i) as string);
responseData = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
returnData.push(responseData);
}
}
if (operation === 'getAll') {
for (let i = 0; i < length; i++) {
const body: IDataObject = {
filter: { property: 'object', value: 'database' },
};
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', `/search`, body);
} else {
body['page_size'] = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequest.call(this, 'POST', `/search`, body);
responseData = responseData.results;
}
returnData.push.apply(returnData, responseData);
}
}
}
if (resource === 'databasePage') {
if (operation === 'create') {
for (let i = 0; i < length; i++) {
const simple = this.getNodeParameter('simple', i) as boolean;
// tslint:disable-next-line: no-any
const body: { [key: string]: any } = {
parent: {},
properties: {},
};
body.parent['database_id'] = this.getNodeParameter('databaseId', i) as string;
const properties = this.getNodeParameter('propertiesUi.propertyValues', i, []) as IDataObject[];
if (properties.length !== 0) {
body.properties = mapProperties(properties, timezone) as IDataObject;
}
body.children = formatBlocks(this.getNodeParameter('blockUi.blockValues', i, []) as IDataObject[]);
responseData = await notionApiRequest.call(this, 'POST', '/pages', body);
if (simple === true) {
responseData = simplifyObjects(responseData, false, 1);
}
returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]);
}
}
if (operation === 'getAll') {
for (let i = 0; i < length; i++) {
const simple = this.getNodeParameter('simple', 0) as boolean;
const databaseId = this.getNodeParameter('databaseId', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const filters = this.getNodeParameter('options.filter', i, {}) as IDataObject;
const sort = this.getNodeParameter('options.sort.sortValue', i, []) as IDataObject[];
const body: IDataObject = {
filter: {},
};
if (filters.singleCondition) {
body['filter'] = mapFilters([filters.singleCondition] as IDataObject[], timezone);
}
if (filters.multipleCondition) {
const { or, and } = (filters.multipleCondition as IDataObject).condition as IDataObject;
if (Array.isArray(or) && or.length !== 0) {
Object.assign(body.filter, { or: (or as IDataObject[]).map((data) => mapFilters([data], timezone)) });
}
if (Array.isArray(and) && and.length !== 0) {
Object.assign(body.filter, { and: (and as IDataObject[]).map((data) => mapFilters([data], timezone)) });
}
}
if (!Object.keys(body.filter as IDataObject).length) {
delete body.filter;
}
if (sort) {
//@ts-expect-error
body['sorts'] = mapSorting(sort);
}
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', `/databases/${databaseId}/query`, body, {});
} else {
body.page_size = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequest.call(this, 'POST', `/databases/${databaseId}/query`, body, qs);
responseData = responseData.results;
}
if (simple === true) {
responseData = simplifyObjects(responseData, false, 1);
}
returnData.push.apply(returnData, responseData);
}
}
if (operation === 'update') {
for (let i = 0; i < length; i++) {
const pageId = extractPageId(this.getNodeParameter('pageId', i) as string);
const simple = this.getNodeParameter('simple', i) as boolean;
const properties = this.getNodeParameter('propertiesUi.propertyValues', i, []) as IDataObject[];
// tslint:disable-next-line: no-any
const body: { [key: string]: any } = {
properties: {},
};
if (properties.length !== 0) {
body.properties = mapProperties(properties, timezone) as IDataObject;
}
responseData = await notionApiRequest.call(this, 'PATCH', `/pages/${pageId}`, body);
if (simple === true) {
responseData = simplifyObjects(responseData, false, 1);
}
returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]);
}
}
}
if (resource === 'user') {
if (operation === 'get') {
for (let i = 0; i < length; i++) {
const userId = this.getNodeParameter('userId', i) as string;
responseData = await notionApiRequest.call(this, 'GET', `/users/${userId}`);
returnData.push(responseData);
}
}
if (operation === 'getAll') {
for (let i = 0; i < length; i++) {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'GET', '/users');
} else {
qs.limit = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequestAllItems.call(this, 'results', 'GET', '/users');
responseData = responseData.splice(0, qs.limit);
}
returnData.push.apply(returnData, responseData);
}
}
}
if (resource === 'page') {
if (operation === 'create') {
for (let i = 0; i < length; i++) {
const simple = this.getNodeParameter('simple', i) as boolean;
// tslint:disable-next-line: no-any
const body: { [key: string]: any } = {
parent: {},
properties: {},
};
body.parent['page_id'] = extractPageId(this.getNodeParameter('pageId', i) as string);
body.properties = formatTitle(this.getNodeParameter('title', i) as string);
body.children = formatBlocks(this.getNodeParameter('blockUi.blockValues', i, []) as IDataObject[]);
responseData = await notionApiRequest.call(this, 'POST', '/pages', body);
if (simple === true) {
responseData = simplifyObjects(responseData, false, 1);
}
returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]);
}
}
if (operation === 'get') {
for (let i = 0; i < length; i++) {
const pageId = extractPageId(this.getNodeParameter('pageId', i) as string);
const simple = this.getNodeParameter('simple', i) as boolean;
responseData = await notionApiRequest.call(this, 'GET', `/pages/${pageId}`);
if (simple === true) {
responseData = simplifyObjects(responseData, false, 1);
}
returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]);
}
}
if (operation === 'search') {
for (let i = 0; i < length; i++) {
const text = this.getNodeParameter('text', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const simple = this.getNodeParameter('simple', i) as boolean;
const body: IDataObject = {};
if (text) {
body['query'] = text;
}
if (options.filter) {
const filter = (options.filter as IDataObject || {}).filters as IDataObject[] || [];
body['filter'] = filter;
}
if (options.sort) {
const sort = (options.sort as IDataObject || {}).sortValue as IDataObject || {};
body['sort'] = sort;
}
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', '/search', body);
} else {
qs.limit = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', '/search', body);
responseData = responseData.splice(0, qs.limit);
}
if (simple === true) {
responseData = simplifyObjects(responseData, false, 1);
}
returnData.push.apply(returnData, responseData);
}
}
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View File

@ -0,0 +1,137 @@
import {
databaseFields,
databaseOperations,
} from '../DatabaseDescription';
import {
userFields,
userOperations,
} from '../UserDescription';
import {
pageFields,
pageOperations,
} from '../PageDescription';
import {
blockFields,
blockOperations,
} from '../BlockDescription';
import {
databasePageFields,
databasePageOperations,
} from '../DatabasePageDescription';
import {
INodeTypeDescription,
} from 'n8n-workflow';
export const versionDescription: INodeTypeDescription = {
displayName: 'Notion (Beta)',
name: 'notion',
icon: 'file:notion.svg',
group: ['output'],
version: 1,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Notion API (Beta)',
defaults: {
name: 'Notion',
color: '#000000',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'notionApi',
required: true,
// displayOptions: {
// show: {
// authentication: [
// 'apiKey',
// ],
// },
// },
},
// {
// name: 'notionOAuth2Api',
// required: true,
// displayOptions: {
// show: {
// authentication: [
// 'oAuth2',
// ],
// },
// },
// },
],
properties: [
// {
// displayName: 'Authentication',
// name: 'authentication',
// type: 'options',
// options: [
// {
// name: 'API Key',
// value: 'apiKey',
// },
// {
// name: 'OAuth2',
// value: 'oAuth2',
// },
// ],
// default: 'apiKey',
// description: 'The resource to operate on.',
// },
{
displayName: 'To access content, make sure it\'s shared with your integration in Notion',
name: 'notionNotice',
type: 'notice',
default: '',
},
{
displayName: 'Version',
name: 'version',
type: 'hidden',
default: 1,
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Block',
value: 'block',
},
{
name: 'Database',
value: 'database',
},
{
name: 'Database Page',
value: 'databasePage',
},
{
name: 'Page',
value: 'page',
},
{
name: 'User',
value: 'user',
},
],
default: 'page',
},
...blockOperations,
...blockFields,
...databaseOperations,
...databaseFields,
...databasePageOperations,
...databasePageFields,
...pageOperations,
...pageFields,
...userOperations,
...userFields,
],
};

View File

@ -0,0 +1,556 @@
import {
IExecuteFunctions,
} from 'n8n-core';
import {
ICredentialDataDecryptedObject,
ICredentialsDecrypted,
ICredentialTestFunctions,
IDataObject,
ILoadOptionsFunctions,
INodeExecutionData,
INodePropertyOptions,
INodeType,
INodeTypeBaseDescription,
INodeTypeDescription,
NodeApiError,
NodeCredentialTestResult,
} from 'n8n-workflow';
import {
downloadFiles,
extractDatabaseId,
extractPageId,
formatBlocks,
formatTitle,
getBlockTypes,
mapFilters,
mapProperties,
mapSorting,
notionApiRequest,
notionApiRequestAllItems,
simplifyObjects,
validateCrendetials,
validateJSON,
} from '../GenericFunctions';
import * as moment from 'moment-timezone';
import {
versionDescription
} from './VersionDescription';
export class NotionV2 implements INodeType {
description: INodeTypeDescription;
constructor(baseDescription: INodeTypeBaseDescription) {
this.description = {
...baseDescription,
...versionDescription,
};
}
methods = {
loadOptions: {
async getDatabases(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const body: IDataObject = {
page_size: 100,
filter: { property: 'object', value: 'database' },
};
const databases = await notionApiRequestAllItems.call(this, 'results', 'POST', `/search`, body);
for (const database of databases) {
returnData.push({
name: database.title[0]?.plain_text || database.id,
value: database.id,
});
}
returnData.sort((a, b) => {
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { return -1; }
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) { return 1; }
return 0;
});
return returnData;
},
async getDatabaseProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const databaseId = this.getCurrentNodeParameter('databaseId') as string;
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
for (const key of Object.keys(properties)) {
//remove parameters that cannot be set from the API.
if (!['created_time', 'last_edited_time', 'created_by', 'last_edited_by', 'formula', 'files', 'rollup'].includes(properties[key].type)) {
returnData.push({
name: `${key}`,
value: `${key}|${properties[key].type}`,
});
}
}
returnData.sort((a, b) => {
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { return -1; }
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) { return 1; }
return 0;
});
return returnData;
},
async getFilterProperties(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const databaseId = this.getCurrentNodeParameter('databaseId') as string;
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
for (const key of Object.keys(properties)) {
returnData.push({
name: `${key}`,
value: `${key}|${properties[key].type}`,
});
}
returnData.sort((a, b) => {
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { return -1; }
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) { return 1; }
return 0;
});
return returnData;
},
async getBlockTypes(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
return getBlockTypes();
},
async getPropertySelectValues(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const [name, type] = (this.getCurrentNodeParameter('&key') as string).split('|');
const databaseId = this.getCurrentNodeParameter('databaseId') as string;
const resource = this.getCurrentNodeParameter('resource') as string;
const operation = this.getCurrentNodeParameter('operation') as string;
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
if (resource === 'databasePage') {
if (['multi_select', 'select'].includes(type) && operation === 'getAll') {
return (properties[name][type].options)
.map((option: IDataObject) => ({ name: option.name, value: option.name }));
} else if (['multi_select', 'select'].includes(type) && ['create', 'update'].includes(operation)) {
return (properties[name][type].options)
.map((option: IDataObject) => ({ name: option.name, value: option.name }));
}
}
return (properties[name][type].options).map((option: IDataObject) => ({ name: option.name, value: option.id }));
},
async getUsers(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const users = await notionApiRequestAllItems.call(this, 'results', 'GET', '/users');
for (const user of users) {
returnData.push({
name: user.name,
value: user.id,
});
}
return returnData;
},
async getDatabaseIdFromPage(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
const pageId = extractPageId(this.getCurrentNodeParameter('pageId') as string);
const { parent: { database_id: databaseId } } = await notionApiRequest.call(this, 'GET', `/pages/${pageId}`);
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
for (const key of Object.keys(properties)) {
//remove parameters that cannot be set from the API.
if (!['created_time', 'last_edited_time', 'created_by', 'last_edited_by', 'formula', 'files', 'rollup'].includes(properties[key].type)) {
returnData.push({
name: `${key}`,
value: `${key}|${properties[key].type}`,
});
}
}
returnData.sort((a, b) => {
if (a.name.toLocaleLowerCase() < b.name.toLocaleLowerCase()) { return -1; }
if (a.name.toLocaleLowerCase() > b.name.toLocaleLowerCase()) { return 1; }
return 0;
});
return returnData;
},
async getDatabaseOptionsFromPage(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
const pageId = extractPageId(this.getCurrentNodeParameter('pageId') as string);
const [name, type] = (this.getCurrentNodeParameter('&key') as string).split('|');
const { parent: { database_id: databaseId } } = await notionApiRequest.call(this, 'GET', `/pages/${pageId}`);
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
return (properties[name][type].options).map((option: IDataObject) => ({ name: option.name, value: option.name }));
},
// Get all the timezones to display them to user so that he can
// select them easily
async getTimezones(
this: ILoadOptionsFunctions,
): Promise<INodePropertyOptions[]> {
const returnData: INodePropertyOptions[] = [];
for (const timezone of moment.tz.names()) {
const timezoneName = timezone;
const timezoneId = timezone;
returnData.push({
name: timezoneName,
value: timezoneId,
});
}
returnData.unshift({
name: 'Default',
value: 'default',
description: 'Timezone set in n8n',
});
return returnData;
},
},
credentialTest: {
async notionApiCredentialTest(this: ICredentialTestFunctions, credential: ICredentialsDecrypted): Promise<NodeCredentialTestResult> {
try {
await validateCrendetials.call(this, credential.data as ICredentialDataDecryptedObject);
} catch (error) {
return {
status: 'Error',
message: 'The security token included in the request is invalid',
};
}
return {
status: 'OK',
message: 'Connection successful!',
};
},
},
};
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
const items = this.getInputData();
const returnData: IDataObject[] = [];
const length = items.length as unknown as number;
let responseData;
const qs: IDataObject = {};
const timezone = this.getTimezone();
const resource = this.getNodeParameter('resource', 0) as string;
const operation = this.getNodeParameter('operation', 0) as string;
let download = false;
if (resource === 'block') {
if (operation === 'append') {
for (let i = 0; i < length; i++) {
const blockId = extractPageId(this.getNodeParameter('blockId', i) as string);
const body: IDataObject = {
children: formatBlocks(this.getNodeParameter('blockUi.blockValues', i, []) as IDataObject[]),
};
const block = await notionApiRequest.call(this, 'PATCH', `/blocks/${blockId}/children`, body);
returnData.push(block);
}
}
if (operation === 'getAll') {
for (let i = 0; i < length; i++) {
const blockId = extractPageId(this.getNodeParameter('blockId', i) as string);
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'GET', `/blocks/${blockId}/children`, {});
} else {
qs.page_size = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequest.call(this, 'GET', `/blocks/${blockId}/children`, {}, qs);
responseData = responseData.results;
}
returnData.push.apply(returnData, responseData);
}
}
}
if (resource === 'database') {
if (operation === 'get') {
const simple = this.getNodeParameter('simple', 0) as boolean;
for (let i = 0; i < length; i++) {
const databaseId = extractDatabaseId(this.getNodeParameter('databaseId', i) as string);
responseData = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
if (simple === true) {
responseData = simplifyObjects(responseData, download)[0];
}
returnData.push(responseData);
}
}
if (operation === 'getAll') {
const simple = this.getNodeParameter('simple', 0) as boolean;
for (let i = 0; i < length; i++) {
const body: IDataObject = {
filter: { property: 'object', value: 'database' },
};
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', `/search`, body);
} else {
body['page_size'] = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequest.call(this, 'POST', `/search`, body);
responseData = responseData.results;
}
if (simple === true) {
responseData = simplifyObjects(responseData, download);
}
returnData.push.apply(returnData, responseData);
}
}
if (operation === 'search') {
for (let i = 0; i < length; i++) {
const text = this.getNodeParameter('text', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const simple = this.getNodeParameter('simple', i) as boolean;
const body: IDataObject = {
filter: {
property: 'object',
value: 'database',
},
};
if (text) {
body['query'] = text;
}
if (options.sort) {
const sort = (options.sort as IDataObject || {}).sortValue as IDataObject || {};
body['sort'] = sort;
}
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', '/search', body);
} else {
qs.limit = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', '/search', body);
responseData = responseData.splice(0, qs.limit);
}
if (simple === true) {
responseData = simplifyObjects(responseData, download);
}
returnData.push.apply(returnData, responseData);
}
}
}
if (resource === 'databasePage') {
if (operation === 'create') {
const databaseId = this.getNodeParameter('databaseId', 0) as string;
const { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`);
let titleKey = '';
for (const key of Object.keys(properties)) {
if (properties[key].type === 'title') {
titleKey = key;
}
}
for (let i = 0; i < length; i++) {
const title = this.getNodeParameter('title', i) as string;
const simple = this.getNodeParameter('simple', i) as boolean;
// tslint:disable-next-line: no-any
const body: { [key: string]: any } = {
parent: {},
properties: {},
};
if (title !== '') {
body.properties[titleKey] = {
title: [
{
text: {
content: title,
},
},
],
};
}
body.parent['database_id'] = this.getNodeParameter('databaseId', i) as string;
const properties = this.getNodeParameter('propertiesUi.propertyValues', i, []) as IDataObject[];
if (properties.length !== 0) {
body.properties = Object.assign(body.properties, mapProperties(properties, timezone, 2) as IDataObject);
}
body.children = formatBlocks(this.getNodeParameter('blockUi.blockValues', i, []) as IDataObject[]);
responseData = await notionApiRequest.call(this, 'POST', '/pages', body);
if (simple === true) {
responseData = simplifyObjects(responseData);
}
returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]);
}
}
if (operation === 'get') {
for (let i = 0; i < length; i++) {
const pageId = extractPageId(this.getNodeParameter('pageId', i) as string);
const simple = this.getNodeParameter('simple', i) as boolean;
responseData = await notionApiRequest.call(this, 'GET', `/pages/${pageId}`);
if (simple === true) {
responseData = simplifyObjects(responseData, download);
}
returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]);
}
}
if (operation === 'getAll') {
for (let i = 0; i < length; i++) {
download = this.getNodeParameter('options.downloadFiles', 0, false) as boolean;
const simple = this.getNodeParameter('simple', 0) as boolean;
const databaseId = this.getNodeParameter('databaseId', i) as string;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const filterType = this.getNodeParameter('filterType', 0) as string;
const conditions = this.getNodeParameter('filters.conditions', i, []) as IDataObject[];
const sort = this.getNodeParameter('options.sort.sortValue', i, []) as IDataObject[];
const body: IDataObject = {
filter: {},
};
if (filterType === 'manual') {
const matchType = this.getNodeParameter('matchType', 0) as string;
if (matchType === 'anyFilter') {
Object.assign(body.filter, { or: conditions.map((data) => mapFilters([data], timezone)) });
} else if (matchType === 'allFilters') {
Object.assign(body.filter, { and: conditions.map((data) => mapFilters([data], timezone)) });
}
} else if (filterType === 'json') {
const filterJson = this.getNodeParameter('filterJson', i) as string;
if (validateJSON(filterJson) !== undefined) {
body.filter = JSON.parse(filterJson);
} else {
throw new NodeApiError(this.getNode(), { message: 'Filters (JSON) must be a valid json' });
}
}
if (!Object.keys(body.filter as IDataObject).length) {
delete body.filter;
}
if (sort) {
//@ts-expect-error
body['sorts'] = mapSorting(sort);
}
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', `/databases/${databaseId}/query`, body, {});
} else {
body.page_size = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequest.call(this, 'POST', `/databases/${databaseId}/query`, body, qs);
responseData = responseData.results;
}
if (download === true) {
responseData = await downloadFiles.call(this, responseData);
}
if (simple === true) {
responseData = simplifyObjects(responseData, download);
}
returnData.push.apply(returnData, responseData);
}
}
if (operation === 'update') {
for (let i = 0; i < length; i++) {
const pageId = extractPageId(this.getNodeParameter('pageId', i) as string);
const simple = this.getNodeParameter('simple', i) as boolean;
const properties = this.getNodeParameter('propertiesUi.propertyValues', i, []) as IDataObject[];
// tslint:disable-next-line: no-any
const body: { [key: string]: any } = {
properties: {},
};
if (properties.length !== 0) {
body.properties = mapProperties(properties, timezone, 2) as IDataObject;
}
responseData = await notionApiRequest.call(this, 'PATCH', `/pages/${pageId}`, body);
if (simple === true) {
responseData = simplifyObjects(responseData, false);
}
returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]);
}
}
}
if (resource === 'user') {
if (operation === 'get') {
for (let i = 0; i < length; i++) {
const userId = this.getNodeParameter('userId', i) as string;
responseData = await notionApiRequest.call(this, 'GET', `/users/${userId}`);
returnData.push(responseData);
}
}
if (operation === 'getAll') {
for (let i = 0; i < length; i++) {
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'GET', '/users');
} else {
qs.limit = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequestAllItems.call(this, 'results', 'GET', '/users');
responseData = responseData.splice(0, qs.limit);
}
returnData.push.apply(returnData, responseData);
}
}
}
if (resource === 'page') {
if (operation === 'archive') {
for (let i = 0; i < length; i++) {
const pageId = extractPageId(this.getNodeParameter('pageId', i) as string);
const simple = this.getNodeParameter('simple', i) as boolean;
responseData = await notionApiRequest.call(this, 'PATCH', `/pages/${pageId}`, { archived: true });
if (simple === true) {
responseData = simplifyObjects(responseData, download);
}
returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]);
}
}
if (operation === 'create') {
for (let i = 0; i < length; i++) {
const simple = this.getNodeParameter('simple', i) as boolean;
// tslint:disable-next-line: no-any
const body: { [key: string]: any } = {
parent: {},
properties: {},
};
body.parent['page_id'] = extractPageId(this.getNodeParameter('pageId', i) as string);
body.properties = formatTitle(this.getNodeParameter('title', i) as string);
body.children = formatBlocks(this.getNodeParameter('blockUi.blockValues', i, []) as IDataObject[]);
responseData = await notionApiRequest.call(this, 'POST', '/pages', body);
if (simple === true) {
responseData = simplifyObjects(responseData, download);
}
returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]);
}
}
if (operation === 'search') {
for (let i = 0; i < length; i++) {
const text = this.getNodeParameter('text', i) as string;
const options = this.getNodeParameter('options', i) as IDataObject;
const returnAll = this.getNodeParameter('returnAll', i) as boolean;
const simple = this.getNodeParameter('simple', i) as boolean;
const body: IDataObject = {};
if (text) {
body['query'] = text;
}
if (options.filter) {
const filter = (options.filter as IDataObject || {}).filters as IDataObject[] || [];
body['filter'] = filter;
}
if (options.sort) {
const sort = (options.sort as IDataObject || {}).sortValue as IDataObject || {};
body['sort'] = sort;
}
if (returnAll) {
responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', '/search', body);
} else {
qs.limit = this.getNodeParameter('limit', i) as number;
responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', '/search', body);
responseData = responseData.splice(0, qs.limit);
}
if (simple === true) {
responseData = simplifyObjects(responseData, download);
}
returnData.push.apply(returnData, responseData);
}
}
}
if (download === true) {
return this.prepareOutputData(returnData as INodeExecutionData[]);
}
return [this.helpers.returnJsonArray(returnData)];
}
}

View File

@ -0,0 +1,138 @@
import {
databaseFields,
databaseOperations,
} from '../DatabaseDescription';
import {
userFields,
userOperations,
} from '../UserDescription';
import {
pageFields,
pageOperations,
} from '../PageDescription';
import {
blockFields,
blockOperations,
} from '../BlockDescription';
import {
databasePageFields,
databasePageOperations,
} from '../DatabasePageDescription';
import {
INodeTypeDescription,
} from 'n8n-workflow';
export const versionDescription: INodeTypeDescription = {
displayName: 'Notion (Beta)',
name: 'notion',
icon: 'file:notion.svg',
group: ['output'],
version: 2,
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
description: 'Consume Notion API (Beta)',
defaults: {
name: 'Notion',
color: '#000000',
},
inputs: ['main'],
outputs: ['main'],
credentials: [
{
name: 'notionApi',
required: true,
testedBy: 'notionApiCredentialTest',
// displayOptions: {
// show: {
// authentication: [
// 'apiKey',
// ],
// },
// },
},
// {
// name: 'notionOAuth2Api',
// required: true,
// displayOptions: {
// show: {
// authentication: [
// 'oAuth2',
// ],
// },
// },
// },
],
properties: [
// {
// displayName: 'Authentication',
// name: 'authentication',
// type: 'options',
// options: [
// {
// name: 'API Key',
// value: 'apiKey',
// },
// {
// name: 'OAuth2',
// value: 'oAuth2',
// },
// ],
// default: 'apiKey',
// description: 'The resource to operate on.',
// },
{
displayName: 'To access content, make sure it\'s shared with your integration in Notion',
name: 'notionNotice',
type: 'notice',
default: '',
},
{
displayName: 'Version',
name: 'version',
type: 'hidden',
default: 2,
},
{
displayName: 'Resource',
name: 'resource',
type: 'options',
options: [
{
name: 'Block',
value: 'block',
},
{
name: 'Database',
value: 'database',
},
{
name: 'Database Page',
value: 'databasePage',
},
{
name: 'Page',
value: 'page',
},
{
name: 'User',
value: 'user',
},
],
default: 'page',
},
...blockOperations,
...blockFields,
...databaseOperations,
...databaseFields,
...databasePageOperations,
...databasePageFields,
...pageOperations,
...pageFields,
...userOperations,
...userFields,
],
};