From e83b9ff00a1f2eda9a616c46f81737829f2ff8d6 Mon Sep 17 00:00:00 2001 From: chime3 Date: Tue, 10 Dec 2024 23:18:42 -0700 Subject: [PATCH 1/9] feat: notion provider class --- assets/notion/icon.png | Bin 0 -> 16120 bytes package.json | 3 + src/providers/notion/index.ts | 110 +++++++++++++++++++++++++++++ src/providers/notion/interfaces.ts | 11 +++ src/serverconfig.example.json | 8 +++ yarn.lock | 80 +++++++-------------- 6 files changed, 157 insertions(+), 55 deletions(-) create mode 100644 assets/notion/icon.png create mode 100644 src/providers/notion/index.ts create mode 100644 src/providers/notion/interfaces.ts diff --git a/assets/notion/icon.png b/assets/notion/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..833be386fe3b851dbfcc496ebd2c5cb466f84205 GIT binary patch literal 16120 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 { + this.init(); + const auth = passport.authenticate("notion", { + scope: this.getScopes().join(" "), + }); + return auth(req, res, next); + } + + public async callback(req: Request, res: Response, next: any): Promise { + this.init(); + return new Promise((resolve, reject) => { + passport.authenticate( + "notion", + { failureRedirect: "/failure/notion", failureMessage: true }, + (err: any, user: any) => { + + console.log("++++++") + console.log(user) + if (err) { + return reject(err); + } + if (!user) { + return reject(new Error("No user data returned from Notion")); + } + + const profile = this.formatProfile(user.profile); + + resolve({ + id: profile.id, + accessToken: user.accessToken, + refreshToken: user.refreshToken, + profile: { + username: profile.connectionProfile.username, + ...profile, + }, + }); + } + )(req, res, next); + }); + } + + public async getApi(accessToken?: string): Promise { + if (!accessToken) { + throw new Error("Access token is required"); + } + return new Client({ auth: accessToken }); + } + + public init() { + passport.use( + new NotionStrategy({ + clientID: this.config.clientId, + clientSecret: this.config.clientSecret, + callbackURL: this.config.callbackUrl, + }) + ); + } + + private formatProfile(notionProfile: any): PassportProfile { + const email = notionProfile.email || null; + + return { + id: notionProfile.id, + provider: this.getProviderName(), + displayName: notionProfile.name || email || notionProfile.id, + name: { + familyName: "", + givenName: notionProfile.name || "", + }, + photos: [], + connectionProfile: { + username: email ? email.split("@")[0] : notionProfile.id, + readableId: email || notionProfile.id, + email: email, + verified: true, + }, + }; + } +} diff --git a/src/providers/notion/interfaces.ts b/src/providers/notion/interfaces.ts new file mode 100644 index 00000000..1aafa243 --- /dev/null +++ b/src/providers/notion/interfaces.ts @@ -0,0 +1,11 @@ +import { BaseHandlerConfig, BaseProviderConfig } from "../../../src/interfaces"; + +export interface NotionProviderConfig extends BaseProviderConfig { + clientId: string; + clientSecret: string; + callbackUrl: string; +} + +export interface NotionHandlerConfig extends BaseHandlerConfig { + batchSize: number +} \ No newline at end of file diff --git a/src/serverconfig.example.json b/src/serverconfig.example.json index 31f27355..7d45037b 100644 --- a/src/serverconfig.example.json +++ b/src/serverconfig.example.json @@ -125,6 +125,14 @@ "messagesPerGroupLimit": 100, "maxGroupSize": 50, "useDbPos": true + }, + "notion": { + "status": "active", + "label": "Notion", + "clientId": "", + "clientSecret": "", + "batchSize": 50, + "maxSyncLoops": 1 } }, "providerDefaults": { diff --git a/yarn.lock b/yarn.lock index 6934bea8..6a209491 100644 --- a/yarn.lock +++ b/yarn.lock @@ -115,55 +115,6 @@ "@smithy/util-utf8" "^3.0.0" tslib "^2.6.2" -"@aws-sdk/client-bedrock@^3.693.0": - version "3.693.0" - resolved "https://registry.yarnpkg.com/@aws-sdk/client-bedrock/-/client-bedrock-3.693.0.tgz#dd10cd5b9d5e1f35107768c9ba71da7879fafd80" - integrity sha512-N/aSSdfeqCczWP9o4kSYwH0cARurO2S7TBUNe3RHx5Oe+3McLyrPvL2NJsCDdu4aPrhRvCV1K95X9I3z5bBw5A== - dependencies: - "@aws-crypto/sha256-browser" "5.2.0" - "@aws-crypto/sha256-js" "5.2.0" - "@aws-sdk/client-sso-oidc" "3.693.0" - "@aws-sdk/client-sts" "3.693.0" - "@aws-sdk/core" "3.693.0" - "@aws-sdk/credential-provider-node" "3.693.0" - "@aws-sdk/middleware-host-header" "3.693.0" - "@aws-sdk/middleware-logger" "3.693.0" - "@aws-sdk/middleware-recursion-detection" "3.693.0" - "@aws-sdk/middleware-user-agent" "3.693.0" - "@aws-sdk/region-config-resolver" "3.693.0" - "@aws-sdk/types" "3.692.0" - "@aws-sdk/util-endpoints" "3.693.0" - "@aws-sdk/util-user-agent-browser" "3.693.0" - "@aws-sdk/util-user-agent-node" "3.693.0" - "@smithy/config-resolver" "^3.0.11" - "@smithy/core" "^2.5.2" - "@smithy/fetch-http-handler" "^4.1.0" - "@smithy/hash-node" "^3.0.9" - "@smithy/invalid-dependency" "^3.0.9" - "@smithy/middleware-content-length" "^3.0.11" - "@smithy/middleware-endpoint" "^3.2.2" - "@smithy/middleware-retry" "^3.0.26" - "@smithy/middleware-serde" "^3.0.9" - "@smithy/middleware-stack" "^3.0.9" - "@smithy/node-config-provider" "^3.1.10" - "@smithy/node-http-handler" "^3.3.0" - "@smithy/protocol-http" "^4.1.6" - "@smithy/smithy-client" "^3.4.3" - "@smithy/types" "^3.7.0" - "@smithy/url-parser" "^3.0.9" - "@smithy/util-base64" "^3.0.0" - "@smithy/util-body-length-browser" "^3.0.0" - "@smithy/util-body-length-node" "^3.0.0" - "@smithy/util-defaults-mode-browser" "^3.0.26" - "@smithy/util-defaults-mode-node" "^3.0.26" - "@smithy/util-endpoints" "^2.1.5" - "@smithy/util-middleware" "^3.0.9" - "@smithy/util-retry" "^3.0.9" - "@smithy/util-utf8" "^3.0.0" - "@types/uuid" "^9.0.1" - tslib "^2.6.2" - uuid "^9.0.1" - "@aws-sdk/client-sso-oidc@3.693.0": version "3.693.0" resolved "https://registry.yarnpkg.com/@aws-sdk/client-sso-oidc/-/client-sso-oidc-3.693.0.tgz#2fd7f93bd81839f5cd08c5e6e9a578b80572d3c4" @@ -934,6 +885,14 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" +"@jitl/passport-notion@^2.0.0": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@jitl/passport-notion/-/passport-notion-2.0.0.tgz#03b08c2ac77898ca913f1ce93ff4d68f739dccaf" + integrity sha512-vf0vCn44bdcY/F7CyA5p+6hdNxv9BvGlu44tQ/EZxPv/xwHQEGwKKy/Ls/QImmT1VBTZUBiXaDI9nowB3KdDsQ== + dependencies: + "@notionhq/client" "0.4.2" + passport-strategy "^1.0.0" + "@jsdevtools/ono@^7.1.3": version "7.1.3" resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" @@ -951,6 +910,14 @@ resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.3.2.tgz#6f26dbc8fbc7205873ce3cee2f690eba0d421b39" integrity sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ== +"@notionhq/client@0.4.2": + version "0.4.2" + resolved "https://registry.yarnpkg.com/@notionhq/client/-/client-0.4.2.tgz#7e6744cf7bfaa3f82d24cd72f47eadc2b8461173" + integrity sha512-OfU587WZ5YzH9f59BJb/+QY74B6rZEPyqZEn3vQB2dOu1EXtzyWDHkfZfakYXYogJ24mvBfS77KjnaCRrEzSKw== + dependencies: + "@types/node-fetch" "^2.5.10" + node-fetch "^2.6.1" + "@notionhq/client@^2.2.15": version "2.2.15" resolved "https://registry.yarnpkg.com/@notionhq/client/-/client-2.2.15.tgz#739fc8edb1357a2e2e35d026571fafe17c089606" @@ -1835,6 +1802,14 @@ "@types/oauth" "*" "@types/passport" "*" +"@types/passport-strategy@^0.2.38": + version "0.2.38" + resolved "https://registry.yarnpkg.com/@types/passport-strategy/-/passport-strategy-0.2.38.tgz#482abba0b165cd4553ec8b748f30b022bd6c04d3" + integrity sha512-GC6eMqqojOooq993Tmnmp7AUTbbQSgilyvpCYQjT+H6JfG/g6RGc7nXEniZlp0zyKJ0WUdOiZWLBZft9Yug1uA== + dependencies: + "@types/express" "*" + "@types/passport" "*" + "@types/passport@*", "@types/passport@1.x": version "1.0.12" resolved "https://registry.yarnpkg.com/@types/passport/-/passport-1.0.12.tgz#7dc8ab96a5e895ec13688d9e3a96920a7f42e73e" @@ -1897,11 +1872,6 @@ resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-10.0.0.tgz#e9c07fe50da0f53dc24970cca94d619ff03f6f6d" integrity sha512-7gqG38EyHgyP1S+7+xomFtL+ZNHcKv6DwNaCZmJmo1vgMugyF3TCnXVg4t1uk89mLNwnLtnY3TpOpCOyp1/xHQ== -"@types/uuid@^9.0.1": - version "9.0.8" - resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-9.0.8.tgz#7545ba4fc3c003d6c756f651f3bf163d8f0f29ba" - integrity sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA== - "@types/ws@^8.5.10": version "8.5.12" resolved "https://registry.yarnpkg.com/@types/ws/-/ws-8.5.12.tgz#619475fe98f35ccca2a2f6c137702d85ec247b7e" @@ -5750,7 +5720,7 @@ passport-oauth2@^1.5.0, passport-oauth2@^1.6.1: uid2 "0.0.x" utils-merge "1.x.x" -passport-strategy@1.x.x: +passport-strategy@1.x.x, passport-strategy@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= From afc02b77f015d8d47e15577d8eae255f056dba9a Mon Sep 17 00:00:00 2001 From: chime3 Date: Tue, 10 Dec 2024 23:19:07 -0700 Subject: [PATCH 2/9] feat: defined notion strategy class --- src/providers/notion/NotionStrategy.ts | 102 +++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 src/providers/notion/NotionStrategy.ts diff --git a/src/providers/notion/NotionStrategy.ts b/src/providers/notion/NotionStrategy.ts new file mode 100644 index 00000000..ce53157c --- /dev/null +++ b/src/providers/notion/NotionStrategy.ts @@ -0,0 +1,102 @@ +import { Request } from "express"; +import https from "https"; +import { URL } from "url"; +import passport from "passport"; + +export default class NotionStrategy { + name = "notion"; + private _clientID: string; + private _clientSecret: string; + private _callbackURL: string; + private _authorizationURL: string; + private _tokenURL: string; + + constructor({ clientID, clientSecret, callbackURL }: { clientID: string; clientSecret: string; callbackURL: string }) { + if (!clientID || !clientSecret || !callbackURL) { + throw new TypeError("Missing required options for NotionStrategy"); + } + this._clientID = clientID; + this._clientSecret = clientSecret; + this._callbackURL = callbackURL; + this._authorizationURL = "https://api.notion.com/v1/oauth/authorize"; + this._tokenURL = "https://api.notion.com/v1/oauth/token"; + } + + async authenticate(req: Request, options: any) { + options = options || {}; + if (req.query && req.query.code) { + try { + const oauthData = await this.getOAuthAccessToken(req.query.code as string); + + if (oauthData.owner.type !== "user") { + throw new Error(`Notion API token not owned by user, instead: ${oauthData.owner.type}`); + } + + return oauthData; + } catch (error) { + this.error(error as Error); + } + } else { + const authUrl = new URL(this._authorizationURL); + authUrl.searchParams.set("client_id", this._clientID); + authUrl.searchParams.set("redirect_uri", this._callbackURL); + authUrl.searchParams.set("response_type", "code"); + this.redirect(authUrl.toString()); + } + } + + error(err: any) { + throw new Error("Error occurred in NotionStrategy"); + } + + fail(info: any) { + throw new Error("Failure in NotionStrategy"); + } + + success(user: any) { + throw new Error("Success callback not implemented."); + } + + redirect(url: string) { + throw new Error("Redirect callback not implemented."); + } + + private async getOAuthAccessToken(code: string): Promise { + const accessTokenBody = { + grant_type: "authorization_code", + code, + redirect_uri: this._callbackURL, + }; + const encodedCredential = Buffer.from(`${this._clientID}:${this._clientSecret}`).toString("base64"); + + const requestOptions = { + hostname: new URL(this._tokenURL).hostname, + path: new URL(this._tokenURL).pathname, + headers: { + Authorization: `Basic ${encodedCredential}`, + "Content-Type": "application/json", + }, + method: "POST", + }; + + return new Promise((resolve, reject) => { + const accessTokenRequest = https.request(requestOptions, (res) => { + let data = ""; + res.on("data", (d) => { + data += d; + }); + res.on("end", () => { + try { + resolve(JSON.parse(data)); + } catch (error) { + reject(error); + } + }); + }); + + accessTokenRequest.on("error", reject); + accessTokenRequest.write(JSON.stringify(accessTokenBody)); + accessTokenRequest.end(); + }); + } +} From 72ca1c960107eeaa32fce08fa6a515622eabbe28 Mon Sep 17 00:00:00 2001 From: chime3 Date: Sun, 2 Feb 2025 16:01:23 -0700 Subject: [PATCH 3/9] feat: refactored NotionStrategy class using Passport Strategy --- src/providers/notion/NotionStrategy.ts | 39 ++++++++++---------------- 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/src/providers/notion/NotionStrategy.ts b/src/providers/notion/NotionStrategy.ts index ce53157c..1bbb0e65 100644 --- a/src/providers/notion/NotionStrategy.ts +++ b/src/providers/notion/NotionStrategy.ts @@ -3,7 +3,13 @@ import https from "https"; import { URL } from "url"; import passport from "passport"; -export default class NotionStrategy { +interface NotionStrategyOptions { + clientID: string; + clientSecret: string; + callbackURL: string; +} + +export default class NotionStrategy extends passport.Strategy { name = "notion"; private _clientID: string; private _clientSecret: string; @@ -11,7 +17,8 @@ export default class NotionStrategy { private _authorizationURL: string; private _tokenURL: string; - constructor({ clientID, clientSecret, callbackURL }: { clientID: string; clientSecret: string; callbackURL: string }) { + constructor({ clientID, clientSecret, callbackURL }: NotionStrategyOptions) { + super(); if (!clientID || !clientSecret || !callbackURL) { throw new TypeError("Missing required options for NotionStrategy"); } @@ -22,45 +29,29 @@ export default class NotionStrategy { this._tokenURL = "https://api.notion.com/v1/oauth/token"; } - async authenticate(req: Request, options: any) { + async authenticate(req: Request, options?: any) { options = options || {}; - if (req.query && req.query.code) { + if (req.query?.code) { try { const oauthData = await this.getOAuthAccessToken(req.query.code as string); if (oauthData.owner.type !== "user") { - throw new Error(`Notion API token not owned by user, instead: ${oauthData.owner.type}`); + return this.fail(`Notion API token not owned by user, instead: ${oauthData.owner.type}`); } - return oauthData; + return this.success(oauthData); } catch (error) { - this.error(error as Error); + return this.error(error); } } else { const authUrl = new URL(this._authorizationURL); authUrl.searchParams.set("client_id", this._clientID); authUrl.searchParams.set("redirect_uri", this._callbackURL); authUrl.searchParams.set("response_type", "code"); - this.redirect(authUrl.toString()); + return this.redirect(authUrl.toString()); } } - error(err: any) { - throw new Error("Error occurred in NotionStrategy"); - } - - fail(info: any) { - throw new Error("Failure in NotionStrategy"); - } - - success(user: any) { - throw new Error("Success callback not implemented."); - } - - redirect(url: string) { - throw new Error("Redirect callback not implemented."); - } - private async getOAuthAccessToken(code: string): Promise { const accessTokenBody = { grant_type: "authorization_code", From 015f7d8eead379d874bcf0ab1b2d93b4a2afa4bf Mon Sep 17 00:00:00 2001 From: chime3 Date: Sun, 2 Feb 2025 16:02:26 -0700 Subject: [PATCH 4/9] feat: completed notion authentication --- src/providers/notion/index.ts | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/providers/notion/index.ts b/src/providers/notion/index.ts index 7305cc15..c7b1a5fb 100644 --- a/src/providers/notion/index.ts +++ b/src/providers/notion/index.ts @@ -4,7 +4,7 @@ import Base from "../BaseProvider"; import { NotionProviderConfig } from "./interfaces"; import { ConnectionCallbackResponse, PassportProfile } from "../../interfaces"; import passport from "passport"; -import NotionStrategy from "./NotionStrategy"; // Import the corrected NotionStrategy class +import NotionStrategy from "./NotionStrategy"; export default class NotionProvider extends Base { protected config: NotionProviderConfig; @@ -44,9 +44,6 @@ export default class NotionProvider extends Base { "notion", { failureRedirect: "/failure/notion", failureMessage: true }, (err: any, user: any) => { - - console.log("++++++") - console.log(user) if (err) { return reject(err); } @@ -54,12 +51,12 @@ export default class NotionProvider extends Base { return reject(new Error("No user data returned from Notion")); } - const profile = this.formatProfile(user.profile); + const profile = this.formatProfile(user); resolve({ id: profile.id, - accessToken: user.accessToken, - refreshToken: user.refreshToken, + accessToken: user.access_token, + refreshToken: user.access_token, // Notion does not provide refresh tokens currently profile: { username: profile.connectionProfile.username, ...profile, @@ -87,21 +84,22 @@ export default class NotionProvider extends Base { ); } - private formatProfile(notionProfile: any): PassportProfile { - const email = notionProfile.email || null; + private formatProfile(notionData: any): PassportProfile { + const owner = notionData.owner?.user; + const email = owner?.person?.email || null; return { - id: notionProfile.id, + id: owner?.id || "", provider: this.getProviderName(), - displayName: notionProfile.name || email || notionProfile.id, + displayName: owner?.name || email || owner?.id || "Unknown", name: { familyName: "", - givenName: notionProfile.name || "", + givenName: owner?.name || "", }, - photos: [], + photos: owner?.avatar_url ? [{ value: owner.avatar_url }] : [], connectionProfile: { - username: email ? email.split("@")[0] : notionProfile.id, - readableId: email || notionProfile.id, + username: email ? email.split("@")[0] : owner?.id || "unknown", + readableId: email || owner?.id || "unknown", email: email, verified: true, }, From f6484cbb93034bdf16111fcfd0244288aae5a7cf Mon Sep 17 00:00:00 2001 From: chime3 Date: Sun, 2 Feb 2025 16:04:09 -0700 Subject: [PATCH 5/9] feat: added Notion readme --- src/providers/notion/README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/providers/notion/README.md diff --git a/src/providers/notion/README.md b/src/providers/notion/README.md new file mode 100644 index 00000000..e69de29b From 3dd52b4190ed704db958ab1ed30785ab8b341a09 Mon Sep 17 00:00:00 2001 From: chime3 Date: Tue, 4 Feb 2025 22:28:21 -0700 Subject: [PATCH 6/9] feat: updated notion readme --- src/providers/notion/README.md | 55 ++++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/src/providers/notion/README.md b/src/providers/notion/README.md index e69de29b..8a8ec2aa 100644 --- a/src/providers/notion/README.md +++ b/src/providers/notion/README.md @@ -0,0 +1,55 @@ +# Notion Provider Configuration + +## Notion Integration Setup + +1. Go to [Notion Integrations](https://www.notion.so/my-integrations) +2. Create a new integration: + - Click **New Integration** + - Add an integration name and redirect Urls + - Choose the capabilities needed (Read Content, Read Comments, Read Users) +3. Copy the `Integration Token` (Secret Key) - store it securely + +## Authentication + +The Notion provider uses Integration Token authentication: +- Use the Integration Token as the `accessToken` +- No `refreshToken` is required + +## Data Access + +Notion API provides access to: +- Pages +- Databases +- Blocks (content elements) +- Users +- Comments + +### Pagination + +Notion uses cursor-based pagination: +- `start_cursor` and `has_more` for pagination control +- Default page size of 100 items +- Maximum page size of 100 items + +## Rate Limits + +- Rate limits vary by tier +- Standard tier: ~3 requests per second +- See [Notion API Limits](https://developers.notion.com/reference/request-limits) for current limits + +## Notes + +- Database queries support filtering and sorting +- Block content is retrieved recursively for nested structures +- Rich text content includes formatting metadata +- User permissions are respected based on integration access +- Some features may require specific Notion plan types + +#### Example – Cursor-Based Pagination + +```javascript +const response = await notion.databases.query({ + database_id: databaseId, + page_size: 10, + start_cursor: nextCursor, // optional, for pagination +}); From 248c0d81c0c99ae1452c9ca7ff15a2069ed2f792 Mon Sep 17 00:00:00 2001 From: chime3 Date: Wed, 5 Feb 2025 13:07:12 -0700 Subject: [PATCH 7/9] fix: updated Notion readme --- src/providers/notion/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/providers/notion/README.md b/src/providers/notion/README.md index 8a8ec2aa..30b427d2 100644 --- a/src/providers/notion/README.md +++ b/src/providers/notion/README.md @@ -7,7 +7,7 @@ - Click **New Integration** - Add an integration name and redirect Urls - Choose the capabilities needed (Read Content, Read Comments, Read Users) -3. Copy the `Integration Token` (Secret Key) - store it securely +3. Copy the `Secret Key` and `Public Key` - store it securely ## Authentication From ad63bd2ca8c65acb867d414ba80246cb811afacc Mon Sep 17 00:00:00 2001 From: chime3 Date: Thu, 6 Feb 2025 17:29:46 -0700 Subject: [PATCH 8/9] feat: notion block handler --- src/providers/notion/block.ts | 163 ++++++++++++++++++++++++++++++++++ src/providers/notion/index.ts | 5 +- 2 files changed, 167 insertions(+), 1 deletion(-) create mode 100644 src/providers/notion/block.ts diff --git a/src/providers/notion/block.ts b/src/providers/notion/block.ts new file mode 100644 index 00000000..b2920d76 --- /dev/null +++ b/src/providers/notion/block.ts @@ -0,0 +1,163 @@ +import CONFIG from "../../config"; +import { BaseHandlerConfig, SyncItemsBreak, SyncItemsResult, SyncProviderLogEvent, SyncProviderLogLevel } from "../../interfaces"; +import { Client } from "@notionhq/client"; +import { ItemsRangeTracker } from "../../helpers/itemsRangeTracker"; +import { + SyncResponse, + SyncHandlerStatus, + ProviderHandlerOption, + ConnectionOptionType, +} from "../../interfaces"; +import { SchemaRecord } from "../../schemas"; +import AccessDeniedError from "../AccessDeniedError"; +import InvalidTokenError from "../InvalidTokenError"; +import BaseSyncHandler from "../BaseSyncHandler"; + +const MAX_BATCH_SIZE = 500; + +export interface SyncBlockItemsResult extends SyncItemsResult { + items: any[]; +} + +export default class NotionBlockHandler extends BaseSyncHandler { + protected config: BaseHandlerConfig; + + public getLabel(): string { + return "Notion Blocks"; + } + + public getName(): string { + return "notion_blocks"; + } + + public getSchemaUri(): string { + return CONFIG.verida.schemas.BLOCK; + } + + public getProviderApplicationUrl(): string { + return "https://notion.so/"; + } + + public getNotionClient(): Client { + return new Client({ auth: this.connection.accessToken }); + } + + public getOptions(): ProviderHandlerOption[] { + return [ + { + id: "syncDepth", + label: "Sync Depth", + type: ConnectionOptionType.ENUM, + enumOptions: [ + { value: "1-level", label: "1 Level" }, + { value: "2-levels", label: "2 Levels" }, + { value: "all", label: "All Levels" }, + ], + defaultValue: "1-level", + }, + ]; + } + + public async _sync(api: any, syncPosition: any): Promise { + try { + if (this.config.batchSize > MAX_BATCH_SIZE) { + throw new Error(`Batch size (${this.config.batchSize}) exceeds max limit (${MAX_BATCH_SIZE})`); + } + + const notion = this.getNotionClient(); + const rangeTracker = new ItemsRangeTracker(syncPosition.thisRef); + let items: any[] = []; + + let currentRange = rangeTracker.nextRange(); + + const pages = await this.getPageList(); + + const page = await notion.pages.retrieve({ + page_id: pages[0].id + }) + + let query = { block_id: pages[0].id, start_cursor: currentRange.startId }; + + const latestResponse = await notion.blocks.children.list(query); + + + const latestResult = await this.buildResults(notion, latestResponse, currentRange.endId); + + items = latestResult.items; + let nextPageCursor = latestResponse.next_cursor; + + if (items.length) { + rangeTracker.completedRange({ + startId: items[0].id, + endId: nextPageCursor, + }, latestResult.breakHit === SyncItemsBreak.ID); + } else { + rangeTracker.completedRange({ startId: undefined, endId: undefined }, false); + } + + if (!items.length) { + syncPosition.syncMessage = "Stopping. No results found."; + syncPosition.status = SyncHandlerStatus.ENABLED; + } else { + syncPosition.syncMessage = items.length !== this.config.batchSize && !nextPageCursor + ? `Processed ${items.length} items. Stopping. No more results.` + : `Batch complete (${this.config.batchSize}). More results pending.`; + } + + syncPosition.thisRef = rangeTracker.export(); + + return { results: items, position: syncPosition }; + } catch (err: any) { + if (err.status === 403) throw new AccessDeniedError(err.message); + if (err.status === 401) throw new InvalidTokenError(err.message); + throw err; + } + + + } + + protected async buildResults( + notion: Client, + serverResponse: any, + breakId: string + ): Promise { + const results: any[] = []; + let breakHit: SyncItemsBreak; + + for (const block of serverResponse.results) { + if (block.id === breakId) { + this.emit("log", { level: SyncProviderLogLevel.DEBUG, message: `Break ID hit (${breakId})` }); + breakHit = SyncItemsBreak.ID; + break; + } + + results.push({ + _id: this.buildItemId(block.id), + type: block.type, + sourceId: block.id, + sourceApplication: this.getProviderApplicationUrl(), + content: JSON.stringify(block), + insertedAt: new Date().toISOString(), + }); + } + + return { items: results, breakHit }; + } + + public async getPageList(): Promise { + try { + const notion = await this.getNotionClient(); + const response = await notion.search({ + filter: { property: "object", value: "page" }, + sort: { direction: "ascending", timestamp: "last_edited_time" }, + page_size: 50 // Max is 100 + }); + + return response.results; + + } catch (error) { + console.error("Error fetching Notion pages:", error); + } + } + +} diff --git a/src/providers/notion/index.ts b/src/providers/notion/index.ts index c7b1a5fb..204ab78f 100644 --- a/src/providers/notion/index.ts +++ b/src/providers/notion/index.ts @@ -5,6 +5,7 @@ import { NotionProviderConfig } from "./interfaces"; import { ConnectionCallbackResponse, PassportProfile } from "../../interfaces"; import passport from "passport"; import NotionStrategy from "./NotionStrategy"; +import NotionBlockHandler from "./block"; export default class NotionProvider extends Base { protected config: NotionProviderConfig; @@ -22,7 +23,9 @@ export default class NotionProvider extends Base { } public syncHandlers(): any[] { - return []; + return [ + NotionBlockHandler + ]; } public getScopes(): string[] { From 6402b32999ea458b1e8d61a7dbe954477147f553 Mon Sep 17 00:00:00 2001 From: chime3 Date: Thu, 6 Feb 2025 17:51:25 -0700 Subject: [PATCH 9/9] chore: clean yarn lock deps --- yarn.lock | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/yarn.lock b/yarn.lock index 203e7b7e..46e3cf7f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1399,14 +1399,6 @@ "@ethersproject/properties" "^5.7.0" "@ethersproject/strings" "^5.7.0" -"@jitl/passport-notion@^2.0.0": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@jitl/passport-notion/-/passport-notion-2.0.0.tgz#03b08c2ac77898ca913f1ce93ff4d68f739dccaf" - integrity sha512-vf0vCn44bdcY/F7CyA5p+6hdNxv9BvGlu44tQ/EZxPv/xwHQEGwKKy/Ls/QImmT1VBTZUBiXaDI9nowB3KdDsQ== - dependencies: - "@notionhq/client" "0.4.2" - passport-strategy "^1.0.0" - "@jsdevtools/ono@^7.1.3": version "7.1.3" resolved "https://registry.yarnpkg.com/@jsdevtools/ono/-/ono-7.1.3.tgz#9df03bbd7c696a5c58885c34aa06da41c8543796" @@ -1514,14 +1506,6 @@ basic-auth "2.0.1" type-is "1.6.18" -"@notionhq/client@0.4.2": - version "0.4.2" - resolved "https://registry.yarnpkg.com/@notionhq/client/-/client-0.4.2.tgz#7e6744cf7bfaa3f82d24cd72f47eadc2b8461173" - integrity sha512-OfU587WZ5YzH9f59BJb/+QY74B6rZEPyqZEn3vQB2dOu1EXtzyWDHkfZfakYXYogJ24mvBfS77KjnaCRrEzSKw== - dependencies: - "@types/node-fetch" "^2.5.10" - node-fetch "^2.6.1" - "@notionhq/client@^2.2.15": version "2.2.15" resolved "https://registry.yarnpkg.com/@notionhq/client/-/client-2.2.15.tgz#739fc8edb1357a2e2e35d026571fafe17c089606"