From 6be8b55daa06f9c9bf202430235fe2006cae35cc Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Mon, 12 Jun 2023 22:45:37 +0000 Subject: [PATCH] feat(notion): add auth and new DB items trigger --- .../src/apps/notion/assets/favicon.svg | 7 +++ .../src/apps/notion/auth/generate-auth-url.ts | 21 +++++++ .../backend/src/apps/notion/auth/index.ts | 49 +++++++++++++++ .../src/apps/notion/auth/is-still-verified.ts | 9 +++ .../apps/notion/auth/verify-credentials.ts | 53 ++++++++++++++++ .../src/apps/notion/common/add-auth-header.ts | 14 +++++ .../common/add-notion-version-header.ts | 9 +++ .../apps/notion/common/get-current-user.ts | 17 ++++++ .../src/apps/notion/dynamic-data/index.ts | 3 + .../dynamic-data/list-databases/index.ts | 60 +++++++++++++++++++ packages/backend/src/apps/notion/index.d.ts | 0 packages/backend/src/apps/notion/index.ts | 24 ++++++++ .../backend/src/apps/notion/triggers/index.ts | 3 + .../triggers/new-database-items/index.ts | 32 ++++++++++ .../new-database-items/new-database-items.ts | 50 ++++++++++++++++ 15 files changed, 351 insertions(+) create mode 100644 packages/backend/src/apps/notion/assets/favicon.svg create mode 100644 packages/backend/src/apps/notion/auth/generate-auth-url.ts create mode 100644 packages/backend/src/apps/notion/auth/index.ts create mode 100644 packages/backend/src/apps/notion/auth/is-still-verified.ts create mode 100644 packages/backend/src/apps/notion/auth/verify-credentials.ts create mode 100644 packages/backend/src/apps/notion/common/add-auth-header.ts create mode 100644 packages/backend/src/apps/notion/common/add-notion-version-header.ts create mode 100644 packages/backend/src/apps/notion/common/get-current-user.ts create mode 100644 packages/backend/src/apps/notion/dynamic-data/index.ts create mode 100644 packages/backend/src/apps/notion/dynamic-data/list-databases/index.ts create mode 100644 packages/backend/src/apps/notion/index.d.ts create mode 100644 packages/backend/src/apps/notion/index.ts create mode 100644 packages/backend/src/apps/notion/triggers/index.ts create mode 100644 packages/backend/src/apps/notion/triggers/new-database-items/index.ts create mode 100644 packages/backend/src/apps/notion/triggers/new-database-items/new-database-items.ts diff --git a/packages/backend/src/apps/notion/assets/favicon.svg b/packages/backend/src/apps/notion/assets/favicon.svg new file mode 100644 index 00000000..ebcbe816 --- /dev/null +++ b/packages/backend/src/apps/notion/assets/favicon.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/packages/backend/src/apps/notion/auth/generate-auth-url.ts b/packages/backend/src/apps/notion/auth/generate-auth-url.ts new file mode 100644 index 00000000..e34c1ead --- /dev/null +++ b/packages/backend/src/apps/notion/auth/generate-auth-url.ts @@ -0,0 +1,21 @@ +import { IField, IGlobalVariable } from '@automatisch/types'; +import { URL, URLSearchParams } from 'url'; + +export default async function generateAuthUrl($: IGlobalVariable) { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field: IField) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value as string; + const searchParams = new URLSearchParams({ + client_id: $.auth.data.clientId as string, + redirect_uri: redirectUri, + response_type: 'code', + owner: 'user', + }); + + const url = new URL(`/v1/oauth/authorize?${searchParams}`, $.app.apiBaseUrl).toString(); + + await $.auth.set({ + url, + }); +} diff --git a/packages/backend/src/apps/notion/auth/index.ts b/packages/backend/src/apps/notion/auth/index.ts new file mode 100644 index 00000000..8c9d1aa6 --- /dev/null +++ b/packages/backend/src/apps/notion/auth/index.ts @@ -0,0 +1,49 @@ +import generateAuthUrl from './generate-auth-url'; +import verifyCredentials from './verify-credentials'; +import isStillVerified from './is-still-verified'; + +export default { + fields: [ + { + key: 'oAuthRedirectUrl', + label: 'OAuth Redirect URL', + type: 'string' as const, + required: true, + readOnly: true, + value: '{WEB_APP_URL}/app/notion/connections/add', + placeholder: null, + description: + 'When asked to input an OAuth callback or redirect URL in Notion OAuth, enter the URL above.', + docUrl: 'https://automatisch.io/docs/notion#oauth-redirect-url', + clickToCopy: true, + }, + { + key: 'clientId', + label: 'Client ID', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + docUrl: 'https://automatisch.io/docs/notion#client-id', + clickToCopy: false, + }, + { + key: 'clientSecret', + label: 'Client Secret', + type: 'string' as const, + required: true, + readOnly: false, + value: null, + placeholder: null, + description: null, + docUrl: 'https://automatisch.io/docs/notion#client-secret', + clickToCopy: false, + }, + ], + + generateAuthUrl, + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/notion/auth/is-still-verified.ts b/packages/backend/src/apps/notion/auth/is-still-verified.ts new file mode 100644 index 00000000..befb7694 --- /dev/null +++ b/packages/backend/src/apps/notion/auth/is-still-verified.ts @@ -0,0 +1,9 @@ +import { IGlobalVariable } from '@automatisch/types'; +import getCurrentUser from '../common/get-current-user'; + +const isStillVerified = async ($: IGlobalVariable) => { + const user = await getCurrentUser($); + return !!user.id; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/notion/auth/verify-credentials.ts b/packages/backend/src/apps/notion/auth/verify-credentials.ts new file mode 100644 index 00000000..f3104eca --- /dev/null +++ b/packages/backend/src/apps/notion/auth/verify-credentials.ts @@ -0,0 +1,53 @@ +import { IGlobalVariable, IField } from '@automatisch/types'; +import getCurrentUser from '../common/get-current-user'; + +const verifyCredentials = async ($: IGlobalVariable) => { + const oauthRedirectUrlField = $.app.auth.fields.find( + (field: IField) => field.key == 'oAuthRedirectUrl' + ); + const redirectUri = oauthRedirectUrlField.value as string; + const response = await $.http.post( + `${$.app.apiBaseUrl}/v1/oauth/token`, + { + redirect_uri: redirectUri, + code: $.auth.data.code, + grant_type: 'authorization_code', + }, + { + headers: { + Authorization: `Basic ${Buffer.from( + $.auth.data.clientId + ':' + $.auth.data.clientSecret + ).toString('base64')}`, + }, + additionalProperties: { + skipAddingAuthHeader: true + } + } + ); + + const data = response.data; + + $.auth.data.accessToken = data.access_token; + + await $.auth.set({ + clientId: $.auth.data.clientId, + clientSecret: $.auth.data.clientSecret, + accessToken: data.access_token, + botId: data.bot_id, + duplicatedTemplateId: data.duplicated_template_id, + owner: data.owner, + tokenType: data.token_type, + workspaceIcon: data.workspace_icon, + workspaceId: data.workspace_id, + workspaceName: data.workspace_name, + screenName: data.workspace_name, + }); + + const currentUser = await getCurrentUser($); + + await $.auth.set({ + screenName: `${currentUser.name} @ ${data.workspace_name}`, + }); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/notion/common/add-auth-header.ts b/packages/backend/src/apps/notion/common/add-auth-header.ts new file mode 100644 index 00000000..d16f394f --- /dev/null +++ b/packages/backend/src/apps/notion/common/add-auth-header.ts @@ -0,0 +1,14 @@ +import { TBeforeRequest } from '@automatisch/types'; + +const addAuthHeader: TBeforeRequest = ($, requestConfig) => { + if (requestConfig.additionalProperties?.skipAddingAuthHeader) return requestConfig; + + if ($.auth.data?.accessToken) { + const authorizationHeader = `Bearer ${$.auth.data.accessToken}`; + requestConfig.headers.Authorization = authorizationHeader; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/notion/common/add-notion-version-header.ts b/packages/backend/src/apps/notion/common/add-notion-version-header.ts new file mode 100644 index 00000000..0c6197a4 --- /dev/null +++ b/packages/backend/src/apps/notion/common/add-notion-version-header.ts @@ -0,0 +1,9 @@ +import { TBeforeRequest } from '@automatisch/types'; + +const addNotionVersionHeader: TBeforeRequest = ($, requestConfig) => { + requestConfig.headers['Notion-Version'] = '2022-06-28'; + + return requestConfig; +}; + +export default addNotionVersionHeader; diff --git a/packages/backend/src/apps/notion/common/get-current-user.ts b/packages/backend/src/apps/notion/common/get-current-user.ts new file mode 100644 index 00000000..1110d23b --- /dev/null +++ b/packages/backend/src/apps/notion/common/get-current-user.ts @@ -0,0 +1,17 @@ +import { IGlobalVariable, IJSONObject } from '@automatisch/types'; + +type Owner = { + user: { + id: string + } +} + +const getCurrentUser = async ($: IGlobalVariable): Promise => { + const userId = ($.auth.data.owner as Owner).user.id; + const response = await $.http.get(`/v1/users/${userId}`); + + const currentUser = response.data; + return currentUser; +}; + +export default getCurrentUser; diff --git a/packages/backend/src/apps/notion/dynamic-data/index.ts b/packages/backend/src/apps/notion/dynamic-data/index.ts new file mode 100644 index 00000000..70002de4 --- /dev/null +++ b/packages/backend/src/apps/notion/dynamic-data/index.ts @@ -0,0 +1,3 @@ +import listDatabases from './list-databases'; + +export default [listDatabases]; diff --git a/packages/backend/src/apps/notion/dynamic-data/list-databases/index.ts b/packages/backend/src/apps/notion/dynamic-data/list-databases/index.ts new file mode 100644 index 00000000..ca16aaa2 --- /dev/null +++ b/packages/backend/src/apps/notion/dynamic-data/list-databases/index.ts @@ -0,0 +1,60 @@ +import { IGlobalVariable, IJSONObject } from '@automatisch/types'; + +type Database = { + id: string; + name: string; + title: [ + { + plain_text: string; + } + ]; +} + +type ResponseData = { + results: Database[]; + next_cursor?: string; +} + +type Payload = { + filter: { + value: 'database'; + property: 'object'; + }; + start_cursor?: string; +}; + +export default { + name: 'List databases', + key: 'listDatabases', + + async run($: IGlobalVariable) { + const databases: { + data: IJSONObject[]; + error: IJSONObject | null; + } = { + data: [], + error: null, + }; + const payload: Payload = { + filter: { + value: 'database', + property: 'object' + }, + }; + + do { + const response = await $.http.post('/v1/search', payload); + + payload.start_cursor = response.data.next_cursor; + + for (const database of response.data.results) { + databases.data.push({ + value: database.id as string, + name: database.title[0].plain_text as string, + }); + } + } while (payload.start_cursor); + + return databases; + }, +}; diff --git a/packages/backend/src/apps/notion/index.d.ts b/packages/backend/src/apps/notion/index.d.ts new file mode 100644 index 00000000..e69de29b diff --git a/packages/backend/src/apps/notion/index.ts b/packages/backend/src/apps/notion/index.ts new file mode 100644 index 00000000..b26b178c --- /dev/null +++ b/packages/backend/src/apps/notion/index.ts @@ -0,0 +1,24 @@ +import defineApp from '../../helpers/define-app'; +import addAuthHeader from './common/add-auth-header'; +import addNotionVersionHeader from './common/add-notion-version-header'; +import auth from './auth'; +import triggers from './triggers'; +import dynamicData from './dynamic-data'; + +export default defineApp({ + name: 'Notion', + key: 'notion', + baseUrl: 'https://notion.com', + apiBaseUrl: 'https://api.notion.com', + iconUrl: '{BASE_URL}/apps/notion/assets/favicon.svg', + authDocUrl: 'https://automatisch.io/docs/apps/notion/connection', + primaryColor: '000000', + supportsConnections: true, + beforeRequest: [ + addAuthHeader, + addNotionVersionHeader, + ], + auth, + triggers, + dynamicData, +}); diff --git a/packages/backend/src/apps/notion/triggers/index.ts b/packages/backend/src/apps/notion/triggers/index.ts new file mode 100644 index 00000000..260f1756 --- /dev/null +++ b/packages/backend/src/apps/notion/triggers/index.ts @@ -0,0 +1,3 @@ +import newDatabaseItems from './new-database-items'; + +export default [newDatabaseItems]; diff --git a/packages/backend/src/apps/notion/triggers/new-database-items/index.ts b/packages/backend/src/apps/notion/triggers/new-database-items/index.ts new file mode 100644 index 00000000..989c97f7 --- /dev/null +++ b/packages/backend/src/apps/notion/triggers/new-database-items/index.ts @@ -0,0 +1,32 @@ +import defineTrigger from '../../../../helpers/define-trigger'; +import newDatabaseItems from './new-database-items'; + +export default defineTrigger({ + name: 'New database items', + key: 'newDatabaseItems', + pollInterval: 15, + description: 'Triggers when a new database item is created', + arguments: [ + { + label: 'Database', + key: 'databaseId', + type: 'dropdown' as const, + required: false, + variables: false, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listDatabases', + }, + ], + }, + }, + ], + + async run($) { + await newDatabaseItems($); + }, +}); diff --git a/packages/backend/src/apps/notion/triggers/new-database-items/new-database-items.ts b/packages/backend/src/apps/notion/triggers/new-database-items/new-database-items.ts new file mode 100644 index 00000000..90b619c8 --- /dev/null +++ b/packages/backend/src/apps/notion/triggers/new-database-items/new-database-items.ts @@ -0,0 +1,50 @@ +import { IGlobalVariable } from '@automatisch/types'; + +type DatabaseItem = { + id: string; +} + +type ResponseData = { + results: DatabaseItem[]; + next_cursor?: string; +} + +type Payload = { + sorts: [ + { + timestamp: 'created_time' | 'last_edited_time'; + direction: 'ascending' | 'descending'; + } + ]; + start_cursor?: string; +}; + +const newDatabaseItems = async ($: IGlobalVariable) => { + const payload: Payload = { + sorts: [ + { + timestamp: 'created_time', + direction: 'descending' + } + ], + }; + + const databaseId = $.step.parameters.databaseId as string; + const path = `/v1/databases/${databaseId}/query`; + do { + const response = await $.http.post(path, payload); + + payload.start_cursor = response.data.next_cursor; + + for (const databaseItem of response.data.results) { + $.pushTriggerItem({ + raw: databaseItem, + meta: { + internalId: databaseItem.id, + } + }) + } + } while (payload.start_cursor); +}; + +export default newDatabaseItems;