diff --git a/packages/cli/src/Server.ts b/packages/cli/src/Server.ts index 549055fee6ed9..0861eab3b06d2 100644 --- a/packages/cli/src/Server.ts +++ b/packages/cli/src/Server.ts @@ -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! ]; } diff --git a/packages/nodes-base/nodes/Notion/BlockDescription.ts b/packages/nodes-base/nodes/Notion/BlockDescription.ts index 75c04ad3ec9c9..5d1c15ec52b87 100644 --- a/packages/nodes-base/nodes/Notion/BlockDescription.ts +++ b/packages/nodes-base/nodes/Notion/BlockDescription.ts @@ -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[]; diff --git a/packages/nodes-base/nodes/Notion/Blocks.ts b/packages/nodes-base/nodes/Notion/Blocks.ts index 759a4a62613ee..e9f365485860e 100644 --- a/packages/nodes-base/nodes/Notion/Blocks.ts +++ b/packages/nodes-base/nodes/Notion/Blocks.ts @@ -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[] = ], }, ]; - diff --git a/packages/nodes-base/nodes/Notion/DatabaseDescription.ts b/packages/nodes-base/nodes/Notion/DatabaseDescription.ts index 58dd1b94c88cd..10c6fdb16b619 100644 --- a/packages/nodes-base/nodes/Notion/DatabaseDescription.ts +++ b/packages/nodes-base/nodes/Notion/DatabaseDescription.ts @@ -2,13 +2,16 @@ import { INodeProperties, } from 'n8n-workflow'; -export const databaseOperations: INodeProperties[] = [ +export const databaseOperations = [ { displayName: 'Operation', name: 'operation', type: 'options', displayOptions: { show: { + version: [ + 2, + ], resource: [ 'database', ], @@ -25,19 +28,51 @@ export const databaseOperations: INodeProperties[] = [ value: 'getAll', description: 'Get all databases', }, + { + name: 'Search', + value: 'search', + description: 'search databases using text search', + }, ], default: 'get', - description: 'The operation to perform.', }, -]; + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + version: [ + 1, + ], + resource: [ + 'database', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a database', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all databases', + }, + ], + default: 'get', + }, +] 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[]; diff --git a/packages/nodes-base/nodes/Notion/DatabasePageDescription.ts b/packages/nodes-base/nodes/Notion/DatabasePageDescription.ts index eaf861ccaedd3..c300bcf635e50 100644 --- a/packages/nodes-base/nodes/Notion/DatabasePageDescription.ts +++ b/packages/nodes-base/nodes/Notion/DatabasePageDescription.ts @@ -2,6 +2,11 @@ import { INodeProperties, } from 'n8n-workflow'; +import { + getConditions, + getSearchFilters, +} from './GenericFunctions'; + import { blocks, text, @@ -11,13 +16,16 @@ import { filters, } from './Filters'; -export const databasePageOperations: INodeProperties[] = [ +export const databasePageOperations = [ { displayName: 'Operation', name: 'operation', type: 'options', displayOptions: { show: { + version: [ + 2, + ], resource: [ 'databasePage', ], @@ -29,6 +37,11 @@ export const databasePageOperations: INodeProperties[] = [ 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', @@ -41,17 +54,49 @@ export const databasePageOperations: INodeProperties[] = [ }, ], default: 'create', - description: 'The operation to perform.', }, -]; + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + version: [ + 1, + ], + resource: [ + 'databasePage', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a pages 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', + }, +] 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[]; diff --git a/packages/nodes-base/nodes/Notion/Filters.ts b/packages/nodes-base/nodes/Notion/Filters.ts index 4d55760d4793d..f0d8e20b9e03c 100644 --- a/packages/nodes-base/nodes/Notion/Filters.ts +++ b/packages/nodes-base/nodes/Notion/Filters.ts @@ -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. true represents checked. false represents unchecked.', + description: 'Whether or not the checkbox is checked. true represents checked. false 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. true represents checked. false 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', +}, +]; \ No newline at end of file diff --git a/packages/nodes-base/nodes/Notion/GenericFunctions.ts b/packages/nodes-base/nodes/Notion/GenericFunctions.ts index 77f60e65902f2..0f6709d8a8fa7 100644 --- a/packages/nodes-base/nodes/Notion/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Notion/GenericFunctions.ts @@ -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 { // 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 { + 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 Notion guide 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; +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Notion/Notion.node.ts b/packages/nodes-base/nodes/Notion/Notion.node.ts index 602cb83035ffe..1c8970c0819bb 100644 --- a/packages/nodes-base/nodes/Notion/Notion.node.ts +++ b/packages/nodes-base/nodes/Notion/Notion.node.ts @@ -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'; - -import { - pageFields, - pageOperations, -} from './PageDescription'; - -import { - blockFields, - blockOperations, -} from './BlockDescription'; - -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 { - 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 { - 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 { - 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 { - return getBlockTypes(); - }, - async getPropertySelectValues(this: ILoadOptionsFunctions): Promise { - 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 { - 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 { - 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 { - 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 { - 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 { - 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; - } + NodeVersionedType, +} from '../../src/NodeVersionedType'; - 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); - } +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, + }; - if (simple === true) { - responseData = simplifyObjects(responseData); - } + const nodeVersions: INodeVersionedType['nodeVersions'] = { + 1: new NotionV1(baseDescription), + 2: new NotionV2(baseDescription), + }; - returnData.push.apply(returnData, responseData); - } - } - } - return [this.helpers.returnJsonArray(returnData)]; + super(nodeVersions, baseDescription); } -} +} \ No newline at end of file diff --git a/packages/nodes-base/nodes/Notion/NotionTrigger.node.ts b/packages/nodes-base/nodes/Notion/NotionTrigger.node.ts index 236ab0a9ca61b..ecaa6e3de7d91 100644 --- a/packages/nodes-base/nodes/Notion/NotionTrigger.node.ts +++ b/packages/nodes-base/nodes/Notion/NotionTrigger.node.ts @@ -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; diff --git a/packages/nodes-base/nodes/Notion/PageDescription.ts b/packages/nodes-base/nodes/Notion/PageDescription.ts index c75277a6e9be2..1898e57150ed8 100644 --- a/packages/nodes-base/nodes/Notion/PageDescription.ts +++ b/packages/nodes-base/nodes/Notion/PageDescription.ts @@ -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[]; diff --git a/packages/nodes-base/nodes/Notion/UserDescription.ts b/packages/nodes-base/nodes/Notion/UserDescription.ts index b5fc5c012f32a..2e582b29ea180 100644 --- a/packages/nodes-base/nodes/Notion/UserDescription.ts +++ b/packages/nodes-base/nodes/Notion/UserDescription.ts @@ -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[]; diff --git a/packages/nodes-base/nodes/Notion/v1/NotionV1.node.ts b/packages/nodes-base/nodes/Notion/v1/NotionV1.node.ts new file mode 100644 index 0000000000000..89d78682c3e52 --- /dev/null +++ b/packages/nodes-base/nodes/Notion/v1/NotionV1.node.ts @@ -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 { + 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 { + 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 { + 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 { + return getBlockTypes(); + }, + async getPropertySelectValues(this: ILoadOptionsFunctions): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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)]; + } +} diff --git a/packages/nodes-base/nodes/Notion/v1/VersionDescription.ts b/packages/nodes-base/nodes/Notion/v1/VersionDescription.ts new file mode 100644 index 0000000000000..68dee6e8939f4 --- /dev/null +++ b/packages/nodes-base/nodes/Notion/v1/VersionDescription.ts @@ -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, + ], + }; diff --git a/packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts b/packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts new file mode 100644 index 0000000000000..c609f7669ba76 --- /dev/null +++ b/packages/nodes-base/nodes/Notion/v2/NotionV2.node.ts @@ -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 { + 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 { + 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 { + 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 { + return getBlockTypes(); + }, + async getPropertySelectValues(this: ILoadOptionsFunctions): Promise { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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)]; + } +} diff --git a/packages/nodes-base/nodes/Notion/v2/VersionDescription.ts b/packages/nodes-base/nodes/Notion/v2/VersionDescription.ts new file mode 100644 index 0000000000000..11718f69632d4 --- /dev/null +++ b/packages/nodes-base/nodes/Notion/v2/VersionDescription.ts @@ -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, + ], +};