2023-01-27 03:22:44 -08:00
import type {
2023-03-09 09:13:15 -08:00
IPollFunctions ,
2023-01-27 03:22:44 -08:00
IDataObject ,
INodeExecutionData ,
INodeType ,
INodeTypeDescription ,
} from 'n8n-workflow' ;
2021-05-20 14:31:23 -07:00
2022-08-17 08:50:24 -07:00
import { notionApiRequest , simplifyObjects } from './GenericFunctions' ;
2021-05-20 14:31:23 -07:00
2022-04-08 14:32:08 -07:00
import moment from 'moment' ;
2022-11-11 04:37:52 -08:00
import { getDatabases } from './SearchFunctions' ;
2021-05-20 14:31:23 -07:00
export class NotionTrigger implements INodeType {
description : INodeTypeDescription = {
2022-07-04 02:12:08 -07:00
// eslint-disable-next-line n8n-nodes-base/node-class-description-display-name-unsuffixed-trigger-node
2023-01-20 04:49:02 -08:00
displayName : 'Notion Trigger' ,
2021-05-20 14:31:23 -07:00
name : 'notionTrigger' ,
icon : 'file:notion.svg' ,
group : [ 'trigger' ] ,
version : 1 ,
description : 'Starts the workflow when Notion events occur' ,
subtitle : '={{$parameter["event"]}}' ,
defaults : {
feat(editor): Node creator actions (#4696)
* WIP: Node Actions List UI
* WIP: Recommended Actions and preseting of fields
* WIP: Resource category
* :art: Moved actions categorisation to the server
* :label: Add missing INodeAction type
* :sparkles: Improve SSR categorisation, fix adding of mixed actions
* :recycle: Refactor CategorizedItems to composition api, style fixes
* WIP: Adding multiple nodes
* :recycle: Refactor rest of the NodeCreator component to composition API, conver globalLinkActions to composable
* :sparkles: Allow actions dragging, fix search and refactor passing of actions to categorized items
* :lipstick: Fix node actions title
* Migrate to the pinia store, add posthog feature and various fixes
* :bug: Fix filtering of trigger actions when not merged
* fix: N8N-5439 — Do not use simple node item when at NodeHelperPanel root
* :bug: Design review fixes
* :bug: Fix disabling of merged actions
* Fix trigger root filtering
* :sparkles: Allow for custom node actions parser, introduce hubspot parser
* :bug: Fix initial node params validation, fix position of second added node
* :bug: Introduce operations category, removed canvas node names overrride, fix API actions display and prevent dragging of action nodes
* :sparkles: Prevent NDV auto-open feature flag
* :bug: Inject recommened action for trigger nodes without actions
* Refactored NodeCreatorNode to Storybook, change filtering of merged nodes for the trigger helper panel, minor fixes
* Improve rendering of app nodes and animation
* Cleanup, any only enable accordion transition on triggerhelperpanel
* Hide node creator scrollbars in Firefox
* Minor styles fixes
* Do not copy the array in rendering method
* Removed unused props
* Fix memory leak
* Fix categorisation of regular nodes with a single resource
* Implement telemetry calls for node actions
* Move categorization to FE
* Fix client side actions categorisation
* Skip custom action show
* Only load tooltip for NodeIcon if necessary
* Fix lodash startCase import
* Remove lodash.startcase
* Cleanup
* Fix node creator autofocus on "tab"
* Prevent posthog getFeatureFlag from crashing
* Debugging preview env search issues
* Remove logs
* Make sure the pre-filled params are update not overwritten
* Get rid of transition in itemiterator
* WIP: Rough version of NodeActions keyboard navigation, replace nodeCreator composable with Pinia store module
* Rewrite to add support for ActionItem to ItemIterator and make CategorizedItems accept items props
* Fix category item counter & cleanup
* Add APIHint to actions search no-result, clean up NodeCreatorNode
* Improve node actions no results message
* Remove logging, fix filtering of recommended placeholder category
* Remove unused NodeActions component and node merging feature falg
* Do not show regular nodes without actions
* Make sure to add manual trigger when adding http node via actions hint
* Fixed api hint footer line height
* Prevent pointer-events od NodeIcon img and remove "this" from template
* Address PR points
* Fix e2e specs
* Make sure canvas ia loaded
* Make sure canvas ia loaded before opening nodeCreator in e2e spec
* Fix flaky workflows tags e2e getter
* Imrpove node creator click outside UX, add manual node to regular nodes added from trigger panel
* Add manual trigger node if dragging regular from trigger panel
2022-12-09 01:56:36 -08:00
name : 'Notion Trigger (Beta)' ,
2021-05-20 14:31:23 -07:00
} ,
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' ,
} ,
2021-12-29 14:23:22 -08:00
{
2022-05-06 08:12:14 -07:00
name : 'Page Updated in Database' ,
2021-12-29 14:23:22 -08:00
value : 'pagedUpdatedInDatabase' ,
} ,
2021-05-20 14:31:23 -07:00
] ,
required : true ,
2022-11-09 02:26:13 -08:00
default : 'pageAddedToDatabase' ,
2021-05-20 14:31:23 -07:00
} ,
2022-07-04 00:41:44 -07:00
{
2022-08-17 08:50:24 -07:00
displayName :
2023-01-20 04:49:02 -08:00
'In Notion, make sure to <a href="https://www.notion.so/help/add-and-manage-connections-with-the-api" target="_blank">add your connection</a> to the pages you want to access.' ,
2022-07-04 00:41:44 -07:00
name : 'notionNotice' ,
type : 'notice' ,
default : '' ,
} ,
2021-05-20 14:31:23 -07:00
{
2022-11-11 04:37:52 -08:00
displayName : 'Database' ,
2021-05-20 14:31:23 -07:00
name : 'databaseId' ,
2022-11-11 04:37:52 -08:00
type : 'resourceLocator' ,
default : { mode : 'list' , value : '' } ,
required : true ,
modes : [
{
displayName : 'Database' ,
name : 'list' ,
type : 'list' ,
placeholder : 'Select a Database...' ,
typeOptions : {
searchListMethod : 'getDatabases' ,
searchable : true ,
} ,
} ,
{
displayName : 'Link' ,
name : 'url' ,
type : 'string' ,
placeholder :
'https://www.notion.so/0fe2f7de558b471eab07e9d871cdf4a9?v=f2d424ba0c404733a3f500c78c881610' ,
validation : [
{
type : 'regex' ,
properties : {
regex :
2022-11-11 07:07:50 -08:00
'(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12}).*' ,
2022-11-11 04:37:52 -08:00
errorMessage : 'Not a valid Notion Database URL' ,
} ,
} ,
] ,
extractValue : {
type : 'regex' ,
2022-11-11 07:07:50 -08:00
regex :
'(?:https|http)://www.notion.so/(?:[a-z0-9-]{2,}/)?([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12})' ,
2022-11-11 04:37:52 -08:00
} ,
} ,
{
displayName : 'ID' ,
name : 'id' ,
type : 'string' ,
placeholder : 'ab1545b247fb49fa92d6f4b49f4d8116' ,
validation : [
{
type : 'regex' ,
properties : {
2022-11-11 07:07:50 -08:00
regex :
'^(([0-9a-f]{8}[0-9a-f]{4}4[0-9a-f]{3}[89ab][0-9a-f]{3}[0-9a-f]{12})|([0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}))[ \t]*' ,
2022-11-11 04:37:52 -08:00
errorMessage : 'Not a valid Notion Database ID' ,
} ,
} ,
] ,
extractValue : {
type : 'regex' ,
regex : '^([0-9a-f]{8}-?[0-9a-f]{4}-?4[0-9a-f]{3}-?[89ab][0-9a-f]{3}-?[0-9a-f]{12})' ,
} ,
url : '=https://www.notion.so/{{$value.replace(/-/g, "")}}' ,
} ,
] ,
2021-05-20 14:31:23 -07:00
displayOptions : {
show : {
2022-08-17 08:50:24 -07:00
event : [ 'pageAddedToDatabase' , 'pagedUpdatedInDatabase' ] ,
2021-05-20 14:31:23 -07:00
} ,
} ,
2022-11-11 07:07:50 -08:00
description : 'The Notion Database to operate on' ,
2021-05-20 14:31:23 -07:00
} ,
{
2022-05-20 14:47:24 -07:00
displayName : 'Simplify' ,
2021-05-20 14:31:23 -07:00
name : 'simple' ,
type : 'boolean' ,
displayOptions : {
show : {
2022-08-17 08:50:24 -07:00
event : [ 'pageAddedToDatabase' , 'pagedUpdatedInDatabase' ] ,
2021-05-20 14:31:23 -07:00
} ,
} ,
default : true ,
2022-08-17 08:50:24 -07:00
description :
'Whether to return a simplified version of the response instead of the raw data' ,
2021-05-20 14:31:23 -07:00
} ,
] ,
} ;
methods = {
2022-11-11 04:37:52 -08:00
listSearch : {
getDatabases ,
2021-05-20 14:31:23 -07:00
} ,
} ;
async poll ( this : IPollFunctions ) : Promise < INodeExecutionData [ ] [ ] | null > {
const webhookData = this . getWorkflowStaticData ( 'node' ) ;
2022-11-11 04:37:52 -08:00
const databaseId = this . getNodeParameter ( 'databaseId' , '' , { extractValue : true } ) as string ;
2021-05-20 14:31:23 -07:00
const event = this . getNodeParameter ( 'event' ) as string ;
const simple = this . getNodeParameter ( 'simple' ) as boolean ;
2022-11-09 02:26:13 -08:00
const lastTimeChecked = webhookData . lastTimeChecked
? moment ( webhookData . lastTimeChecked as string )
: moment ( ) . set ( { second : 0 , millisecond : 0 } ) ; // Notion timestamp accuracy is only down to the minute
2021-05-20 14:31:23 -07:00
2022-11-09 02:26:13 -08:00
// update lastTimeChecked to now
webhookData . lastTimeChecked = moment ( ) . set ( { second : 0 , millisecond : 0 } ) ;
2021-05-20 14:31:23 -07:00
2022-11-09 02:26:13 -08:00
// because Notion timestamp accuracy is only down to the minute some duplicates can be fetch
const possibleDuplicates = ( webhookData . possibleDuplicates as string [ ] ) ? ? [ ] ;
2021-05-20 14:31:23 -07:00
2022-08-17 08:50:24 -07:00
const sortProperty = event === 'pageAddedToDatabase' ? 'created_time' : 'last_edited_time' ;
2021-05-20 14:31:23 -07:00
2022-11-09 02:26:13 -08:00
const option : IDataObject = {
headers : {
'Notion-Version' : '2022-02-22' ,
} ,
} ;
2021-05-20 14:31:23 -07:00
const body : IDataObject = {
page_size : 1 ,
sorts : [
{
timestamp : sortProperty ,
direction : 'descending' ,
} ,
] ,
2022-11-09 02:26:13 -08:00
. . . ( this . getMode ( ) !== 'manual' && {
filter : {
timestamp : sortProperty ,
[ sortProperty ] : {
on_or_after : lastTimeChecked.utc ( ) . format ( ) ,
} ,
} ,
} ) ,
2021-05-20 14:31:23 -07:00
} ;
let records : IDataObject [ ] = [ ] ;
let hasMore = true ;
//get last record
2022-08-17 08:50:24 -07:00
let { results : data } = await notionApiRequest . call (
this ,
'POST' ,
` /databases/ ${ databaseId } /query ` ,
body ,
2022-11-09 02:26:13 -08:00
{ } ,
'' ,
option ,
2022-08-17 08:50:24 -07:00
) ;
2021-05-20 14:31:23 -07:00
if ( this . getMode ( ) === 'manual' ) {
2022-12-02 12:54:28 -08:00
if ( simple ) {
2021-12-29 14:23:22 -08:00
data = simplifyObjects ( data , false , 1 ) ;
2021-05-20 14:31:23 -07:00
}
if ( Array . isArray ( data ) && data . length ) {
return [ this . helpers . returnJsonArray ( data ) ] ;
}
}
// if something changed after the last check
2023-02-27 19:39:43 -08:00
if ( Array . isArray ( data ) && data . length && Object . keys ( data [ 0 ] as IDataObject ) . length !== 0 ) {
2021-05-20 14:31:23 -07:00
do {
body . page_size = 10 ;
2022-08-17 08:50:24 -07:00
const { results , has_more , next_cursor } = await notionApiRequest . call (
this ,
'POST' ,
` /databases/ ${ databaseId } /query ` ,
body ,
2022-11-09 02:26:13 -08:00
{ } ,
'' ,
option ,
2022-08-17 08:50:24 -07:00
) ;
2023-02-27 19:39:43 -08:00
records . push ( . . . ( results as IDataObject [ ] ) ) ;
2021-05-20 14:31:23 -07:00
hasMore = has_more ;
if ( next_cursor !== null ) {
2022-12-02 12:54:28 -08:00
body . start_cursor = next_cursor ;
2021-05-20 14:31:23 -07:00
}
2022-11-09 02:26:13 -08:00
// Only stop when we reach records strictly before last recorded time to be sure we catch records from the same minute
2022-08-17 08:50:24 -07:00
} while (
2022-11-09 02:26:13 -08:00
! moment ( records [ records . length - 1 ] [ sortProperty ] as string ) . isBefore ( lastTimeChecked ) &&
2022-12-02 12:54:28 -08:00
hasMore
2022-08-17 08:50:24 -07:00
) ;
2021-05-20 14:31:23 -07:00
2022-11-09 02:26:13 -08:00
// Filter out already processed left over records:
// with a time strictly before the last record processed
// or from the same minute not present in the list of processed records
records = records . filter (
( record : IDataObject ) = > ! possibleDuplicates . includes ( record . id as string ) ,
) ;
// Save the time of the most recent record processed
if ( records [ 0 ] ) {
const latestTimestamp = moment ( records [ 0 ] [ sortProperty ] as string ) ;
// Save record ids with the same timestamp as the latest processed records
webhookData . possibleDuplicates = records
. filter ( ( record : IDataObject ) = >
moment ( record [ sortProperty ] as string ) . isSame ( latestTimestamp ) ,
)
. map ( ( record : IDataObject ) = > record . id ) ;
} else {
webhookData . possibleDuplicates = undefined ;
2021-05-20 14:31:23 -07:00
}
2022-12-02 12:54:28 -08:00
if ( simple ) {
2021-12-29 14:23:22 -08:00
records = simplifyObjects ( records , false , 1 ) ;
2021-05-20 14:31:23 -07:00
}
if ( Array . isArray ( records ) && records . length ) {
return [ this . helpers . returnJsonArray ( records ) ] ;
}
}
return null ;
}
}