diff --git a/packages/nodes-base/credentials/NotionApi.credentials.ts b/packages/nodes-base/credentials/NotionApi.credentials.ts new file mode 100644 index 0000000000..b90fe7f364 --- /dev/null +++ b/packages/nodes-base/credentials/NotionApi.credentials.ts @@ -0,0 +1,18 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class NotionApi implements ICredentialType { + name = 'notionApi'; + displayName = 'Notion API'; + documentationUrl = 'notion'; + properties = [ + { + displayName: 'API Key', + name: 'apiKey', + type: 'string' as NodePropertyTypes, + default: '', + }, + ]; +} diff --git a/packages/nodes-base/credentials/NotionOAuth2Api.credentials.ts b/packages/nodes-base/credentials/NotionOAuth2Api.credentials.ts new file mode 100644 index 0000000000..d89c92a170 --- /dev/null +++ b/packages/nodes-base/credentials/NotionOAuth2Api.credentials.ts @@ -0,0 +1,47 @@ +import { + ICredentialType, + NodePropertyTypes, +} from 'n8n-workflow'; + +export class NotionOAuth2Api implements ICredentialType { + name = 'notionOAuth2Api'; + extends = [ + 'oAuth2Api', + ]; + displayName = 'Notion OAuth2 API'; + documentationUrl = 'notion'; + properties = [ + { + displayName: 'Authorization URL', + name: 'authUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.notion.com/v1/oauth/authorize', + required: true, + }, + { + displayName: 'Access Token URL', + name: 'accessTokenUrl', + type: 'hidden' as NodePropertyTypes, + default: 'https://api.notion.com/v1/oauth/token', + required: true, + }, + { + displayName: 'Scope', + name: 'scope', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Auth URI Query Parameters', + name: 'authQueryParameters', + type: 'hidden' as NodePropertyTypes, + default: '', + }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'hidden' as NodePropertyTypes, + default: 'header', + }, + ]; +} diff --git a/packages/nodes-base/nodes/Jira/GenericFunctions.ts b/packages/nodes-base/nodes/Jira/GenericFunctions.ts index 8e384c1a76..88ec689ae5 100644 --- a/packages/nodes-base/nodes/Jira/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Jira/GenericFunctions.ts @@ -66,6 +66,8 @@ export async function jiraSoftwareCloudApiRequest(this: IHookFunctions | IExecut delete options.qs; } + console.log(options); + try { return await this.helpers.request!(options); } catch (error) { diff --git a/packages/nodes-base/nodes/Notion/BlockDescription.ts b/packages/nodes-base/nodes/Notion/BlockDescription.ts new file mode 100644 index 0000000000..a9d2b773ca --- /dev/null +++ b/packages/nodes-base/nodes/Notion/BlockDescription.ts @@ -0,0 +1,123 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + blocks, +} from './Blocks'; + +export const blockOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'block', + ], + }, + }, + options: [ + { + name: 'Append', + value: 'append', + description: 'Append a block', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all children blocks', + }, + ], + default: 'append', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const blockFields = [ + + /* -------------------------------------------------------------------------- */ + /* block:append */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Block ID', + name: 'blockId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'block', + ], + operation: [ + 'append', + ], + }, + }, + description: `The ID of block. A page it is also considered a block. Hence, a Page ID can be used as well.`, + }, + ...blocks('block', 'append'), + /* -------------------------------------------------------------------------- */ + /* block:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Block ID', + name: 'blockId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'block', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'block', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'block', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + 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 new file mode 100644 index 0000000000..7e5fe98c76 --- /dev/null +++ b/packages/nodes-base/nodes/Notion/Blocks.ts @@ -0,0 +1,571 @@ +import { + IDisplayOptions, + INodeProperties, +} from 'n8n-workflow'; + +const colors = [ + { + name: 'Default', + value: 'default', + }, + { + name: 'Gray', + value: 'gray', + }, + { + name: 'Brown', + value: 'brown', + }, + { + name: 'Orange', + value: 'orange', + }, + { + name: 'Yellow', + value: 'yellow', + }, + { + name: 'Green', + value: 'green', + }, + { + name: 'Blue', + value: 'blue', + }, + { + name: 'Purple', + value: 'purple', + }, + { + name: 'Pink', + value: 'pink', + }, + { + name: 'Red', + value: 'red', + }, + { + name: 'Gray Background', + value: 'gray_background', + }, + { + name: 'Brown Background', + value: 'brown_background', + }, + { + name: 'Orange Background', + value: 'orange_background', + }, + { + name: 'Yellow Background', + value: 'yellow_background', + }, + { + name: 'Green Background', + value: 'green_background', + }, + { + name: 'Blue Background', + value: 'blue_background', + }, + { + name: 'Purple Background', + value: 'purple_background', + }, + { + name: 'Pink Background', + value: 'pink_background', + }, + { + name: 'Red Background', + value: 'red_background', + }, +]; + +const annotation = [ + { + displayName: 'Annotations', + name: 'annotationUi', + type: 'collection', + placeholder: 'Add Annotation', + default: {}, + options: [ + { + displayName: 'Bold', + name: 'bold', + type: 'boolean', + default: false, + description: 'Whether the text is bolded.', + }, + { + displayName: 'Italic', + name: 'italic', + type: 'boolean', + default: false, + description: 'Whether the text is italicized.', + }, + { + displayName: 'Strikethrough', + name: 'strikethrough', + type: 'boolean', + default: false, + description: 'Whether the text is struck through.', + }, + { + displayName: 'Underline', + name: 'underline', + type: 'boolean', + default: false, + description: 'Whether the text is underlined.', + }, + { + displayName: 'Code', + name: 'code', + type: 'boolean', + default: false, + description: 'Whether the text is code style.', + }, + { + displayName: 'Color', + name: 'color', + type: 'options', + options: colors, + default: '', + description: 'Color of the text.', + }, + ], + description: 'All annotations that apply to this rich text.', + }, +] as INodeProperties[]; + +const typeMention = [ + { + displayName: 'Type', + name: 'mentionType', + type: 'options', + displayOptions: { + show: { + textType: [ + 'mention', + ], + }, + }, + options: [ + { + name: 'Database', + value: 'database', + }, + { + name: 'Date', + value: 'date', + }, + { + name: 'Page', + value: 'page', + }, + { + name: 'User', + value: 'user', + }, + ], + 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.`, + }, + { + displayName: 'User ID', + name: 'user', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + displayOptions: { + show: { + mentionType: [ + 'user', + ], + }, + }, + default: '', + description: 'The id of the user being mentioned.', + }, + { + displayName: 'Page ID', + name: 'page', + type: 'string', + displayOptions: { + show: { + mentionType: [ + 'page', + ], + }, + }, + default: '', + description: 'The id of the page being mentioned.', + }, + { + displayName: 'Database ID', + name: 'database', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDatabases', + }, + displayOptions: { + show: { + mentionType: [ + 'database', + ], + }, + }, + default: '', + description: 'The id of the database being mentioned.', + }, + { + displayName: 'Range', + name: 'range', + displayOptions: { + show: { + mentionType: [ + 'date', + ], + }, + }, + type: 'boolean', + default: false, + description: 'Weather or not you want to define a date range.', + }, + { + displayName: 'Date', + name: 'date', + displayOptions: { + show: { + mentionType: [ + 'date', + ], + range: [ + false, + ], + }, + }, + type: 'dateTime', + default: '', + description: 'An ISO 8601 format date, with optional time.', + }, + { + displayName: 'Date Start', + name: 'dateStart', + displayOptions: { + show: { + mentionType: [ + 'date', + ], + range: [ + true, + ], + }, + }, + type: 'dateTime', + default: '', + description: 'An ISO 8601 format date, with optional time.', + }, + { + displayName: 'Date End', + name: 'dateEnd', + displayOptions: { + show: { + range: [ + true, + ], + mentionType: [ + 'date', + ], + }, + }, + type: 'dateTime', + default: '', + description: `An ISO 8601 formatted date, with optional time. Represents the end of a date range.`, + }, +] as INodeProperties[]; + +const typeEquation = [ + { + displayName: 'Expression', + name: 'expression', + type: 'string', + displayOptions: { + show: { + textType: [ + 'equation', + ], + }, + }, + default: '', + description: '', + }, +] as INodeProperties[]; + +const typeText = [ + { + displayName: 'Text', + name: 'text', + displayOptions: { + show: { + textType: [ + 'text', + ], + }, + }, + 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.`, + }, + { + displayName: 'Is Link', + name: 'isLink', + displayOptions: { + show: { + textType: [ + 'text', + ], + }, + }, + type: 'boolean', + default: false, + }, + { + displayName: 'Text Link', + name: 'textLink', + displayOptions: { + show: { + textType: [ + 'text', + ], + isLink: [ + true, + ], + }, + }, + type: 'string', + default: '', + description: 'The URL that this link points to.', + }, +] as INodeProperties[]; + +export const text = (displayOptions: IDisplayOptions) => [ + { + displayName: 'Text', + name: 'text', + placeholder: 'Add Text', + type: 'fixedCollection', + default: '', + typeOptions: { + multipleValues: true, + }, + displayOptions, + options: [ + { + name: 'text', + displayName: 'Text', + values: [ + { + displayName: 'Type', + name: 'textType', + type: 'options', + options: [ + { + name: 'Equation', + value: 'equation', + }, + { + name: 'Mention', + value: 'mention', + }, + { + name: 'Text', + value: 'text', + }, + ], + default: 'text', + description: '', + }, + ...typeText, + ...typeMention, + ...typeEquation, + + ...annotation, + ], + }, + ], + description: 'Rich text in the block.', + }] as INodeProperties[]; + + +const todo = (type: string) => [{ + displayName: 'Checked', + name: 'checked', + type: 'boolean', + default: false, + displayOptions: { + show: { + type: [ + type, + ], + }, + }, + description: 'Whether the to_do is checked or not.', +}] as INodeProperties[]; + +const title = (type: string) => [{ + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + displayOptions: { + show: { + type: [ + type, + ], + }, + }, + description: 'Plain text of page title.', +}] as INodeProperties[]; + +const richText = (displayOptions: IDisplayOptions) => [ + { + displayName: 'Rich Text', + name: 'richText', + type: 'boolean', + displayOptions, + default: false, + }, +] as INodeProperties[]; + +const textContent = (displayOptions: IDisplayOptions) => [ + { + displayName: 'Text', + name: 'textContent', + type: 'string', + displayOptions, + default: '', + }, +] as INodeProperties[]; + +const block = (blockType: string) => { + const data: INodeProperties[] = []; + switch (blockType) { + case 'to_do': + data.push(...todo(blockType)); + data.push(...richText({ + show: { + type: [ + blockType, + ], + }, + })); + data.push(...textContent({ + show: { + type: [ + blockType, + ], + richText: [ + false, + ], + }, + })); + data.push(...text({ + show: { + type: [ + blockType, + ], + richText: [ + true, + ], + }, + })); + break; + case 'child_page': + data.push(...title(blockType)); + break; + default: + data.push(...richText({ + show: { + type: [ + blockType, + ], + }, + })); + data.push(...textContent({ + show: { + type: [ + blockType, + ], + richText: [ + false, + ], + }, + })); + data.push(...text({ + show: { + type: [ + blockType, + ], + richText: [ + true, + ], + }, + })); + break; + } + return data; +}; + +export const blocks = (resource: string, operation: string) => [{ + displayName: 'Blocks', + name: 'blockUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: '', + displayOptions: { + show: { + resource: [ + resource, + ], + operation: [ + operation, + ], + }, + }, + placeholder: 'Add Block', + options: [ + { + name: 'blockValues', + displayName: 'Block', + values: [ + { + displayName: 'Type', + name: 'type', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getBlockTypes', + }, + description: 'Type of block', + default: 'paragraph', + }, + ...block('paragraph'), + ...block('heading_1'), + ...block('heading_2'), + ...block('heading_3'), + ...block('toggle'), + ...block('to_do'), + ...block('child_page'), + ...block('bulleted_list_item'), + ...block('numbered_list_item'), + ], + }, + ], +}, +] as INodeProperties[]; + diff --git a/packages/nodes-base/nodes/Notion/DatabaseDescription.ts b/packages/nodes-base/nodes/Notion/DatabaseDescription.ts new file mode 100644 index 0000000000..5213e581ae --- /dev/null +++ b/packages/nodes-base/nodes/Notion/DatabaseDescription.ts @@ -0,0 +1,100 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const databaseOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'database', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a database', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all databases', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const databaseFields = [ + + /* -------------------------------------------------------------------------- */ + /* database:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Database ID', + name: 'databaseId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'database', + ], + operation: [ + 'get', + ], + }, + }, + }, + /* -------------------------------------------------------------------------- */ + /* database:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'database', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'database', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Notion/DatabasePageDescription.ts b/packages/nodes-base/nodes/Notion/DatabasePageDescription.ts new file mode 100644 index 0000000000..5ef0485fb9 --- /dev/null +++ b/packages/nodes-base/nodes/Notion/DatabasePageDescription.ts @@ -0,0 +1,994 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + blocks, + text, +} from './Blocks'; + +import { + filters, +} from './Filters'; + +export const databasePageOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + 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', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const databasePageFields = [ + + /* -------------------------------------------------------------------------- */ + /* databasePage:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Database ID', + name: 'databaseId', + type: 'options', + default: '', + typeOptions: { + loadOptionsMethod: 'getDatabases', + }, + required: true, + displayOptions: { + show: { + resource: [ + 'databasePage', + ], + operation: [ + 'create', + ], + }, + }, + description: 'The ID of the database that this databasePage belongs to.', + }, + { + displayName: 'Simple', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'databasePage', + ], + operation: [ + 'create', + ], + }, + }, + default: true, + description: 'When set to true a simplify version of the response will be used else the raw data.', + }, + { + displayName: 'Properties', + name: 'propertiesUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + resource: [ + 'databasePage', + ], + operation: [ + 'create', + ], + }, + }, + default: '', + placeholder: 'Add Property', + options: [ + { + name: 'propertyValues', + displayName: 'Property', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDatabaseProperties', + loadOptionsDependsOn: [ + 'databaseId', + ], + }, + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'hidden', + default: '={{$parameter["&key"].split("|")[1]}}', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + displayOptions: { + show: { + type: [ + 'title', + ], + }, + }, + default: '', + }, + { + displayName: 'Rich Text', + name: 'richText', + type: 'boolean', + displayOptions: { + show: { + type: [ + 'rich_text', + ], + }, + }, + default: false, + }, + { + displayName: 'Text', + name: 'textContent', + type: 'string', + displayOptions: { + show: { + type: [ + 'rich_text', + ], + richText: [ + false, + ], + }, + }, + default: '', + }, + ...text({ + show: { + type: [ + 'rich_text', + ], + richText: [ + true, + ], + }, + }), + { + displayName: 'Phone Number', + name: 'phoneValue', + type: 'string', + displayOptions: { + show: { + type: [ + 'phone_number', + ], + }, + }, + default: '', + description: `Phone number. No structure is enforced.`, + }, + { + displayName: 'Options', + name: 'multiSelectValue', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getPropertySelectValues', + }, + displayOptions: { + show: { + type: [ + 'multi_select', + ], + }, + }, + default: [], + description: `Name of the options you want to set. + Multiples can be defined separated by comma.`, + }, + { + displayName: 'Option', + name: 'selectValue', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getPropertySelectValues', + }, + displayOptions: { + show: { + type: [ + 'select', + ], + }, + }, + default: '', + description: `Name of the option you want to set.`, + }, + { + displayName: 'Email', + name: 'emailValue', + type: 'string', + displayOptions: { + show: { + type: [ + 'email', + ], + }, + }, + default: '', + description: 'Email address.', + }, + { + displayName: 'URL', + name: 'urlValue', + type: 'string', + displayOptions: { + show: { + type: [ + 'url', + ], + }, + }, + default: '', + description: 'Web address.', + }, + { + displayName: 'User IDs', + name: 'peopleValue', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + displayOptions: { + show: { + type: [ + 'people', + ], + }, + }, + default: [], + description: 'List of users. Multiples can be defined separated by comma.', + }, + { + displayName: 'Relation IDs', + name: 'relationValue', + type: 'string', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + type: [ + 'relation', + ], + }, + }, + default: [], + description: 'List of databases that belong to another database. Multiples can be defined separated by comma.', + }, + { + displayName: 'Checked', + name: 'checkboxValue', + displayOptions: { + show: { + type: [ + 'checkbox', + ], + }, + }, + type: 'boolean', + default: false, + description: ` + Whether or not the checkbox is checked.
+ true represents checked.
+ false represents unchecked. + `, + }, + { + displayName: 'Number', + name: 'numberValue', + displayOptions: { + show: { + type: [ + 'number', + ], + }, + }, + type: 'number', + default: 0, + description: 'Number value.', + }, + { + displayName: 'Range', + name: 'range', + displayOptions: { + show: { + type: [ + 'date', + ], + }, + }, + type: 'boolean', + default: false, + description: 'Weather or not you want to define a date range.', + }, + { + displayName: 'Date', + name: 'date', + displayOptions: { + show: { + range: [ + false, + ], + type: [ + 'date', + ], + }, + }, + type: 'dateTime', + default: '', + description: 'An ISO 8601 format date, with optional time.', + }, + { + displayName: 'Date Start', + name: 'dateStart', + displayOptions: { + show: { + range: [ + true, + ], + type: [ + 'date', + ], + }, + }, + type: 'dateTime', + default: '', + description: 'An ISO 8601 format date, with optional time.', + }, + { + displayName: 'Date End', + name: 'dateEnd', + displayOptions: { + show: { + range: [ + true, + ], + type: [ + 'date', + ], + }, + }, + type: 'dateTime', + default: '', + description: ` + An ISO 8601 formatted date, with optional time. Represents the end of a date range.`, + }, + ], + }, + ], + }, + ...blocks('databasePage', 'create'), + /* -------------------------------------------------------------------------- */ + /* databasePage:update */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Page ID', + name: 'pageId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'databasePage', + ], + operation: [ + 'update', + ], + }, + }, + description: 'The ID of the databasePage to update.', + }, + { + displayName: 'Simple', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'databasePage', + ], + operation: [ + 'update', + ], + }, + }, + default: true, + description: 'When set to true a simplify version of the response will be used else the raw data.', + }, + { + displayName: 'Properties', + name: 'propertiesUi', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + resource: [ + 'databasePage', + ], + operation: [ + 'update', + ], + }, + }, + default: '', + placeholder: 'Add Property', + options: [ + { + name: 'propertyValues', + displayName: 'Property', + values: [ + { + displayName: 'Key', + name: 'key', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDatabaseIdFromPage', + loadOptionsDependsOn: [ + 'pageId', + ], + }, + default: '', + }, + { + displayName: 'Type', + name: 'type', + type: 'hidden', + default: '={{$parameter["&key"].split("|")[1]}}', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + displayOptions: { + show: { + type: [ + 'title', + ], + }, + }, + default: '', + }, + { + displayName: 'Rich Text', + name: 'richText', + type: 'boolean', + displayOptions: { + show: { + type: [ + 'rich_text', + ], + }, + }, + default: false, + }, + { + displayName: 'Text', + name: 'textContent', + type: 'string', + displayOptions: { + show: { + type: [ + 'rich_text', + ], + richText: [ + false, + ], + }, + }, + default: '', + }, + ...text({ + show: { + type: [ + 'rich_text', + ], + richText: [ + true, + ], + }, + }), + { + displayName: 'Phone Number', + name: 'phoneValue', + type: 'string', + displayOptions: { + show: { + type: [ + 'phone_number', + ], + }, + }, + default: '', + description: `Phone number. No structure is enforced.`, + }, + { + displayName: 'Options', + name: 'multiSelectValue', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getDatabaseOptionsFromPage', + }, + displayOptions: { + show: { + type: [ + 'multi_select', + ], + }, + }, + default: [], + description: `Name of the options you want to set. + Multiples can be defined separated by comma.`, + }, + { + displayName: 'Option', + name: 'selectValue', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDatabaseOptionsFromPage', + }, + displayOptions: { + show: { + type: [ + 'select', + ], + }, + }, + default: '', + description: `Name of the option you want to set.`, + }, + { + displayName: 'Email', + name: 'emailValue', + type: 'string', + displayOptions: { + show: { + type: [ + 'email', + ], + }, + }, + default: '', + description: 'Email address.', + }, + { + displayName: 'URL', + name: 'urlValue', + type: 'string', + displayOptions: { + show: { + type: [ + 'url', + ], + }, + }, + default: '', + description: 'Web address.', + }, + { + displayName: 'User IDs', + name: 'peopleValue', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + displayOptions: { + show: { + type: [ + 'people', + ], + }, + }, + default: [], + description: 'List of users. Multiples can be defined separated by comma.', + }, + { + displayName: 'Relation IDs', + name: 'relationValue', + type: 'string', + typeOptions: { + multipleValues: true, + }, + displayOptions: { + show: { + type: [ + 'relation', + ], + }, + }, + default: [], + description: 'List of databases that belong to another database. Multiples can be defined separated by comma.', + }, + { + displayName: 'Checked', + name: 'checkboxValue', + displayOptions: { + show: { + type: [ + 'checkbox', + ], + }, + }, + type: 'boolean', + default: false, + description: ` + Whether or not the checkbox is checked.
+ true represents checked.
+ false represents unchecked. + `, + }, + { + displayName: 'Number', + name: 'numberValue', + displayOptions: { + show: { + type: [ + 'number', + ], + }, + }, + type: 'number', + default: 0, + description: 'Number value.', + }, + { + displayName: 'Range', + name: 'range', + displayOptions: { + show: { + type: [ + 'date', + ], + }, + }, + type: 'boolean', + default: false, + description: 'Weather or not you want to define a date range.', + }, + { + displayName: 'Date', + name: 'date', + displayOptions: { + show: { + range: [ + false, + ], + type: [ + 'date', + ], + }, + }, + type: 'dateTime', + default: '', + description: 'An ISO 8601 format date, with optional time.', + }, + { + displayName: 'Date Start', + name: 'dateStart', + displayOptions: { + show: { + range: [ + true, + ], + type: [ + 'date', + ], + }, + }, + type: 'dateTime', + default: '', + description: 'An ISO 8601 format date, with optional time.', + }, + { + displayName: 'Date End', + name: 'dateEnd', + displayOptions: { + show: { + range: [ + true, + ], + type: [ + 'date', + ], + }, + }, + type: 'dateTime', + default: '', + description: ` + An ISO 8601 formatted date, with optional time. Represents the end of a date range.`, + }, + ], + }, + ], + }, + /* -------------------------------------------------------------------------- */ + /* databasePage:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Database ID', + name: 'databaseId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDatabases', + }, + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'databasePage', + ], + operation: [ + 'getAll', + ], + }, + }, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'databasePage', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'databasePage', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Simple', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'databasePage', + ], + operation: [ + 'getAll', + ], + }, + }, + default: true, + description: 'When set to true a simplify version of the response will be used else the raw data.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + operation: [ + 'getAll', + ], + resource: [ + 'databasePage', + ], + }, + }, + default: {}, + placeholder: 'Add Field', + options: [ + { + displayName: 'Filters', + name: 'filter', + placeholder: 'Add Filter', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + displayName: 'Single Condition', + name: 'singleCondition', + values: [ + ...filters, + ], + }, + { + displayName: 'Multiple Condition', + name: 'multipleCondition', + values: [ + { + displayName: 'Condition', + name: 'condition', + placeholder: 'Add Condition', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'OR', + name: 'or', + values: [ + ...filters, + ], + }, + { + displayName: 'AND', + name: 'and', + values: [ + ...filters, + ], + }, + ], + }, + ], + }, + ], + }, + { + displayName: 'Sort', + name: 'sort', + placeholder: 'Add Sort', + type: 'fixedCollection', + typeOptions: { + multipleValues: true, + }, + default: {}, + options: [ + { + displayName: 'Sort', + name: 'sortValue', + values: [ + { + displayName: 'Timestamp', + name: 'timestamp', + type: 'boolean', + default: false, + description: `Whether or not to use the record's timestamp to sort the response.`, + }, + { + displayName: 'Property Name', + name: 'key', + type: 'options', + displayOptions: { + show: { + timestamp: [ + false, + ], + }, + }, + typeOptions: { + loadOptionsMethod: 'getFilterProperties', + loadOptionsDependsOn: [ + 'datatabaseId', + ], + }, + default: '', + description: 'The name of the property to filter by.', + }, + { + displayName: 'Property Name', + name: 'key', + type: 'options', + options: [ + { + name: 'Created Time', + value: 'created_time', + }, + { + name: 'Last Edited Time', + value: 'last_edited_time', + }, + ], + displayOptions: { + show: { + timestamp: [ + true, + ], + }, + }, + default: '', + description: 'The name of the property to filter by.', + }, + { + displayName: 'Type', + name: 'type', + type: 'hidden', + displayOptions: { + show: { + timestamp: [ + true, + ], + }, + }, + default: '={{$parameter["&key"].split("|")[1]}}', + }, + { + displayName: 'Direction', + name: 'direction', + type: 'options', + options: [ + { + name: 'Ascending', + value: 'ascending', + }, + { + name: 'Descending', + value: 'descending', + }, + ], + default: '', + description: 'The direction to sort.', + }, + ], + }, + ], + }, + ], + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Notion/Filters.ts b/packages/nodes-base/nodes/Notion/Filters.ts new file mode 100644 index 0000000000..dcf159afff --- /dev/null +++ b/packages/nodes-base/nodes/Notion/Filters.ts @@ -0,0 +1,371 @@ +import { + getConditions +} from './GenericFunctions'; + +export const filters = [{ + displayName: 'Property Name', + name: 'key', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getFilterProperties', + loadOptionsDependsOn: [ + 'datatabaseId', + ], + }, + default: '', + description: 'The name of the property to filter by.', +}, +{ + displayName: 'Type', + name: 'type', + type: 'hidden', + default: '={{$parameter["&key"].split("|")[1]}}', +}, +...getConditions(), +{ + displayName: 'Title', + name: 'titleValue', + type: 'string', + displayOptions: { + show: { + type: [ + 'title', + ], + }, + hide: { + condition: [ + 'is_empty', + 'is_not_empty', + ], + }, + }, + default: '', +}, +{ + displayName: 'Text', + name: 'richTextValue', + type: 'string', + displayOptions: { + show: { + type: [ + 'rich_text', + ], + }, + hide: { + condition: [ + 'is_empty', + 'is_not_empty', + ], + }, + }, + default: '', +}, +{ + displayName: 'Phone Number', + name: 'phoneNumberValue', + type: 'string', + displayOptions: { + show: { + type: [ + 'phone_number', + ], + }, + hide: { + condition: [ + 'is_empty', + 'is_not_empty', + ], + }, + }, + default: '', + description: `Phone number. No structure is enforced.`, +}, +{ + displayName: 'Option', + name: 'multiSelectValue', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getPropertySelectValues', + }, + displayOptions: { + show: { + type: [ + 'multi_select', + ], + }, + hide: { + condition: [ + 'is_empty', + 'is_not_empty', + ], + }, + }, + default: [], + description: `Name of the options you want to set. + Multiples can be defined separated by comma.`, +}, +{ + displayName: 'Option', + name: 'selectValue', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getPropertySelectValues', + }, + displayOptions: { + show: { + type: [ + 'select', + ], + }, + hide: { + condition: [ + 'is_empty', + 'is_not_empty', + ], + }, + }, + default: '', + description: `Name of the option you want to set.`, +}, +{ + displayName: 'Email', + name: 'emailValue', + type: 'string', + displayOptions: { + show: { + type: [ + 'email', + ], + }, + hide: { + condition: [ + 'is_empty', + 'is_not_empty', + ], + }, + }, + default: '', + description: 'Email address.', +}, +{ + displayName: 'URL', + name: 'urlValue', + type: 'string', + displayOptions: { + show: { + type: [ + 'url', + ], + }, + hide: { + condition: [ + 'is_empty', + 'is_not_empty', + ], + }, + }, + default: '', + description: 'Web address.', +}, +{ + displayName: 'User ID', + name: 'peopleValue', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + displayOptions: { + show: { + type: [ + 'people', + ], + }, + hide: { + condition: [ + 'is_empty', + 'is_not_empty', + ], + }, + }, + default: '', + description: 'List of users. Multiples can be defined separated by comma.', +}, +{ + displayName: 'User ID', + name: 'createdByValue', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + displayOptions: { + show: { + type: [ + 'created_by', + ], + }, + hide: { + condition: [ + 'is_empty', + 'is_not_empty', + ], + }, + }, + default: '', + description: 'List of users. Multiples can be defined separated by comma.', +}, +{ + displayName: 'User ID', + name: 'lastEditedByValue', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getUsers', + }, + displayOptions: { + show: { + type: [ + 'last_edited_by', + ], + }, + hide: { + condition: [ + 'is_empty', + 'is_not_empty', + ], + }, + }, + default: '', + description: 'List of users. Multiples can be defined separated by comma.', +}, +{ + displayName: 'Relation ID', + name: 'relationValue', + type: 'string', + displayOptions: { + show: { + type: [ + 'relation', + ], + }, + hide: { + condition: [ + 'is_empty', + 'is_not_empty', + ], + }, + }, + default: '', +}, +{ + displayName: 'Checked', + name: 'checkboxValue', + displayOptions: { + show: { + type: [ + 'checkbox', + ], + }, + }, + type: 'boolean', + default: false, + description: `Whether or not the checkbox is checked.
+ true represents checked.
+ false represents unchecked.`, +}, +{ + displayName: 'Number', + name: 'numberValue', + displayOptions: { + show: { + type: [ + 'number', + ], + }, + hide: { + condition: [ + 'is_empty', + 'is_not_empty', + ], + }, + }, + type: 'number', + default: 0, + description: 'Number value.', +}, +{ + displayName: 'Date', + name: 'date', + displayOptions: { + show: { + type: [ + '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.', +}, +{ + displayName: 'Created Time', + name: 'createdTimeValue', + displayOptions: { + show: { + type: [ + 'created_time', + ], + }, + 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.', +}, +{ + displayName: 'Last Edited Time', + name: 'lastEditedTime', + displayOptions: { + show: { + type: [ + 'last_edited_time', + ], + }, + 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.', +}]; diff --git a/packages/nodes-base/nodes/Notion/GenericFunctions.ts b/packages/nodes-base/nodes/Notion/GenericFunctions.ts new file mode 100644 index 0000000000..df54434d28 --- /dev/null +++ b/packages/nodes-base/nodes/Notion/GenericFunctions.ts @@ -0,0 +1,542 @@ +import { + OptionsWithUri, +} from 'request'; + +import { + IExecuteFunctions, + IExecuteSingleFunctions, + IHookFunctions, + ILoadOptionsFunctions, +} from 'n8n-core'; + +import { + IDataObject, + IDisplayOptions, + INodeProperties, + IPollFunctions, + NodeApiError, +} from 'n8n-workflow'; + +import { + camelCase, + capitalCase, +} from 'change-case'; + +import * as moment from 'moment-timezone'; + +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', + }, + method, + qs, + body, + uri: uri || `https://api.notion.com/v1${resource}`, + json: true, + }; + + options = Object.assign({}, options, option); + const credentials = this.getCredentials('notionApi') as IDataObject; + options!.headers!['Authorization'] = `Bearer ${credentials.apiKey}`; + return this.helpers.request!(options); + + } catch (error) { + throw new NodeApiError(this.getNode(), error); + } +} + +export async function notionApiRequestAllItems(this: IExecuteFunctions | ILoadOptionsFunctions | IPollFunctions, propertyName: string, method: string, endpoint: string, body: any = {}, query: IDataObject = {}): Promise { // tslint:disable-line:no-any + + const returnData: IDataObject[] = []; + + let responseData; + + do { + responseData = await notionApiRequest.call(this, method, endpoint, body, query); + const { next_cursor } = responseData; + query['start_cursor'] = next_cursor; + body['start_cursor'] = next_cursor; + returnData.push.apply(returnData, responseData[propertyName]); + if (query.limit && query.limit <= returnData.length) { + return returnData; + } + } while ( + responseData.has_more !== false + ); + + return returnData; +} + +export function getBlockTypes() { + return [ + { + name: 'Paragraph', + value: 'paragraph', + }, + { + name: 'Heading 1', + value: 'heading_1', + }, + { + name: 'Heading 2', + value: 'heading_2', + }, + { + name: 'Heading 3', + value: 'heading_3', + }, + { + name: 'Toggle', + value: 'toggle', + }, + { + name: 'To-Do', + value: 'to_do', + }, + // { + // name: 'Child Page', + // value: 'child_page', + // }, + { + name: 'Bulleted List Item', + value: 'bulleted_list_item', + }, + { + name: 'Numbered List Item', + value: 'numbered_list_item', + }, + ]; +} + +function textContent(content: string) { + return { + text: { + content, + }, + }; +} + +export function formatTitle(content: string) { + return { + title: [ + textContent(content), + ], + }; +} + +export function formatText(content: string) { + return { + text: [ + textContent(content), + ], + }; +} + +function getLink(text: { textLink: string, isLink: boolean }) { + if (text.isLink === true) { + return { + link: { + url: text.textLink, + }, + }; + } + return {}; +} + +function getTexts(texts: [{ textType: string, text: string, isLink: boolean, range: boolean, textLink: string, mentionType: string, dateStart: string, dateEnd: string, date: string, annotationUi: IDataObject, expression: string }]) { + const results = []; + for (const text of texts) { + if (text.textType === 'text') { + results.push({ + type: 'text', + text: { + content: text.text, + ...getLink(text), + }, + annotations: text.annotationUi, + }); + } else if (text.textType === 'mention') { + if (text.mentionType === 'date') { + results.push({ + type: 'mention', + mention: { + type: text.mentionType, + [text.mentionType]: (text.range === true) + ? { start: text.dateStart, end: text.dateEnd } + : { start: text.date, end: null }, + }, + annotations: text.annotationUi, + }); + } else { + //@ts-ignore + results.push({ + type: 'mention', + mention: { + type: text.mentionType, + //@ts-ignore + [text.mentionType]: { id: text[text.mentionType] as string }, + }, + annotations: text.annotationUi, + }); + } + } else if (text.textType === 'equation') { + results.push({ + type: 'equation', + equation: { + expression: text.expression, + }, + annotations: text.annotationUi, + }); + } + } + return results; +} + +export function formatBlocks(blocks: IDataObject[]) { + const results = []; + for (const block of blocks) { + results.push({ + object: 'block', + type: block.type, + [block.type as string]: { + ...(block.type === 'to_do') ? { checked: block.checked } : { checked: false }, + //@ts-expect-error + // tslint:disable-next-line: no-any + text: (block.richText === false) ? formatText(block.textContent).text : getTexts(block.text.text as any || []), + }, + }); + } + return results; +} + +// tslint:disable-next-line: no-any +function getPropertyKeyValue(value: any, type: string, timezone: string) { + let result = {}; + switch (type) { + case 'rich_text': + if (value.richText === false) { + result = { rich_text: [{ text: { content: value.textContent } }] }; + } else { + result = { rich_text: getTexts(value.text.text) }; + } + break; + case 'title': + result = { title: [{ text: { content: value.title } }] }; + break; + case 'number': + result = { type: 'number', number: value.numberValue }; + break; + case 'url': + result = { type: 'url', url: value.urlValue }; + break; + case 'checkbox': + result = { type: 'checkbox', checkbox: value.checkboxValue }; + break; + case 'relation': + result = { + // tslint:disable-next-line: no-any + type: 'relation', relation: (value.relationValue).reduce((acc: [], cur: any) => { + return acc.concat(cur.split(',').map((relation: string) => ({ id: relation }))); + }, []), + }; + break; + case 'multi_select': + result = { + // tslint:disable-next-line: no-any + type: 'multi_select', multi_select: value.multiSelectValue.filter((id: any) => id !== null).map((option: string) => ({ id: option })), + }; + break; + case 'email': + result = { + type: 'email', email: value.emailValue, + }; + break; + case 'people': + result = { + type: 'people', people: value.peopleValue.map((option: string) => ({ id: option })), + }; + break; + case 'phone_number': + result = { + type: 'phone_number', phone_number: value.phoneValue, + }; + break; + case 'select': + result = { + type: 'select', select: { id: value.selectValue }, + }; + break; + case 'date': + if (value.range === true) { + result = { + type: 'date', date: { start: moment.tz(value.dateStart, timezone).utc().format(), end: moment.tz(value.dateEnd, timezone).utc().format() }, + }; + } else { + result = { + type: 'date', date: { start: moment.tz(value.date, timezone).utc().format(), end: null }, + }; + } + break; + default: + } + return result; +} + +function getNameAndType(key: string) { + const [name, type] = key.split('|'); + return { + name, + type, + }; +} + +export function mapProperties(properties: IDataObject[], timezone: string) { + return properties.reduce((obj, value) => Object.assign(obj, { + [`${(value.key as string).split('|')[0]}`]: getPropertyKeyValue(value, (value.key as string).split('|')[1], timezone), + }), {}); +} + +export function mapSorting(data: [{ key: string, type: string, direction: string, timestamp: boolean }]) { + return data.map((sort) => { + return { + direction: sort.direction, + [(sort.timestamp) ? 'timestamp' : 'property']: sort.key.split('|')[0], + }; + }); +} + +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; + let valuePropertyName = value[`${camelCase(key)}Value`]; + if (['is_empty', 'is_not_empty'].includes(value.condition as string)) { + valuePropertyName = true; + } else if (['past_week', 'past_month', 'past_year', 'next_week', 'next_month', 'next_year'].includes(value.condition as string)) { + valuePropertyName = {}; + } + if (key === 'rich_text') { + key = 'text'; + } else if (key === 'phone_number') { + key = 'phone'; + } else if (key === 'date') { + valuePropertyName = (valuePropertyName !== undefined && !Object.keys(valuePropertyName).length) ? {} : moment.tz(value.date, timezone).utc().format(); + } + return Object.assign(obj, { + ['property']: getNameAndType(value.key).name, + [key]: { [`${value.condition}`]: valuePropertyName }, + }); + }, {}); +} + +// tslint:disable-next-line: no-any +export function simplifyProperties(properties: any) { + // tslint:disable-next-line: no-any + const results: any = {}; + for (const key of Object.keys(properties)) { + const type = (properties[key] as IDataObject).type as string; + if (['text'].includes(properties[key].type)) { + const texts = properties[key].text.map((e: { plain_text: string }) => e.plain_text || {}).join(''); + results[`${key}`] = texts; + } else if (['url', 'created_time', 'checkbox', 'number', 'last_edited_time', 'email', 'phone_number', 'date'].includes(properties[key].type)) { + // tslint:disable-next-line: no-any + results[`${key}`] = properties[key][type] as any; + } else if (['title'].includes(properties[key].type)) { + if (Array.isArray(properties[key][type]) && properties[key][type].length !== 0) { + results[`${key}`] = properties[key][type][0].plain_text; + } else { + results[`${key}`] = ''; + } + } else if (['created_by', 'last_edited_by', 'select'].includes(properties[key].type)) { + results[`${key}`] = properties[key][type].name; + } 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 || {}); + } else { + results[`${key}`] = properties[key][type]; + } + } else if (['multi_select'].includes(properties[key].type)) { + if (Array.isArray(properties[key][type])) { + results[`${key}`] = properties[key][type].map((e: IDataObject) => e.name || {}); + } else { + results[`${key}`] = properties[key][type].options.map((e: IDataObject) => e.name || {}); + } + } else if (['relation'].includes(properties[key].type)) { + if (Array.isArray(properties[key][type])) { + results[`${key}`] = properties[key][type].map((e: IDataObject) => e.id || {}); + } else { + results[`${key}`] = properties[key][type].database_id; + } + } else if (['formula'].includes(properties[key].type)) { + results[`${key}`] = properties[key][type][properties[key][type].type]; + + } else if (['rollup'].includes(properties[key].type)) { + //TODO figure how to resolve rollup field type + // results[`${key}`] = properties[key][type][properties[key][type].type]; + } + } + return results; +} + +// tslint:disable-next-line: no-any +export function simplifyObjects(objects: any) { + if (!Array.isArray(objects)) { + objects = [objects]; + } + const results: IDataObject[] = []; + for (const { object, id, properties, parent, title } of objects) { + if (object === 'page' && (parent.type === 'page_id' || parent.type === 'workspace')) { + results.push({ + id, + title: properties.title.title[0].plain_text, + }); + } else if (object === 'page' && parent.type === 'database_id') { + results.push({ + id, + ...simplifyProperties(properties), + }); + } else if (object === 'database') { + results.push({ + id, + title: title[0].plain_text, + }); + } + } + return results; +} + +export function getFormattedChildren(children: IDataObject[]) { + const results: IDataObject[] = []; + for (const child of children) { + const type = child.type; + results.push({ [`${type}`]: child, object: 'block', type }); + } + return results; +} + +export function getConditions() { + + const elements: INodeProperties[] = []; + + const types: { [key: string]: string } = { + title: 'rich_text', + rich_text: 'rich_text', + number: 'number', + checkbox: 'checkbox', + select: 'select', + multi_select: 'multi_select', + date: 'date', + people: 'people', + files: 'files', + url: 'rich_text', + email: 'rich_text', + phone_number: 'rich_text', + relation: 'relation', + //formula: 'formula', + created_by: 'people', + created_time: 'date', + last_edited_by: 'people', + last_edited_time: 'date', + }; + + const typeConditions: { [key: string]: string[] } = { + rich_text: [ + 'equals', + 'does_not_equal', + 'contains', + 'does_not_contain', + 'starts_with', + 'ends_with', + 'is_empty', + 'is_not_empty', + ], + number: [ + 'equals', + 'does_not_equal', + 'grater_than', + 'less_than', + 'greater_than_or_equal_to', + 'less_than_or_equal_to', + 'is_empty', + 'is_not_empty', + ], + checkbox: [ + 'equals', + 'does_not_equal', + ], + select: [ + 'equals', + 'does_not_equal', + 'is_empty', + 'is_not_empty', + ], + multi_select: [ + 'contains', + 'does_not_equal', + 'is_empty', + 'is_not_empty', + ], + date: [ + 'equals', + 'before', + 'after', + 'on_or_before', + 'is_empty', + 'is_not_empty', + 'on_or_after', + 'past_week', + 'past_month', + 'past_year', + 'next_week', + 'next_month', + 'next_year', + ], + people: [ + 'contains', + 'does_not_contain', + 'is_empty', + 'is_not_empty', + ], + files: [ + 'is_empty', + 'is_not_empty', + ], + relation: [ + 'contains', + 'does_not_contain', + 'is_empty', + 'is_not_empty', + ], + formula: [ + 'contains', + 'does_not_contain', + 'is_empty', + 'is_not_empty', + ], + }; + + for (const type of Object.keys(types)) { + elements.push( + { + displayName: 'Condition', + name: 'condition', + type: 'options', + displayOptions: { + show: { + type: [ + type, + ], + }, + } as IDisplayOptions, + options: (typeConditions[types[type]] as string[]).map((type: string) => ({ name: capitalCase(type), value: type })), + default: '', + description: 'The value of the property to filter by.', + } as INodeProperties, + ); + } + return elements; +} diff --git a/packages/nodes-base/nodes/Notion/Notion.node.ts b/packages/nodes-base/nodes/Notion/Notion.node.ts new file mode 100644 index 0000000000..78b3cf921a --- /dev/null +++ b/packages/nodes-base/nodes/Notion/Notion.node.ts @@ -0,0 +1,514 @@ +import { + IExecuteFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + formatBlocks, + formatTitle, + getBlockTypes, + mapFilters, + mapProperties, + mapSorting, + notionApiRequest, + notionApiRequestAllItems, + simplifyObjects, +} from './GenericFunctions'; + +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'; + +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', + 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: '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, + 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 { properties } = await notionApiRequest.call(this, 'GET', `/databases/${databaseId}`); + 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 })); + }, + }, + }; + + 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 = { + page_size: 100, + filter: { property: 'object', value: 'database' }, + }; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + if (returnAll) { + responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', `/search`, body); + } else { + body['page_size'] = this.getNodeParameter('limit', i) as number; + responseData = await notionApiRequest.call(this, 'POST', `/search`, body); + responseData = responseData.results; + } + returnData.push.apply(returnData, responseData); + } + } + } + + if (resource === 'databasePage') { + + if (operation === 'create') { + for (let i = 0; i < length; i++) { + const simple = this.getNodeParameter('simple', i) as boolean; + // tslint:disable-next-line: no-any + const body: { [key: string]: any } = { + parent: {}, + properties: {}, + }; + body.parent['database_id'] = this.getNodeParameter('databaseId', i) as string; + const properties = this.getNodeParameter('propertiesUi.propertyValues', i, []) as IDataObject[]; + if (properties.length !== 0) { + body.properties = mapProperties(properties, timezone) as IDataObject; + } + body.children = formatBlocks(this.getNodeParameter('blockUi.blockValues', i, []) as IDataObject[]); + responseData = await notionApiRequest.call(this, 'POST', '/pages', body); + if (simple === true) { + responseData = simplifyObjects(responseData); + } + returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]); + } + } + + if (operation === 'getAll') { + for (let i = 0; i < length; i++) { + const simple = this.getNodeParameter('simple', 0) as boolean; + const databaseId = this.getNodeParameter('databaseId', i) as string; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const filters = this.getNodeParameter('options.filter', i, {}) as IDataObject; + const sort = this.getNodeParameter('options.sort.sortValue', i, []) as IDataObject[]; + const body: IDataObject = { + filter: {}, + }; + if (filters.singleCondition) { + body['filter'] = mapFilters([filters.singleCondition] as IDataObject[], timezone); + } + if (filters.multipleCondition) { + const { or, and } = (filters.multipleCondition as IDataObject).condition as IDataObject; + if (Array.isArray(or) && or.length !== 0) { + Object.assign(body.filter, { or: (or as IDataObject[]).map((data) => mapFilters([data], timezone)) }); + } + if (Array.isArray(and) && and.length !== 0) { + Object.assign(body.filter, { and: (and as IDataObject[]).map((data) => mapFilters([data], timezone)) }); + } + } + if (!Object.keys(body.filter as IDataObject).length) { + delete body.filter; + } + if (sort) { + //@ts-expect-error + body['sorts'] = mapSorting(sort); + } + if (returnAll) { + responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', `/databases/${databaseId}/query`, body, {}); + } else { + body.page_size = this.getNodeParameter('limit', i) as number; + responseData = await notionApiRequest.call(this, 'POST', `/databases/${databaseId}/query`, body, qs); + responseData = responseData.results; + } + if (simple === true) { + responseData = simplifyObjects(responseData); + } + returnData.push.apply(returnData, responseData); + } + } + + if (operation === 'update') { + for (let i = 0; i < length; i++) { + const pageId = this.getNodeParameter('pageId', i) as string; + const simple = this.getNodeParameter('simple', i) as boolean; + const properties = this.getNodeParameter('propertiesUi.propertyValues', i, []) as IDataObject[]; + // tslint:disable-next-line: no-any + const body: { [key: string]: any } = { + properties: {}, + }; + if (properties.length !== 0) { + body.properties = mapProperties(properties, timezone) as IDataObject; + } + responseData = await notionApiRequest.call(this, 'PATCH', `/pages/${pageId}`, body); + if (simple === true) { + responseData = simplifyObjects(responseData); + } + returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]); + } + } + } + + if (resource === 'user') { + + if (operation === 'get') { + for (let i = 0; i < length; i++) { + const userId = this.getNodeParameter('userId', i) as string; + responseData = await notionApiRequest.call(this, 'GET', `/users/${userId}`); + returnData.push(responseData); + } + } + if (operation === 'getAll') { + for (let i = 0; i < length; i++) { + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + if (returnAll) { + responseData = await notionApiRequestAllItems.call(this, 'results', 'GET', '/users'); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await notionApiRequestAllItems.call(this, 'results', 'GET', '/users'); + responseData = responseData.splice(0, qs.limit); + } + returnData.push.apply(returnData, responseData); + } + } + } + + if (resource === 'page') { + + if (operation === 'create') { + for (let i = 0; i < length; i++) { + const simple = this.getNodeParameter('simple', i) as boolean; + // tslint:disable-next-line: no-any + const body: { [key: string]: any } = { + parent: {}, + properties: {}, + }; + body.parent['page_id'] = this.getNodeParameter('pageId', i) as string; + body.properties = formatTitle(this.getNodeParameter('title', i) as string); + body.children = formatBlocks(this.getNodeParameter('blockUi.blockValues', i, []) as IDataObject[]); + responseData = await notionApiRequest.call(this, 'POST', '/pages', body); + if (simple === true) { + responseData = simplifyObjects(responseData); + } + returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]); + } + } + + if (operation === 'get') { + for (let i = 0; i < length; i++) { + const pageId = this.getNodeParameter('pageId', i) as string; + const simple = this.getNodeParameter('simple', i) as boolean; + responseData = await notionApiRequest.call(this, 'GET', `/pages/${pageId}`); + if (simple === true) { + responseData = simplifyObjects(responseData); + } + returnData.push.apply(returnData, Array.isArray(responseData) ? responseData : [responseData]); + } + } + + if (operation === 'search') { + for (let i = 0; i < length; i++) { + const text = this.getNodeParameter('text', i) as string; + const options = this.getNodeParameter('options', i) as IDataObject; + const returnAll = this.getNodeParameter('returnAll', i) as boolean; + const simple = this.getNodeParameter('simple', i) as boolean; + const body: IDataObject = {}; + + if (text) { + body['query'] = text; + } + + if (options.filter) { + const filter = (options.filter as IDataObject || {}).filters as IDataObject[] || []; + body['filter'] = filter; + } + + if (options.sort) { + const sort = (options.sort as IDataObject || {}).sortValue as IDataObject || {}; + body['sort'] = sort; + } + if (returnAll) { + responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', '/search', body); + } else { + qs.limit = this.getNodeParameter('limit', i) as number; + responseData = await notionApiRequestAllItems.call(this, 'results', 'POST', '/search', body); + responseData = responseData.splice(0, qs.limit); + } + + if (simple === true) { + responseData = simplifyObjects(responseData); + } + + returnData.push.apply(returnData, responseData); + } + } + } + return [this.helpers.returnJsonArray(returnData)]; + } +} diff --git a/packages/nodes-base/nodes/Notion/NotionTrigger.node.ts b/packages/nodes-base/nodes/Notion/NotionTrigger.node.ts new file mode 100644 index 0000000000..66e09fbc8e --- /dev/null +++ b/packages/nodes-base/nodes/Notion/NotionTrigger.node.ts @@ -0,0 +1,188 @@ +import { + IPollFunctions, +} from 'n8n-core'; + +import { + IDataObject, + ILoadOptionsFunctions, + INodeExecutionData, + INodePropertyOptions, + INodeType, + INodeTypeDescription, +} from 'n8n-workflow'; + +import { + notionApiRequest, + simplifyObjects, +} from './GenericFunctions'; + +import * as moment from 'moment'; + +export class NotionTrigger implements INodeType { + description: INodeTypeDescription = { + displayName: 'Notion Trigger (Beta)', + name: 'notionTrigger', + icon: 'file:notion.svg', + group: ['trigger'], + version: 1, + description: 'Starts the workflow when Notion events occur', + subtitle: '={{$parameter["event"]}}', + defaults: { + name: 'Notion Trigger', + color: '#000000', + }, + credentials: [ + { + name: 'notionApi', + required: true, + }, + ], + polling: true, + inputs: [], + outputs: ['main'], + properties: [ + { + displayName: 'Event', + name: 'event', + type: 'options', + options: [ + { + name: 'Page Added to Database', + value: 'pageAddedToDatabase', + }, + // { + // name: 'Record Updated', + // value: 'recordUpdated', + // }, + ], + required: true, + default: '', + }, + { + displayName: 'Database', + name: 'databaseId', + type: 'options', + typeOptions: { + loadOptionsMethod: 'getDatabases', + }, + displayOptions: { + show: { + event: [ + 'pageAddedToDatabase', + ], + }, + }, + default: '', + required: true, + description: 'The ID of this database.', + }, + { + displayName: 'Simple', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + event: [ + 'pageAddedToDatabase', + ], + }, + }, + default: true, + description: 'When set to true a simplify version of the response will be used else the raw data.', + }, + ], + }; + + methods = { + loadOptions: { + async getDatabases(this: ILoadOptionsFunctions): Promise { + const returnData: INodePropertyOptions[] = []; + const { results: databases } = await notionApiRequest.call(this, 'POST', `/search`, { page_size: 100, filter: { property: 'object', value: 'database' } }); + for (const database of databases) { + returnData.push({ + name: database.title[0].plain_text, + 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 poll(this: IPollFunctions): Promise { + const webhookData = this.getWorkflowStaticData('node'); + const databaseId = this.getNodeParameter('databaseId') as string; + const event = this.getNodeParameter('event') as string; + const simple = this.getNodeParameter('simple') as boolean; + + const now = moment().utc().format(); + + const startDate = webhookData.lastTimeChecked as string || now; + + const endDate = now; + + webhookData.lastTimeChecked = endDate; + + const sortProperty = (event === 'pageAddedToDatabase') ? 'created_time' : 'last_edited_time'; + + const body: IDataObject = { + page_size: 1, + sorts: [ + { + timestamp: sortProperty, + direction: 'descending', + }, + ], + }; + + let records: IDataObject[] = []; + + let hasMore = true; + + //get last record + let { results: data } = await notionApiRequest.call(this, 'POST', `/databases/${databaseId}/query`, body); + + if (this.getMode() === 'manual') { + if (simple === true) { + data = simplifyObjects(data); + } + if (Array.isArray(data) && data.length) { + return [this.helpers.returnJsonArray(data)]; + } + } + + // if something changed after the last check + if (Object.keys(data[0]).length !== 0 && webhookData.lastRecordProccesed !== data[0].id) { + do { + body.page_size = 10; + const { results, has_more, next_cursor } = await notionApiRequest.call(this, 'POST', `/databases/${databaseId}/query`, body); + records.push.apply(records, results); + hasMore = has_more; + if (next_cursor !== null) { + body['start_cursor'] = next_cursor; + } + } while (!moment(records[records.length - 1][sortProperty] as string).isSameOrBefore(startDate) && hasMore === true); + + if (this.getMode() !== 'manual') { + records = records.filter((record: IDataObject) => moment(record[sortProperty] as string).isBetween(startDate, endDate)); + } + + if (simple === true) { + records = simplifyObjects(records); + } + + webhookData.lastRecordProccesed = data[0].id; + + if (Array.isArray(records) && records.length) { + return [this.helpers.returnJsonArray(records)]; + } + } + + return null; + } +} diff --git a/packages/nodes-base/nodes/Notion/PageDescription.ts b/packages/nodes-base/nodes/Notion/PageDescription.ts new file mode 100644 index 0000000000..4612eb8be8 --- /dev/null +++ b/packages/nodes-base/nodes/Notion/PageDescription.ts @@ -0,0 +1,332 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +import { + blocks, +} from './Blocks'; + +export const pageOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'page', + ], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a page', + }, + { + name: 'Get', + value: 'get', + description: 'Get a page', + }, + { + name: 'Search', + value: 'search', + description: 'Text search of pages', + }, + ], + default: 'create', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const pageFields = [ + + /* -------------------------------------------------------------------------- */ + /* page:create */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Parent Page ID', + name: 'pageId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'page', + ], + operation: [ + 'create', + ], + }, + }, + description: 'The ID of the parent page that this child page belongs to.', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'page', + ], + operation: [ + 'create', + ], + }, + }, + description: 'Page title. Appears at the top of the page and can be found via Quick Find.', + }, + { + displayName: 'Simple', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'page', + ], + operation: [ + 'create', + ], + }, + }, + default: true, + description: 'When set to true a simplify version of the response will be used else the raw data.', + }, + ...blocks('page', 'create'), + /* -------------------------------------------------------------------------- */ + /* page:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Page ID', + name: 'pageId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'page', + ], + operation: [ + 'get', + ], + }, + }, + }, + { + displayName: 'Simple', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'page', + ], + operation: [ + 'get', + ], + }, + }, + default: true, + description: 'When set to true a simplify version of the response will be used else the raw data.', + }, + /* -------------------------------------------------------------------------- */ + /* page:search */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Search Text', + name: 'text', + type: 'string', + default: '', + displayOptions: { + show: { + resource: [ + 'page', + ], + operation: [ + 'search', + ], + }, + }, + description: 'The text to search for.', + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'page', + ], + operation: [ + 'search', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'page', + ], + operation: [ + 'search', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, + { + displayName: 'Simple', + name: 'simple', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'page', + ], + operation: [ + 'search', + ], + }, + }, + default: true, + description: 'When set to true a simplify version of the response will be used else the raw data.', + }, + { + displayName: 'Options', + name: 'options', + type: 'collection', + displayOptions: { + show: { + resource: [ + 'page', + ], + operation: [ + 'search', + ], + }, + }, + default: {}, + placeholder: 'Add Field', + options: [ + { + displayName: 'Filters', + name: 'filter', + placeholder: 'Add Filter', + type: 'fixedCollection', + typeOptions: { + multipleValues: false, + }, + default: {}, + options: [ + { + displayName: 'Filter', + name: 'filters', + values: [ + { + displayName: 'Property', + name: 'property', + type: 'options', + options: [ + { + name: 'Object', + value: 'object', + }, + ], + default: 'object', + description: 'The name of the property to filter by.', + }, + { + displayName: 'Value', + name: 'value', + type: 'options', + options: [ + { + name: 'Database', + value: 'database', + }, + { + name: 'Page', + value: 'page', + }, + ], + default: '', + description: 'The value of the property to filter by.', + }, + ], + }, + ], + }, + { + 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: '', + 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/UserDescription.ts b/packages/nodes-base/nodes/Notion/UserDescription.ts new file mode 100644 index 0000000000..42ce84bf49 --- /dev/null +++ b/packages/nodes-base/nodes/Notion/UserDescription.ts @@ -0,0 +1,100 @@ +import { + INodeProperties, +} from 'n8n-workflow'; + +export const userOperations = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + displayOptions: { + show: { + resource: [ + 'user', + ], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get a user', + }, + { + name: 'Get All', + value: 'getAll', + description: 'Get all users', + }, + ], + default: 'get', + description: 'The operation to perform.', + }, +] as INodeProperties[]; + +export const userFields = [ + + /* -------------------------------------------------------------------------- */ + /* user:get */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'User ID', + name: 'userId', + type: 'string', + default: '', + required: true, + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'get', + ], + }, + }, + }, + /* -------------------------------------------------------------------------- */ + /* user:getAll */ + /* -------------------------------------------------------------------------- */ + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + }, + }, + default: false, + description: 'If all results should be returned or only up to a given limit.', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: [ + 'user', + ], + operation: [ + 'getAll', + ], + returnAll: [ + false, + ], + }, + }, + typeOptions: { + minValue: 1, + maxValue: 100, + }, + default: 50, + description: 'How many results to return.', + }, +] as INodeProperties[]; diff --git a/packages/nodes-base/nodes/Notion/notion.svg b/packages/nodes-base/nodes/Notion/notion.svg new file mode 100644 index 0000000000..2fd319930d --- /dev/null +++ b/packages/nodes-base/nodes/Notion/notion.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/packages/nodes-base/package.json b/packages/nodes-base/package.json index 5ab51ed341..e659f4a3e0 100644 --- a/packages/nodes-base/package.json +++ b/packages/nodes-base/package.json @@ -171,6 +171,8 @@ "dist/credentials/NasaApi.credentials.js", "dist/credentials/NextCloudApi.credentials.js", "dist/credentials/NextCloudOAuth2Api.credentials.js", + "dist/credentials/NotionApi.credentials.js", + "dist/credentials/NotionOAuth2Api.credentials.js", "dist/credentials/OAuth1Api.credentials.js", "dist/credentials/OAuth2Api.credentials.js", "dist/credentials/OpenWeatherMapApi.credentials.js", @@ -449,6 +451,8 @@ "dist/nodes/Nasa/Nasa.node.js", "dist/nodes/NextCloud/NextCloud.node.js", "dist/nodes/NoOp.node.js", + "dist/nodes/Notion/Notion.node.js", + "dist/nodes/Notion/NotionTrigger.node.js", "dist/nodes/OpenThesaurus/OpenThesaurus.node.js", "dist/nodes/OpenWeatherMap.node.js", "dist/nodes/Orbit/Orbit.node.js",