diff --git a/packages/api/src/resolvers/integrations/index.ts b/packages/api/src/resolvers/integrations/index.ts index fec1a9a82..502b06880 100644 --- a/packages/api/src/resolvers/integrations/index.ts +++ b/packages/api/src/resolvers/integrations/index.ts @@ -40,6 +40,7 @@ import { saveIntegration, updateIntegration, } from '../../services/integrations' +import { NotionClient } from '../../services/integrations/notion' import { analytics } from '../../utils/analytics' import { deleteTask, @@ -57,15 +58,14 @@ export const setIntegrationResolver = authorized< ...input, user: { id: uid }, id: input.id || undefined, - type: input.type || IntegrationType.Export, + type: input.type || undefined, syncedAt: input.syncedAt ? new Date(input.syncedAt) : undefined, importItemState: input.type === IntegrationType.Import ? input.importItemState || ImportItemState.Unarchived // default to unarchived : undefined, - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment - settings: input.settings, } + if (input.id) { // Update const existingIntegration = await findIntegration({ id: input.id }, uid) @@ -96,6 +96,22 @@ export const setIntegrationResolver = authorized< if (integration.name.toLowerCase() === 'readwise') { // create a task to export all the items for readwise temporarily await enqueueExportToIntegration(integration.id, uid) + } else if ( + integration.name.toLowerCase() === 'notion' && + integration.settings + ) { + const settings = integration.settings as { parentDatabaseId?: string } + if (settings.parentDatabaseId) { + // update notion database properties + const notion = new NotionClient(integration.token, integration) + try { + await notion.updateDatabase(settings.parentDatabaseId) + } catch (error) { + return { + errorCodes: [SetIntegrationErrorCode.BadRequest], + } + } + } } analytics.capture({ diff --git a/packages/api/src/services/integrations/notion.ts b/packages/api/src/services/integrations/notion.ts index 7e891287d..ef9b63319 100644 --- a/packages/api/src/services/integrations/notion.ts +++ b/packages/api/src/services/integrations/notion.ts @@ -1,6 +1,5 @@ import { Client } from '@notionhq/client' import axios from 'axios' -import { updateIntegration } from '.' import { Integration } from '../../entity/integration' import { LibraryItem } from '../../entity/library_item' import { env } from '../../env' @@ -111,7 +110,7 @@ type Property = 'highlights' interface Settings { parentPageId: string parentDatabaseId: string - properties: Property[] + properties?: Property[] } export class NotionClient implements IntegrationClient { @@ -244,7 +243,7 @@ export class NotionClient implements IntegrationClient { : undefined, }, children: - settings.properties.includes('highlights') && item.highlights + settings.properties?.includes('highlights') && item.highlights ? item.highlights .filter( (highlight) => !lastSync || highlight.updatedAt > lastSync // only new highlights @@ -315,103 +314,92 @@ export class NotionClient implements IntegrationClient { return false } - const pageId = settings.parentPageId - if (!pageId) { - logger.error('Notion parent page id not found') - return false - } - - let databaseId = settings.parentDatabaseId + const databaseId = settings.parentDatabaseId if (!databaseId) { - // create a database for the items - const database = await this.client.databases.create({ - parent: { - page_id: pageId, - }, - title: [ - { - text: { - content: 'Library', - }, - }, - ], - description: [ - { - text: { - content: 'Library of saved items from Omnivore', - }, - }, - ], - properties: { - Title: { - title: {}, - }, - Author: { - rich_text: {}, - }, - 'Original URL': { - url: {}, - }, - 'Omnivore URL': { - url: {}, - }, - 'Saved At': { - date: {}, - }, - 'Last Updated': { - date: {}, - }, - Tags: { - multi_select: {}, - }, - }, - }) - - // save the database id - databaseId = database.id - settings.parentDatabaseId = databaseId - await updateIntegration( - this.integrationData.id, - { - settings, - }, - this.integrationData.user.id - ) + logger.error('Notion database id not found') + return false } await Promise.all( items.map(async (item) => { - const notionPage = this.itemToNotionPage( - item, - settings, - this.integrationData?.syncedAt - ) - const url = notionPage.properties['Omnivore URL'].url + try { + const notionPage = this.itemToNotionPage( + item, + settings, + this.integrationData?.syncedAt + ) + const url = notionPage.properties['Omnivore URL'].url - const existingPage = await this.findPage(url, databaseId) - if (existingPage) { - // update the page - await this.client.pages.update({ - page_id: existingPage.id, - properties: notionPage.properties, - }) - - // append the children incrementally - if (notionPage.children && notionPage.children.length > 0) { - await this.client.blocks.children.append({ - block_id: existingPage.id, - children: notionPage.children, + const existingPage = await this.findPage(url, databaseId) + if (existingPage) { + // update the page + await this.client.pages.update({ + page_id: existingPage.id, + properties: notionPage.properties, }) + + // append the children incrementally + if (notionPage.children && notionPage.children.length > 0) { + await this.client.blocks.children.append({ + block_id: existingPage.id, + children: notionPage.children, + }) + } + + return } - return + // create the page + return this.createPage(notionPage) + } catch (error) { + logger.error(error) + return false } - - // create the page - return this.createPage(notionPage) }) ) return true } + + private findDatabase = async (databaseId: string) => { + return this.client.databases.retrieve({ + database_id: databaseId, + }) + } + + updateDatabase = async (databaseId: string) => { + const database = await this.findDatabase(databaseId) + // find the title property and update it + const titleProperty = Object.entries(database.properties).find( + ([, property]) => property.type === 'title' + ) + const title = titleProperty ? titleProperty[0] : 'Name' + + await this.client.databases.update({ + database_id: database.id, + properties: { + [title]: { + name: 'Title', + }, + Author: { + rich_text: {}, + }, + 'Original URL': { + url: {}, + }, + 'Omnivore URL': { + url: {}, + }, + 'Saved At': { + date: {}, + }, + 'Last Updated': { + date: {}, + }, + Tags: { + multi_select: {}, + }, + }, + }) + } } diff --git a/packages/web/pages/settings/integrations/notion.tsx b/packages/web/pages/settings/integrations/notion.tsx index f45693f74..6a402deaa 100644 --- a/packages/web/pages/settings/integrations/notion.tsx +++ b/packages/web/pages/settings/integrations/notion.tsx @@ -30,8 +30,7 @@ import { applyStoredTheme } from '../../../lib/themeUpdater' import { showSuccessToast } from '../../../lib/toastHelpers' type FieldType = { - parentPageId?: string - parentDatabaseId?: string + parentDatabaseId: string properties?: string[] } @@ -47,7 +46,6 @@ export default function Notion(): JSX.Element { useEffect(() => { form.setFieldsValue({ - parentPageId: notion.settings?.parentPageId, parentDatabaseId: notion.settings?.parentDatabaseId, properties: notion.settings?.properties, }) @@ -72,6 +70,28 @@ export default function Notion(): JSX.Element { }) } + const normalizeDatabaseId = useCallback( + (value: string) => { + // check if database id is in UUIDv4 format + const uuidRegex = + /^[0-9a-fA-F]{8}[0-9a-fA-F]{4}[0-9a-fA-F]{4}[0-9a-fA-F]{4}[0-9a-fA-F]{12}$/ + if (uuidRegex.test(value)) { + return value + } + + // extract the database id from the URL + // https://www.notion.so/ec460c235baa4da5bb412971a12e9dbe?v=8f4e324c0b584b67b8b7cfe9a2f996d7 -> ec460c235baa4da5bb412971a12e9dbe + const urlRegex = /https:\/\/www.notion.so\/([a-f0-9]{32})\?*/ + const match = value.match(urlRegex) + if (!match || match.length < 2) { + messageApi.error('Invalid Notion Database ID.') + return value + } + return match[1] + }, + [messageApi] + ) + const onFinish: FormProps['onFinish'] = async (values) => { try { await updateNotion(values) @@ -168,27 +188,39 @@ export default function Notion(): JSX.Element { onFinishFailed={onFinishFailed} > - label="Notion Page Id" - name="parentPageId" - help="The id of the Notion page where the items will be exported to. You can find it in the URL of the page." + label="Notion Database ID" + name="parentDatabaseId" + help="The ID of the Notion database where the items will be exported to. You can find it in the URL of the database." + normalize={normalizeDatabaseId} rules={[ { required: true, - message: 'Please input your Notion Page Id!', + message: 'Please input your Notion Database ID!', + }, + { + validator: (_, value) => { + // check if database id is in UUIDv4 format + const uuidRegex = /^[0-9a-fA-F]{8}[0-9a-fA-F]{4}[0-9a-fA-F]{4}[0-9a-fA-F]{4}[0-9a-fA-F]{12}$/ + if (uuidRegex.test(value)) { + return Promise.resolve() + } + // extract the database id from the URL + const urlRegex = + /https:\/\/www.notion.so\/([a-f0-9]{32})\?*/ + const match = value.match(urlRegex) + if (match && match.length >= 2) { + return Promise.resolve() + } + return Promise.reject( + new Error('Invalid Notion Database ID.') + ) + }, }, ]} > - - label="Notion Database Id" - name="parentDatabaseId" - hidden - > - - - label="Properties to Export" name="properties"